diff --git a/apps/desktop/e2e/slash-command-menu.spec.ts b/apps/desktop/e2e/slash-command-menu.spec.ts index d78016aa9b..4a1c4a026b 100644 --- a/apps/desktop/e2e/slash-command-menu.spec.ts +++ b/apps/desktop/e2e/slash-command-menu.spec.ts @@ -17,7 +17,6 @@ * under the License. */ -import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/test-only/fake-backend'; import { expect, test, COMPOSER_INPUT } from './fixtures'; test('shows only slash commands executable in the current session state', async ({ @@ -208,34 +207,6 @@ test('opens the slash menu after a DOM block break', async ({ await expect(menu).toBeVisible(); }); -test('dispatches /side instead of steering it into a running turn', async ({ - invocableSkillsWindow: page, -}) => { - const composer = page.locator(COMPOSER_INPUT); - const runningPrompt = FAKE_HOLD_OPEN_PROMPT; - await composer.fill(runningPrompt); - await composer.press('Enter'); - await expect(page.locator('.maka-user-message', { hasText: runningPrompt })).toBeVisible(); - await expect(page.getByRole('button', { name: '停止' })).toBeVisible(); - - await composer.click(); - await composer.pressSequentially('/'); - const menu = page.getByRole('listbox', { name: '命令和技能' }); - const commands = menu.getByRole('group', { name: '命令' }); - const side = commands.getByRole('option', { name: /打开侧聊.*\/side/ }); - await expect(side).toBeVisible(); - await expect(commands.getByRole('option', { name: /\/compact/ })).toHaveCount(0); - await side.click(); - await expect.poll(() => composer.textContent()).toBe('/side '); - await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(0); - - await composer.fill('/side discuss separately'); - await composer.press('Enter'); - - await expect(page.locator('.maka-quote-workbar-panel')).toHaveCount(1); - await page.getByRole('button', { name: '停止' }).click(); -}); - test('an open menu keeps its container and skills group across projection refreshes', async ({ invocableSkillsWindow: page, }) => { diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index 1ab3bb754b..305684c186 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -891,7 +891,7 @@ "react": 1 }, "importSpecifiers": 148, - "nonTriviaTokens": 15620 + "nonTriviaTokens": 15617 }, "src/renderer/use-app-shell-composer-quotes.ts": { "importDeclarations": 2, diff --git a/apps/desktop/src/main/__tests__/follow-up-submit-routing.test.ts b/apps/desktop/src/main/__tests__/follow-up-submit-routing.test.ts index b829b09021..596a3dd656 100644 --- a/apps/desktop/src/main/__tests__/follow-up-submit-routing.test.ts +++ b/apps/desktop/src/main/__tests__/follow-up-submit-routing.test.ts @@ -50,6 +50,7 @@ describe('follow-up submit routing', () => { assert.equal( resolveFollowUpModeAtSubmit({ hasActiveTurn: true, + slashCommand: null, }), 'queue', ); @@ -57,6 +58,7 @@ describe('follow-up submit routing', () => { resolveFollowUpModeAtSubmit({ requestedMode: 'steer', hasActiveTurn: true, + slashCommand: null, }), 'steer', ); @@ -66,6 +68,27 @@ describe('follow-up submit routing', () => { assert.equal( resolveFollowUpModeAtSubmit({ hasActiveTurn: false, + slashCommand: null, + }), + undefined, + ); + }); + + it('dispatches a slash command mid-turn instead of steering it into the Turn', () => { + assert.equal( + resolveFollowUpModeAtSubmit({ + hasActiveTurn: true, + slashCommand: { kind: 'side' }, + }), + undefined, + ); + // An explicit steer request loses to the command too: Shift+Enter on + // `/side` still opens the side chat. + assert.equal( + resolveFollowUpModeAtSubmit({ + requestedMode: 'steer', + hasActiveTurn: true, + slashCommand: { kind: 'side' }, }), undefined, ); diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index e650ee81e6..e3ea229f2c 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -1919,12 +1919,11 @@ function AppShellContent({ const runningTurnIds = sessionId ? sessionsRef.current.find((session) => session.id === sessionId)?.runningTurnIds : undefined; - const followUpAtSubmit = !slashCommand - ? resolveFollowUpModeAtSubmit({ - requestedMode: metadata?.followUpMode, - hasActiveTurn: hasActiveTurnAtSubmit({ liveTurn, runningTurnIds }), - }) - : undefined; + const followUpAtSubmit = resolveFollowUpModeAtSubmit({ + requestedMode: metadata?.followUpMode, + hasActiveTurn: hasActiveTurnAtSubmit({ liveTurn, runningTurnIds }), + slashCommand, + }); if (sessionId && followUpAtSubmit) { const queued = await enqueueFollowUp(sessionId, text, followUpAtSubmit, { ...metadata, diff --git a/apps/desktop/src/renderer/follow-up-submit-routing.ts b/apps/desktop/src/renderer/follow-up-submit-routing.ts index 951a467d99..3c2959f83a 100644 --- a/apps/desktop/src/renderer/follow-up-submit-routing.ts +++ b/apps/desktop/src/renderer/follow-up-submit-routing.ts @@ -35,7 +35,12 @@ export function hasActiveTurnAtSubmit(input: { export function resolveFollowUpModeAtSubmit(input: { requestedMode?: FollowUpMode; hasActiveTurn: boolean; + /** The parsed command, if the text was one. Only its presence matters here. */ + slashCommand: object | null; }): FollowUpMode | undefined { + // A slash command tells the app to do something; it is not text for the + // Turn that happens to be running. Dispatch it instead of queueing it. + if (input.slashCommand) return undefined; if (input.requestedMode) return input.requestedMode; // Mid-turn submits always queue; Shift+Enter carries the one-shot steer as // the requested mode.