diff --git a/NEXT-SESSION.md b/NEXT-SESSION.md new file mode 100644 index 00000000..289e47e0 --- /dev/null +++ b/NEXT-SESSION.md @@ -0,0 +1,113 @@ +# Next session — two chat fixes + +**Disposable.** Written 2026-08-18 ~05:00 as a handoff across a compaction. Delete it when both are done. + +Both are in `/chat`. Do them in order — the first is small and well understood, the second is a design +question wearing a UI job's clothes. + +--- + +## 1. The model dropdown says `claude-code` instead of the model that is answering + +### Symptom + +In the composer, bottom right, the model select renders the literal string **`claude-code`**. It should +name a model — the owner expects "Opus". + +### Why, exactly + +`DEFAULT_MODEL` in `src/servers/api/chat/websocket.ts` is the bare harness string `claude-code`, used when +the client sends no model and the account has no default. It routes correctly — `isClaudeModel` is a prefix +test and `model.split('/')[1]` is `undefined`, so no `--model` is passed and **the CLI picks for itself**. + +It names no catalogue entry, so Radix's `SelectValue` matches no item and renders nothing. `773f0e1c` added +a fallback in `ModelSelector.tsx` so a blank trigger became the raw string's last segment +(`displayModel.split('/').pop()`), which for `claude-code` is `claude-code`. Honest, and useless. + +### The fix, and the distinction it turns on + +**The server does not know which model answered** — it deliberately did not choose one. The transcript does: +every assistant entry carries `message.model` (`claude-opus-5`), and `officerModelForApiModel()` in +`src/servers/api/chat/model-aliases.ts` already maps that back to `claude-code/opus`. + +So: when the effective model is the bare harness, display **what answered**, not what was asked for. That is +the same `effective`-vs-requested distinction `createMessageHandler` already makes for tier switches. + +Options, cheapest first: + +1. **Client-side.** The turn's `result`/assistant messages already reach `useChat`. If a message carries a + model, prefer it over the bare harness for display only — do not write it back to `selectedModel`, or the + next turn will request a tier the user never picked. +2. **Server-side.** `GET /chat/sessions/:id` already returns `officerModel`, resolved from the stored row + with a family-guess fallback. A live session could report the same thing on `session:init`. + +Option 1 is smaller. Option 2 is more consistent with how a resumed session already works. + +### Traps + +- **Do not store the resolved value.** `record()` in `websocket.ts` refuses to persist a model without a `/` + precisely because a bare harness overwrote a correctly backfilled `claude-code/opus` and made the picker + render blank. Read that comment before touching either end. +- **This picker has been broken once already today.** Verify: a session with no explicit model, a session on + `opus[1m]`, and a resumed session all show something sensible. +- `chat_session_models.model` holds Officer ids (`claude-code/opus[1m]`), not API ids. Do not mix them. + +--- + +## 2. Usage popover on the "Claude Code" pill + +### What is wanted + +Clicking the harness pill — the left-hand label in the composer's control row, which reads **Claude Code** +and is inert once a conversation has started — should open a panel showing what Claude Code's own usage +display shows: + +- **Limits**: Session %, Weekly %, Fable Weekly %, each with a reset countdown +- **Tokens by day**, last seven days, as bars +- **Tokens by model**: Opus 5, Opus 4.8, and any others, with totals + +The owner supplied a screenshot of the target and a reference implementation: +**https://github.com/basecamp/omarchy/tree/quattro/shell/plugins/agents** + +### Read the reference FIRST — this is a data question, not a UI one + +**Officer does not have any of these numbers.** Per-session cost is accumulated in memory in +`websocket.ts` (search `session.meta.cost`), it dies with `pm2 restart officer`, and nothing aggregates it +across sessions, days or models. There is no usage table. + +So before writing any component, answer: **where does omarchy get this?** Two possibilities, and they are +completely different jobs: + +- **It reads local Claude Code state** — something under `~/.claude`. Then this is a display job: a sidecar + route reads the file, the panel renders it, and the numbers are the same ones the CLI shows. +- **It calls an Anthropic endpoint** — then it needs the OAuth credential, which lives with + `officer-anthropic-proxy` and never leaves it, and the route belongs there rather than in the chat socket. + +Do not build a schema for this until that is known. The wrong answer is inventing Officer's own token +accounting to reproduce a number the CLI already computes. + +### Where the pill is + +`src/workspaces/officerdev/src/apps/Chat/components/ModelSelector.tsx` — the provider label rendered when +`providerStripLocked` is true (a `` with `bg-muted`, currently not a button). It sits left of the +background-job button and the model select. + +Note the owner's framing: _"in a running chat that is not responding to harness change anymore"_ — the pill +is already inert in that state, which is exactly why it is free to become a button. + +### Constraints + +- **Owner-only data.** These are the owner's subscription limits. A member must not see them — gate it. +- **No palette classes.** Semantic tokens only (`bg-muted`, `text-muted-foreground`, `bg-primary/15`); the + ratchet is `bun run check:theme`. +- **Do not block the composer.** If the source is unreachable the popover says so; it must not spin. + +--- + +## Before you start + +`bunx tsgo` is clean and `bun test` is at **7 pre-existing failures** (registry/totality drift and a cliamp +socket — none yours). Compare against that baseline, not against zero. + +Everything else from tonight is committed and pushed across four repositories: `platform`, `officer-oas`, +`plugins/email`, `plugins/kiosk`.