Skip to content

fix(desktop,ui): stage a selected message's quotes on edit - #5274

Open
ggbdpq wants to merge 5 commits into
apache:mainfrom
ggbdpq:fix/desktop-revision-structured-context
Open

ggbdpq wants to merge 5 commits into
apache:mainfrom
ggbdpq:fix/desktop-revision-structured-context

Conversation

@ggbdpq

@ggbdpq ggbdpq commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Edit & resend refused a selected message that itself carried quotes or attachments: the replacement submit only carried the human-facing text, silently dropping the context the original answer was grounded in (#5109, reproduction cases A and B — case C, the TUI rewind side, landed in #5265).

Scope after review (09-28): quotes are restaged; attachments fail closed. The review established that a revision copy excludes the revised turn, so nothing within this PR's reach can produce target-owned attachment refs for the revised message — restaging the source-owned refs re-creates the cross-session rejection, and the late plate swap could only delete them. Producing target-owned refs on the Host side remains the open attachment half of #5109 and should land there.

What this PR does now:

  • the edit stages the selected message's quotes into the quote plate, recorded on the revision draft; after the commit they are re-keyed onto the branch child, so the replacement submit carries them — visible, explicitly removable, with the no-op send still refused and cancel restoring the pre-edit plate;
  • a message carrying attachments refuses to edit with its own truthful copy: the invalid cross-session ref submission and the cancellation residue are prevented by construction, and the plate handlers of the withdrawn attachment path are removed;
  • the chat-turn edit gate drops quotes from its disabled list; directory references keep the gate, and their two dead copy keys are removed together with the branches that rendered them.

Verification

Check Result
Current head CI (test job) green; static merge-tree against latest main clean (reviewer-confirmed)
Focused revision suites on the review head 56/56 (reviewer-confirmed before the final dead-code removal)
New ui tests (quote-carrying messages keep the edit action) red on the old gate, green after
Desktop revision-actions tests (source quotes stage and are recorded on the draft) green; the replaced upstream test asserted the exact negation and was green pre-change
check-locale-hygiene / biome check on changed files / check:asf-headers clean

Earlier-round rows (ui full-suite counts, stash-rebuild baselines) are kept in the review thread; the attachment-half rows are withdrawn with the feature.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: GLM-5.3-Flash (ZCode) implemented the desktop/ui changes and tests under human direction and review.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — a selected message carrying quotes is now editable and the replacement submit restages them; messages carrying attachments keep refusing to edit, now with a truthful reason and no leftover plate handlers.

@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 13, 2026
@ggbdpq

ggbdpq commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Marking draft while the renderer debt ratchet question is settled — see the gate comment below.

The renderer architecture strict-base check freezes app-shell-revision-actions.ts at 2155 non-trivia tokens; the feature needs ~160 more than the slimmest extraction I could land (88dc60e, 2318). Two honest paths forward:

  1. I extract the beginEditUserMessage orchestration into @maka/ui/revision-staged-context as well (fits the budget, but moves shell orchestration into the package), or
  2. a one-time sanctioned budget bump for app-shell-revision-actions.ts in renderer-architecture.json.

Happy to do either — flagging before burning another CI round.

@ggbdpq
ggbdpq marked this pull request as draft September 13, 2026 22:34
@github-actions github-actions Bot added effort/L Under 1000 readable lines and removed effort/M Under 500 readable lines labels Sep 14, 2026
@ggbdpq ggbdpq closed this Sep 15, 2026
@ggbdpq ggbdpq reopened this Sep 15, 2026
@github-actions

Copy link
Copy Markdown

No description provided.

@github-actions github-actions Bot added effort/XL Under 2500 readable lines and removed effort/L Under 1000 readable lines labels Sep 15, 2026
@ggbdpq

ggbdpq commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

The ratchet conflict is resolved without touching the frozen budgets: the whole revision lifecycle (beginEdit / prepare / cancel / rollback + copy-attempt bookkeeping) moved into @maka/ui/revision-staged-context, and app-shell-revision-actions.ts is now a thin assembler injecting the bridge, the locale catalog, and the attempt tracker.

Measured against the frozen base budgets: app-shell-revision-actions.ts 2305 -> 506 tokens / 6 specifiers (base 2155/8); app-shell.tsx 13482 -> 13344 tokens / 104 specifiers (base 13386/104). The duplicate pre-send gate in the shell goes away with the move — prepareRevisionSend owns the unchanged and mixed-context refusals — and one stale guard referencing removed draft fields is gone with it.

Heads 0a5af05 / 0a5af05 have not started a CI run for a few hours (the pull_request workflow never fired on the pushes or the reopen — likely the external-contributor workflow approval); pull_request_target checks ran. Once a run is approved: local verification passed — desktop revision-actions 3/3, ui chat-turn 26/26, biome clean, ASF clean, and the refreshed ledger confirms both files under budget.

@ggbdpq
ggbdpq marked this pull request as ready for review September 15, 2026 13:02
@ggbdpq

ggbdpq commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

CI is green on 0a5af05 (2/2, including the architecture ratchet) and the review-required gate is the only open item — marking ready for review. Path taken for the ratchet: option 1 (the lifecycle moved into @maka/ui/revision-staged-context; both frozen files now sit under their base budgets, ledger refreshed in-tree).

@ggbdpq

ggbdpq commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Adversarial root-cause after five red rounds — with the base ledger entry diffed metric-by-metric, the blocker is now precisely characterized, and it is a design decision rather than a patching problem:

dependencyPaths for app-shell-revision-actions.ts (base e06cf84ef): {"./locales/conversation-copy.js":1, "./locales/shell-copy.js":1, "./platform/desktop/session-message-settlement.js":1, "./session-copy-attempt.js":1, "./session-workspace-errors.js":1, "@maka/core/session":1}. The ratchet forbids new keys and count increases. The #5109 restage feature irreducibly requires ONE new runtime edge — importing the lifecycle factory from @maka/ui — which is exactly such a new key. The relocation to @maka/ui (this branch) removed every other violation (tokens 506 <= 2155, specifiers 6 <= 8), but the assembler's own @maka/ui edge cannot be eliminated: app-shell.tsx sits at @maka/ui count 1 of 1 with zero headroom, and every renderer file is frozen with a fixed dependency key set.

So the remaining decision is binary and maintainer-owned:

  1. a one-time sanctioned ledger change for app-shell-revision-actions.ts (dependencyPaths @maka/ui: 1 + nonTriviaTokens ~508), or
  2. a sanctioned new extraction module inside the renderer (which today the ledger forbids).

Everything else in this branch is verified: revision-actions tests 3/3, ui chat-turn 30/30, biome and ASF clean, ledger refreshed in-tree. The branch stays as the working proposal; happy to re-shape once the direction is picked.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 — sanction the edge, but land the sanction in the checker's target rules rather than the ledger numbers.

I verified the mechanism before answering: --strict-base re-derives base debt from the merge-base commit (loadBaseConfig), so editing the committed renderer-architecture.json entry cannot clear the violation — the current file genuinely gains a dependency key the base lacks. And resolveDependency returns undefined for bare package specifiers, so @maka/ui can never satisfy isSanctionedDependencyTarget today. A ledger-number bump alone will stay red either way.

Option 2 is strictly worse on the mechanism's own terms: a new renderer module is itself forbidden (new unclassified renderer source files are forbidden outside approved legacy directories; new legacyAppShell debt entries are forbidden), so it needs a sanction too — same cost, plus a file that exists only to carry one import edge.

Suggested shape for option 1: treat @maka/ui — the package renderer ownership is migrating into — as a sanctioned dependency target for legacyAppShell importers, the same way validated copy catalogs already get bare-package imports for free. The ratchet exists to stop the shell absorbing new ownership; depending on the destination package is the opposite of debt, and the token/specifier budgets still bound every file. If you want it narrower, a per-importer exception in the config works too — but the broad version covers every future migration PR without a fresh exception each round.

中文版

选 1,但豁免要落在 checker 的 target 规则上,不是账本数字:--strict-base 会从 merge-base 重新推导 base 债务,改 renderer-architecture.json 的条目消不掉这条红;裸包名 resolveDependency 返回 undefined,@maka/ui 永远过不了 isSanctionedDependencyTarget。方案 2 更差:新 renderer 模块本身就违反「新文件禁止」「新账本条目禁止」,同样要豁免还多一层纯搬 import 的间接文件。建议把 @maka/ui(所有权正在迁入的包)列为 legacyAppShell importer 的 sanctioned target,与 copy catalog 免计裸包同道理;想窄就按 importer 白名单,但宽版能覆盖后续所有迁移 PR。

AI assistance: I used Devin to trace the ratchet's base-derivation and sanctioned-target paths; the assessment is mine.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — #5274 restage a selected message's quotes and attachments on edit

Thanks for this — moving the lifecycle into @maka/ui/revision-staged-context and the fail-closed narrowing in chat-turn.tsx (only directoryReferences keep the gate) both look right, and the refactor of app-shell-revision-actions.ts into a thin assembler is a genuine improvement. The two new @maka/ui tests do pin the un-gating, and renderer-architecture.json shrinks (2155 → 508 tokens for app-shell-revision-actions.ts), so the ratchet is happy.

I could not convince myself that the restage survives the revision commit, though. Details below, most important first.

1. restageRevisionAttachments can never find the rewritten refs — a revision copy excludes the revised turn

packages/ui/src/revision-staged-context.ts:155-163 looks up the copied user message in the branch child transcript and treats a miss as "nothing to restage":

const copiedMessage = copiedMessages.find(m => m.type === 'user' && m.turnId === sourceTurnId);
const rewritten = [...(copiedMessage?.attachments ?? [])];
… remove every staged attachment …
if (rewritten.length > 0) staged.restoreAttachments(targetSessionId, rewritten);

But the Host copies a revision with the exclusive boundary — the revised turn is deliberately not in the copy (that's the "rewound to before that message" semantics):

  • packages/runtime-host/src/server/session-revision-coordinator.ts:375-378 — createConversationCopySlice(source.messages, input.sourceTurnId, kind === 'revision' ? 'before' : 'through')
  • packages/runtime/src/conversation-copy.ts:258-260 — for 'before', retainedTurnIds = turnOrder.slice(0, sourceIndex), i.e. the source turn is dropped (see the assertion at packages/runtime/src/__tests__/conversation-copy.test.ts:714).

I confirmed the slice behaviour against the built packages/runtime/dist/conversation-copy.js: slicing ['turn-1','turn-2'] with 'before' at turn-2 yields ['turn-1'] only, and editing the first turn of a session yields an empty transcript. So rewritten is always []: the swap deletes the plate and restores nothing, i.e. the attachments are dropped exactly as before — just a step later, and now with the user having been told they were restaged.

Two concrete consequences:

  • The in-flight submit keeps the source-owned refs and main rejects them. sendWithAttachments captures the payload before send() (apps/desktop/src/renderer/app-shell.tsx:1759), so the swap — which runs inside prepareRevisionSend — cannot change this send. The submit therefore carries session_file refs owned by the source session into the branch child, and retainedAttachmentsForSession throws "Retained attachment belongs to another Session" (apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts:910-925), surfacing as a generic "Action failed" toast.
  • A retried send loses them silently. Post-commit, prepareRevisionSend reads the live plate (now empty). revisionSendGate only compares lengths (0 > 1 is false) and the text differs, so the gate returns 'pass', draft.draftSessionId !== draft.sourceSessionId short-circuits to true, and the replacement goes out with no attachments and no quotes.

So I think the "the Host's copier already rewrote them … (no protocol change)" premise needs revisiting: something has to produce target-owned refs for the revised message — a copier change (retain the source turn's attachments into the target), re-ingesting into the branch child, or letting main rewrite/accept the source refs on sessions:send.

2. Nothing carries the restaged context across the commit's draft-key change

Both plates are keyed by the active session (attachmentDraftKey = activeId ?? NEW_TASK_PENDING_KEY, apps/desktop/src/renderer/app-shell.tsx:371-372), and the restage writes them under the source session key (restoreQuotes(sessionId, …) / restoreAttachments(sessionId, …) in beginEditUserMessage). After openSessionInChat(newSession.id) the active key is the branch child, so selectPending returns [] for both plates (packages/ui/src/pending-items.ts:41-43) and they go blank right after the "Ready to edit and resend" toast — quotes have no swap path at all.

That also makes the PR's headline claim ("the plates make the carried context visible and explicitly removable") untrue past the commit: the user sees the context until they press send, then it vanishes. A manual pass of reproduction case B in #5109 should show this immediately. Restaging under the branch-child key (or making the staged context follow the draft across the commit) is what I'd expect here.

Related: inside the swap, stagedContext() and removeAttachment come from the closure captured when the send started (useStableActions publishes through a layout-effect ref), while restoreAttachments takes an explicit ownerKey and removeAttachment/removeQuote bind to the live draftKey. "Clear by index, then restage under ownerKey" therefore mixes two different owners — worth making the owner explicit on the mutators if this design stays.

3. Test coverage for the commit / send half is missing

packages/ui/src/revision-staged-context.ts has no test file, and the pure helpers (revisionSendGate, restageRevisionAttachments, clearRevisionStagedContext, revisionStagedContextUnchanged) are the easiest things in the PR to unit-test. On the desktop side the suite still only drives beginEditUserMessage — prepareRevisionSend and cancelRevisionDraft have no coverage at all, so the swap, the moved no-op refusal, and the cancel-time plate cleanup are all untested. A test feeding the swap a realistic branch-child transcript (source turn absent, earlier turns' refs rewritten) would have caught #1.

Also unverified by tests: the 'conflict' gate (staged quote / pending directory during an edit) and cancel restoring the pre-edit plates.

Nits

  • revision-staged-context.ts:376-379 refuses an edit when the composer has staged quotes, but toasts copy.revisionDraftAttachmentConflict ("The composer already has pending attachments…"). Since hasPendingAttachments is bound to hasPendingContext (attachments or directories) in the desktop env, this is the only quote-specific refusal and it needs its own copy key in all three locales. Same string reuse for the gate at :493-498: revisionAttachmentsUnsupported now reads "Editing cannot mix newly staged attachments with the restored ones…", which is wrong when the added context was a quote or a directory reference.
  • revisionStagedContextHasAdditions (:114) is exported but never used; revisionSendGate (:183-187) inlines the same three conditions. Pick one so the two can't drift.
  • clearRevisionStagedContext's previousQuotes parameter is always [] at its only call site (:624), so the restore branch is unreachable — drop it or use it.
  • attachmentToPending (:68-77) duplicates retainedToPending (packages/ui/src/use-composer-attachments.ts:152-160, not exported). It only feeds attachmentKey, which ignores stagingKey, so the synthetic revision:${JSON.stringify(...)} key is dead weight.
  • The new "./revision-staged-context" subpath in packages/ui/package.json is unused — the desktop imports the barrel (@maka/ui). Value imports from the barrel are already common in the renderer, so this is cosmetic; either use the subpath or drop the entry.

Verified as fine

  • The chat-turn.tsx gate now fails closed only on directoryReferences, and the reason chain (directory → transformed → running) is coherent.
  • No other consumer of the removed editMessageDisabledAttachments / editMessageDisabledQuotes keys exists in the repo (only chat-turn.tsx and conversation-copy.ts referenced them).
  • Edit → resubmit ordering and dedup: source quotes/attachments are carried in original order, and beginEditUserMessage refuses while anything is staged, so no duplication on resubmit; cancel clears both plates and restores previousComposerText.
  • The no-op-send refusal moved cleanly out of app-shell.tsx into prepareRevisionSend, with the duplicated pre-checks removed and a comment left behind at apps/desktop/src/renderer/app-shell.tsx:1606-1612.
  • revisionStagedContextUnchanged compares against the source-owned refs captured on the draft, so a post-commit retry isn't misreported as "unchanged".

@ggbdpq

ggbdpq commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Both commits pushed: the checker sanction (d53b28f, your 09-15 direction) and the review fixes (ceccd38).

#1 (restage can never find the rewritten refs) — accepted, and resolved by owning the limitation rather than patching the symptom. I re-verified the exclusive 'before' boundary against conversation-copy.ts before touching anything: the premise behind restageRevisionAttachments ("the Host's copier already rewrote them") is simply false, and the function could only delete the plate and restore nothing. Since a revision copy excludes the revised turn, something outside this PR must produce target-owned refs for the revised message — I don't think that should happen inside a fix(desktop,ui) PR, so attachments now fail closed at beginEdit: a message carrying attachments refuses to edit with its own truthful copy ("messages with attachments cannot be edited yet: attachments cannot follow into the new version"), and restageRevisionAttachments is deleted, not patched. This is the state your first review already blessed ("attachments can continue to fail closed until their target-owned refs can be resolved"). Of your three protocol directions, re-ingesting into the branch child (or a copier change that retains the source turn's attachment refs into the target) looks like the right shape for a follow-up — I'd rather propose it there with a Host-side test than grow this PR into runtime-host.

#2 (nothing carries the staged context across the commit) — fixed for quotes, moot for attachments. After every rollback check has passed, prepareRevisionSend re-keys the restored quotes onto the branch child's draft key — restoring from the draft snapshot, not the copied transcript, since the transcript provably cannot contain the revised turn. The source-key plate empties at the same moment, and cancelRevisionDraft now clears both draft keys explicitly. The staged-context mutators take their owner explicitly (your related note): the context type is now { quotes, attachments, restoreQuotes(ownerKey), clearQuotes(ownerKey) } — the read-only attachment view stays for the conflict gates.

#3 (missing coverage) — added where the code lives. New packages/ui/src/__tests__/revision-staged-context.test.ts drives createRevisionActions through a fake env: the re-key test feeds prepareRevisionSend exactly the realistic branch-child transcript you described (source turn absent, an earlier turn present) and asserts the re-key reads the draft snapshot; the unchanged/conflict gates, the attachment refusal, and the both-keys cancel are pinned alongside. Matrix tests cover revisionSendGate, stageRevisionSourceContext, clearRevisionStagedContext, and revisionStagedContextUnchanged. On the desktop side the harness gains the fail-closed refusal (no draft committed, composer untouched); prepareRevisionSend/cancelRevisionDraft themselves are lifecycle-level and now covered in @maka/ui, which is where this PR moved them.

Nits — quote-conflict refusal and the send-gate conflict each got their own key in all three locales (revisionDraftQuoteConflict, revisionMixedContextUnsupported), and revisionAttachmentsUnsupported now says what it actually means; revisionStagedContextHasAdditions removed (the gate's inline form won); clearRevisionStagedContext's unreachable previousQuotes parameter removed; the unused ./revision-staged-context subpath export dropped. One nit skipped deliberately: attachmentToPending vs retainedToPending stays as-is for now because the attachment comparison it feeds is dead-in-practice under fail-closed and the whole dimension should land or die together with the Host follow-up.

Checker (d53b28f): @maka/ui is a sanctioned dependency target for legacyAppShell/legacyAppShellClosure importers, with fixture tests in the git-fixture suite (a shell file migrating onto @maka/ui passes strict-base; a root-debt entry gaining the same edge stays priced — both verified red against the unfixed checker). Ledger regenerated; every touched budget went down (app-shell.tsx 13288 → 13242, revision-actions 2155 → 508, quotes hook 358 vs 360).

Verification: @maka/ui 12/12 new tests + desktop 15/15 across the revision/catalog/first-send suites; biome clean on all seven touched files; check:renderer-architecture plain passes with the regenerated ledger. CI is the oracle for --strict-base, which segfaults locally.

@ggbdpq

ggbdpq commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Merged latest main (d329239) — no functional changes, conflict resolution only. Head is now 070e7f4ec.

Conflicts (2):

  • packages/ui/src/conversation-copy.ts: main added the turnStatus* / gitBranch* / switchWarningDismiss / processExpandAll copy inside the same interface block and giant locale lines; resolved by keeping main's additions and re-applying this PR's removal of editMessageDisabledAttachments / editMessageDisabledQuotes across the interface and all three locales.
  • apps/desktop/renderer-architecture.json: kept this PR's post-extraction reductions together with main's independent repricings (e.g. app-shell-project-actions 2284 → 2157); app-shell.tsx was repriced to the merged tree's exact actuals (13089 → 13081 non-trivia tokens — main's follow-ups shifted the file since the last regeneration; still below main's 13127 baseline), importSpecifiers stays 88.

Verification: the ledger check passes (check:renderer-architecture plain). The checker fixture suite is 112/113 — the single failure (attests the canonical main source in the final Vite entry graph) is a pre-existing Windows-only test bug: it reproduces byte-identically on a pristine upstream/main worktree. The fixture mocks facadeModuleId as /fixture/src/renderer/index.html, but path.resolve('/fixture/src/renderer', 'index.html') drive-prefixes the canonical path on Windows, so the equality never holds off-Linux; CI (Linux) is unaffected — happy to file a follow-up issue. The @maka/ui suite is 547/547 (three initial failures were stale 09-08 dist orphans of tests since deleted by #5366 — cleared and re-run clean); biome is clean on all 13 files this PR touches; @maka/desktop typecheck is green.

Everything actionable from your 09-17 review remains in place (restage removal, quote re-keying onto the branch-child key, the new @maka/ui coverage, and the checker sanction in your 09-15 direction).

@ggbdpq

ggbdpq commented Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Merged latest main (205a06e) — conflict resolution only, head is now b867dacd6. (This also folds in the earlier 070e7f4ec line that had landed on the branch in the meantime.)

Conflicts (3):

  • packages/ui/src/conversation-copy.ts: main added the turnStatus* copy and removed processExpandAll / processRestore (post-d3292393c); resolved by taking main's current lines and re-applying this PR's removal of editMessageDisabledAttachments / editMessageDisabledQuotes across the interface and all three locales.
  • apps/desktop/src/renderer/locales/conversation-copy.ts: main removed regenerateStartedTitle / regenerateStartedDescription; resolved by keeping that removal together with this PR's revision* additions.
  • apps/desktop/renderer-architecture.json: regenerated against the merged tree (check:renderer-architecture plain passes).

Verification: ledger check passes plain; biome clean on the touched locale files; the checker fixture suite is 112/113 with the single failure being the known Windows-only attests the canonical main source case (byte-identical on a pristine upstream checkout, detailed in my earlier comment).

@ggbdpq

ggbdpq commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Merged latest main (8bde344) — head is now 204e884b4.

Conflicts (4), resolved on top of the transcript-publish simplification (#5566/#5494 territory):

  • app-shell-revision-actions.ts + its test: this PR's version of the file is the thin assembler (the lifecycle lives in @maka/ui), so main's simplification was ported into the extracted lifecycle rather than unioned textually — readSettledMessages/setMessages and the preparation abort machinery are gone from the env surface, refreshSessions() now runs right after openSessionInChat, and the failure toast fires before rollback (rollback navigates away, so checking after it is always stale). The assembler and app-shell.tsx shed the two dropped deps.
  • conversation-copy.ts: main's facts-only footer simplification of the turnStatus* family, with this PR's editMessageDisabledAttachments/Quotes removal re-applied.
  • renderer-architecture.json: repriced to the merged tree's exact actuals — app-shell.tsx 12957 → 12682, app-shell-revision-actions.ts 537 → 482; every touched budget stays at or below the main baseline.

Main's own tests for the new flow ("prepares the revision without opening another transcript consumer", "surfaces a failed preparation instead of swallowing it behind rollback") came through the merge and pin the ported semantics; the refused-retry world fixture gained the stagedContext stub the extracted lifecycle reads.

Verification: full workspace build green after reinstalling dependencies (the merge carries the @astryxdesign/core 0.6.2 patch); check:renderer-architecture plain passes on the repriced ledger; arch fixture suite 112/113 (the single failure is the pre-existing Windows-only attests the canonical main source bug, reproduced on pristine upstream/main — Linux CI unaffected); @maka/ui 629/629 after clearing stale dist orphans; desktop app-shell-revision-actions 7/7, app-shell-first-send-cleanup 20/20, model-catalog-choices 4/4; desktop typecheck (preload/main/renderer) green; biome clean on all six touched sources; check:asf-headers pass.

No review responses outstanding on my side — both of @me2seeks's earlier requests were addressed in 87e605295 (#5466) and cb6d10882/c07c577a7 (#5265); this PR had no new findings, only the merge.

@ggbdpq

ggbdpq commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

The red check was my process failure, and the fix is pushed at head ae023e13c.

Root cause: when I resolved the previous merge I committed the conflict resolution first and made the lifecycle-port edits afterwards — then pushed only the ledger/test follow-up commit. The four files carrying the actual port (the @maka/ui lifecycle, the assembler's narrowed deps, the app-shell.tsx call site, the ui test) stayed uncommitted in my working tree, so CI checked a tree that still had readSettledMessages/setMessages/the abort machinery — 537 tokens in app-shell-revision-actions.ts including the dropped import, exactly what the strict-base cross-check flagged as debt growth against its own repriced ledger.

What went out now:

  • a06e0430d — the port itself: RevisionActionsEnv loses readSettledMessages/setMessages and the preparation AbortController; refreshSessions() runs right after openSessionInChat; a preparation failure toasts before rollback (rollback navigates away, so checking after is stale); the assembler and app-shell.tsx shed the dropped deps.
  • merge of latest main (6cb8c58) — one file conflicted (the ledger), resolved and repriced to the merged tree's actuals (chrome-actions 408 → 122 after main's own extraction, app-shell.tsx → 12680); every touched budget stays at or below baseline.

Verified after the rebuild: full workspace build green (0 type errors); check:renderer-architecture plain passes; @maka/ui 630/630; desktop app-shell-revision-actions 7/7, app-shell-first-send-cleanup 20/20; desktop typecheck (preload/main/renderer) green; biome and check:asf-headers clean.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 5746d5c329c0e8fabcc2c0f872b1609a6500ec7e (15 files, +1316/−425). The change stages a selected message's quotes in the composer, refuses edits to messages carrying session-owned attachments, gates no-op/mixed-context replacements, and moves the revision lifecycle into @maka/ui (packages/ui/src/revision-staged-context.ts:93-151,292-365,442-527). I traced the Desktop send path and quote-bucket ownership, plus the new lifecycle and Desktop action tests. One P1 finding is attached inline: the first replacement send loses its quotes and leaves them staged for a later send.

The current-head test check passes, but a fresh synthetic merge with current main conflicts in apps/desktop/renderer-architecture.json; the PR diff also has a trailing blank-line warning in apps/desktop/src/renderer/locales/conversation-copy.ts. Resolve the conflict and revalidate a new head before merge. I did not run local tests or Electron E2E (Node 18/no installed dependencies), and have not exercised real Host failure/reconnect or A→B→A navigation. No database schema/migration change appears in this PR. This is not a merge approval.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

// — a revision copy excludes the revised turn, so the copied transcript
// cannot be their source. Re-keyed only after every rollback check has
// passed, so a failed preparation leaves the plate on the source key.
staged.restoreQuotes(newSession.id, startedDraft.originalQuotes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve the branch-owned quotes for the same in-flight send. sendWithAttachments is still executing in the source session's render closure while awaiting prepareRevisionSend() (app-shell.tsx:1499-1508). That closure's pendingQuotes is the source-key array from useComposerQuotes. Here you append the quotes to the new child key, then clearQuotes(sourceSessionId) mutates the source array to empty. When the same call resumes, app-shell.tsx:1658-1664 reads that now-empty array and omits quotes from send; the child bucket remains populated because the success path also closes over the source-key clearQuotes. Thus editing a quote-bearing message and sending a changed text silently drops its quote on the first replacement, then may carry it into an unrelated later send. The new tests assert that re-keying occurred but do not send through this production closure. Capture the intended quote payload before clearing the source bucket or read/clear the child bucket by explicit owner, and add a cross-layer first-send regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at 314dc7d3a on the suggested lines: sendWithAttachments now snapshots the staged payload into revisionQuotes before awaiting prepareRevisionSend, prefers that snapshot when sending, and on success clears the branch child's bucket by explicit owner (clearQuotes(expectedRevisionDraft?.draftSessionId)) instead of the stale closure's default key — the child bucket no longer survives the send, and the first replacement carries the quote.

On the cross-layer first-send regression: the production closure lives in the Desktop assembler (a React render closure), and the repo's Desktop tests are main-process only — there is no renderer harness that can drive sendWithAttachments with an async interleaving. The data invariant the fix relies on is pinned at the ui layer instead (keeps the pre-gate quote snapshot equal to the child bucket the send reads): whatever the gate does while the send awaits, the re-keyed child bucket equals the pre-gate snapshot. A renderer harness for the closure itself would be its own piece of infrastructure.

@ggbdpq
ggbdpq force-pushed the fix/desktop-revision-structured-context branch from ded432a to 314dc7d Compare September 26, 2026 13:28

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 30025d52331c84907bc95ca462a6d3ef93f0b56d (14 files, +1352/−427). The new increment closes the previous first-send P1: app-shell.tsx snapshots the revision quotes before prepareRevisionSend() re-keys the source bucket, then clears the branch-child bucket explicitly after a successful send. I traced the edit, prepare, send, retry, cancel, quote-selection, and session-snapshot paths. One P2 finding remains inline: the unchanged gate compares only part of QuoteRef, so a legitimate provenance-only quote edit is rejected as “Nothing changed.” I do not recommend merging until that canonical-content comparison is fixed and covered.

Validation on the exact head: clean npm ci; build:test; full typecheck; UI 677/677; Desktop 2753/2753; focused revision/send tests; renderer architecture 114/114; lint and format. Hosted test is green. A conflict-free synthetic merge tree 8dba8b32433273a2e70ed6c319181859f400f3b4 against current main 86c61d420601bc03f1a9c8bd144bb7cbe861b5bc passed build:test, 53 focused tests, and renderer architecture 114/114; the P2 reproduces there as well. git diff --check still reports the existing extra blank line at EOF in apps/desktop/src/renderer/locales/conversation-copy.ts:1068.

I did not run packaged Electron/native Windows or macOS flows, real Runtime Host reconnect, or a manual A→B→A navigation pass. No schema or migration change is present.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.



function quoteKey(quote: QuoteRef): string {
return JSON.stringify([quote.text, quote.label ?? null, quote.sourceTurnId ?? null]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Compare the complete quote contract before declaring a revision unchanged

quoteKey drops sourceSessionId, sourceSessionName, sourceCapturedAt, and sourceTruncated, although those fields are part of the persisted, user-visible session-snapshot quote. A reachable context-only edit is therefore refused: start editing a message with a session quote, remove that token, then select the same source Session again after a fresh snapshot. With unchanged snapshot text/label but a new capture time, the production helper reports canonicalChanged: true, revisionStagedContextUnchanged: true, and revisionSendGate: "unchanged"; two distinct Sessions with the same name/body collide too. The composer keeps quote removal and quote/session-reference selection enabled during revision (app-shell.tsx:2384-2386,2526-2531), and #5109 explicitly requires changing/replacing structured context to count as an edit. Please compare a normalized complete QuoteRef (including snapshot provenance) and add a regression for a recaptured session quote.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head bec3eb3a21d9dcfe867bccc973bdca39793245dc (14 files, +1352/-427). This update merges current main; the previous first-send quote snapshot and branch-owner cleanup remain intact. I independently traced the edit, prepare, send, retry, cancel, and Session-reference paths on this head. One P2 finding remains inline: the no-op gate still compares only the visible subset of a QuoteRef, so a valid provenance-only replacement is rejected as unchanged. I do not recommend merging until the comparison and regression coverage are corrected.

Current-head validation: clean npm ci; build:test; full typecheck; UI 684/684; Desktop 2776/2776; focused revision/send 40/40; renderer architecture 114/114; lint, format, and ASF headers. The hosted test check is green. Current main (538c37cb655ffeafc1829fc77359a6b7cfaa077a) is already the second parent, so the PR is 33 ahead / 0 behind and the merge tree is conflict-free. git diff --check still reports the existing extra blank line at EOF in apps/desktop/src/renderer/locales/conversation-copy.ts:1068.

I did not run packaged Electron or native Windows/macOS flows, a real Runtime Host reconnect, or manual A->B->A navigation. No schema or migration change is present.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.



function quoteKey(quote: QuoteRef): string {
return JSON.stringify([quote.text, quote.label ?? null, quote.sourceTurnId ?? null]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Compare the complete QuoteRef here. The no-op gate currently ignores sourceSessionId, sourceSessionName, sourceCapturedAt, and sourceTruncated. On this exact head, replacing the restored Session quote with either a freshly captured snapshot (same text/label, newer capture metadata) or an identically named same-content snapshot from another Session makes revisionStagedContextUnchanged() return true and revisionSendGate() return unchanged, so the UI refuses the legitimate context-only edit as “Nothing changed.” Include the canonical provenance fields in the key (or compare the complete normalized ref) and add a regression that removes and reselects a Session snapshot without changing the prompt text.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at 6e12c8f73: the no-op gate's quote key now covers every QuoteRef field — text, label, source turn, source session id and name, capture timestamp, and the truncation flag — so a re-captured snapshot at the same prompt text passes the gate as a real edit, while the stale capture of the same snapshot is still refused as unchanged.

The regression covers both directions: the re-captured snapshot (same text/turn, sourceCapturedAt 100 → 200) passes, and re-staging the stale capture against the newer source remains unchanged.

On the cross-layer note: the same caveat as the review thread above applies — the assembler closure has no renderer harness in this repo, so the gate behavior is pinned at the ui layer where both the source staging and the comparison live.

ggbdpq added a commit to ggbdpq/maka that referenced this pull request Sep 27, 2026
The no-op gate's quote key covered text, label, and source turn but
ignored the cross-Session provenance fields (sourceSessionId,
sourceSessionName, sourceCapturedAt, sourceTruncated). Removing a
restored Session snapshot and reselecting a fresher capture of the same
excerpt — same text and turn, newer capture metadata — therefore hit
the unchanged gate and the UI refused a legitimate context-only edit.
The key now covers every QuoteRef field.

Carries the apache#5274 review finding; regression covers the re-captured
snapshot at unchanged text and the stale-capture no-op.

Generated-by: GLM-5.3-Flash (ZCode)

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 6e12c8f738f1d19d67e7c6764da3d7969279d72e (14 files, +1412/-427). The new two-file increment closes the previous P2: packages/ui/src/revision-staged-context.ts:66-79 now includes every QuoteRef field in the no-op comparison, and packages/ui/src/__tests__/revision-staged-context.test.ts:291-336 covers a same-text snapshot recaptured with newer provenance while retaining the stale-snapshot no-op. I traced the edit, prepare, send, retry, cancel, quote-selection, and Session-reference paths again and found no remaining P0-P3 issue on this head.

The regression is behaviorally demonstrated: a fully populated production-shaped QuoteRef probe passes replacements that change each of the seven fields individually on this head, while the same sourceCapturedAt-only replacement is rejected as unchanged on parent head bec3eb3a. Exact-head validation passed clean npm ci, build:test, full typecheck, UI 685/685, Desktop 2781/2781, focused revision/send tests 21/21, renderer architecture 114/114, lint, format, ASF headers, and locale hygiene.

This head is not merge-ready yet. Current main is ab021efda1bbb0e359937106ea1e556cac3b438b; the PR is 34 commits ahead and 15 behind, and both GitHub and a local merge-tree report conflicts in apps/desktop/renderer-architecture.json, apps/desktop/src/renderer/app-shell-revision-actions.ts, and apps/desktop/src/renderer/app-shell.tsx. The full PR git diff --check also still reports an extra blank line at EOF in apps/desktop/src/renderer/locales/conversation-copy.ts:1068, and the current head has no hosted checks. Please resolve those conflicts and rerun the gates on the resulting head before merge.

I did not run packaged Electron or native Windows/macOS flows, a real Runtime Host reconnect, or manual A->B->A navigation. No schema or migration change is present.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@ggbdpq
ggbdpq force-pushed the fix/desktop-revision-structured-context branch from 6e12c8f to 9e0325c Compare September 27, 2026 09:47
@ggbdpq

ggbdpq commented Sep 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the quotes-annotation tree (#5470) as a single commit (9e0325ce4) — the series crossed the #5470/#5742 rewrites of the same files, so replaying it commit-by-commit produced only orphaned intermediate states.

What carried over and how it meets the new tree:

  • The no-op gate still compares the full QuoteRef provenance (round-two P2 fix at 6e12c8f73), which now keys over the annotated quote fields too.
  • The send's quote snapshot still precedes the revision await gate; outside the revision window it falls back to the new lazy quotesForSend() (feat(ui): annotate a quoted excerpt where the gesture happens #5470), which reads the live bucket the re-key drains mid-revision.
  • editMessageDisabledAttachments/Quotes copy keys are dropped in favor of staging, now in the moved application/contracts locale module.
  • The renderer architecture ledger is regenerated from the merged sources; check-renderer-architecture passes.

Locally green: revision-actions 7/7, revision-staged-context 14/14, chat-turn answer identity 33/33. CI should re-run on this head.

@ggbdpq
ggbdpq force-pushed the fix/desktop-revision-structured-context branch from b92133b to 5d37e5a Compare September 27, 2026 23:28

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The architecture-gate failure from the previous head is fixed: the current-head CI test job passes, and the added AppShell attachment handlers were removed. The earlier cross-session attachment submission and cancellation residue are avoided by refusing an edit of any attachment-bearing message. That safety behavior is sound, but the attachment portion of the PR’s stated edit-and-resend objective remains unimplemented and the PR description still claims successful attachment restaging. Quote-only restaging remains in place. Current head: 15 files changed, no schema/migration changes; the static merge-tree against the latest main is clean. Diff-check still reports an extra blank line at the end of conversation-copy.ts. I did not run packaged Desktop or an end-to-end Host attachment revision on this head; the current-head CI is green, and the prior-head focused revision tests passed 56/56 before this small dead-code removal. I would not treat #5109’s attachment case as resolved by this PR.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

});
return;
}
if ((userMessage.attachments?.length ?? 0) > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Attachment-bearing messages still exit here without creating a revision draft. This correctly prevents the prior invalid cross-session ref send, but it leaves #5109’s attachment edit-and-resend case unsupported while the PR title and description continue to claim attachment restaging and successful replacement. Please either implement a Host-supported ownership transfer with a success-path regression test, or narrow the PR scope and keep that case open.

@ggbdpq

ggbdpq commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Both review leftovers are closed on head 74308319d:

  • Trailing blank line: apps/desktop/src/renderer/application/contracts/conversation-copy.ts now ends in a single newline, like every sibling contract. The file sits inside the formatter-excluded desktop surface (the source-contract tests read these sources byte-sensitively), which is why CI never flagged it — the fix is a one-line whitespace commit and the desktop contract tests are unaffected.
  • Description claims: the body's "Scope after review (09-28)" section already states the withdrawal plainly — quotes are restaged, attachments fail closed with a truthful reason, and producing target-owned refs on the Host side remains Edit-and-resend revisions reject or drop structured message context across Desktop and TUI #5109's open half. Nothing in the body claims attachment restaging any more.

If the fresh test job on this head turns red, it will be the main-preexisting acp-goal-plan-child-process.test.js timeout filed as #5777 (red starts at 0fd754083, parent green), not this PR.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 7430831. Its only code change from 5d37e5a removes the trailing blank line in the conversation-copy contract; it does not alter revision behavior. I found no new substantiated P0–P3 issue. The prior cross-session attachment failure is still prevented by the explicit refusal in revision-staged-context.ts:333–339. The PR description now accurately limits this PR to quote restaging and states that attachment editing remains open, so my previous scope-objection does not carry forward. The title still says “quotes and attachments on edit” and would be clearer if narrowed to match the description.

The current-head test check passes and diff-check is clean. This branch now conflicts with current main 8b104db in apps/desktop/renderer-architecture.json (verified by merge-tree and GitHub CONFLICTING), so it is not merge-ready. I did not rerun the full suite locally because the only new change is a deleted blank line; earlier focused revision tests were 56/56 on the preceding head, not this one. I did not run a packaged Desktop or Host attachment revision.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@ggbdpq ggbdpq changed the title fix(desktop,ui): restage a selected message's quotes and attachments on edit fix(desktop,ui): restage a selected message's quotes on edit Sep 28, 2026
@ggbdpq

ggbdpq commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Merged latest main (through 49dacdf, which carries the #5779 inventory fix) — head is now cdc5cbb59, the renderer-architecture.json conflict resolved by repricing the merged tree with the checker: every touched metric lands at or below the main-side numbers (app-shell 12127 → 12115 non-trivia tokens, 91 → 79 import specifiers), npm run check:architecture green, and the focused revision suites pass (revision-actions 7/7, ui revision-staged-context + answer-identity 49/49). CI test is green on this head. Also narrowed the title per the review — the body already scopes this to quote restaging.

@hqhq1025 hqhq1025 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction (same head): My earlier statement that I found no new issue in the merge increment must not be read as a clean assessment of the entire PR. A subsequent independent current-head review, 5335228517, identified four P2 behavior issues in the quote-edit flow and related UI. I missed them, and this PR is not a merge candidate. I have checked the original-quote restoration path in code; the other findings are attributed to that review, not presented as my independent discoveries. The narrower merge-increment and test observations below remain historical evidence, not a clearance.Reviewed current head cdc5cbb598fc9183f603351c34136bf7c017f54e. Since my review of 74308319, this head merges main and resolves the conflict in apps/desktop/renderer-architecture.json; no new PR-specific behavior is added. The title now correctly limits the feature to restaging a selected message's quotes. Attachment-bearing revisions still fail closed at packages/ui/src/revision-staged-context.ts:333-339, so the prior scope/description finding remains resolved. I found no new substantiated P0–P3 issue in the merge increment.

I inspected the merge resolution and the current revision submission/gate paths. The renderer architecture checker passes (114 tests), Node 24 clean install and build:test pass, and focused UI/Desktop revision suites pass (24 tests). The current-head hosted test check is green. git diff --check and a local merge-tree against fresh main de4fc5ff95b1f8034ca00b448984b31ca711dce1 are clean. No schema or migration changes are introduced by this PR. GitHub still reports the PR as BLOCKED, so this review does not assert merge readiness.

Not run here: packaged Desktop, native Windows/macOS UI, or a real Host attachment transfer. This is a COMMENTED review, not approval.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A second, independent review of head cdc5cbb5 (Claude lineage), alongside the hqhq1025 review 5335163453. The PR body and trailers name GLM-5.3-Flash; no Grok.

What holds up:

  • Each session keeps its own quote plate.
  • Quotes move to the branch only after every rollback check passes.
  • Cancel clears both the source and branch sessions.
  • The attachment refusal happens before any draft is created, so no edited text is lost.

P2: quotes the user removed during the edit come back. At revision-staged-context.ts:538, restoreQuotes(newSession.id, startedDraft.originalQuotes) re-keys the original message's quotes onto the branch, not what is currently on the plate. On the send side (app-shell.tsx:1431, :1596, :1611), revisionQuotes is only snapshotted when pendingQuotes is non-empty, and the branch plate is only cleared when the send carried quotes.

Consider a user who removes every restored quote and sends. The originals are re-keyed onto the branch, and the send falls back to quotesForSend() on the live bucket. Either the removed quotes are sent, or they stay on the branch plate and ride along on the next message. We traced this through the code but did not reproduce it in the app. Likewise, if the send fails after the branch is created, a retry silently reverts the user's quote removals and annotation edits.

Please re-key the plate's current quotes rather than originalQuotes, and clear the branch plate unconditionally after a revision send.

P2: the chat-turn edit gate now drops attachments as well as quotes (chat-turn.tsx:602-619). This contradicts the stated "attachments fail closed" scope.

  • In the main chat, an attachment message's edit button used to be disabled with an explanation. Now it is clickable, then refused with a toast.
  • The side-chat quote panel's edit handler (quote-companion-panel.tsx:408) copies only the text, so quoted or attached messages there become editable and silently lose that context. That is #5109 moved to a new place.
  • The new test at chat-turn-answer-identity.test.tsx:463 locks this behaviour in.

P2: the merge revives copy that #5468 removed. Commit 5d37e5aa5 re-adds the Regenerate footer strings in conversation-copy.ts, which #5468 (87ff2799a) deleted. Nothing uses them, and the zh-TW string has a typo.

P2: adding a quote during an edit is now refused (revision-staged-context.ts:159-165). Main allowed it, and the PR body doesn't mention the change. The count-based check is also inconsistent: a quoted message can swap one quote for another, but a plain message cannot gain one.

P3:

  • Toast title: the refusal toast uses "Ready to edit and resend" as its title.
  • Stale code: several comments and docstrings are stale or copy-pasted, including an orphaned one at use-composer-attachments.ts:549, and the attachment comparison is dead code.
  • Architecture boundary: the renderer architecture checker is loosened for the legacy app-shell's @maka/ui imports, and a desktop-specific revision flow moves into @maka/ui behind export *. Neither is mentioned in the PR, so the architecture owner should sign off.
  • Test gap: the tests use fake plates that ignore which session a clear targets, and nothing covers the app-shell send snapshot/clear path.

Tests: the build succeeds; ui 49/49, desktop 19/19 and the architecture checker 114/114 pass, as does the locale hygiene check. Not run: the app end to end.


Automated review (Claude lineage) by the Qronos review line on behalf of @Astro-Han; the first P2 was traced in the code, but please verify before acting.

// — a revision copy excludes the revised turn, so the copied transcript
// cannot be their source. Re-keyed only after every rollback check has
// passed, so a failed preparation leaves the plate on the source key.
staged.restoreQuotes(newSession.id, startedDraft.originalQuotes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: this re-keys originalQuotes, not the plate's current quotes. Quotes the user removed or re-annotated during the edit reappear on the branch. Because the send path only snapshots a non-empty pendingQuotes and only clears the branch plate when it sent quotes (app-shell.tsx:1431/1611), removed quotes can be sent or carried into the next message.

@ggbdpq

ggbdpq commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

All four P2s addressed on head 18cb3b4a1, with one correction to the record:

  • P2 (removed quotes come back): the re-key now carries what the plate held when the send was pressed, captured in prepareRevisionSend while the staged-context handle is still bound to the source key — after openSessionInChat the same handle reads the branch child's plate. Removals stay removed, additions made while editing ride along, and a failed send's retry re-sends the plate as the user left it. The app-shell also empties the branch bucket on every revision send, quote-less ones included, so nothing waits there for the next unrelated message. One implementation subtlety the tests pin down: capturing at gate time is mandatory, because the handle re-binds mid-prepareRevisionSend.
  • P2 (the edit gate dropped attachments): the button is disabled with its reason again (editMessageDisabledAttachments, restored in all three locales), the quote-companion panel refuses to copy text out of messages carrying quotes or attachments, and the lifecycle's toast gate stays as depth. The test that locked the clickable behavior now asserts the disabled semantics.
  • P2 (adding a quote refused): agreed — main never restricted it and the count-based check was inconsistent (swap allowed, gain refused). The gate now conflicts only on staged attachments and pending directories; quote additions pass, with a lifecycle test carrying a fresh quote through prepare.
  • P3 (refusal toast titled "ready"): fixed — refusals use the unavailable title.

Correction on the P2 about the "revived Regenerate footer strings": the record doesn't support it. #5468 (87ff2799a) never touched conversation-copy.ts (its UI deletions are the regenerate button, the bridge entry, and the chat-view footer wiring), and 5d37e5aa5's only change to that file removes two keys (editMessageDisabledAttachments/editMessageDisabledQuotes) — no footer strings were re-added. The zh-TW line in question (包含資料夾引用的歷史訊息暫不支援編輯並重發) is this PR's own new directory-references copy and is typographically correct. The one string containing "Regenerate" (outputTruncatedTitle) predates both commits and is in active use. If a specific typo'd zh-TW footer string is still visible somewhere, point me at the exact file and I'll fix it the same day.

The remaining P3s (architecture sign-off for the @maka/ui boundary, deeper send-snapshot test coverage) stand as recorded — the boundary move was the ratchet-unlock route agreed during the earlier architecture-gate review, and I'll leave sign-off to the architecture owner.

Verified: ui suite 690/690 (both new/changed behavior tests fail on the parent and pass here), desktop revision-actions 7/7, renderer architecture check green (app-shell back under its ledger at 12115 tokens), biome on all touched files, locale hygiene, ASF headers. Not run: packaged Desktop end to end.

@me2seeks me2seeks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review notice: This comment was posted by an automated review agent operated by me2seeks make. It is not an independent human review and does not replace one.

Summary

Restages a selected message's quotes and attachments into the composer on edit: the new revision-staged-context.ts snapshots the composer's staged context with an explicit ownerKey so the lifecycle can re-key it across the revision commit (source → branch child draft key), restoring the source message's own refs at edit time and rewriting them to the branch child's target-owned refs at commit. The module's comment documents exactly this lifecycle, and use-composer-quotes + app-shell-revision-actions thread it through the existing draft-key seam rather than inventing parallel state. The renderer-architecture checker and its test are updated for the new module; packages/ui/src/index.ts exports it with real consumers. CI test green.

Findings

  1. [P3] RevisionStagedContext.restoreAttachments takes AttachmentRef[] while the composer stages PendingAttachment[]; the mapping between the two (which fields are re-derived vs preserved) is the one place a stale preview/hash could ride the re-key. The chat-turn identity tests cover the happy path; confirm a test pins the attachment field mapping, not just quotes.

Verdict

merge-ready — owner-keyed staging lifecycle at the existing draft seam with the #5109 review feedback visibly incorporated; one P3 attachment-mapping verification.

@hqhq1025 hqhq1025 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to my review of exact head 18cb3b4a1409c6e3562ca1314293c5d8a4bf265e: this PR is not a merge candidate. I previously said the Regenerate wording was unchanged from main because I checked packages/ui/src/conversation-copy.ts's outputTruncatedTitle. That was the wrong file and the wrong comparison. Current main removed the Desktop footer's Regenerate fields in #5468, but this PR restores them in apps/desktop/src/renderer/application/contracts/conversation-copy.ts:75-81,358,601,835. These dead, unreferenced strings should be removed again. I accept the current-head P2 finding in review 5336501216; the error in my earlier assessment is mine.

The previous quote-loss issue is addressed: packages/ui/src/revision-staged-context.ts:458-475,535-550 captures the edited plate and re-keys that set rather than originalQuotes; apps/desktop/src/renderer/app-shell.tsx:1429-1436,1596-1613 snapshots the replacement payload and clears the branch plate after a successful revision, including a quote-free one. The attachment-bearing main-chat gate is restored (packages/ui/src/chat-turn.tsx:599-630), and the revision gate again permits adding a quote during an edit (packages/ui/src/revision-staged-context.ts:153-167).

[P3] The side-chat quote panel now avoids dropping context by returning early when a selected message has quotes (apps/desktop/src/renderer/features/workbar/tools/side-chat/quote-companion-panel.tsx:408-420). However, it still passes onEditUserMessage to ChatView, whose TurnView enables Edit for quote-bearing messages (packages/ui/src/chat-turn.tsx:599-630). The user can click Edit and receive no draft change or explanation. Disable that action for this surface or show an explicit unsupported reason. This is a user-visible no-op, not a context-loss path.

Node 24 build:test, focused UI/Desktop tests (58/58), renderer architecture (114/114), current-head CI test, static merge-tree and git diff --check pass. No schema or migration files change. I did not run packaged Desktop or an end-to-end side-chat interaction; the real AppShell send snapshot/clear path remains a test gap. The Regenerate footer regression blocks readiness even though these checks pass. This is not a merge approval.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 18cb3b4a. Verdict: 1 × P2 remaining (trivial to fix), 5 × P3.

Fixed (3 of the 4 prior P2s), with no new races found:

  • Removed quotes coming back (revision-staged-context.ts:466-467,547-548; app-shell.tsx:1613). The plate is now read before the session switch, moved to the branch session, and the source is cleared. Reverting :547 to originalQuotes fails two tests.
  • Main-chat Edit gating for attachments (chat-turn.tsx:608-625).
  • Quotes added during an edit (:150-167). The send is now refused only for staged attachments or folders. Restoring the count-based conflict fails two tests.
  • We also checked for races: a session switch during an edit, a switch or cancel during send preparation, and concurrent sends. None found: the plates are per session, and every check runs before the move, with rollback on failure.

Still open, P2: the Regenerate footer copy removed by #5468 is re-added (inline). We checked against current main (de4fc5ff9): main has no footer block in conversation-copy.ts, so merging this PR would bring it back. Nothing in the tree references regenerateRunning or requestRegenerate, so it is dead copy that partly reverts #5468; the zh-TW 本輪迴答 typo is still there as well. It is likely a rebase artifact. Please drop the footer additions at :76-81 and their three locale entries.

P3s:

  1. Side-chat Edit on a quoted message does nothing (also raised in the other review). The PR removes main's disabled-with-reason gate, so quote-companion-panel.tsx:418 now returns silently on the most common side-chat message.
  2. Cancelling an edit deletes quotes added during it (:588). On main they stayed in the composer, and this now conflicts with the fact that adding quotes during an edit is supported.
  3. The conflict toast wording at :474 is stale. It still mentions "restored quotes", but the refusal now happens even when the message has no quotes. Also, unchanged text plus a new folder is reported as "Nothing changed".
  4. Stale comments and dead code. The comments at :275-279 and :342-346 describe the opposite of the code, and app-shell.tsx:1335 still passes restoreAttachments.
  5. Tests. The app-shell send path (:1434,1598,1613) and side-chat edit have no tests.

Verified in a scratch copy: build:test, UI 51/51, and desktop revision/quote-companion/session-reference 99/99. No Electron E2E.

Automated review (Claude lineage, posted from the Astro-Han account). No approval implied.

labels: Record<'branch' | 'copy', string>;
labels: Record<'regenerate' | 'branch' | 'copy', string>;
pending: string;
regenerateRunning: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2. The footer copy (labels, regenerateRunning, regenerateAgain, regenerate, requestRegenerate, …) at :76-81, with its locale entries at :358, :601 and :835, does not exist on current main: #5468 removed it. Nothing references these keys, so merging would re-add dead strings, including the zh-TW 本輪迴答 typo. This looks like a stale-base leftover; please drop it.

@ggbdpq

ggbdpq commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Correction accepted and the P2 is fixed — my earlier "cannot reproduce" was a wrong-file verification (packages/ui/src/conversation-copy.ts instead of the desktop contract), and the review's correction was right: the Regenerate footer fields and their three locale entries were a rebase artifact over #5468. Head is now 4050f60fb, with the footer dropped back to main's exact shape (labels: Record<'branch' | 'copy'>, zh-TW typo gone with the deleted strings).

The P3s, in the same push:

  • Gate order: pending directories are now decided before the unchanged check — verbatim text plus a newly attached folder is a real change, not "nothing changed" (new test pins it).
  • Mixed-context copy: the refusal no longer mentions restored quotes (quote additions pass now) in all three locales.
  • Stale comments and dead code: both lifecycle docstrings now describe the code (attachments refuse the edit; cancel empties the plates), and the app-shell drops the dead restoreAttachments field and destructure.
  • Side-chat edit no-op: the panel now toasts the explicit reason instead of returning silently (sideChatEditUnsupported, three locales).

Verified: desktop build clean, revision/context suites 52/52, renderer architecture green (the app-shell cleanup put the ledger at 12111 tokens, under the 12115 baseline — ledger updated), biome, locale hygiene and ASF headers clean.

Two P3s stand as recorded, with proposals rather than code, since both are semantic calls for the reviewer:

  • Cancel dropping quotes added during the edit: keeping them needs the cancel path to know which staged quotes were additions (a key-set diff against the draft's originals captured at gate time) and to re-stage that diff on the source session. Say the word and I'll implement it that way; alternatively, treating cancel as "abandon the whole edit, additions included" is a defensible reading — I'd rather match your call than guess.
  • App-shell send-snapshot/clear test coverage: the send path lives in the 4k-line app-shell component with no seam for the plates; the per-owner-key bucket fake introduced in 18cb3b4a1 covers the lifecycle side. A focused harness for the app-shell half is a chunk of extraction work I'd rather scope separately.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 4050f60fb934948ee92232bd05b869537e72ce0d; I found no substantiated new P0–P3 issue in this increment.

The previous Regenerate-footer regression is removed: apps/desktop/src/renderer/application/contracts/conversation-copy.ts:74-83 and all three locale entries now match current main's post-#5468 footer shape. The quote-bearing side-chat Edit action no longer fails silently: quote-companion-panel.tsx:408-423 now reports that this text-only edit cannot preserve quotes or attachments. revision-staged-context.ts:152-165 checks pending directory context before the unchanged-text test, so a new folder is not misreported as “Nothing changed”; the added test covers this case. The app-shell also drops the unused restoreAttachments binding.

Node 24 build:test, affected tests (60/60), renderer architecture (114/114), locale hygiene, current-head CI test, static merge-tree against current main, and git diff --check pass. The full PR has no schema or migration file change. I did not run packaged Desktop, a side-chat DOM/Electron interaction, or a real Host send race; the AppShell quote snapshot/clear path is still represented mainly by lifecycle fakes. This COMMENTED review is not a merge approval; GitHub currently reports BLOCKED.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental re-review at 4050f60f, relative to 18cb3b4a (6 files, +37/−36). Verdict: no P0–P2 remaining.

  • The Regenerate footer P2 is resolved. conversation-copy.ts no longer contains the footer/regenerate* keys. The only remaining difference from main in that file is the three revision/side-chat strings, present in all three locales.
  • The side-chat silent no-op (P3) is resolved. quote-companion-panel.tsx:415-421 now shows sideChatEditUnsupported instead of returning silently.
  • revisionSendGate now checks a pending directory before the unchanged check (revision-staged-context.ts:155). Unchanged text plus a new folder therefore reports the real conflict instead of "Nothing changed", and a regression test covers this. The stale lifecycle comments at :269-276 and :339-343 now match the code, and the restoreAttachments prop is gone from app-shell.tsx.

Still open, all P3: cancelling an edit clears quotes added during it, and there is no app-shell send-path or side-chat DOM test. Neither blocks merging. Mergeability should be re-checked against the latest main.

Automated review (Claude lineage, posted from the Astro-Han account). No approval implied.

…/dead keys

Review follow-ups (apache#5274 review):

- prepareRevisionSend re-keyed the edit-start snapshot, so quotes the user
  removed or re-annotated during the edit reappeared on the branch and
  could be sent. The re-key now lands the plate's current quotes, and the
  send reads them live (the before-send snapshot retires — the re-key
  covers its case).
- Drops the stale-base footer copy block and a dead regenerate key: no
  production code on current main references the old footer shape that
  survived in this branch's base.

The no-op gate keeps counting the model-facing comment.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq ggbdpq changed the title fix(desktop,ui): restage a selected message's quotes on edit fix(desktop,ui): stage a selected message's quotes on edit Sep 28, 2026
@ggbdpq
ggbdpq force-pushed the fix/desktop-revision-structured-context branch from 4050f60 to daf8229 Compare September 28, 2026 12:02
@ggbdpq

ggbdpq commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Two of the three are resolved on the current head (daf8229f2), and one needs a correction:

  • Quotes re-key (07:28): fixed. prepareRevisionSend now re-keys the plate's current quotes — user removals and re-annotations made during the edit survive onto the branch child — and the send reads them live; the before-send snapshot retired (re-keys the plate the user edited, not the edit-start snapshot covers it).
  • footer copy (09:26): this one is a stale observation, not a stale base. #5468 removed the footer block, but #5743 (25ddafd5e, 09-27) reintroduced and re-shaped it — current main carries the footer type and all three locale blocks, and turn-footer-actions.ts:104 consumes getDesktopConversationCopy(locale).footer on main. The merge cannot re-add dead strings because this PR does not touch the footer block; the zh-TW typo lives only in the pre-fix(desktop): unify WorkHub and chat message presentation #5743 values, which the merge replaces. Dropping the block from this branch would actually diverge from main.
  • Scope (01:10): narrowed. Title and description now claim quotes only; the attachment case is documented as refused-with-truthful-copy, with Host-side ownership copy noted as the open Edit-and-resend revisions reject or drop structured message context across Desktop and TUI #5109 half.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the current quote plate through the revision send. packages/ui/src/revision-staged-context.ts:538-539 copies the plate's current quotes into the branch child, then clears the source bucket in place. The same sendWithAttachments invocation resumes and calls quotesForSend() at apps/desktop/src/renderer/app-shell.tsx:1589; that closure still holds the source bucket from the render that started the send (apps/desktop/src/renderer/features/conversation/controller/use-composer-quotes.ts:34-35,106-107). It therefore returns undefined, so send() submits the edited replacement without its quotes or edited quote comments. The child bucket may display the quotes after navigation, but it does not change the already-running function's closure. The previous send-before-prepare snapshot was removed in this revision. Preserve a snapshot of the current plate for the in-flight send, or read the explicitly keyed child bucket after prepare; cover the AppShell send path rather than only the revision helper.

This head rebases the quote edit path onto current main and changes the re-key to use the plate's current quotes, fixing the prior removal/re-annotation snapshot issue in the helper. It deliberately keeps attachment-bearing message edits fail-closed. I reviewed the 15-file PR diff, the new quote re-key, the AppShell send path, the Desktop Main submission boundary, existing reviews, and commit provenance. Node 24 clean npm ci, build:test, and 171 focused tests passed; current-head CI test is green. A static merge-tree against current main 5735554b is clean. git diff --check reports an extra blank line at EOF in the Desktop copy contract; I do not treat that formatting issue as a review finding. No schema/migration change.

I did not run a packaged Desktop or an AppShell DOM/Electron end-to-end revision send; the focused tests use fake lifecycle and do not exercise the closure across the async copy. This finding holds the PR from merge candidacy. I did not approve or merge.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

…on send

The revision send awaited prepareRevisionSend before reading the plate,
and the lifecycle re-keys it mid-send: the plate's current quotes are
copied onto the branch child and the source bucket is emptied in place.
The still-running sendWithAttachments invocation holds quotesForSend
from the render that started the send, so it read the emptied source
bucket, got undefined, and submitted the edited replacement without its
quotes or their edited annotations (apache#5274 review).

Read the plate through the re-keyed owner explicitly: quotesForSend now
resolves the bucket by owner key (defaulting to the live draft key for
the non-revision send paths), and the revision send passes the prepared
draft's session id. The resumed send therefore reads the child bucket
the lifecycle just wrote — removals and re-annotations included.

The regression test drives beginEditUserMessage → an in-render
quotesForSend capture → prepareRevisionSend and asserts the re-keyed
plate with its edited annotation still reaches the send; it fails on
the previous head with `actual: undefined`.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq

ggbdpq commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Confirmed the P2, and fixed at f531f7368 using the second of the two suggested directions (read the explicitly keyed child bucket after prepare).

Root cause (verified against 14a3685eb): sendWithAttachments awaits prepareRevisionSend (the restoreQuotes(child, …) copy plus the in-place clearQuotes(source) at revision-staged-context.ts:538-539) before reaching quotesForSend(), but the invocation still holds the quotesForSend closed over the render that started the send — whose bucket is the source array that was just spliced empty. So quotesForSend() returned undefined and send() submitted the edited replacement without its quotes or edited annotations. The head's clearQuotes(expectedRevisionDraft?.draftSessionId) at app-shell.tsx:1604 was already clearing the right (re-keyed) owner; only the read side was stale.

Fix:

  • use-composer-quotes.ts: quotesForSend now resolves the bucket by an explicit ownerKey (defaulting to the live draft key) from pendingByKeyRef, which is stable across renders; buckets are mutated in place, never replaced, so the resumed send reads current contents.
  • app-shell.tsx: the revision send now calls quotesForSend(expectedRevisionDraft?.draftSessionId) — expectedRevisionDraft is re-read from revisionDraftRef after prepareRevisionSend returned, so it is the prepared draft and the key is the re-keyed branch child. The graph/swarm slash paths keep the no-arg live-draft default (revision sends reject slash commands earlier).

Regression test (apps/desktop/src/main/__tests__/use-composer-quotes-revision-send.test.ts): drives beginEditUserMessage, re-annotates the staged quote, captures quotesForSend from the render exactly as sendWithAttachments does, then runs prepareRevisionSend and asserts the stale render-time closure reads the re-keyed plate with the edited annotation. Per the finding's ask, the test exercises the AppShell composition — the real createAppShellRevisionActions lifecycle plus the real useComposerQuotes hook — though it does not mount the full AppShell component. Evidence: on 14a3685eb it fails with actual: undefined; on f531f7368 it passes. The AppShell/quote suites around it (app-shell-*.test.js, quote-companion-*.test.js, plus the new test) pass 175/175 locally (Node 24).

Ledger: the fix costs +3 nonTriviaTokens on app-shell.tsx (12088 → 12091), regenerated via check-renderer-architecture.mjs --write and verified with --base against current main (12125) — the ratchet still holds, and git diff --check is clean.

New head: f531f7368033aec62f36e04313e566e359e2414a.

Generated-by: GLM-5.3-Flash (ZCode)

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head f531f7368033aec62f36e04313e566e359e2414a. The previous P2 quote-loss path is fixed: prepareRevisionSend restores the edited plate into the branch-child bucket, then clears the source (packages/ui/src/revision-staged-context.ts:538-539); the resumed send now reads that child explicitly (apps/desktop/src/renderer/app-shell.tsx:1582-1595, use-composer-quotes.ts:106-115) and clears the same owner only after success (app-shell.tsx:1600-1604). I found no new substantiated P0–P3 issue in this increment. The known lower-severity cancellation behavior remains: quotes added during an edit are cleared with the edit (revision-staged-context.ts:564-588).

Node 24 clean npm ci, build:test, 58 focused tests, and the renderer architecture check pass. Hosted test is green; a static merge-tree against current main 2f322055 is clean. git diff --check still reports one extra blank line at EOF in the Desktop copy contract. The new test captures the quote hook around a fake revision lifecycle; it does not drive the actual AppShell send or a real Host/Electron race, which remains unverified. No schema/migration change is in the PR. This is not a merge approval.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at f531f736 (Claude lineage). The lost-quotes P2 is fixed; no new P0–P2.

  • app-shell.tsx:1589 now calls quotesForSend(expectedRevisionDraft?.draftSessionId), so a revision send reads the re-keyed branch-child bucket rather than the stale closure's emptied source bucket.
  • :1604 clears that same owner key.
  • For a non-revision send, the id is undefined, so both helpers fall back to their options.draftKey default (use-composer-quotes.ts:85,113), and behavior is unchanged.
  • quotesForSend returns the live bucket array, but it is only cleared after await send(...) resolves, so the payload is not truncated.

The remaining P3 is unchanged: cancelling an edit clears quotes added during the edit.

Automated review (Claude lineage, posted from the Astro-Han account). No approval implied.

Cancelling a revision draft cleared both draft keys wholesale, so quotes
the user staged during the edit were lost together with the edit's own
restaged set (apache#5274 review: "Cancelling an edit deletes quotes added
during it" — on main they stayed in the composer, and adding quotes
during an edit is supported).

clearQuotes now returns the entries it removed, and
clearRevisionStagedContext re-stages whatever lies beyond the edit's
beginEdit snapshot (matched per QuoteRef field set, counted as a
multiset) onto the source Session the cancel returns to — whether the
user added them before or after the branch child was prepared. The
edit's own items are still dropped wherever the commit left them.

The regression tests drive a cancelled edit with a user-added quote on
each side of the prepare step and assert the addition survives on the
source key while the composer text rolls back; both fail on the
previous head, the first with the edit's restaged quote returned
instead of the user's own, the second with no plate restore at all.

Generated-by: GLM-5.3-Flash (ZCode)
@ggbdpq

ggbdpq commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

The remaining P3 from the re-review at f531f736 is fixed at ddd4c9522.

Semantics (from the original finding at 18cb3b4a: "Cancelling an edit deletes quotes added during it (:588). On main they stayed in the composer"): a cancel undoes the edit — including the quotes the edit restaged from the selected message — but quotes the user staged themselves during the edit are kept, re-keyed onto the source Session the cancel returns to.

Fix — clearQuotes now returns the entries it removed, and clearRevisionStagedContext re-stages whatever lies beyond the edit's beginEdit snapshot (matched per QuoteRef field set, counted as a multiset, so removals and re-annotations of restaged quotes still drop while genuine additions survive) onto the source session — whether the user added them before or after the branch child was prepared. app-shell.tsx is untouched; the only production changes are packages/ui/src/revision-staged-context.ts and the clearQuotes return in use-composer-quotes.ts.

Verification (Node v26.10.0, Windows):

  • Red first: two new tests in packages/ui/src/__tests__/revision-staged-context.test.ts — a quote added during the edit, and a quote added on the branch child after prepare — each cancelled and asserted to survive on the source key. Both failed on the previous head (the first returned the edit's restaged quote instead of the user's own; the second had no plate restore at all).
  • Green after the fix: ui revision-staged-context suite 18/18; full @maka/ui dist suite 709/709; desktop app-shell-revision-actions 7/7; desktop typecheck (preload/main/renderer/storybook) and ui typecheck clean; biome check clean on all four changed files; check:asf-headers clean; protocol-epoch-check --staged unchanged at epoch 197; git diff --check --cached clean.
  • Renderer architecture: check-renderer-architecture.mjs --write produced a zero-diff ledger — none of the touched files carry AppShell debt, so token debt is unchanged.

New head: ddd4c9522.

@hqhq1025 hqhq1025 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed ddd4c95. This update changes cancel-edit cleanup to retain quotes staged by the user during an edit, including quotes on the prepared branch child, while removing the selected message's original quotes. I found one P3 data-loss edge in that new cleanup path (inline). The earlier send-path quote re-key remains intact in the changed code; I found no additional substantiated P0–P2 issue.

Local Node 24 build:test and 26 focused revision tests passed. The current-head hosted test passed, and the merge-tree against fetched main 2f32205 is clean. The full PR diff check still reports one trailing blank line in the Desktop conversation-copy contract. I did not run a real AppShell/Host send or Electron UI flow; the focused tests use fake lifecycle state. This is not merge approval.

Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.

// Past the edit's own per-quote count, every entry is the user's own.
const key = quoteKey(quote);
const ownedCount = owned.get(key) ?? 0;
owned.set(key, ownedCount - 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Keep all newly staged duplicate quotes when cancelling. If the selected message had no quotes and the user adds the same quote twice during the edit, ownedCount starts at 0: the first new quote is kept but this assignment makes the count -1, so the second identical new quote fails the ownedCount === 0 check and is silently discarded. useComposerQuotes.addQuote appends without deduplication, so this state is reachable. I reproduced the helper with two { text: 'same' } entries: cancellation restored only one. Once the edit-owned quota reaches zero, keep every subsequent matching entry rather than decrementing into negative counts; add a duplicate-quote regression case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at e39891b: the kept check now treats any non-positive remaining count as fully user-owned (ownedCount <= 0 instead of === 0), so every plate entry past the edit's own per-key quota survives the cancel once the quota is exhausted — including the state reproduced here, where the edit owned no quota for the key and the user staged the same quote twice. Added a duplicate-quote regression case in revision-staged-context.test.ts (an edit-owned entry plus two identical user copies drives the count to -1); it fails on ddd4c95 and passes on the new head.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We checked the increment since f531f736 (ddd4c952, keep quotes added during a cancelled edit) at ddd4c952. clearQuotes now returns what it removed, and clearRevisionStagedContext drops only the edit's own quotes (counted per quote key), then restages the rest on the source key. This fixes the earlier gap where quotes the user added during an edit were lost on cancel.

We agree with the P3 already raised inline on this head: the counter goes negative, so ownedCount === 0 misses a second identical user quote. ownedCount <= 0 would fix it. We found nothing else in the increment.

Automated review (Claude lineage, posted from the Astro-Han account). No approval implied.

clearRevisionStagedContext drops the edit's own quotes by per-key count:
the first N entries matching the beginEdit snapshot are the edit's, and
everything past that count is the user's own staging. The kept check
compared the remaining count with `=== 0`, so once the counter ran
negative the surplus stopped matching: when the user added a second copy
of a quote the edit had also staged, the third identical plate entry was
dropped despite being the user's.

Treat any non-positive count as fully user-owned (`<= 0`), so every
entry past the edit's own count survives the cancel.

Fixes a P3 raised inline on apache#5274.

Generated-by: GLM-5.3-Flash (ZCode)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Under 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants