diff --git a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx index 2538b7181..443009471 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx +++ b/frontend-modern/src/components/AI/Chat/__tests__/AIChat.test.tsx @@ -2046,6 +2046,42 @@ describe('AIChat', () => { expect(mockChat.sendMessage).not.toHaveBeenCalled(); }); + it('searches Assistant sessions from /sessions arguments without sending a provider prompt', async () => { + mockAIChatAPI.listSessions + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { + id: 's-backup', + title: 'Backup Patrol Follow-up', + created_at: '', + updated_at: '', + message_count: 3, + }, + ]); + + renderChat(); + await waitFor(() => { + expect(mockAIChatAPI.listSessions).toHaveBeenCalledWith({ limit: 30 }); + }); + mockAIChatAPI.listSessions.mockClear(); + const textarea = screen.getByPlaceholderText('Ask about your infrastructure...'); + + fireEvent.input(textarea, { target: { value: '/sessions backup' } }); + fireEvent.keyDown(textarea, { key: 'Enter' }); + + const searchInput = await screen.findByPlaceholderText('Search sessions...'); + await waitFor(() => { + expect(searchInput).toHaveValue('backup'); + expect(document.activeElement).toBe(searchInput); + }); + await waitFor(() => { + expect(mockAIChatAPI.listSessions).toHaveBeenCalledWith({ search: 'backup', limit: 30 }); + expect(screen.getByText('Backup Patrol Follow-up')).toBeInTheDocument(); + }); + expect(mockAIChatAPI.listSessions).not.toHaveBeenCalledWith({ limit: 30 }); + expect(mockChat.sendMessage).not.toHaveBeenCalled(); + }); + it('opens the model selector from /models without sending a provider prompt', async () => { renderChat(); const textarea = screen.getByPlaceholderText('Ask about your infrastructure...'); diff --git a/frontend-modern/src/components/AI/Chat/__tests__/assistantSlashCommands.test.ts b/frontend-modern/src/components/AI/Chat/__tests__/assistantSlashCommands.test.ts index 59063ea62..dc8e1c3a4 100644 --- a/frontend-modern/src/components/AI/Chat/__tests__/assistantSlashCommands.test.ts +++ b/frontend-modern/src/components/AI/Chat/__tests__/assistantSlashCommands.test.ts @@ -63,6 +63,22 @@ describe('assistantSlashCommands', () => { expect(parseAssistantSlashCommand('/model openrouter:qwen/qwen3.7-plus')).toBeNull(); }); + it('parses session commands with search arguments for local composer handling', () => { + expect(parseAssistantSlashCommandInput('/sessions backup patrol')).toEqual({ + action: 'sessions', + args: 'backup patrol', + }); + expect(parseAssistantSlashCommandInput('/resume blinds automation')).toEqual({ + action: 'sessions', + args: 'blinds automation', + }); + expect(parseAssistantSlashCommandInput('/continue router')).toEqual({ + action: 'sessions', + args: 'router', + }); + expect(parseAssistantSlashCommand('/sessions backup patrol')).toBeNull(); + }); + it('parses dev fixture commands with stream fixture arguments', () => { expect(parseAssistantSlashCommandInput('/fixture provider-retry')).toEqual({ action: 'fixture', diff --git a/frontend-modern/src/components/AI/Chat/assistantSlashCommands.ts b/frontend-modern/src/components/AI/Chat/assistantSlashCommands.ts index 8f40ab184..6674b8af8 100644 --- a/frontend-modern/src/components/AI/Chat/assistantSlashCommands.ts +++ b/frontend-modern/src/components/AI/Chat/assistantSlashCommands.ts @@ -90,8 +90,9 @@ export const ASSISTANT_SLASH_COMMANDS: AssistantSlashCommand[] = [ { name: 'sessions', aliases: ['resume', 'continue'], + acceptsArgs: true, action: 'sessions', - description: 'Open Assistant session history', + description: 'Search or resume Assistant sessions (/sessions backup)', }, { name: 'compact', diff --git a/frontend-modern/src/components/AI/Chat/index.tsx b/frontend-modern/src/components/AI/Chat/index.tsx index 7c6ba311e..b5c56ac28 100644 --- a/frontend-modern/src/components/AI/Chat/index.tsx +++ b/frontend-modern/src/components/AI/Chat/index.tsx @@ -1270,7 +1270,10 @@ export const AIChat: Component = (props) => { resetSessionRename(); }; const focusSessionSearch = () => { - queueMicrotask(() => sessionSearchInputRef?.focus()); + queueMicrotask(() => { + sessionSearchInputRef?.focus(); + window.setTimeout(() => sessionSearchInputRef?.focus(), 0); + }); }; const focusSessionTriggerAfterClose = () => { const trigger = sessionButtonRef; @@ -2954,6 +2957,15 @@ export const AIChat: Component = (props) => { return true; }; + const runSessionsSlashCommand = (args: string) => { + setShowCommandHelp(false); + if (args.trim()) { + void openSessionPicker(args); + return; + } + void handleToggleSessions(); + }; + const executeSlashCommand = (command: AssistantSlashCommandAction, args = '') => { const commandArgs = args.trim(); const disabledReason = disabledAssistantCommandReason(command); @@ -2985,8 +2997,7 @@ export const AIChat: Component = (props) => { void handleNewConversation(); break; case 'sessions': - setShowCommandHelp(false); - void handleToggleSessions(); + runSessionsSlashCommand(commandArgs); break; case 'compact': setShowCommandHelp(false); @@ -3490,15 +3501,7 @@ export const AIChat: Component = (props) => { } }; - const handleToggleSessions = async () => { - const next = !showSessions(); - if (!next) { - setShowSessions(false); - setSessionRefreshLoading(false); - resetSessionSearch(); - return; - } - + const openSessionPicker = async (initialSearchQuery = '') => { if (sessionButtonRef) { const rect = sessionButtonRef.getBoundingClientRect(); setSessionDropdownPosition({ @@ -3507,9 +3510,19 @@ export const AIChat: Component = (props) => { }); } + const searchQuery = initialSearchQuery.trim(); resetSessionSearch(); + if (searchQuery) { + setSessionSearchQuery(searchQuery); + } setShowSessions(true); focusSessionSearch(); + + if (searchQuery) { + setSessionRefreshLoading(false); + return; + } + setSessionRefreshLoading(true); try { @@ -3522,6 +3535,18 @@ export const AIChat: Component = (props) => { } }; + const handleToggleSessions = async () => { + const next = !showSessions(); + if (!next) { + setShowSessions(false); + setSessionRefreshLoading(false); + resetSessionSearch(); + return; + } + + await openSessionPicker(); + }; + const formatSessionPickerMessageCount = (count: number) => `${count} ${count === 1 ? 'message' : 'messages'}`; const formatSessionPickerUpdatedAt = (session: ChatSession) => {