# Chat UI walkthrough — 2026-08-06 A guided tour of the twelve changes made to the web chat on 2026-08-06, in the order they are easiest to click through. Each item says **where to look**, **what to do**, and **what changed** — and, where it matters, what the old behaviour actually was, because several of these are only visible if you know what was broken. **Before starting:** `pm2 restart officer`, then hard-refresh the browser (Ctrl/Cmd-Shift-R). The frontend is served bundled from `index.gen.html`; without the hard refresh you will be looking at the old JS and none of this will be there. **Honesty note up front: none of this has been rendered in a browser.** `bunx tsgo` is clean and prettier is clean after every change, and the diff algorithm has real unit-test coverage run under `bun`. Everything visual is reasoned from the source and the CSS token values, not seen. Treat this document as a list of claims to check, not a list of things known to work. Commits, oldest first: `d7f5cd5`, `8c27901`, `306def1`, `9daf420`, `9eb8fa1`. --- ## 1. Dark mode: the white-on-white labels **Where:** any chat session that has already started — the provider label beside the model selector, and the active provider tab. **What to do:** switch to dark mode and open an existing conversation. **What changed:** the label was `bg-duck-dark/80 text-white`. `--duck-dark` is `#14532d` in light mode but **`#f1f5f9` in dark** — it inverts to near-white. So the label was white text on a near-white background for every started session, which is why it looked like the label had simply gone missing. This turned out to be a whole class of defect rather than one bug, so `duck-dark` was swept out of the chat entirely — 13 files — and replaced with the semantic tokens that carry their own dark variants: `foreground`, `foreground/80`, `muted-foreground`, `border`, `border-input`, `bg-muted`. Raw `text-red-500` / `bg-red-500` went to `destructive` at the same time. Two things were deliberately **not** swept: `--duck-teal` (it has a real dark override, so it works), and the `bg-gray-900 text-green-400` terminal look on bash output and log tails, which is meant to look like a terminal in both themes. --- ## 2. Dead air between sending and the first token **Where:** the transcript, immediately after you press send. **What to do:** send a message with thinking turned on, and watch the gap. **What changed:** the streaming bubble returned `null` when its text was empty, so the entire wait between send and the first token — tens of seconds with extended thinking — rendered _nothing_. No bubble, no spinner, no acknowledgement that the message went anywhere. It now shows the bubble immediately with three pulsing dots. --- ## 3. Text that broke in the wrong places **Where:** any tool call with a long shell command; any user message containing a pasted URL. **What changed:** `break-all` → `break-words`. `break-all` splits mid-identifier, so a path or a flag would break across lines in the middle of a word and become unreadable. The user bubble also gained `break-words`, so a pasted URL now stays inside the pane instead of pushing it wide. Sub-12px labels also went up to `text-xs`. There were several `text-[10px]` and `text-[11px]` labels that were legible on the machine they were written on and not much else. --- ## 4. Edit and Write tool calls render as real diffs **Where:** the transcript, any `Edit` or `Write` tool call. Click it to expand. **What to do:** ask for a small edit to a file and expand the tool row. **What changed:** the biggest single change of the day. Expanding an `Edit` used to print a `key: value` dump — `old_string: …`, `new_string: …` — as raw text, and reading what actually changed meant diffing two blobs by eye. It now renders a proper diff: added lines tinted with `bg-success/10`, removed with `bg-destructive/10`, context in muted grey. **Collapsed rows now carry the stat** — `+12 −4` in the row itself, so you can see the size of an edit without opening it. Details worth knowing: - **No line numbers, deliberately.** An `Edit`'s `old_string`/`new_string` are fragments with no file position attached. Any number printed beside them would be invented, and a plausible-looking wrong line number is worse than none. - **Long diffs clamp at 40 lines** in the view and the differ refuses anything over 800 lines outright, falling back to the old input dump rather than locking the tab computing an LCS over a huge file. - **Copy gives you the code, not the diff.** The `+`/`−` gutter is `select-none`, so selecting a diff and copying gets the source lines. The copy button on a `Bash` row gives the command; on a `Write` row it gives the resulting file text — never the `key: value` dump. - The differ is 85 lines of LCS in `src/workspaces/officerdev/src/apps/Chat/components/line-diff.ts`, written rather than pulled in — jsdiff would be a runtime dependency shipped to the browser to run a textbook algorithm. It has unit tests; they caught a real bug (`''.split('\n')` is `['']`, not `[]`, so every new-file diff opened with a phantom deleted blank line). --- ## 5. The session list is a list of links now **Where:** the left pane of `/chat`. **What to do:** **cmd-click a session.** It should open in a new tab. Middle-click it. Tab to it with the keyboard. **What changed:** rows were `
` — the "opaque click" anti-pattern `docs/navigation-audit.md` names. The id lived in a closure, not the DOM, so there was no cmd-click, no middle-click, no link-focus, and nothing to copy the address of. Rows are now real ``s built on the shared `DataRow`, and they carry the query string, so `?cwd=` survives the click. The whole list was rebuilt on the shared data primitives (`DataList`, `DataRow`, `RelativeTime`, `LoadingBlock`, `EmptyBlock`, `ErrorBlock`) — the same vocabulary the other rebuilt screens use. Two files went away with it: `SessionBar.tsx` and `SessionContextMenu.tsx`, both unused. **Note the row action buttons are siblings of the anchor, not inside it.** A `