Skip to content

fix(webui): restore message edits when Escape cancels - #1545

Open
lihongguang-0014 wants to merge 24 commits into
mainfrom
fix/edit-escape-restore
Open

fix(webui): restore message edits when Escape cancels#1545
lihongguang-0014 wants to merge 24 commits into
mainfrom
fix/edit-escape-restore

Conversation

@lihongguang-0014

@lihongguang-0014 lihongguang-0014 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Scope

Scope boundary: Restore the current WebUI transcript and pre-edit composer draft when Escape cancels an uncommitted user-message edit. Bind the one-shot in-memory restore stack to the active session, exact transcript array/items, fork anchor, and composer-send generation. Preserve newer drafts, attachments, prompt annotations, intent, branch ownership, and live-render ownership while an older idempotent receipt is replayed.

Root cause: entering message edit mode immediately truncates the local transcript and overwrites the composer, while the prior Escape path only cleared the composer. Async preflight, history replacement, nested branch actions, busy delivery, definitely rejected retries, and unknown-acceptance receipt replay could then outlive or overwrite the edit owner.

The correction also refuses regenerate while an edit restore frame is active and refuses a new edit over a foreign pending fork, because either transition would orphan the existing branch without a coherent Escape target. Older receipt recovery uses its immutable payload and exact client-message identity; its task bookkeeping remains authoritative, while its live and terminal echo cluster stays off the newer Edit's visible stream.

Non-goals: No visible Cancel control, no backend/wire or IndexedDB schema changes, no persistent undo or general edit history, and no change to ordinary regenerate/send behavior outside an active conflicting branch.

Compatibility: Existing Escape clearing for ordinary drafts remains unchanged when no edit is active. The slash menu still consumes Escape first. Normal sends still use the clicked composer snapshot while preserving text typed later. Fork-owned edits are not flattened into ordinary follow-up queue entries. The optional WebUI handoff-WAL metadata is backward compatible with existing records; new records use the existing owner/revision fields for monotonic compare-and-swap updates. No public API, RPC, backend schema, dependency, or generated artifact changes.

Platform impact: Platform-neutral Vue/TypeScript state handling. The issue was reported on Windows 11; the change introduces no OS-specific APIs or assumptions.

Branch

Base branch: main

Target exception: N/A

Issue

Linked issue: Fixes #1372

Release Note

Release note: Pressing Escape during a message edit now restores the conversation and the draft that the edit replaced. No separate changelog file was added.

Tests

Ruff: N/A (WebUI-only change)

Pytest: N/A (WebUI-only change)

Build: cd opensquilla-webui && npm run build passed on main@f4e65ef26cb2bb190dd9e6910027f571effa4922 (architecture, RPC architecture, chat security, theme, i18n, vue-tsc --noEmit, Vite production build, runtime/theme bundle guards, and 395-file artifact verification).

Regression tests: added

Notes:

  • Latest follow-up validation at bb80dfb8d: 7 related WebUI test files — 593 tests passed; the focused send/event-handler suites passed all 385 tests; npm run typecheck and git diff --check origin/main...HEAD passed. Pre-ACK queued/running lifecycle now survives same-session acceptance and blocks parent drain until the exact task terminates; richer terminal session snapshots outrank later sparse echoes. Every offscreen response registers its task quarantine, accepted background WAL rows retain optional task identity/status across a second crash, and recovered accepted owners retry queue release/CAS cleanup from refreshed WAL state without requiring another reconnect.
  • Focused latest-head validation: 7 related WebUI test files — 588 tests passed. npm run typecheck also passed all architecture, RPC architecture, chat security, theme, motion, radius, cross-surface, i18n, and locale gates.
  • Complete WebUI unit suite: npm run test:unit -- --maxWorkers=4 — 425 files, 5,273 tests passed.
  • An initial unconstrained-worker full run had three unrelated 5-second view-test timeouts; the 96 affected view tests passed together in isolation before the bounded-worker full suite passed completely.
  • git diff --check origin/main...HEAD passed.
  • Coverage includes Edit→Escape restoration, empty edited composer, repeated and nested cancellation, session switches, exact transcript replacement, async send preflight, fork/busy transitions, definitely rejected retries, transport-unknown→exact replay→rejection, same-client row replacement, rejection after authoritative history replacement, regenerate/edit fork collisions, active-stream receipt recovery, stale recovered-attempt detachment, and old ordinary receipt replay while a newer Edit or ordinary composer owns the composer, including Edit beginning during asynchronous project preflight, revision/document-context and changed-back ownership during preflight or the original RPC, a second handoff-write race, forward-pagination apply fencing, and Edit→new-draft hold release.
  • Receipt-replay coverage includes running acceptance, complete terminal acceptance, definite rejection, still-unknown acceptance, immutable attachments/routing/context, exact client-message correlation, an interleaved unrelated same-session task, and the real done → sessions.changed → task.succeeded → turn.committed echo order. The newer Edit's transcript, input, attachment, annotation, intent, and Escape restore remain unchanged throughout; terminal reconciliation is deferred until Edit settles, already scheduled or in-flight history reads cannot replace an Edit-owned transcript, stale subscription epochs cannot claim receipt ownership, and unrelated successor-task run state remains visible.
  • Final receipt coverage also proves offscreen fork children never claim parent task ownership or navigate the UI, resolved and accepted-error child handoffs retire their WAL, invalidated exact replays retain the original unknown-acceptance WAL, and terminal echo clusters restore run state and drain queued work exactly once.
  • Background-only completion coverage injects WAL CAS (including final deletion), put, and list failures; proves the same recovery worker survives them; replays a retained submitting disposition without child adoption; releases an accepted owner queue back to the parent; wakes a drain after late hydration; and normalizes killed terminal echoes to cancelled without draining. Background replay dispatch is CAS-gated, failed background records retire without composer restoration, and stale non-fork terminals cannot project across a session switch. Automatic recovery workers persist background disposition before dispatch and quarantine the full replay RPC. Crash-restored background RPCs hold that same client-message quarantine across attachment refresh retries, and successful ACKs bind their task identity before the quarantine ends so later task-only events remain isolated. Definite background rejections transition the WAL to failed, release the parent queue owner, and delete the handoff; CAS or queue-release failures keep both live and crash-recovery workers alive to retry cleanup without reconnecting or resending the rejected request. Ordinary Regenerate snapshots distinguish inactive generation zero from an active Edit, preserving child adoption after exact replay. Superseded Edit receipts now let the same click send the newer Edit after either acceptance or durably completed definite-rejection retirement.

The default test path remains offline, deterministic, credential-free, and safe for forks.

Maintainer Live Check

Maintainer live check: yes

Surface: browser

Suggested check: complete a user/assistant turn, place an unrelated draft in the composer, edit the user message, then press Escape. Confirm the full transcript and original draft return and no fork remains pending. Also confirm Escape still clears a normal draft when no edit is active.

Live/manual limitation: No interactive gateway/browser session or Windows machine was available in this run; verification was unit, type, architecture, and production-build based.

Safety

This remains client-side UI and local handoff-WAL state. The optional background-only marker and existing owner/revision compare-and-swap fields prevent stale recovery from adopting or resurrecting an offscreen child. No trust boundary, authorization, secret handling, backend, or network contract changes. No private data, local paths, generated artifacts, or runtime transcripts are included.

Commit Lineage

  • Source contribution: fix(webui): let Escape undo a message edit instead of stranding the transcript #1422
  • Source commit: cfca176fe75ab3ed1e921b566dec618901285f3f
  • Replayed with cherry-pick -x as 26f525ae16d15ba711f8576a6e94ca209fe7ffa7, preserving the original author and message.
  • Stable patch IDs match: 3853a643d864941818072af330200a01cb0d7a00.
  • Latest-main session/transcript ownership refinements: 47f0dd550bbe60ebf500850d666d79f4096de603.
  • First independent-review ownership correction: 927319c25978d2e930af85e5de955bef0881b53a.
  • Second independent-review async send-generation correction: b45391d3b0510f86c972ab0ee1e451bf8195d33d.
  • Third independent-review transcript-replacement and busy-queue correction: e15a974d94a99e591646e23d66a4628f854da2c2.
  • Fourth review/retry ownership correction, including the later ownerless-receipt replay finding: 6d63e61715f9fd7138a395bebcc703ffc9932708.
  • Exact client-message receipt/task stream isolation and terminal-echo correction: a99e82c62edf09ff0b4176d9ca979c519a11bd4a.
  • Exact acceptance lease, edit commit, atomic successor projection, early quarantine, and deferred reconciliation correction: b17e2ea67245ce0248b1a88bab32a81cd28b6d3b.
  • Full same-generation composer ownership, stale-epoch rejection, idempotent receipt registration, and history apply-time fencing: e886fc8926c5f242140c6635b4947a603e073831.
  • Pre-RPC composer ownership epochs, cross-session Edit-hold release, and per-page history apply fencing: 2d29d722adf9ad55749c708dd976b834182111ee.
  • Offscreen child ownership/WAL finalization, replay-cancellation WAL retention, and background terminal drain/teardown: beb6ded922e0db299705b81d408d2bb8bb74d86e.
  • Recovered-receipt terminal ownership, durable background-only recovery, monotonic handoff-WAL updates, and parent queue release: c9ee32da67aa6693fa91446fb9cfc7c53751aef3.
  • Durable background-only completion, parent queue recovery, late-hydration drain, and killed-status normalization: 956225ab6e83de11f823cec128f52f3554d7a7ef.
  • Final background-only WAL deletion durability and retry: 2d562531c9985f4373a0b4a7b4dc18e509a076bb.
  • Pre-dispatch background disposition, failed-record retirement, and original-session terminal fencing: e643a327fd28b5f9d8ef24c7a38701caa99c90dc.
  • Automatic worker pre-dispatch background persistence and full-RPC quarantine: 951adc4de665a9bf47e16ce5f88a19fa8acc7f62.
  • Active-Edit snapshot qualification and ordinary Regenerate receipt adoption: 84def130c1000b3d06873af31a7aef395ab943f0.
  • Superseded Edit receipt background settlement and same-click newer Edit delivery: 4ebe861a3a07e41033e06d7dd6507e914f59a89b.
  • Crash-recovery quarantine and durable definite-rejection retirement: 90280848fe095e07696110c581340038a5c58576.
  • ACK task-only isolation, local crash-retirement retry, and delayed same-click Edit continuation: 3f91f888e92c13c59183aef5e8ae98d6a70234ef.
  • Child-terminal receipt projection and durable-rejection re-click fencing: 2fc6c49e7234625f1e6757b3ed767964163b14ea.
  • Child-terminal cleanup/projection separation and single-flight rejection continuation: 8019aa9cd95a9bdfedba44adb5c2735cf6ddd6a8.
  • Pre-ACK lifecycle deferral and ACK-gated receipt projection: 1aa1b04260d1780ca28041909924d6fcad3c840f.
  • Same-session lifecycle adoption, rich terminal precedence, universal offscreen task registration, and accepted-WAL recovery retry: bb80dfb8d3cb9c9a4d23e12be03dd54344bc986e.

Third-Party Origin

Third-party origin: none

Details if non-none: N/A. Contributor lineage within this repository is documented above.

Documentation Changes

  • No documentation links, code fences, tables, secrets, private paths, or private transcripts were added.

@lihongguang-0014
lihongguang-0014 force-pushed the fix/edit-escape-restore branch 2 times, most recently from aeedd39 to d2c9a41 Compare September 2, 2026 09:23
RickyYii and others added 7 commits September 2, 2026 17:39
…ranscript

Clicking edit on a user message truncates the in-memory transcript at that
message before the user has confirmed anything: `editMessage` slices
`messages` and drops the edited text into the composer. On a two-message
conversation that leaves the empty state — "暂无消息。" — on screen. Escape
then cleared the composer and did nothing else, so the only exit from edit
mode destroyed the draft and left the empty transcript in place. Reloading
brings the history back, because the server still has it, but nothing on
screen says so: what the user sees is a conversation that vanished (#1372).

`editMessage` now records what it is about to overwrite — the transcript
array and the composer's previous contents — and `cancelEdit()` puts both
back. Escape offers that before it clears the draft, and offers it even when
the composer is already empty, which the old guard refused: emptying the box
by hand used to remove the last way out of the truncated state.

The restore point is honoured only while `pendingForkBeforeMessageId` still
holds the id the edit set. Sending consumes that id and a second edit
replaces it; in both cases the truncation has been made real by something the
user did mean, and putting the old array back would resurrect messages the
fork has already replaced. Cancelling is also one-shot, so a later Escape
cannot reach a stale transcript.

`regenerateMessage` truncates the same way but sends on the next tick, so it
is never left sitting in the truncated state and needs no restore point.

Closes #1372.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgCn6RgA7fr4Fi1zTWcJt7
(cherry picked from commit cfca176)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Escape after starting message edit clears the draft and leaves an empty transcript

2 participants