Make the decision card the one place to settle, and keep reopening copy truthful #375
- MA
Stacked on #353 (conversation analysis result lines). Base is `design/conversation-analysis-markers`; it will be rebased onto `main` once #353 lands.
## Why
- **Two Save buttons.** After a resolution message, Chat posted a card with its own primary "Save decision" beside the card's "Save". - **Objections ignored.** The Chat prompt said nothing about objections raised a minute earlier. - **Unclear link.** Its only link was a page glyph labelled "Open in plan". - **Odd sub-label.** The card marked the suggested option with "from chat", styled like an option description. - **False "Reopened" line.** A reopening message like "Let's revisit the small pilot decision." made #353's result line say "Reopened <card>", but the card stayed decided. - The server only records a pending `reopening` candidate. `domain-corrections` refuses to confirm candidates for card-linked threads ("decisions are recorded on the card"), so a person has to reopen from the card.
## What changed
- **Chat prompt is now a quiet activity line.** It reads `Ready to settle: <suggested or selected option> · N objections · Open card`, in `text-sm` tertiary text with `DecisionIcon`. With no option selected it reads `Needs a choice · … · Open card`. - The objection count uses `text-warning-ink`. It counts distinct people who oppose the shown option, or the whole decision. When no option is shown, it counts everyone who opposes the decision. One person is one objection. - The spike (scoped-choice) prompt keeps its own Save on purpose. Scoped choices are provisional and never decide the card. - Chat has no Save button. The card is the only place to commit a decision. - **Retired prompts use the same row.** "Decided: X · @owner · Open card", "Discarded · Open card" and so on. The owner now has the `@` prefix, matching the document's decided line. - **"Open card" is a text link** in petrol (`text-brand-ink`), so it always reads as a link. Its accessible name is "Open card: <title>". "plan" no longer appears in this copy. - On coarse pointers it grows an invisible 44 px tap target. It uses the same `::after` pattern as the existing `btn-sm` touch targets, so the line doesn't move. - "· Open card", "· N objections" and "· Suggested in Chat" each stay on one line, so a separator never dangles and "Suggested in / Chat" never splits. - **Card hint.** "from chat" is now an inline, quiet "· Suggested in Chat" next to the option label (`text-xs` tertiary). It no longer takes a second line. - **Reopening suggestions.** A pending candidate on a card that is still decided now gets the link label "Suggested reopening". The result line says **"Suggested reopening <card>"**, and the details heading says "Suggested reopening". - A confirmed candidate says "Reopened". So does a pending one once the card was reopened after the suggestion. - A rejected candidate gets no line. - **Open card goes to Reopen for resolved cards.** For a decided or discarded card, "Open card" and the "Suggested reopening <title>" result line now open the Decisions tab. They expand the resolved history and focus that card's entry, where Reopen already lives. Chat gets no Reopen button of its own. - Open cards still go to the document as before. - The reveal now scrolls only the Decisions list. `scrollIntoView` used to scroll a clipped ancestor too, which hid the Document/Decisions tabs. - No server changes.
## Screenshots
Before: a second Save in Chat, no mention of the objection, and "from chat" on the card  
After: one quiet line with the objection and Open card, and the hint inline on the card  
Before: the reopening message claims "Reopened" while the card is still decided 
After: "Suggested reopening", and the retired prompt keeps its Open card link 
After: "Suggested reopening" opens the card's history entry with Reopen (desktop and phone)  
After review: brand-ink Open card and unbroken separators (desktop, phone Chat, phone card)   
Phone (390 px), after 
## Testing
- `bun test apps/web/src/chat apps/web/src/conversation-plan packages/question`: 351 pass, 0 fail. - `bun run types`: pass. - `bun run ci`: fails only on the design-contract hashes below. `markers.tsx` is pre-existing from #353. - E2E selectors updated for the new copy: `conversation-plan-prompts`, `conversation-plan-stress`, `conversation-plan-ui`, `sidecar-option-contracts` and two `e2e/source` contracts. Prompt-Save clicks now save through the card. The reopen test now reaches Reopen through the retired prompt's Open card and checks that the history entry is focused. Local E2E is suspended for this run, so this relies on the PR's CI browser job. - Checked by hand in a real browser against a fake-Jev server on port 8873, at desktop and phone sizes.
## Needs a design-contract review
- `packages/question/src/react/question-view.tsx` in `scripts/design-contract/exceptions/dynamic-packages.json`. The new hash is `67231b520916377facbd67afd7cf13f81317f2d9dd0caf85ca5a213e34093cff` (old: `5db9f37e…`). This is safe: the only change moves the static suggestion hint inside the label span, changes its copy and adds `whitespace-nowrap`. No dynamic class logic changed.
## Overlap with #293
#293 also edits these files: - `apps/web/src/chat/decision-entry.tsx` and `decision-entry.test.tsx` - `apps/web/src/chat/scoped-choice-entry.tsx` and its test - `packages/question/src/react/question-view.tsx` and `question-view.test.ts` - `scripts/design-contract/exceptions/dynamic-packages.json` - `e2e/conversation-plan-prompts.e2e.ts`, `e2e/conversation-plan-ui.e2e.ts` and `e2e/sidecar-option-contracts.e2e.ts` - `e2e/source/question-view-contracts.native.ts`
How to resolve: - **Chat decision prompt** (`decision-entry*` and the prompts e2e): take this PR's version. There is no Chat Save left to hide. - **Card and spike prompt:** keep #293's read-only handling (`AddOption` gating in `question-view.tsx`, `scoped-choice-entry.tsx`) together with this PR's "Suggested in Chat" hint. - **Design-contract hash:** recompute `dynamic-packages.json` for the merged `question-view.tsx`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
ownermentionedlast month on GitHub
- MA
**Review:** Changes needed (one small copy fix; the rest is non-blocking)
Checked in a real browser (fake Jev, desktop 1440x900 and phone 390x844) and in the diff. Shots: `.design-orchestrator/audit-2026-10-07/shots/review-375/`.
**What works** - Live line renders `Ready to settle: <option> · Open card`; objection count appears in warning ink (`Ready to settle · 1 objection · Open card`, count read from the thread's current `oppose` stances). Chat has no Save button. - Decided line: `Decided: <option> · @ana · Open card`. "Suggested in Chat" sits inline on the card (desktop). - Reopening: with a pending `reopening` candidate on a decided card the message reads "Suggested reopening <card>" and the card stays decided, which matches the server (`domain-corrections` refuses to confirm candidates on card-linked threads). - Routing, desktop and phone: Open card on an open card goes to the Document, card focused (article gets focus). On a decided card it switches to Decisions, expands "1 resolved", focuses the history article (Reopen visible). On the phone the Document/Decisions tabs stay visible and the page does not scroll (`scrollY` 0), so the scroller fix in `decisions.tsx` works.
**Findings** 1. Blocking-ish, copy truthfulness: `decision-entry.tsx` (`DecisionPrompt`, "Ready to settle" branch). When nothing is selected, the line still says "Ready to settle" (plus objections), and the card's Save is disabled. Reproduced: a compound message (support + objection + question) superseded the suggestion; Chat showed `Ready to settle · 1 objection · Open card` while the card had no selection and a disabled Save. The old prompt said "Choose an option on the card". The new unit test codifies this wording. Suggest `Needs a choice` / `Choose an option` (plus objections) when `selection.label` is empty. 2. Objection count is the number of current oppose stances (per participant and option), not people, and not tied to the suggested option. One person opposing two options reads "2 objections"; an objection to a different option than the one shown is counted too. Fine for a quiet cue, but consider counting distinct participants, or only stances on the shown option, or label it "N objections raised". 3. Fast path: removing Chat Save costs one extra click (Open card, then Save; focus lands on the card, option is preselected) and a context switch. On phone it also leaves the Chat tab. For the "everyone agreed, Save" fast path I think this is acceptable: it removes the duplicated Save and the prompt now tells you what is contentious. Not blocking. Note the spike prompt (`scoped-choice-entry.tsx`) still has Save in Chat, so the two prompt kinds now differ; worth stating that as intentional (scoped choices are provisional and never decide the card). 4. Rejected candidates still say "Suggested reopening". True historically, but a dismissed suggestion reads as live. Same after the card is actually reopened: the old message keeps saying "Suggested reopening". Consider dropping the label for `rejected`. 5. Phone polish: the separator wraps as a dangling `·` at line end with `Open card` alone on the next line (`Ready to settle: Start with a small pilot. ·` / `Open card`); "Suggested in Chat" wraps as "Suggested in / Chat". The `Open card` link is a text-height target (~20px), small for touch (#299). Consider `whitespace-nowrap` on the link plus its separator, and a taller hit area on touch. 6. Affordance: `Open card` is only distinguished by weight and darker tone; underline is hover-only. Acceptable in a quiet row, but there is no visible non-hover cue that it is interactive.
**CI**: only the design-contract hash job fails (markers.tsx from #353 and question-view.tsx), which is acceptable per the audit rules. container and e2e pass.
**Overlap with #293**: #293 also edits `decision-entry.tsx`, `decision-entry.test.tsx`, `question-view.tsx`, `question-view.test.ts`, `dynamic-packages.json`, `e2e/conversation-plan-prompts.e2e.ts`, `e2e/conversation-plan-ui.e2e.ts`, `e2e/sidecar-option-contracts.e2e.ts`, `e2e/source/question-view-contracts.native.ts` and `scoped-choice-entry.tsx`. The PR body names only decision-entry and the prompts e2e; the `question-view.tsx` hunk and its design-contract hash will conflict too. Take this PR's version in the Chat prompt and keep #293's read-only handling for the card and the spike prompt.
- MA
Reply to review: all six points addressed in ea568b43.
1. **No selection:** the line now reads `Needs a choice · … · Open card`. The unit test is updated, and a new one covers the no-selection case. 2. **Objections:** `objectors()` counts distinct participants who oppose the shown option, plus anyone opposing the whole decision. With no option shown, it counts everyone who opposes the decision. One person opposing two options counts as one objection. Unit tests cover this. 3. **Reopening states:** - A rejected candidate gets no line. - A pending candidate reads "Suggested reopening" only while the card is still decided. - Once a `decision.reopened` follows the candidate's proposal, or the candidate is confirmed, the line reads "Reopened". - `links.test.ts` covers all of these cases. 4. **Phone:** - "· Open card", "· N objections" and "· Suggested in Chat" each sit in a `whitespace-nowrap` group, so no separator dangles and no phrase splits. - Open card gets a 44 px-tall invisible tap target on coarse pointers. It uses the existing `btn-sm`/`btn-icon` `::after` pattern in `theme.css` instead of padding, so the line height doesn't change. Verified on an emulated touch phone: the `::after` measured 44 px over the 20 px link. 5. **Link cue:** Open card now uses `text-brand-ink` (petrol), with an underline on hover. 6. **PR body:** it now lists every file that overlaps #293 and the rule for resolving each. It also documents that the scoped-choice prompt keeps its Save on purpose (scoped choices are provisional).
CI on ea568b43: e2e and container pass. "format, lint, types, tests" fails only on the design-contract hashes. `question-view.tsx` has a new hash (`67231b52…`, in the body); `markers.tsx` comes from #353. New screenshots are in the body.
MaggieAppletonbase-ref-force-pushedlast month on GitHub
MaggieAppletonhead-ref-force-pushed f71096b729f4last month on GitHub