diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx index 770632ddc9d9..28fd021fcf10 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx @@ -1167,6 +1167,56 @@ describe('usePromptActions slash.exec dispatch payloads', () => { $queuedPromptsBySession.set({}) }) + it("sends a skill's kickoff into the TAB that invoked it, not the foreground chat", async () => { + // `/work` in a fresh ⌘T tab: slash.exec returns a skill dispatch whose + // `message` is the kickoff prompt. The dispatcher resolved the tab as its + // target, printed "⚡ loading skill" there — then submitted the kickoff + // with no target at all, so submit re-resolved from activeSessionIdRef and + // fired it as a user message into whatever conversation was on screen. + const tabRuntimeId = 'tab-runtime' + const tabStoredId = 'tab-stored' + + $queuedPromptsBySession.set({}) + publishSessionState(tabRuntimeId, createClientSessionState(tabStoredId)) + + const submitted: (Record | undefined)[] = [] + + const requestGateway = vi.fn(async (method: string, params?: Record) => { + if (method === 'prompt.submit') { + submitted.push(params) + } + + return ( + method === 'slash.exec' + ? { type: 'skill', name: 'work', message: 'Load the work skill, then: fix the tab bug' } + : {} + ) as never + }) + + let handle: HarnessHandle | null = null + await actRender( + (handle = h)} + refreshSessions={async () => undefined} + requestGateway={requestGateway} + storedSessionId="foreground-stored" + /> + ) + + await handle!.submitText('/work fix the tab bug', { sessionId: tabRuntimeId }) + + expect(submitted).toEqual([ + expect.objectContaining({ + session_id: tabRuntimeId, + text: 'Load the work skill, then: fix the tab bug' + }) + ]) + + dropSessionState(tabRuntimeId) + $queuedPromptsBySession.set({}) + }) + it('slash status header carries the command token, not the full invocation', async () => { // `/goal ` used to echo the entire invocation in the mono // header AND the goal text again in the backend notice right under it. diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts index 07d80f02291d..f8f044ec028f 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts @@ -285,7 +285,16 @@ export function useSlashCommand(deps: SlashCommandDeps) { return } - await submitPromptText(message) + // Submit into the session this command was resolved against — the + // same pair the output writer and the busy gate above already use. + // Bare `submitPromptText(message)` let submit re-resolve from + // `activeSessionIdRef`, which names the FOREGROUND chat: a `/work` + // typed into a fresh ⌘T tab loaded the skill in that tab, printed + // "⚡ loading skill" there, then fired its kickoff as a user message + // into whatever conversation was on screen. Every other target the + // dispatcher serves (tile, background queue drain, a session created + // by this very call) had the same leak. + await submitPromptText(message, { sessionId, storedSessionId }) } try {