diff --git a/src/renderer/src/components/native-chat/NativeChatComposer.test.tsx b/src/renderer/src/components/native-chat/NativeChatComposer.test.tsx
index c35ebca2666..fe6cbdd6f58 100644
--- a/src/renderer/src/components/native-chat/NativeChatComposer.test.tsx
+++ b/src/renderer/src/components/native-chat/NativeChatComposer.test.tsx
@@ -32,7 +32,9 @@ const mocks = vi.hoisted(() => ({
sendNativeChatMessageVerified: vi.fn(),
trackPendingSend: vi.fn(),
setDraft: vi.fn(),
- draftScopeKeys: [] as string[]
+ draftScopeKeys: [] as string[],
+ clearNativeChatLaunchDraft: vi.fn(),
+ markNativeChatLaunchDraftAdopted: vi.fn()
}))
vi.mock('../../store', () => {
@@ -40,8 +42,8 @@ vi.mock('../../store', () => {
dictationState: 'idle',
settings: { voice: { enabled: false }, nativeChatSessionOptions: {} },
updateSettings: vi.fn(),
- clearNativeChatLaunchDraft: vi.fn(),
- markNativeChatLaunchDraftAdopted: vi.fn()
+ clearNativeChatLaunchDraft: mocks.clearNativeChatLaunchDraft,
+ markNativeChatLaunchDraftAdopted: mocks.markNativeChatLaunchDraftAdopted
}
const useAppStore = (selector: (value: typeof state) => unknown) => selector(state)
useAppStore.getState = () => state
@@ -203,6 +205,22 @@ describe('NativeChatComposer', () => {
expect(mocks.trackPendingSend).toHaveBeenCalledWith(mocks.sendHandle, 'pending-1')
})
+ it('retires the launch-draft seed once a send clears the TUI input line', () => {
+ render(
+
+ )
+ expect(mocks.clearNativeChatLaunchDraft).not.toHaveBeenCalled()
+
+ act(() => mocks.fieldProps?.onSend?.())
+
+ expect(mocks.clearNativeChatLaunchDraft).toHaveBeenCalledWith('tab-1')
+ })
+
it('keeps the draft scope anchored to the pane while the PTY reconnects', () => {
const view = render(
{
expect(mocks.seedNativeChatLaunchPrompt).not.toHaveBeenCalled()
})
+ it('does not seed a launch draft for multi-line content', async () => {
+ // The chat send pre-clears the TUI with Ctrl+U (kill-to-start-of-LINE), so a
+ // multi-line prefill (e.g. scraped session-fork context) would leave earlier
+ // lines behind to glue onto the next message.
+ await deliverLaunchPromptToAgentTab({
+ tabId: 'fork-tab',
+ agent: 'codex',
+ content: 'Forked from session\n\nhttps://example.test/context',
+ submit: false,
+ forcePaste: false
+ })
+
+ expect(mocks.seedNativeChatLaunchDraft).not.toHaveBeenCalled()
+ })
+
it('does not seed a launch draft for submitted, unsupported, or empty content', async () => {
await deliverLaunchPromptToAgentTab({
tabId: 'submit-tab',
diff --git a/src/renderer/src/lib/agent-launch-prompt-delivery.ts b/src/renderer/src/lib/agent-launch-prompt-delivery.ts
index 9aedb82ba34..1536c8bede6 100644
--- a/src/renderer/src/lib/agent-launch-prompt-delivery.ts
+++ b/src/renderer/src/lib/agent-launch-prompt-delivery.ts
@@ -43,9 +43,12 @@ export function deliverLaunchPromptToAgentTab(args: {
text: content,
createdAt: Date.now()
})
- } else if (submit !== true) {
+ } else if (submit !== true && !content.includes('\n')) {
// Why: an unsubmitted draft lives only in the TUI input buffer; seed the
// chat-composer copy so the context isn't invisible in the GUI view.
+ // Single-line only: the chat send pre-clears the TUI with Ctrl+U
+ // (kill-to-start-of-LINE), so a multi-line prefill (e.g. scraped fork
+ // context) would leave earlier lines to glue onto the next message.
seedNativeChatLaunchDraftForAgentTab({ tabId, agent, text: content })
}