Add Assistant session search command

This commit is contained in:
rcourtman
2026-06-08 02:09:23 +01:00
parent 84a7dfe5d8
commit 8576eaed06
4 changed files with 91 additions and 13 deletions
@@ -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...');
@@ -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',
@@ -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',
@@ -1270,7 +1270,10 @@ export const AIChat: Component<AIChatProps> = (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<AIChatProps> = (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<AIChatProps> = (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<AIChatProps> = (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<AIChatProps> = (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<AIChatProps> = (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) => {