diff --git a/docs/chat-chain-changes/2026-06-16-web-slash-command-title.md b/docs/chat-chain-changes/2026-06-16-web-slash-command-title.md new file mode 100644 index 00000000..b17f1087 --- /dev/null +++ b/docs/chat-chain-changes/2026-06-16-web-slash-command-title.md @@ -0,0 +1,6 @@ +--- +date: 2026-06-16 +pr: 1599 +feature: Web slash command session titles +impact: New Web UI slash-command sessions now use the command text for their initial title instead of the generic Bridge command title. +--- diff --git a/packages/server/src/services/hermes/run-chat/session-command.ts b/packages/server/src/services/hermes/run-chat/session-command.ts index 6a5ebea4..a9d8c2df 100644 --- a/packages/server/src/services/hermes/run-chat/session-command.ts +++ b/packages/server/src/services/hermes/run-chat/session-command.ts @@ -88,7 +88,7 @@ export async function handleSessionCommand( ): Promise { const state = getOrCreateSession(ctx.sessionMap, sessionId) ctx.socket.join(`session:${sessionId}`) - ensureCommandSession(sessionId, ctx) + ensureCommandSession(sessionId, command, ctx) const isKnownCommand = Boolean(COMMAND_ALIASES[command.rawName]) if (command.name !== 'plan' && command.name !== 'skill' && isKnownCommand) { persistCommandMessage(sessionId, state, `/${command.rawName}${command.args ? ` ${command.args}` : ''}`) @@ -827,17 +827,23 @@ function formatReloadSkillItem(item: unknown): string { return description ? `${name}: ${description}` : name } -function ensureCommandSession(sessionId: string, ctx: SessionCommandContext) { +function ensureCommandSession(sessionId: string, command: ParsedSessionCommand, ctx: SessionCommandContext) { if (getSession(sessionId)) return createSession({ id: sessionId, profile: ctx.profile, source: 'cli', model: ctx.model, - title: 'Bridge command', + title: buildCommandSessionTitle(command), }) } +function buildCommandSessionTitle(command: ParsedSessionCommand): string { + const prefix = `[${command.rawName}]` + const args = command.args.replace(/\s+/g, ' ').trim() + return args ? `${prefix} ${args}`.slice(0, 120) : prefix +} + function persistCommandMessage(sessionId: string, state: SessionState, content: string) { const now = Math.floor(Date.now() / 1000) const id = addMessage({ diff --git a/tests/server/session-command-plan.test.ts b/tests/server/session-command-plan.test.ts index d77c9a3b..6c345afa 100644 --- a/tests/server/session-command-plan.test.ts +++ b/tests/server/session-command-plan.test.ts @@ -122,6 +122,36 @@ describe('plan session command', () => { expect(namespaceEmit).not.toHaveBeenCalledWith('session.command', expect.anything()) }) + it('creates a new slash-command session with a command-derived title', async () => { + getSessionMock.mockReturnValueOnce(null) + const state = { messages: [], isWorking: false, events: [], queue: [] } + const { bridge, nsp, runQueuedItem, sessionMap, socket } = makeContext(state, { + handled: true, + type: 'goal', + action: 'set', + message: 'Goal set.', + kickoff_prompt: 'build a todo app', + }) + const { handleSessionCommand, parseSessionCommand } = await import('../../packages/server/src/services/hermes/run-chat/session-command') + const command = parseSessionCommand('/goal build a todo app')! + + await handleSessionCommand('session-1', command, { + nsp: nsp as any, + socket: socket as any, + sessionMap, + bridge: bridge as any, + profile: 'default', + runQueuedItem, + }) + + expect(createSessionMock).toHaveBeenCalledWith(expect.objectContaining({ + id: 'session-1', + profile: 'default', + source: 'cli', + title: '[goal] build a todo app', + })) + }) + it('starts an idle /skill command with expanded storage and visible command display', async () => { const state = { messages: [], isWorking: false, events: [], queue: [] } const { bridge, namespaceEmit, nsp, runQueuedItem, sessionMap, socket } = makeContext(state, {