From a1921c43cc09e290d14df9129496bde385ceb4e4 Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:22:26 -0600 Subject: [PATCH] fix(tui): prefer exact slash command matches (#15813) --- .../src/__tests__/createSlashHandler.test.ts | 39 +++++++++++++++++++ ui-tui/src/app/createSlashHandler.ts | 33 +++++++++------- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 0ba81cd905..68e66b0c0a 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -298,6 +298,45 @@ describe('createSlashHandler', () => { expect(ctx.transcript.panel).toHaveBeenCalledWith(expect.any(String), expect.any(Array)) }) + it('lets exact catalog commands win over longer prefix matches', async () => { + const ctx = buildCtx({ + local: { + catalog: { + canon: { + '/status': '/status', + '/statusbar': '/statusbar' + } + } + } + }) + + expect(createSlashHandler(ctx)('/status')).toBe(true) + await vi.waitFor(() => { + expect(ctx.gateway.gw.request).toHaveBeenCalledWith('slash.exec', { + command: 'status', + session_id: null + }) + }) + expect(ctx.transcript.sys).not.toHaveBeenCalledWith(expect.stringContaining('ambiguous command')) + }) + + it('keeps ambiguous prefix handling when there is no exact catalog match', () => { + const ctx = buildCtx({ + local: { + catalog: { + canon: { + '/status': '/status', + '/statusbar': '/statusbar' + } + } + } + }) + + expect(createSlashHandler(ctx)('/stat')).toBe(true) + expect(ctx.transcript.sys).toHaveBeenCalledWith('ambiguous command: /status, /statusbar') + expect(ctx.gateway.gw.request).not.toHaveBeenCalled() + }) + it('falls through to command.dispatch for skill commands and sends the message', async () => { const skillMessage = 'Use this skill to do X.\n\n## Steps\n1. First step' diff --git a/ui-tui/src/app/createSlashHandler.ts b/ui-tui/src/app/createSlashHandler.ts index 425e778ef3..7bd19431ed 100644 --- a/ui-tui/src/app/createSlashHandler.ts +++ b/ui-tui/src/app/createSlashHandler.ts @@ -47,23 +47,30 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b if (catalog?.canon) { const needle = `/${parsed.name}`.toLowerCase() + const exact = Object.entries(catalog.canon).find(([alias]) => alias.toLowerCase() === needle)?.[1] - const matches = [ - ...new Set( - Object.entries(catalog.canon) - .filter(([alias]) => alias.startsWith(needle)) - .map(([, canon]) => canon) - ) - ] + if (exact) { + if (exact.toLowerCase() !== needle) { + return handler(`${exact}${argTail}`) + } + } else { + const matches = [ + ...new Set( + Object.entries(catalog.canon) + .filter(([alias]) => alias.startsWith(needle)) + .map(([, canon]) => canon) + ) + ] - if (matches.length === 1 && matches[0]!.toLowerCase() !== needle) { - return handler(`${matches[0]}${argTail}`) - } + if (matches.length === 1 && matches[0]!.toLowerCase() !== needle) { + return handler(`${matches[0]}${argTail}`) + } - if (matches.length > 1) { - sys(`ambiguous command: ${matches.slice(0, 6).join(', ')}${matches.length > 6 ? ', …' : ''}`) + if (matches.length > 1) { + sys(`ambiguous command: ${matches.slice(0, 6).join(', ')}${matches.length > 6 ? ', …' : ''}`) - return true + return true + } } }