diff --git a/src/server/__tests__/ws-memory-events.test.ts b/src/server/__tests__/ws-memory-events.test.ts
index 86d7e301..bc289ebc 100644
--- a/src/server/__tests__/ws-memory-events.test.ts
+++ b/src/server/__tests__/ws-memory-events.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'bun:test'
-import { translateCliMessage } from '../ws/handler.js'
+import {
+ createCurrentTurnLocalCommandForwarder,
+ translateCliMessage,
+} from '../ws/handler.js'
+import { parseSlashCommand } from '../../utils/slashCommandParsing.js'
describe('WebSocket memory events', () => {
it('forwards CLI memory_saved system messages to the desktop client', () => {
@@ -208,6 +212,37 @@ describe('WebSocket goal command events', () => {
{ type: 'content_delta', text: 'Goal: active' },
])
})
+
+ it('allows the current slash command lifecycle through the pre-turn mute gate', () => {
+ const shouldForward = createCurrentTurnLocalCommandForwarder(
+ parseSlashCommand('/goal ship the smoke test'),
+ )
+
+ expect(shouldForward({
+ type: 'system',
+ subtype: 'init',
+ })).toBe(false)
+ expect(shouldForward({
+ type: 'system',
+ subtype: 'local_command',
+ content: '/cost\n',
+ })).toBe(false)
+ expect(shouldForward({
+ type: 'system',
+ subtype: 'local_command',
+ content: '/goal\nship the smoke test',
+ })).toBe(true)
+ expect(shouldForward({
+ type: 'system',
+ subtype: 'local_command',
+ content: 'Goal created.\nGoal: active\nObjective: ship the smoke test',
+ })).toBe(true)
+ expect(shouldForward({
+ type: 'system',
+ subtype: 'local_command_output',
+ content: 'late unrelated output',
+ })).toBe(false)
+ })
})
describe('WebSocket stream event translation', () => {
diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts
index f6aaa373..33b06dd6 100644
--- a/src/server/ws/handler.ts
+++ b/src/server/ws/handler.ts
@@ -333,9 +333,16 @@ async function handleUserMessage(
// Keep output muted until the current user turn is enqueued to avoid forwarding
// any pre-turn SDK chatter as fresh chat history.
let userMessageSent = false
+ const shouldForwardCurrentTurnLocalCommand =
+ createCurrentTurnLocalCommandForwarder(desktopSlashCommand)
rebindSessionOutput(sessionId, ws, {
- shouldForward: (cliMsg) => userMessageSent || (cliMsg.type === 'result' && cliMsg.is_error),
+ shouldForward: (cliMsg) => {
+ if (userMessageSent || (cliMsg.type === 'result' && cliMsg.is_error)) {
+ return true
+ }
+ return shouldForwardCurrentTurnLocalCommand(cliMsg)
+ },
})
const sent = conversationService.sendMessage(
@@ -1324,6 +1331,56 @@ function getDesktopSlashCommand(content: string): ReturnType,
+): (cliMsg: any) => boolean {
+ let awaitingCurrentTurnLocalCommandOutput = false
+
+ return (cliMsg: any) => {
+ if (command && isMatchingCurrentTurnLocalCommand(cliMsg, command)) {
+ awaitingCurrentTurnLocalCommandOutput = true
+ return true
+ }
+ if (
+ awaitingCurrentTurnLocalCommandOutput &&
+ isLocalCommandOutputMessage(cliMsg)
+ ) {
+ awaitingCurrentTurnLocalCommandOutput = false
+ return true
+ }
+ return false
+ }
+}
+
+function isMatchingCurrentTurnLocalCommand(
+ cliMsg: any,
+ command: NonNullable>,
+): boolean {
+ if (cliMsg?.type !== 'system' || cliMsg?.subtype !== 'local_command') {
+ return false
+ }
+ const localCommand = extractLocalCommand(cliMsg.content ?? cliMsg.message)
+ if (!localCommand) return false
+ return (
+ localCommand.name === command.commandName &&
+ localCommand.args.trim() === command.args.trim()
+ )
+}
+
+function isLocalCommandOutputMessage(cliMsg: any): boolean {
+ if (
+ cliMsg?.type !== 'system' ||
+ (cliMsg?.subtype !== 'local_command' &&
+ cliMsg?.subtype !== 'local_command_output')
+ ) {
+ return false
+ }
+ return extractLocalCommandOutput(
+ cliMsg.content ?? cliMsg.message,
+ { allowUntagged: cliMsg.subtype === 'local_command_output' },
+ ) !== null
+}
+
function extractLocalCommandOutput(
content: unknown,
options: { allowUntagged?: boolean } = {},