Merge pull request #71891 from NousResearch/bb/slash-tab-target

fix(desktop): send a skill's kickoff into the tab that invoked it
This commit is contained in:
brooklyn! 2026-07-26 05:15:16 -05:00 committed by GitHub
commit 080ee077a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 1 deletions

View file

@ -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<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.

View file

@ -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 {