diff --git a/.changeset/terminal-composer-instruction-threaded.md b/.changeset/terminal-composer-instruction-threaded.md new file mode 100644 index 000000000..c90cb09af --- /dev/null +++ b/.changeset/terminal-composer-instruction-threaded.md @@ -0,0 +1,5 @@ +--- +"@inkeep/open-knowledge-app": patch +--- + +Fix the bottom "Ask AI" composer dropping the typed instruction when launching a Terminal CLI. A composer dispatch carries its instruction (and `@`-mentions / selection) in `input.compose`, but the docked-terminal launcher only checked the top-level `input.instruction` the toolbar popover uses — so every composer-typed message fell through to the bare "load OK, then stop" prompt and the agent never saw what the user asked. The terminal launcher now routes compose-scope dispatches through the same prompt assembler as the deep-link handoff, so the instruction threads through to the launched CLI exactly as it does to a Claude/Codex/Cursor deep link. diff --git a/packages/app/src/components/handoff/useHandoffDispatch.test.ts b/packages/app/src/components/handoff/useHandoffDispatch.test.ts index 0830b8c64..8a4524ec4 100644 --- a/packages/app/src/components/handoff/useHandoffDispatch.test.ts +++ b/packages/app/src/components/handoff/useHandoffDispatch.test.ts @@ -1488,6 +1488,36 @@ describe('composeTerminalLaunchPrompt — docked-terminal bare launch is load + expect(claudeOut).not.toContain(OK_TERMINAL_SURFACE_PREAMBLE); }); + test('composer (compose scope) threads the typed instruction — NOT a bare launch', async () => { + const { composeTerminalLaunchPrompt, selectScopedPrompt } = await import( + './useHandoffDispatch' + ); + const input = { + docContext: null, + compose: { + scope: 'doc' as const, + docRelativePath: 'notes/work-log.md', + instruction: 'What does this file do?', + mentions: [], + }, + projectDir: '/proj', + docPath: '/proj/notes/work-log.md', + }; + expect(composeTerminalLaunchPrompt(input, 'claude')).toBe( + selectScopedPrompt(input, 'claude-code', false), + ); + expect(composeTerminalLaunchPrompt(input, 'codex')).toBe( + selectScopedPrompt(input, 'codex', false), + ); + expect(composeTerminalLaunchPrompt(input, 'cursor')).toBe( + selectScopedPrompt(input, 'cursor', false), + ); + const claudeOut = composeTerminalLaunchPrompt(input, 'claude'); + expect(claudeOut).toContain('What does this file do?'); + expect(claudeOut).not.toBe(composeTerminalBareLaunchPrompt('notes/work-log.md')); + expect(claudeOut).not.toContain(OK_TERMINAL_SURFACE_PREAMBLE); + }); + test('whitespace-only instruction is treated as a bare launch', async () => { const { composeTerminalLaunchPrompt } = await import('./useHandoffDispatch'); const out = composeTerminalLaunchPrompt( diff --git a/packages/app/src/components/handoff/useHandoffDispatch.ts b/packages/app/src/components/handoff/useHandoffDispatch.ts index aed38ad9a..8916ba5b8 100644 --- a/packages/app/src/components/handoff/useHandoffDispatch.ts +++ b/packages/app/src/components/handoff/useHandoffDispatch.ts @@ -417,7 +417,7 @@ export function selectScopedPrompt( export function composeTerminalLaunchPrompt(input: HandoffDispatchInput, cli: TerminalCli): string { const hasInstruction = typeof input.instruction === 'string' && input.instruction.trim() !== ''; - if (input.createDescription !== undefined || hasInstruction) { + if (input.compose !== undefined || input.createDescription !== undefined || hasInstruction) { return selectScopedPrompt(input, TERMINAL_CLIS[cli].handoffTarget, false); } return composeTerminalBareLaunchPrompt(input.docContext?.relativePath ?? null);