fix(crew-editor): extend discard guard to all dirty panes - #8324
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS One sheet-destroying path — "Chat with this crew" — still keys on Watch
Suggestions
[DESIGN-REVIEWED] bb58d9b |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS The guard works and both confirms read correctly cold; the one snag is the new copy calling the thing "crew" while the surface it guards says "agent". Watch
Suggestions
[UX-REVIEWED] bb58d9b |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of I have everything I need. All the repo-convention checks pass (committed capture scripts: 100+ siblings in First-Principles-Verdict: CONCERNS The fix is real and cause-level, but What this change shipsIntent: stop the crew editor from silently destroying unsaved edits when the user dismisses it — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] bb58d9b |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
6c7b44b to
8315950
Compare
The finding holds: the discard confirm is a body-portal
Applied exactly that — the editor |
|
Status on head The only remaining red check is Screenshot Evidence, which is advisory / non-required here —
|
8315950 to
5b7997c
Compare
Re: GPT 5.6 round 6 — "In-flight write failures silently discard the editor draft"The previous five blocking findings were all real and are all fixed on merit in 1. The outcome is identical on
|
|
/ai-review override gpt ce2da53: the flagged discard-on-in-flight-write behaviour is pre-existing and untouched by this diff — origin/main's requestClose already calls closeSheet() immediately when there is no schedule draft (the finding's own scenario), and main documents the prescribed fix as deliberately disallowed ("Dismissing mid-write is DELIBERATELY still allowed ... suppressing it would break that"), so the remedy would revert a documented design and re-create the hung-request trap DISCARD_FORCE_GRACE_MS exists to prevent. |
|
AI-review override not recorded: keep the reason to 500 characters or fewer. |
|
/ai-review override gpt ce2da53: pre-existing behaviour this PR preserves, not a regression -- main's requestClose (KiroCrewAgentsPage.tsx:1119) already calls closeSheet() immediately with no schedule draft, the finding's own scenario. main documents the remedy as disallowed at :1437, and suppressing dismissal re-creates the hung-request lock DISCARD_FORCE_GRACE_MS (:93) exists to break. |
Human judgment recorded@bolichen97 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
The crew editor's discard confirm was keyed on the inline schedule draft alone, so the eight other fields `dirtyPanes` tracks -- agent template, workspace, memory store, model, reasoning effort, triggers, session color and the avatar draft -- were still thrown away silently by the footer Cancel, by Escape, and by an overlay click. `requestClose` now keys on `dirtyPanes.size > 0` and raises the confirm the schedule draft already had, so the page keeps ONE discard mechanism and one dialog. The schedule draft is still tested first, because only its confirm can lock Discard while the draft's create POST is in flight; when other panes are dirty too, that dialog widens to name them rather than asking about the typed schedule alone. Two dismissal paths stay deliberately unprompted. A committing PUT is already away and no answer can recall it, so offering to discard would promise a rollback the backend will not honor. A rail pane switch destroys nothing but the draft, since every other field lives in page state and survives the switch. A save that is only STAGING a picture upload is the opposite case -- it has committed nothing -- so it does prompt, and saveEdit waits on the pending answer instead of committing through the question. Closes #8284 Co-authored-by: Kiro Crew <noreply@kiro.dev>
ce2da53 to
bb58d9b
Compare
Problem / Motivation
Open a crew in the Crew editor, change its Triggers (or its template, workspace,
memory store, model, reasoning effort, session color, or picture), then press
Escape — or click the footer Cancel, or click outside the sheet. The editor
closes and the edit is gone. Nothing asks first.
The editor already knew the edit was there:
dirtyPanestracks all nine fields,the rail shows an unsaved dot for the pane, and the footer says "Unsaved
changes". #5539 added a discard confirm to exactly these close paths, but keyed
it on the inline schedule draft alone. So the one pane whose work was protected
was the schedule draft, and the other eight were destroyed silently by the same
click.
Why it matters
This is the cheapest kind of data loss to hit and the most annoying: the user
did the work, the UI showed them it was pending, and one Escape threw it away
with no warning and no undo. A confirm already existed a few lines away — it
just did not cover them.
What changed (motivation → approach → change)
requestCloseaskedif (schedDraft). It now asksif (dirtyPanes.size > 0),which is the set the rail dot and the footer note already read. There is still
ONE confirm dialog on this page — the nested one #5539 built — and it now
carries whichever of three questions is true instead of always naming a
schedule.
The schedule draft is still tested first, because its leg is not
interchangeable: only that confirm locks its Discard button while the draft's
create POST is in flight, and unlocks it after a grace period. What it did need
is a wider claim.
discardTakesSheetspots the case where that dialog is aboutto close the whole editor while other panes are dirty, and adds a second line
naming them — asking only about "the schedule you typed" while Discard also
throws away a Model edit is the same silent loss, just behind a dialog.
Two dismissals stay unprompted on purpose. A committing PUT is already gone and
no answer here can recall it, so offering "Discard changes" would promise a
rollback the server will not honor. A rail pane switch destroys nothing but the
draft, since every other field lives in page state and survives the switch. A
save that is only STAGING a picture is the opposite case — it has committed
nothing, so it prompts, and
saveEditwaits for the answer instead of firingthe PUT through the open question. Backing out releases that save rather than
killing it.
Tests
New
website/src/test/KiroCrewAgentsPage.discardGuard.test.tsx, 12 testsagainst the real page:
Each guard term was mutation-checked — reverting it turns the suite red:
requestCloseignoringdirtyPanes,committingwidened tosheetBusy,saveEditnot waiting for the answer,discardTakesSheetforced false, andaskSchedOnlyforced false. All five were caught.Main's existing
KiroCrewAgentsPage.schedDraftGuard.test.tsx(13 tests) isunchanged and still green: the schedule-draft behavior it pins is preserved.
Manual verification
Captured from the real built SPA through the committed harness
website/scripts/capture-crew-discard-guard.mjs(fixture-stubbed/api/**, nogateway), in both themes. Verified: the confirm appears over the still-open
editor, Keep editing leaves the edit intact, and the widened schedule dialog
carries both losses.
Gates run locally:
tsc -bclean,eslintclean,npm run i18n:checkclean,npm run lint:i18nclean, full frontend suite 29482 passed, and the backendcross-surface guard set selected by
scripts/local-gate.py.Two pre-existing failures are NOT from this diff and are left alone:
src/i18n/deadKeys.test.tsreports 30 dead keys against BASELINE 29. Thisreproduces on a pristine
origin/maincheckout with a clean tree. The 30thkey is
pages.chatPage.dismiss_upload_error, orphaned by fix(components): route error states through ErrorNotice (batch components-1) #8859 and alreadytracked in main is red: deadKeys.test.ts at 30 vs BASELINE 29 (pages.chatPage.dismiss_upload_error orphaned by #8859) #8959. Ratcheting the baseline from here would collide with that
issue's own fix, so this PR does not touch it.
tests (
test_service.py,test_host_isolation_floor.py,test_source_providers.py, and friends). This diff leavessrc/,test/anddocs/byte-identical toorigin/main, and the failure set is identicalacross two runs, so none of them can be attributable to it.
Screenshots / video
The confirm now raised for a Triggers edit, over the still-open editor:
The schedule draft's own confirm, widened to name the crew edits it also
destroys:
Both themes, all four states
Dirty routing pane, before the dismissal:
The confirm, light:
After Keep editing — sheet still open, edit intact, unsaved dot still on
Triggers:
The widened schedule confirm, light:
Related Issues
Closes #8284
Pattern harvest
Rule candidate: review-prompt
Pattern: a guard whose condition names ONE member of a set the same component
already computes for display.
dirtyPanesdrove the rail's unsaved dot and thefooter's "Unsaved changes" note, while the confirm that protects that same work
keyed on
schedDraft— one element of it. The checkable shape is a dirty/pendingset that a render path consumes in full (
.size, a spread, an iteration) while adestructive path tests a single flag that also feeds that set: the two readings
disagree, and the narrower one is the one that loses data.
Also worth flagging as a shape rather than a rule: the earlier attempt at this
fix added a SECOND confirm mechanism (a
useConfirmbody-portal Modal) besidethe existing nested Radix one, which is what forced a
modal={!confirmOpen}focus-scope release to stop the editor trapping focus behind it. Two independent
confirm mechanisms reachable from one page's close paths is greppable —
useConfirmplus a nested<Dialog>in the same component — and thefocus-scope workaround is the tell that the second one is fighting the first.
Checklist