fix(webui): restore message edits when Escape cancels - #1545
Open
lihongguang-0014 wants to merge 24 commits into
Open
fix(webui): restore message edits when Escape cancels#1545lihongguang-0014 wants to merge 24 commits into
lihongguang-0014 wants to merge 24 commits into
Conversation
lihongguang-0014
force-pushed
the
fix/edit-escape-restore
branch
2 times, most recently
from
September 2, 2026 09:23
aeedd39 to
d2c9a41
Compare
…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)
lihongguang-0014
force-pushed
the
fix/edit-escape-restore
branch
from
September 2, 2026 09:57
d2c9a41 to
a99e82c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 buildpassed onmain@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:
bb80dfb8d: 7 related WebUI test files — 593 tests passed; the focused send/event-handler suites passed all 385 tests;npm run typecheckandgit diff --check origin/main...HEADpassed. 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.npm run typecheckalso passed all architecture, RPC architecture, chat security, theme, motion, radius, cross-surface, i18n, and locale gates.npm run test:unit -- --maxWorkers=4— 425 files, 5,273 tests passed.git diff --check origin/main...HEADpassed.done → sessions.changed → task.succeeded → turn.committedecho 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.submittingdisposition without child adoption; releases anacceptedowner queue back to the parent; wakes a drain after late hydration; and normalizeskilledterminal 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
cfca176fe75ab3ed1e921b566dec618901285f3fcherry-pick -xas26f525ae16d15ba711f8576a6e94ca209fe7ffa7, preserving the original author and message.3853a643d864941818072af330200a01cb0d7a00.47f0dd550bbe60ebf500850d666d79f4096de603.927319c25978d2e930af85e5de955bef0881b53a.b45391d3b0510f86c972ab0ee1e451bf8195d33d.e15a974d94a99e591646e23d66a4628f854da2c2.6d63e61715f9fd7138a395bebcc703ffc9932708.a99e82c62edf09ff0b4176d9ca979c519a11bd4a.b17e2ea67245ce0248b1a88bab32a81cd28b6d3b.e886fc8926c5f242140c6635b4947a603e073831.2d29d722adf9ad55749c708dd976b834182111ee.beb6ded922e0db299705b81d408d2bb8bb74d86e.c9ee32da67aa6693fa91446fb9cfc7c53751aef3.956225ab6e83de11f823cec128f52f3554d7a7ef.2d562531c9985f4373a0b4a7b4dc18e509a076bb.e643a327fd28b5f9d8ef24c7a38701caa99c90dc.951adc4de665a9bf47e16ce5f88a19fa8acc7f62.84def130c1000b3d06873af31a7aef395ab943f0.4ebe861a3a07e41033e06d7dd6507e914f59a89b.90280848fe095e07696110c581340038a5c58576.3f91f888e92c13c59183aef5e8ae98d6a70234ef.2fc6c49e7234625f1e6757b3ed767964163b14ea.8019aa9cd95a9bdfedba44adb5c2735cf6ddd6a8.1aa1b04260d1780ca28041909924d6fcad3c840f.bb80dfb8d3cb9c9a4d23e12be03dd54344bc986e.Third-Party Origin
Third-party origin: none
Details if non-none: N/A. Contributor lineage within this repository is documented above.
Documentation Changes