Limit slash command help fix to desktop

The /goal command already exposes description and argument hints in the CLI. Revert the broader SDK, system/init, and server metadata changes so the remaining behavior change is only the desktop fallback and rendering path.

Constraint: Desktop help should not require changing core CLI or SDK wire contracts.

Rejected: Add slash_commands_metadata to system/init | unnecessary for the reported desktop-only display issue.

Confidence: high

Scope-risk: narrow

Directive: Fix missing desktop slash command descriptions in desktop fallback/rendering unless the CLI wire contract is explicitly being redesigned.

Tested: cd desktop && bun run test -- --run src/components/chat/composerUtils.test.ts src/__tests__/pages.test.tsx src/components/chat/ChatInput.test.tsx

Tested: bun test src/commands/headless.test.ts src/goals/goalState.test.ts src/goals/goalEvaluator.test.ts
This commit is contained in:
程序员阿江(Relakkes) 2026-05-13 21:44:07 +08:00
parent 49b7198da0
commit 30d9cc8254
6 changed files with 13 additions and 158 deletions

View File

@ -4,9 +4,8 @@ const goal = {
type: 'local-jsx',
supportsNonInteractive: true,
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '<objective>|status|pause|resume|clear|complete',
whenToUse: 'Use when you want the session to keep iterating on a concrete objective until it is complete.',
description: 'Set or manage a completion goal that keeps the session working until it is met',
argumentHint: '[status|clear|pause|resume|--tokens <budget> <objective>|<objective>]',
load: () => import('./goal.js'),
} satisfies Command

View File

@ -1473,16 +1473,6 @@ export const SDKSystemMessageSchema = lazySchema(() =>
model: z.string(),
permissionMode: PermissionModeSchema(),
slash_commands: z.array(z.string()),
slash_commands_metadata: z
.array(
z.object({
name: z.string(),
description: z.string(),
argumentHint: z.string().optional(),
whenToUse: z.string().optional(),
}),
)
.optional(),
output_style: z.string(),
skills: z.array(z.string()),
plugins: z.array(

View File

@ -5,7 +5,6 @@ import {
closeSessionConnection,
getActiveSessionIds,
handleWebSocket,
normalizeSlashCommandMetadata,
type WebSocketData,
} from '../ws/handler.js'
import { conversationService } from '../services/conversationService.js'
@ -70,27 +69,4 @@ describe('WebSocket handler session isolation', () => {
expect(clearCallbacks).toHaveBeenCalledWith(sessionId)
expect(cancelComputerUse).toHaveBeenCalledWith(sessionId)
})
it('normalizes slash command metadata while preserving legacy string payloads', () => {
expect(normalizeSlashCommandMetadata([
{
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '<objective>|status|pause|resume|clear|complete',
whenToUse: 'Use when the session should keep iterating until done.',
},
'clear',
{ command: 'compact', description: 'Compact context' },
{ name: ' ' },
])).toEqual([
{
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '<objective>|status|pause|resume|clear|complete',
whenToUse: 'Use when the session should keep iterating until done.',
},
{ name: 'clear', description: '' },
{ name: 'compact', description: 'Compact context' },
])
})
})

View File

@ -32,14 +32,7 @@ const providerService = new ProviderService()
/**
* Cache slash commands from CLI init messages, keyed by sessionId.
*/
type SlashCommandMetadata = {
name: string
description: string
argumentHint?: string
whenToUse?: string
}
const sessionSlashCommands = new Map<string, SlashCommandMetadata[]>()
const sessionSlashCommands = new Map<string, Array<{ name: string; description: string }>>()
/**
* Timers for delayed session cleanup after client disconnect.
@ -93,7 +86,7 @@ async function sendRepositoryStartupStatus(
}
}
export function getSlashCommands(sessionId: string): SlashCommandMetadata[] {
export function getSlashCommands(sessionId: string): Array<{ name: string; description: string }> {
return sessionSlashCommands.get(sessionId) || []
}
@ -791,41 +784,14 @@ function cacheSessionInitMetadata(sessionId: string, cliMsg: any) {
await sessionService.deletePlaceholderSessionFiles(sessionId, cliMsg.cwd)
})()
}
const slashCommandMetadata = normalizeSlashCommandMetadata(cliMsg.slash_commands_metadata)
const legacySlashCommands = normalizeSlashCommandMetadata(cliMsg.slash_commands)
const commands = slashCommandMetadata.length > 0 ? slashCommandMetadata : legacySlashCommands
if (commands.length > 0) {
sessionSlashCommands.set(sessionId, commands)
if (cliMsg.slash_commands && Array.isArray(cliMsg.slash_commands)) {
sessionSlashCommands.set(sessionId, cliMsg.slash_commands.map((cmd: any) => ({
name: typeof cmd === 'string' ? cmd : (cmd.name || cmd.command || ''),
description: typeof cmd === 'string' ? '' : (cmd.description || ''),
})))
}
}
export function normalizeSlashCommandMetadata(value: unknown): SlashCommandMetadata[] {
if (!Array.isArray(value)) return []
return value.flatMap((cmd): SlashCommandMetadata[] => {
if (typeof cmd === 'string') {
return cmd.trim() ? [{ name: cmd, description: '' }] : []
}
if (!cmd || typeof cmd !== 'object') return []
const record = cmd as Record<string, unknown>
const rawName = typeof record.name === 'string'
? record.name
: typeof record.command === 'string'
? record.command
: ''
const name = rawName.trim()
if (!name) return []
const description = typeof record.description === 'string' ? record.description.trim() : ''
const argumentHint = typeof record.argumentHint === 'string' ? record.argumentHint.trim() : ''
const whenToUse = typeof record.whenToUse === 'string' ? record.whenToUse.trim() : ''
return [{
name,
description,
...(argumentHint && { argumentHint }),
...(whenToUse && { whenToUse }),
}]
})
}
function extractAssistantText(cliMsg: any): string {
const content = cliMsg?.message?.content
if (!Array.isArray(content)) return ''

View File

@ -1,54 +0,0 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { buildSystemInitMessage } from './systemInit'
describe('buildSystemInitMessage', () => {
const previousAnthropicApiKey = process.env.ANTHROPIC_API_KEY
beforeEach(() => {
process.env.ANTHROPIC_API_KEY = 'test-key'
;(globalThis as typeof globalThis & { MACRO?: { VERSION: string } }).MACRO = { VERSION: 'test-version' }
})
afterEach(() => {
if (previousAnthropicApiKey === undefined) {
delete process.env.ANTHROPIC_API_KEY
} else {
process.env.ANTHROPIC_API_KEY = previousAnthropicApiKey
}
})
const baseInputs = {
tools: [],
mcpClients: [],
model: 'test-model',
permissionMode: 'default' as const,
agents: [],
skills: [],
plugins: [],
fastMode: false,
}
it('keeps slash command names compatible while exposing command descriptions and usage hints', () => {
const message = buildSystemInitMessage({
...baseInputs,
commands: [
{
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '<objective>|status|pause|resume|clear',
whenToUse: 'Use when the session should keep iterating until a concrete outcome is complete.',
},
],
})
expect(message.slash_commands).toEqual(['goal'])
expect(message.slash_commands_metadata).toEqual([
{
name: 'goal',
description: 'Create or manage an autonomous completion goal',
argumentHint: '<objective>|status|pause|resume|clear',
whenToUse: 'Use when the session should keep iterating until a concrete outcome is complete.',
},
])
})
})

View File

@ -24,20 +24,7 @@ export function sdkCompatToolName(name: string): string {
return name === AGENT_TOOL_NAME ? LEGACY_AGENT_TOOL_NAME : name
}
type CommandLike = {
name: string
description?: string
argumentHint?: string
whenToUse?: string
userInvocable?: boolean
}
type SlashCommandMetadata = {
name: string
description: string
argumentHint?: string
whenToUse?: string
}
type CommandLike = { name: string; userInvocable?: boolean }
export type SystemInitInputs = {
tools: ReadonlyArray<{ name: string }>
@ -66,7 +53,6 @@ export type SystemInitInputs = {
export function buildSystemInitMessage(inputs: SystemInitInputs): SDKMessage {
const settings = getSettings_DEPRECATED()
const outputStyle = settings?.outputStyle ?? DEFAULT_OUTPUT_STYLE_NAME
const userInvocableCommands = inputs.commands.filter(c => c.userInvocable !== false)
const initMessage: SDKMessage = {
type: 'system',
@ -80,17 +66,9 @@ export function buildSystemInitMessage(inputs: SystemInitInputs): SDKMessage {
})),
model: inputs.model,
permissionMode: inputs.permissionMode,
slash_commands: userInvocableCommands.map(c => c.name),
slash_commands_metadata: userInvocableCommands.map(command => ({
name: command.name,
description: command.description?.trim() ?? '',
...(command.argumentHint?.trim() && {
argumentHint: command.argumentHint.trim(),
}),
...(command.whenToUse?.trim() && {
whenToUse: command.whenToUse.trim(),
}),
} satisfies SlashCommandMetadata)),
slash_commands: inputs.commands
.filter(c => c.userInvocable !== false)
.map(c => c.name),
apiKeySource: getAnthropicApiKeyWithSource().source as ApiKeySource,
betas: getSdkBetas(),
claude_code_version: MACRO.VERSION,