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 }) }