Skip to content

fix(chat): preserve drafts across agent switches - #2238

Open
zerob13 wants to merge 2 commits into
devfrom
codex/preserve-agent-composer-drafts
Open

fix(chat): preserve drafts across agent switches#2238
zerob13 wants to merge 2 commits into
devfrom
codex/preserve-agent-composer-drafts

Conversation

@zerob13

@zerob13 zerob13 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Switching away from a new conversation now preserves a separate draft for each agent, including text, attachments, selected skills and inline references. Drafts restore when returning or reloading. Successful sends consume their submitted snapshot, while failed sends and newer edits remain intact.

Existing-session drafts are saved before the input editor unmounts, preserving skills and document nodes. Pending attachment checks are invalidated when their originating view changes or unmounts.

BEFORE
Agent A [draft A] -> all agents -> Agent A [empty]
Agent A [draft A] -> Agent B [draft A]

AFTER
Agent A [draft A] <-> Agent B [draft B]
Leave / reload -> restore the corresponding complete draft

Validation:

  • pnpm run format:check, pnpm run i18n, pnpm run lint, and pnpm run typecheck passed.
  • pnpm run build passed.
  • NODE_OPTIONS=--max-old-space-size=6144 pnpm run test:renderer --silent passed: 267 files and 2373 tests, including deep-link and onboarding coverage using the real draft store with isolated state. Regressions cover agent isolation, full document restoration, debounce/window-close flushing, late send acceptance and attachment checks.
  • pnpm exec playwright test -c test/e2e/playwright.config.ts test/e2e/specs/34-agent-composer-drafts.smoke.spec.ts passed with isolated Electron user data and no provider requests.

Summary by CodeRabbit

  • New Features

    • Added per-agent draft preservation for new conversations.
    • Drafts retain message text, attachments, selected skills, and editor content when switching agents, hiding and reopening the editor, or reloading the page.
    • Drafts are automatically saved while composing and restored when returning to a conversation setup.
  • Bug Fixes

    • Improved cleanup and submission handling to prevent stale drafts or in-progress actions from affecting later conversations.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 81b06a36-371e-4c97-a2f3-75287fba96bf

📥 Commits

Reviewing files that changed from the base of the PR and between 92adf32 and bc4b8c2.

📒 Files selected for processing (2)
  • test/renderer/components/NewThreadPage.onboarding.test.ts
  • test/renderer/pages/NewThreadPage.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The PR adds per-agent new-thread composer drafts. Drafts include message content, attachments, skills, inline items, and editor documents. Drafts restore across agent changes, deselection, remounts, reloads, and submissions.

Per-agent composer drafts

Layer / File(s) Summary
Draft state and persistence
src/renderer/src/stores/ui/draft.ts, src/renderer/src/composables/useNewThreadComposerDraft.ts
The draft store caches drafts by agent. The composable synchronizes editor snapshots, debounces persistence, restores agent drafts, and flushes changes during lifecycle events.
New-thread integration and submission flow
src/renderer/src/pages/NewThreadPage.vue, src/renderer/src/features/chat-page/ChatPage.vue
NewThreadPage uses controlled composer state and captures agent-specific submission snapshots. Agent changes cancel stale submission work. Composer cleanup runs before unmount cleanup.
Draft behavior validation
test/renderer/components/ChatPage.test.ts, test/renderer/components/NewThreadPage.test.ts, test/renderer/components/NewThreadPage.onboarding.test.ts, test/renderer/pages/NewThreadPage.test.ts, test/e2e/specs/34-agent-composer-drafts.smoke.spec.ts
Tests cover document restoration, per-agent state, persistence timing, submission races, attachment retention, agent switching, and reload restoration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to bc4b8

Draft-related tests now use the real store and isolate browser storage between cases, improving test fidelity without changing production behavior. No current merge-readiness risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant ChatInputBox
  participant NewThreadPage
  participant useNewThreadComposerDraft
  participant useDraftStore
  participant localStorage
  ChatInputBox->>NewThreadPage: emit message, skills, files, and document changes
  NewThreadPage->>useNewThreadComposerDraft: record composer change
  useNewThreadComposerDraft->>useDraftStore: update agent draft
  useNewThreadComposerDraft->>localStorage: debounce draft persistence
  NewThreadPage->>useNewThreadComposerDraft: capture submission snapshot
  useNewThreadComposerDraft-->>NewThreadPage: return snapshot for selected agent
  NewThreadPage->>useNewThreadComposerDraft: accept successful submission
  useNewThreadComposerDraft->>useDraftStore: persist originating agent draft
Loading

Suggested reviewers: zhangmo8, yyhhyyyyyy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving chat drafts when switching agents.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/preserve-agent-composer-drafts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zerob13
zerob13 requested a review from zhangmo8 September 5, 2026 16:11

@zerob13 zerob13 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review: ✅ APPROVE — 0 P1 / 0 P2 / 5 P3 follow-ups

Reviewed at bc4b8c2c0 against merge-base e9f909519 (9 files, +544/-50). Full diff and changed files read in place; dependencies (composerDraftState, composerDraftPersistence, ChatInputBox handle API, useComposerSubmit, useSkillsData) cross-checked at merge-base. Verified in an isolated worktree: typecheck:web, oxfmt --check, oxlint all clean; 50/50 NewThreadPage + 95/95 ChatPage renderer tests pass; the new e2e smoke spec passes against a real build (7.1s, isolated user data).

Key behaviors verified

  • PR claim "successful sends consume their submitted snapshot; failed sends and newer edits remain intact" — TRUE. applyAcceptedComposerSubmission (composerDraftState.ts:94) treats content as unchanged only when revision AND rawMessage AND document all match; any edit during send (revision bump) preserves current text while still subtracting the sent files and their document nodes. Failed/cancelled paths never call acceptSubmission.
  • Watcher choreography on agent switch is correct. The composable's sync persist fires before the page's sync cancel watcher, capturing the old agent's live editor state before cancellation side effects. The restoring flag suppresses skills echoes (useSkillsData clears on agentId change; ACP sessionId re-emit), the post-flush watcher re-seeds, and later echoes are fingerprint no-ops. No clobber path found.
  • No restore/emit feedback loop. restoreDocumentSnapshot runs inside syncEditorContent with sync meta, so restores never re-emit update:modelValue/draft-change; updateDraft's fingerprint check short-circuits any residual echo.
  • Mid-submission agent switch fully guarded. Ambient values (agentId, projectDir, targetAcpSessionId) captured at entry before the first await; submission.cancelled re-checked after every await; the finally identity check prevents a resolving old submission from clobbering the switched-to state. Cross-agent sends consume the origin agent's draft only.
  • The ChatPage.vue teardown move is a genuine fix, not churn. disposeComposerSubmitflushComposerDraftPersist needs a live chatInputRef to capture the rich document; in onUnmounted the child would already be unmounted and the persisted draft silently degrades to text-only. The extended ChatPage test pins document + activeSkills surviving unmount.
  • Attachment pending-check invalidation works. attachmentFilterToken bumped on agent switch and unmount; late capability results discarded; raw files re-filtered at submit.

Over-engineering / test assessment

  • The 170-line composable mirrors the established useComposerSubmit pattern at ~1/8 the size; store placement is required (acceptSubmission must mutate shared state after the page unmounts). No premature generality; the store changes are purely additive.
  • Test delta is proportionate, not bloat: +226 in NewThreadPage.test.ts is six tests, each mapping to a durable category (per-agent restore, unmount-flush persistence, debounce/beforeunload contract, send-resolves-after-remount concurrency, cross-agent consumption, late capability invalidation). The 40-line e2e smoke is the only test proving persistence across a real reload and the deselect→unmount path.

P3 follow-ups (non-blocking)

  1. E2e spec numbering collision34-agent-composer-drafts.smoke.spec.ts reuses prefix 34-, already taken by 34-prompt-editor-scroll.smoke.spec.ts. Rename to 35-.
  2. ComposerHandle type duplicationuseNewThreadComposerDraft.ts:14-21 and NewThreadPage.vue:311-321 re-declare the same handle shape. Export the type from the composable and reuse it.
  3. Missing typeof window guarduseNewThreadComposerDraft.ts:152-153 registers pagehide/beforeunload unguarded, while sibling useComposerSubmit.ts:569 guards with typeof window !== 'undefined'. Harmless in the renderer; inconsistent with the pattern being copied.
  4. Orphaned draft keys on agent deletion — deleting an agent leaves deepchat.composerDraft.v1.new-thread:<id> in localStorage forever (clearComposerDraftFromStorage has zero callers in src/). Pre-existing behavior class shared with session drafts — follow-up ticket for both.
  5. Per-keystroke cost note — the post-flush restore watcher (useNewThreadComposerDraft.ts:115-135) does a full copyComposerDraft deep copy plus JSON.stringify comparisons on every draft change. Fine in practice (documents carry only file metadata); ChatPage's version caches fingerprints to avoid redundant copies — worth aligning eventually.

Merge-ready as-is; the P3s can be follow-ups.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant