fix(chat): remove only the complete generated suffix on a ChatPane chip un-toggle - #7616
fix(chat): remove only the complete generated suffix on a ChatPane chip un-toggle#7616leonlaiyc wants to merge 1 commit into
Conversation
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
…ip un-toggle ChatPane's follow-up chip toggle spliced the LAST occurrence of ", <option>" out of the composer. Once the user has deleted the appended tail by hand, that search reaches back into their own draft: "Discuss, Alpha home" with a still-lit "Alpha" chip becomes "Discuss home". ChatPage was rewritten for this (kirodotdev#6092 review) to remove only the complete generated suffix, still intact at the end, and leave the draft alone otherwise. Port that block so the two panes agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b46adba to
3e08d45
Compare
|
Rebased onto current main ( Clean rebase, no conflicts. The un-toggle block on main was byte-identical to this PR's pre-image (only shifted), so both hunks replayed unchanged and the PR's behaviour is untouched. Gates run locally on the rebased tree:
Please review the rebased head. Note that a maintainer push makes the maintainer the last pusher, so under this repo's last-push rule a second approver is needed. Reply here if anything looks wrong. |
UX Review (Fable 5, fork) — ✅ PASSUX-level review of UX-Verdict: PASS Invisible-surface bug fix: the un-toggle now stops silently deleting user drafts and matches ChatPage's exact behavior — no new control, string, or state to evidence. [UX-REVIEWED] 3e08d45 |
Design Review (Fable 5, fork) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix — removal keyed to provenance (the intact generated suffix) instead of substring search — ported faithfully from the ChatPage sibling, with a red-before test. Suggestions
[DESIGN-REVIEWED] 3e08d45 |
First Principles Review (Fable 5, fork) — ✅ PASSPremise-level review of All verification done. The base First-Principles-Verdict: PASS Verify What this change shipsIntent: stop a chip un-toggle in a grid pane from silently deleting text the user typed — a FIX. Inventory (3 items)
The defect is derived, not asserted: it reproduces by reading base [FIRST-PRINCIPLES-REVIEWED] 3e08d45 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed |
GPT 5.6 Review (fork) — 🔴 changes requested (blocking)Reviewed 1 of 1 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands. BLOCKING -- website/src/components/ChatPane.tsx:880 -- Exact-suffix drafts still lose user text Adjudication (Opus 4.8) — is blocking on each finding proportionate?I've read the prompt, the findings, the diff, and the base code. There are 0 adjudicable findings and 1 fenced finding (F1). F1 — ChatPane.tsx:880, exact-suffix draft loss (FENCED) Tracing the new code against the base tree:
The defect is real: draft Harm rung: LOW. The lost text is unsent composer draft in local state, immediately visible in the composer and trivially re-typed — not persisted data, not corruption despite the Recovery: no handler auto-repairs it, but it is visible on the spot and self-corrects on the user's next keystroke. Rarity: requires an exact string coincidence (the user's trailing draft equals Evidence record complete; harm genuinely bounded UI draft loss → FLAG. [ADJUDICATION] 3e08d45 total=0 uphold=0 downgrade=0 [ADJUDICATION-FENCED] 3e08d45 fenced=1 flagged=1 🏷️ Fenced finding(s) machine-flagged as likely edge caseThe security fence keeps these findings blocking regardless of adjudication; the only clearance path is a human override recorded by a repository writer, who must independently verify a rationale before recording it — it is machine-authored, and a wrong override on a security-class finding ships exactly the class the fence exists to stop. (This lane's comment deliberately carries no override command.)
|
Problem / Motivation
In a grid pane, un-toggling a still-lit follow-up chip can delete text the user
typed themselves.
ChatPane's toggle removed the last occurrence of", " + optionanywherein the composer. That is correct only while the text the handler appended is
still present. Once the user has deleted that appended tail by hand — a normal
thing to do, and the chip stays lit because nothing tells it otherwise — the
search falls back onto the next-most-recent match, which is the user's own
draft:
Discuss, Alpha homeAlphachipDiscuss, Alpha home, Alpha, Alphaby handDiscuss, Alpha homeAlphaagain to un-toggleDiscuss home← their, Alphais goneChatPagehad exactly this defect and was rewritten for it; the review of #6092counted
ChatPaneas the unfixed sibling, still on thelastIndexOfsplice.Why it matters
It is silent, unprompted destruction of unsent user input, in the one widget
whose entire contract is "if the user edited the text so it no longer matches,
leave the text alone" — a promise both files' own comments make. Nothing warns,
nothing undoes it, and the deleted span is exactly the kind of short phrase that
is easy not to notice before pressing Enter.
It is also a divergence between two panes that are meant to behave identically:
the same click on the same draft produces different text in the main chat and in
a grid pane.
What changed (motivation → approach → change)
Symptom: un-toggling a chip removes a matching substring of the user's draft.
Root cause: the removal was defined by pattern (
lastIndexOf(', ' + o)),not by provenance. A substring search cannot distinguish text this handler
generated from identical text the user typed, so once the generated copy is gone
it happily takes theirs.
Change: adopt
ChatPage's removal, which is defined by provenance instead.The picked options are appended as one ordered suffix, so the only text the
handler may take back is that whole structure, still intact at the end:
prev === pickedSuffix→ the composer is nothing but the suffix; replace itwith the remaining options.
prev.endsWith(', ' + pickedSuffix)→ strip exactly that tail and re-appendwhat is left picked.
prevuntouched (the chip stillun-highlights, as before).
website/src/components/ChatPane.tsxonly — a 14-line block swapped for the12-line one
ChatPage.tsx:8331-8344already runs. No new helper, no sharedextraction: the two blocks are now textually parallel, which is what makes the
next drift visible in review.
Behavioural delta beyond the bug: a draft the user has appended to after the
generated suffix (
Alpha, Beta and hurry) now keeps its text on un-toggleinstead of being spliced. That is the stated contract, and it is what
ChatPagealready does.Tests
website/src/test/ChatPane.followUpOptions.test.tsx— one added case,leaves earlier draft text alone when the user already deleted the appended option, mirroring theChatPagetest of the same name. It drives the tableabove through the real component and asserts the composer still reads
Discuss, Alpha home.Red-before on this branch's parent (
3a5824d20), with the fix reverted:After the fix, and including the sibling suite that pins the behaviour being
ported from:
The pre-existing
unselecting an option splices its own appended text, never a matching substring of the draftcase (draftPlease, Alphabet) and themulti-option accumulate/remove cases stay green — they are the regression
control for the case this change does not alter.
npx tsc --noEmitclean;npx eslintclean on both touched files.Manual verification
N/A — unit coverage sufficient: the added test drives the real
ChatPanethrough React Testing Library and asserts on the live composer value, so it
exercises the same code path a click does.
Related Issues
Residual named by the review of #6092 (
ChatPane.tsxcounted as the unfixedsibling of the
ChatPage.tsxrewrite). No separate issue.Pattern harvest
Rule candidate: review-prompt
Pattern: "text removal identified by substring match rather than by provenance"
— when a handler appends generated text into a field a user also edits, the
undo must be defined by the exact structure it generated (anchored at a known
end, matched whole), never by searching for a pattern that the user's own input
can also satisfy.
indexOf→lastIndexOflooks like the fix and is only anarrower guess; the generated span has to be identified, not searched for.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement