fix(tui): keep /title session names in sync

Route TUI /title through session.title RPC and queue titles when the session DB row is still initializing, so renamed sessions reliably appear in /resume and browse flows.
This commit is contained in:
Brooklyn Nicholson
2026-04-27 10:51:14 -05:00
parent 512c610058
commit cdfbd89ea5
5 changed files with 162 additions and 4 deletions

View File

@@ -397,6 +397,34 @@ describe('createSlashHandler', () => {
expect(rpc).not.toHaveBeenCalled()
expect(ctx.transcript.sys).toHaveBeenCalledWith('no active session — nothing to save')
})
it('/title <name> uses session.title RPC and bypasses slash.exec', async () => {
patchUiState({ sid: 'sid-abc' })
const rpc = vi.fn(() => Promise.resolve({ pending: false, title: 'my title' }))
const ctx = buildCtx({ gateway: { ...buildGateway(), rpc } })
createSlashHandler(ctx)('/title my title')
expect(rpc).toHaveBeenCalledWith('session.title', { session_id: 'sid-abc', title: 'my title' })
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
await vi.waitFor(() => {
expect(ctx.transcript.sys).toHaveBeenCalledWith('session title set: my title')
})
})
it('/title with no args fetches and displays the current title', async () => {
patchUiState({ sid: 'sid-abc' })
const rpc = vi.fn(() => Promise.resolve({ title: 'demo title' }))
const ctx = buildCtx({ gateway: { ...buildGateway(), rpc } })
createSlashHandler(ctx)('/title')
expect(rpc).toHaveBeenCalledWith('session.title', { session_id: 'sid-abc' })
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
await vi.waitFor(() => {
expect(ctx.transcript.sys).toHaveBeenCalledWith('title: demo title')
})
})
})
const buildCtx = (overrides: Partial<Ctx> = {}): Ctx => ({

View File

@@ -6,6 +6,7 @@ import type {
ConfigGetValueResponse,
ConfigSetResponse,
SessionSaveResponse,
SessionTitleResponse,
SessionSteerResponse,
SessionUndoResponse
} from '../../../gatewayTypes.js'
@@ -151,6 +152,47 @@ export const coreCommands: SlashCommand[] = [
}
},
{
help: 'set or show current session title',
name: 'title',
run: (arg, ctx) => {
if (!ctx.sid) {
return ctx.transcript.sys('no active session')
}
const title = arg.trim()
if (!arg) {
ctx.gateway
.rpc<SessionTitleResponse>('session.title', { session_id: ctx.sid })
.then(
ctx.guarded<SessionTitleResponse>(r => {
const current = (r?.title ?? '').trim()
ctx.transcript.sys(current ? `title: ${current}` : 'no title set')
})
)
.catch(ctx.guardedErr)
return
}
if (!title) {
return ctx.transcript.sys('usage: /title <your session title>')
}
ctx.gateway
.rpc<SessionTitleResponse>('session.title', { session_id: ctx.sid, title })
.then(
ctx.guarded<SessionTitleResponse>(r => {
const next = (r?.title ?? title).trim()
const suffix = r?.pending ? ' (queued while session initializes)' : ''
ctx.transcript.sys(`session title set: ${next}${suffix}`)
})
)
.catch(ctx.guardedErr)
}
},
{
help: 'toggle compact transcript',
name: 'compact',

View File

@@ -119,6 +119,12 @@ export interface SessionListResponse {
sessions?: SessionListItem[]
}
export interface SessionTitleResponse {
pending?: boolean
session_key?: string
title?: string
}
export interface SessionSaveResponse {
file?: string
}