Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
submitText: (...args: Parameters<typeof actions.submitText>) =>
act(async () => actions.submitText(...args)) as Promise<boolean>
})
}, [

Check warning on line 196 in apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx

View workflow job for this annotation

GitHub Actions / JS & TS checks / apps/desktop / check:lint

React Hook useEffect has a missing dependency: 'actions'. Either include it or remove the dependency array
actions.cancelRun,
actions.editMessage,
actions.reloadFromMessage,
Expand Down Expand Up @@ -1167,6 +1167,56 @@
$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<string, unknown> | undefined)[] = []

const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
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(
<Harness
activeSessionId="foreground-runtime"
onReady={h => (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 <long prose>` used to echo the entire invocation in the mono
// header AND the goal text again in the backend notice right under it.
Expand Down
11 changes: 10 additions & 1 deletion apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading