Skip to content

fix(native-chat): text typed during /clear follows the chat to its new conversation - #26054

Closed
brennanb2025 wants to merge 12 commits into
mainfrom
brennanb2025/nc-clear-draft-follows
Closed

brennanb2025 wants to merge 12 commits into
mainfrom
brennanb2025/nc-clear-draft-follows

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1390 $\color{#cf222e}{\Huge{\mathbf{−}}}$​15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1375
Prod 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​352 $\color{#cf222e}{\Huge{\mathbf{−}}}$​73 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​279

ELI5

In a native chat, text and images entered while /clear was running disappeared when the tab switched to its fresh conversation. The unsent draft belonged to the old conversation, while the tab now showed the new one's empty composer. The draft now follows that tab into the fresh conversation.

What Changed

  • The draft store moves text, image references and the saved editor document when an existing chat tab changes conversation. This covers a clear requested in this window, another window, or the phone through the same host tab update.
  • /clear leaves the composer when submitted, so the command itself cannot follow the draft. A failed command is restored when the composer has no text, settled images or pending uploads. New text or images entered while it runs remain untouched. Other commands and normal message sends retain their existing submission behavior.
  • Once saved drafts have loaded, the store moves both conversation records synchronously before notifying the composers. Text, settled images, pending uploads and picker-created skill chips follow together into an empty replacement, with the complete editor document unchanged. If the replacement already has text, an image or a pending upload, both inputs stay on their own conversations and the earlier draft remains reachable from history. The normal save and recovery path persists the move; a refused save keeps the new draft in memory and reports it through the existing save state.
  • Pending image saves and uploads belong to their draft, so local and SSH pastes finish into that draft even if its composer closes. If a clear moves the draft, pending image pastes and paired-server file uploads finish into its current conversation. A file keeps its pending operation ID until its reference is inserted; removing its chip still prevents attachment.
  • Appending a completed file reference or a returned message now preserves picked skill chips even while the editor is closed. The same draft addition updates text and the saved editor document, including delayed loading and recovery after a crash; ordinary completion benefits from this fix too.
  • A clear made while this window is closed, reloading, or before saved drafts finish loading leaves its input in the earlier conversation, still reachable from history. Startup does not infer ownership from old clear markers. Cross-window draft conflicts follow the existing last-saved rule, which protects unsaved local edits.

Why

Keeping a single conversation identity across /clear would avoid this bug class, but requires changing host behavior beyond composer drafts. The host stores a committed source-to-replacement record, creates a fresh provider identity with no previous journal checkpoint, moves the host tab, and closes the source after publishing the replacement. Reusing the source identity would close the fresh conversation through that existing path and would require a new journal boundary plus coordinated client handling. Paired hosts and mobile clients already use this replacement contract; this change preserves it and needs no new wire field or capability.

Drafts remain keyed by conversation so a rejected message can be returned to the right composer even without an open tab. Moving the draft through its existing store matches the common pattern for a view promoted to another conversation identity. Consuming /clear at submission removes the previous in-memory tracking map for sends waiting on replies.

Differences from the common pattern:

  • Intended: /clear creates a distinct conversation to preserve the existing host records, separate journals, and close behavior described above.
  • Intended: a destination that already has any input is preserved and the source draft stays in its conversation’s history. Transferring only into an empty replacement avoids interpreting or combining two editor documents.
  • Intended: removing the command on submission applies only to /clear, keeping this fix focused on the command that changes conversation identity.
  • Intended: draft moves require an observed tab change after the draft store has loaded. Clears made while this window is closed, reloading, or before loading finishes leave the draft available in the earlier conversation’s history.
  • Intended: a pending local or SSH image paste now survives its composer closing, as paired uploads already did. Drafts outlive their composer, so closing the input box preserves the attachment being prepared for that draft; removal or failed completion still ends the operation.
  • Intended: concurrent windows retain the store’s existing last-saved conflict rule; this change adds no separate ownership authority. The previous asynchronous capture and source-removal path is removed in favor of synchronous whole-record movement through the store.

Linked Issue

N/A — maintainer follow-up from a native chat audit.

Visual Proof

Hidden Electron validation on macOS at reviewed 728a8fda; the final sync to 99ae403d preserved all 27 reviewed files byte-for-byte. Automated checks run on the final head. The rendered check used an isolated folder workspace and stand-in OpenCode provider. Before clear, the real composer holds an unsent question, a real image, a picked Review skill and a literal suffix. An outside-composer client calls the real host clear operation; afterward the same tab shows a fresh conversation with the entire draft unchanged, and the saved source draft is removed. Both screenshots show the patched behavior before and after that action.

Before clear in the patched build After clear under the same tab
carry-before.png carry-after.png
Empty composer after a subsequent clear
empty-clear.png

After removing the carried input through the UI, submitting /clear in the actual composer creates another empty conversation under the same tab. Neither replacement contains the command. These checks do not exercise typing during provider shutdown, a real provider, a phone/second UI window, live SSH, Windows or Linux; automated tests cover post-submit typing and upload completion.

Testing

  • Twenty-three explicit files: 315 tests passed on macOS at final 99ae403d, including real host-tab snapshot projection, synchronous repeated clears, intact text/images/skill documents, destination conflict preservation, refused saves and ordinary flush repair, history preservation, pre-load upload settlement and journal recovery, pending-image refusal, skipped-move upload survival, moved paired file references, removed uploads, failed saves, caret insertion, input-method composition, moved and ordinary PDF/PNG completion with actual editor remount, picked-node preservation during delayed loading and crash recovery, and an empty clear.

  • Temporarily removing each of 14 required behaviors made the corresponding test fail; all source files were restored. The final queued renderer, main-process and CLI typechecks pass after synchronizing with main. The earlier main-branch localization failure is repaired in the final sync. All 22 final static gates pass, including localization. Final-head CI run 37740067285 is complete: 18 checks passed and 17 were skipped, with no failures or pending checks. All ten unit shards actually executed and passed; relay integration, static checks, Linux and Windows packaging, the overall verification gate and the advisory report passed. The path-gated skipped checks are not counted as passes.

  • No new saved tab field or message between clients and execution hosts is introduced. No replacement relation is persisted or reconstructed at startup. Folder workspaces use the same tab binding. An older host that does not report the existing tab switching conversations may create a new tab instead; its earlier draft stays in history. SSH image paths keep their connection identifiers. Actual SSH, Windows, Linux and phone rendering were not verified by live QA; phone-origin clears use the shared host snapshot path covered in the renderer test.

  • I manually tested these changes locally

  • Automated tests added/updated

Review

Six reviews found and resolved gaps in draft movement, pending upload ownership, refusal restoration and picked-skill preservation during file completion. Loaded drafts move intact only into empty replacements; pending image pastes and paired file uploads finish in their current owning draft; the generic append path preserves picked editor nodes even when its composer is closed. History and destination-conflict limits are explicit above. The seventh review found no in-scope issues. A separate inherited SSH file-drop/picker upload path has no pending draft entry: if its composer closes before the upload finishes, the result can be lost. That separate path is deferred for a follow-up repair; the pending-paste and paired-file paths covered here are listed above.

Agent skill upstream boundary

  • Not applicable; no upstream skill source or registry copied.

Notes

No host/session identity or wire contract changes. No real-provider login or account configuration is required for QA. Final main synchronization is complete and the reviewed patch is unchanged. Final-head CI is fully complete with no failures; all ten unit shards ran and passed. The inherited SSH file-drop/picker limitation remains deferred as described above.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • Before/after screenshots or videos attached
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote and folder workspaces considered
  • Static gates, typecheck, tests and build verified locally or covered by CI

…w conversation

A structured chat's unsent draft is kept per conversation, and /clear moves the
chat's tab to a new conversation. Whatever the box held while the clear ran stayed
with the old conversation, which nothing shows any more, so it disappeared.

When a chat's tab moves to another conversation, its draft now moves along (after
anything the new conversation's draft already holds). The text of a send still
waiting for its reply, such as the "/clear" itself, stays behind for that send to
clear, since the tab can move before the reply arrives.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Native-chat input entered while /clear was running disappeared because the tab switched conversations while its unsent draft stayed on the earlier one. This change consumes the submitted command and moves the entire loaded draft through its existing store when that same tab binds to an empty replacement, preserving text, images and picked skills; occupied destinations and clears observed before loading retain the earlier draft in history.

Review repairs removed retrospective transfer and document merging, made existing pending operations the authority for upload completion, restored refused clear only into a truly empty composer, and preserved picked editor nodes when completed file references are appended. The seventh review found no in-scope issues. A separate inherited SSH file-drop/picker path can lose an upload if its composer closes and is deferred; pending clipboard pastes and paired-file completion are covered here.

Final-head verification: 315 tests across 23 explicit files on final 99ae403d, queued renderer/main/CLI typechecks, and 14 checks that temporarily removed required behavior and made the corresponding test fail. All 22 local static gates passed after the final main sync repaired the inherited catalog failure. Final-head CI run 37740067285 is fully complete: 18 passed checks, 17 skipped checks, no failures and no pending checks. All ten full-suite unit shards actually executed their test steps and passed; relay integration, static checks, Linux/Windows packaging, overall verification and the advisory report also passed. Skipped checks are not treated as passes.

Hidden macOS Electron validation at reviewed 728a8fda (all 27 reviewed files remain byte-identical after the final 99ae403d sync) passed all four cases in an isolated folder workspace: a real outside-composer host clear preserved the entire visible and saved text/image/Review-skill draft under the same tab, a subsequent rendered /clear left a fresh empty composer, and no command remained in verified drafts. All full screenshots were inspected directly and are embedded above; isolation home paths and unchanged approved configuration hashes were verified, recorded app/provider/client processes exited, and the rig profile was pruned. The live test used a stand-in provider and did not exercise typing during shutdown, a phone/second UI window, live SSH, Linux or Windows.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 8, 2026 07:55
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Walkthrough

The changes preserve structured prompt documents when text or file references are appended. They track pending attachments across draft scopes and move eligible drafts when a tab changes agent sessions. The composer also handles host /clear submissions by clearing an unchanged draft and conditionally restoring it after rejection or failure. Tests cover session replacement, delayed uploads and storage, journal recovery, and command responses.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 99ae4

This change moves unsent chat drafts to the new conversation after /clear. The only remaining issue is a cosmetic test-title placeholder, so the risk of merging is low.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 25 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and follows the repository template. It explains the user impact, implementation, rationale, testing, visual proof, limitations, and checklist status. The maintainer correc…
Title check ✅ Passed The title clearly and concisely describes the primary behavior change: text typed during /clear follows the chat into its new conversation.
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 038fec43-b620-41e4-adfb-4f12173a04f9
📥 Commits

Reviewing files that changed from the base of the PR and between a211489 and 99ae403.

📒 Files selected for processing (27)
  • src/renderer/src/app-shell/native-chat-draft-startup.test.ts
  • src/renderer/src/app-shell/native-chat-draft-startup.ts
  • src/renderer/src/components/native-chat/NativeChatComposer.tsx
  • src/renderer/src/components/native-chat/native-chat-composer-draft-addition.test.ts
  • src/renderer/src/components/native-chat/native-chat-composer-draft-addition.ts
  • src/renderer/src/components/native-chat/native-chat-composer-draft-journal.ts
  • src/renderer/src/components/native-chat/native-chat-composer-draft-load.ts
  • src/renderer/src/components/native-chat/native-chat-composer-draft-store.ts
  • src/renderer/src/components/native-chat/native-chat-draft-cache.ts
  • src/renderer/src/components/native-chat/native-chat-pending-attachment-cache.test.ts
  • src/renderer/src/components/native-chat/native-chat-pending-attachment-cache.ts
  • src/renderer/src/components/native-chat/native-chat-prompt-document-text.test.ts
  • src/renderer/src/components/native-chat/native-chat-prompt-document-text.ts
  • src/renderer/src/components/native-chat/native-chat-prompt-document.ts
  • src/renderer/src/components/native-chat/native-chat-session-attachment-drop.ts
  • src/renderer/src/components/native-chat/structured-agent-session-draft-attachment-move.test.tsx
  • src/renderer/src/components/native-chat/structured-agent-session-draft-file-upload.test.tsx
  • src/renderer/src/components/native-chat/structured-agent-session-draft-move.test.ts
  • src/renderer/src/components/native-chat/structured-agent-session-draft-move.ts
  • src/renderer/src/components/native-chat/structured-agent-session-draft-upload-lifetime.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-composer-attachments.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-composer-attachments.ts
  • src/renderer/src/components/native-chat/use-native-chat-composer-paste.ts
  • src/renderer/src/components/native-chat/use-native-chat-external-attachments.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-paste-lifetime.ts
  • src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.clear-move.test.tsx
  • src/renderer/src/components/native-chat/use-native-chat-structured-composer-send.ts
💤 Files with no reviewable changes (2)
  • src/renderer/src/components/native-chat/native-chat-composer-draft-journal.ts
  • src/renderer/src/components/native-chat/native-chat-composer-draft-load.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment on lines +134 to +135
it.each(['local', 'ssh'])(
'control: moved $s upload survives teardown and settles in destination',

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the test-name placeholder in it.each.

The cases are plain strings. Vitest therefore does not interpolate $s in the title. Use %s so each case name shows the host.

Proposed fix
-  'control: moved $s upload survives teardown and settles in destination',
+  'control: moved %s upload survives teardown and settles in destination',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it.each(['local', 'ssh'])(
'control: moved $s upload survives teardown and settles in destination',
it.each(['local', 'ssh'])(
'control: moved %s upload survives teardown and settles in destination',

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Closing this in favour of a simpler design, after comparing it with the common pattern for /clear.

Why: this PR moves the draft (and queued text) from the cleared conversation to the new one that /clear creates. That move exists only because /clear currently replaces the conversation with a new id, so everything keyed by the conversation (drafts, the tab, the phone's row, queued cards) has to be carried across. The common pattern keeps the same conversation: /clear stops the agent, records a "Context cleared" marker, and starts a fresh agent context inside that conversation. Earlier messages stay visible above the marker. With nothing changing identity, there is nothing to move, and this whole class of "followed / didn't follow the chat" bugs goes away.

What happens to the parts of this PR:

  • The two fixes that don't depend on /clear (picked skill chips surviving text appended to a draft, and pending uploads surviving a closed composer) moved to Keep picked skills and pending uploads when native chat composers close #26538.
  • The draft-move code is dropped. /clear will instead keep the same conversation in follow-up PRs: first a stored form for the agent's handle history that older Orca builds read safely, then the in-place /clear itself.

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