Show what conversation analysis did under each Chat message #353
- MA
## Why
Chat gave no sign of what conversation analysis did with a message. Every analysed message had an always-visible `<>` code icon, and its popover:
- was labelled "Jev analysis", opened without motion and closed with a text button - led with "No plan change" even when the message produced a research offer - showed model thresholds, policy IDs and model names to every collaborator
Failed analysis looked the same as success, with the only difference in `aria-label`. One sentence read "linked this message**to**", with a missing space.
## What changed
**Result line under applied messages** - Each line is a verb phrase with the card title as its object: - "Added as an option to **Title**" - "Counted as support for **Title**" - "Added an option and a constraint to **Title**" - "Reopened **Title**" - "Marked **Title** ready to settle" - "Started a decision". When the card title is not the message itself, it reads "Started the decision **Title**". - When one message changes several cards, the line ends "… and 2 more decisions". - The whole sentence is the button that jumps to the card. A `<button>` can't wrap as inline text, so this is what lets the title wrap with the sentence on phone. - `DecisionIcon` is 12px and the text is `text-xs` tertiary. - A line that appears live fades in with `--duration-base` / `--motion-smooth-out`. Lines already in history don't animate.
**Failed analysis** - Shows a warning glyph and "Couldn’t analyse this message · Retry". After a retry that fails again it reads "Still couldn’t analyse this message · Retry". - A failed research check reads "Couldn’t check for research · Retry".
**Details action** - The `<>` icon is replaced by an info glyph with the tooltip "Details". On fine pointers it shows only on hover, focus-within or while open. - Without hover (`hover: none`), only messages that changed a card, failed, offered research or have a failed job keep the action. - The accessible name comes from the outcome, e.g. "Message details: Research suggested".
**Details popover** - Header: the outcome as heading, plus an icon close. - Body: one sentence, then the excerpt rows. - Each row label (now "Option", not "Proposal") is the link to its card. A row shows a status only when it is held for review or not applied. - Excerpt quotes no longer show Markdown markers. - It sits clear of the message and its result line, below it or flipped above it. Only a very tall message falls back to anchoring on the trigger. - It animates with the shared `motion-popover` recipe. - Model, policy and research admission data, plus every Planner job row, are inside a closed **Diagnostics** disclosure. Opening Diagnostics scrolls it into view. - A failed job gets plain copy outside Diagnostics, e.g. "Chopin couldn’t rewrite the card wording", with Retry. - "Jev" no longer appears in client copy.
**Lazy loading** - The popover body (`analysis-details.tsx`, 16 kB) is prefetched on idle once Chat shows an analysed message, and on pointerenter, touchstart or focus of the trigger. - If the chunk fails to load, a quiet "Couldn’t open details. Try again." appears, and the next click retries the import. - The initial bundle is 255,794 B, under the 256,000 B budget.
`transcript.tsx` is untouched.
## Screenshots
Before: the code icon on every message, and nothing under the message 
After: result lines, a failed line with Retry, and Details on hover 
After: one message changing an option and a constraint 
Before: the research popover says "No plan change" 
After: the research popover leads with "Research suggested" 
Before: failed analysis popover 
After: failed analysis popover 
After: the popover flips above the message, rows link to the card, and quotes have no Markdown markers 
After: Diagnostics expanded and scrolled into view 
After: phone (touch). Only notable messages keep Details, and titles wrap with the sentence. 
## Testing
- `bun run types`: pass - `bun test apps/web/src/conversation-plan`: 48 pass. Includes the new `outcome.test.ts` copy tests and marker tests. - `bun scripts/check-design-record.ts`: pass - `bun run ci`: everything passes except the pinned design-contract hash below. - E2E: selectors are updated for the outcome-based trigger names, Diagnostics, row card links, the "Unfinished work" group and the new focus order. I'm relying on the CI browser job because local E2E is suspended.
## Needs a design-contract review
- `apps/web/src/conversation-plan/markers.tsx` is pinned in `scripts/design-contract/exceptions/dynamic-web.json`. The new sourceHash is `4e112ec8e7bf402ea3976633f968d0f50610cd09a86acf56c9d2a0cf9f71e2e0`. - The dynamic style case is unchanged: `style={position}` on the same portal `<div>` in `MessageMarkers` still supplies only measured geometry and motion origin.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- MA
**Review:** Changes needed
A clear step up from main. The result line is the right idea: the hover and focus reveal works, the popover leads with the outcome, and the motion is right. Checked on the fake Jev server (8850) at 1440×900 and at 390×844 with touch. The problems are copy, repeated information, and dev-facing text that is still outside Diagnostics.
**Result line (markers.tsx / outcome.ts `PHRASES`)** 1. `Noun on Title` reads abruptly, and "New decision Should we ship a small pilot?" just repeats the message above it. Use verb phrases with the title as the object: - Proposal → "Added as an option to **Title**" - Reason / Constraint / Objection → "Added as a reason / constraint / objection to **Title**" - Support → "Counted as support for **Title**" - Several on one card → "Added an option and a constraint to **Title**" - Reopening → "Reopened **Title**" - Settle → "Marked **Title** ready to settle" - Question → "Started a decision". Make the phrase itself the link when the card title matches the message; otherwise use "Started the decision **Title**". - Overflow → "and 2 more decisions" instead of "· 2 more". - Failed: "Couldn’t analyse this message · Retry" is good. 2. The vocabulary is mixed. The line says "Option" while the popover chips and rows say "Proposal". The cards say "Add an option", so use "Option" everywhere. 3. On phone the title `<button>` is inline-block, so the whole title drops to its own line ("Option and constraint on" ↵ "Which of these…"). Make it `inline text-left` so it wraps with the sentence. 4. A second failed Retry gives no feedback: the line flips back from "Retrying…" to the same text. Optional: show "Still couldn’t analyse · Retry".
**Popover (analysis-details.tsx)** 5. The same fact appears four times: the heading "Added to a decision", "Linked to **X?**.", the chips `Proposal` `Constraint`, and rows `✓ Proposal … Applied`. Keep the heading, one sentence and the rows. Drop the chip group and move its card links onto the row labels; the e2e `Linked decisions` / `data-card-link` selectors move with them. Show a row status only when a row is held or not applied; "Applied" next to a check under an "Added" heading is noise. 6. analysis-details.tsx:135–137 renders "Linked to **Should we ship a small pilot?**." The "?." is a punctuation bug. Drop the period when the title ends in punctuation, or drop the sentence once the result line carries the title. 7. Planner job rows are still dev-facing and visible to everyone: "heading · skipped — The Planner is off (AGENT=off)." and "refine · skipped — …". The job kinds and the env var mean nothing to collaborators. Move skipped and done jobs into Diagnostics. Give a failed job human copy outside it, e.g. "Chopin couldn’t rewrite the card wording · Retry". The jobs-only popover can stay as it is behind Diagnostics. 8. Quotes show raw Markdown: `“**Keep the pilot accessible…**”`. Strip emphasis markers in excerpt quotes. This bug predates the PR but is now more visible. 9. The popover anchors under the 28px trigger, so it covers the second line of the message and the result line it explains (see the desktop and phone shots). Anchor it below the message block, result line included, or flip it above when there's room. 10. Expanding Diagnostics adds content below the fold of the scroll box and nothing scrolls to it. Scroll the opened `<details>` into view (`block: "nearest"`).
**Details action** 11. The reveal is good: opacity goes 0→1 on hover and focus-within with `--duration-fast`, it stays while `aria-expanded`, and `:focus-visible` works. On touch every message carries an ⓘ, and on phone that makes a column of icons. Show it on coarse pointers only for messages that have a result, failure or research offer, or make the result line itself the tap target. Discoverability is otherwise fine. 12. `aria-label={`Message details: ${status}`}` exposes internal statuses. A message that produced a research offer is announced as "Message details: unlinked". Use `outcome.heading`, e.g. "Message details: Research suggested".
**Lazy chunk** 13. The hover prefetch works. The chunk is requested about 20 ms after pointerenter, and a warm open mounts in about 25 ms and runs the popover recipe (scale .97→1 plus fade, about 270 ms). There's no flash. On a slow link (400 ms latency, 50 KB/s) a click 300 ms after hover is dead for about 750 ms with no feedback. Touch has no hover, so the first tap always waits on the network. Prefetch when Chat first renders any analysed message (idle callback), or on message `pointerenter`/`touchstart`. 14. `loadDetails()` has no `catch`. A failed chunk (for example after a deploy changes hashes) leaves the button silently dead and raises an unhandled rejection. Catch it and show an error, or retry the import on the next click.
**icon-assets.test.ts exception** The rule pins the shared interface icons to one 14px size, and every exception is scoped to file, icon and size. `DecisionIcon` isn't in the pinned list and is already 12px on chat metadata lines (decision-entry.tsx:163, scoped-choice-entry.tsx:88). A 12px warning beside it on a `text-xs` line is consistent and scoped, so the exception is justified. This is the sixth one-off, though. If 12px metadata lines are becoming a pattern, name that role in the test instead of adding file exceptions.
**Checks** - e2e: pass - container: pass - format/lint/types/tests: fails only on the reviewed dynamic owner `markers.tsx:391` hash, which the PR body declares. - `bun test` conversation-plan: 54/55 locally. The one failure is an icon-assets timeout under machine load, unrelated.
- MA
All 14 review findings are addressed in 53cff718. CI: e2e and container pass. Format/lint/types/tests fails only on the declared `markers.tsx` design-contract hash; the new hash is in the PR body.
**Result line** 1. The copy is now the proposed verb phrases. "Started a decision" is itself the link when the card title is the message. Several cards end with "and N more decisions". 2. "Option" replaces "Proposal" everywhere: link labels, signals, rows and e2e. 3. `inline` has no effect on a `<button>`: browsers render an inline button as inline-block. So the whole sentence is now the button, and only the title takes `group-hover:underline`. The title now wraps with the sentence on phone. This also covers your suggestion in #11 to make the result line the tap target. 4. A retry that fails again reads "Still couldn’t analyse this message · Retry".
**Popover** 5. The chip group is gone. Row labels are the card links (`data-card-link` moved there). A row shows a status only when it is "Held for review" or "Not applied". 6. The "Linked to …." sentence is replaced by the result sentence with no trailing period, so there is no more "?.". 7. Every job row now lives in Diagnostics. A failed job gets plain copy outside it, e.g. "Chopin couldn’t rewrite the card wording", with Retry in an "Unfinished work" group. 8. Excerpt quotes have their Markdown markers stripped. 9. The popover anchors to the whole message block, result line included, below it or flipped above. Only a very tall message falls back to anchoring on the trigger. 10. Opening Diagnostics scrolls it into view (`block: "nearest"`; smooth unless motion is immediate).
**Details action** 11. Under `hover: none`, only notable messages keep Details: a changed card, a failure, a research offer or a failed job. Others drop the reserved padding. 12. The accessible name is `Message details: ${outcome.heading}`, or "Planner jobs" for a jobs-only message.
**Lazy chunk** 13. The chunk is prefetched once on idle when Chat first renders an analysed message, and also on pointerenter, touchstart or focus of the trigger. 14. A failed load shows a quiet "Couldn’t open details. Try again." and clears the cached promise, so the next click retries. The trigger is `aria-busy` while the chunk loads.
Screenshots in the PR body are updated.
MaggieAppletonreferenced this in Show decision evidence as a summary row with objections #361last month on GitHub
MaggieAppletonreferenced this in Make the decision card the one place to settle, and keep reopening copy truthful #375last month on GitHub
MaggieAppletonreferenced this in Share one menu surface recipe across pickers and the language menu #377last month on GitHub
MaggieAppletonhead-ref-force-pushed 2363e477ab74last month on GitHub