Expose Assistant fixture commands

This commit is contained in:
rcourtman
2026-06-08 00:24:25 +01:00
parent a06d7686b6
commit 17ef976140
12 changed files with 283 additions and 45 deletions
@@ -697,6 +697,14 @@ timers when the stream already has a newer workflow/tool state.
the real `provider_retry` workflow state, including attempt metadata and
`retry_after_ms`, so browser proof of retry countdown behavior does not
depend on external provider availability or API spend.
Dev/test fixture prompts are also part of the Assistant command-discovery
surface: slash autocomplete and Assistant command help must expose an
insertable `/fixture` command in development/test builds, search it by
canonical fixture names such as `provider-retry`, and submit completed
`/fixture <name>` prompts through the normal chat send path so
`maybeRunAIChatDevStreamFixture` remains the single local execution boundary.
Production command surfaces must omit that dev command instead of advertising
a fixture mode that the runtime will not intercept.
The local `stream-idle` fixture must exercise the real `stream_idle`
workflow state after selected-provider startup so browser proof of visible
idle liveness does not depend on making a real provider pause on demand.
@@ -134,6 +134,10 @@ Assistant local stream fixtures are part of the same frontend API contract:
`frontend-modern/src/api/aiChatDevStreamFixture.ts` may short-circuit only
explicit `/fixture ...` prompts in development or test mode, must emit the same
typed stream event sequence as live chat, and must never open a provider request.
That helper also owns the exported fixture-name and alias catalog consumed by
Assistant command discovery; frontend command surfaces may search and insert
`/fixture` from those exports, but they must not duplicate the fixture registry
or advertise fixture commands in production.
Fixtures used for visible stream proof must pace status/tool/content events
enough for the browser to paint the intermediate state, including keeping the
`/fixture tool-burst` running tool row visible before the matching `tool_end`;
@@ -16,7 +16,11 @@ vi.mock('@/utils/logger', () => ({
}));
import { AIChatAPI, createAIChatStreamPaintCheckpointPredicate } from '@/api/aiChat';
import { maybeRunAIChatDevStreamFixture } from '@/api/aiChatDevStreamFixture';
import {
AI_CHAT_DEV_STREAM_FIXTURE_ALIAS_NAMES,
AI_CHAT_DEV_STREAM_FIXTURE_NAMES,
maybeRunAIChatDevStreamFixture,
} from '@/api/aiChatDevStreamFixture';
import { apiFetch, apiFetchJSON } from '@/utils/apiClient';
import { logger } from '@/utils/logger';
@@ -302,10 +306,7 @@ describe('AIChatAPI', () => {
expect(onEvent.mock.calls.map(([event]) => event.type)).toEqual(['session']);
await vi.advanceTimersByTimeAsync(25);
expect(onEvent.mock.calls.map(([event]) => event.type)).toEqual([
'session',
'workflow_state',
]);
expect(onEvent.mock.calls.map(([event]) => event.type)).toEqual(['session', 'workflow_state']);
await vi.advanceTimersByTimeAsync(25);
expect(onEvent.mock.calls.map(([event]) => event.type)).toEqual([
@@ -552,6 +553,13 @@ describe('AIChatAPI', () => {
});
});
it('exports fixture names and aliases for command discovery', () => {
expect(AI_CHAT_DEV_STREAM_FIXTURE_NAMES).toContain('provider-retry');
expect(AI_CHAT_DEV_STREAM_FIXTURE_NAMES).toContain('send-hold');
expect(AI_CHAT_DEV_STREAM_FIXTURE_NAMES).not.toContain('/fixture provider-retry');
expect(AI_CHAT_DEV_STREAM_FIXTURE_ALIAS_NAMES).toEqual(['burst-tool', 'queued-follow-up']);
});
it('runs the pending-tool dev stream fixture without opening a provider request', async () => {
const onEvent = vi.fn();
@@ -26,9 +26,14 @@ const AI_CHAT_DEV_STREAM_FIXTURE_ALIASES: Record<
'/fixture queued-follow-up': '/fixture queue-drain',
};
const availableFixtureNames = AI_CHAT_DEV_STREAM_FIXTURE_PROMPTS.map((prompt) =>
export const AI_CHAT_DEV_STREAM_FIXTURE_NAMES = AI_CHAT_DEV_STREAM_FIXTURE_PROMPTS.map((prompt) =>
prompt.replace('/fixture ', ''),
);
export const AI_CHAT_DEV_STREAM_FIXTURE_ALIAS_NAMES = Object.keys(
AI_CHAT_DEV_STREAM_FIXTURE_ALIASES,
).map((prompt) => prompt.replace('/fixture ', ''));
const availableFixtureNames = AI_CHAT_DEV_STREAM_FIXTURE_NAMES;
const DEFAULT_DEV_FIXTURE_STEP_DELAY_MS = 140;
const TEST_FIXTURE_STEP_DELAY_MS = 0;
@@ -656,7 +661,7 @@ const buildLongOutputFixtureEvents = (model?: string): AIChatStreamEvent[] => [
{
type: 'content',
data: {
text: 'The long-output fixture kept a bounded plain-text tool preview visible while preserving the full output in details.',
text: 'The long-output fixture kept the full tool output available in details without flooding the transcript.',
},
},
{
@@ -3,6 +3,7 @@ import ActivityIcon from 'lucide-solid/icons/activity';
import ClockIcon from 'lucide-solid/icons/clock';
import CopyIcon from 'lucide-solid/icons/copy';
import DownloadIcon from 'lucide-solid/icons/download';
import FlaskConicalIcon from 'lucide-solid/icons/flask-conical';
import GitForkIcon from 'lucide-solid/icons/git-fork';
import KeyRoundIcon from 'lucide-solid/icons/key-round';
import Minimize2Icon from 'lucide-solid/icons/minimize-2';
@@ -47,6 +48,8 @@ export const AssistantSlashCommandIcon = (props: { action: AssistantSlashCommand
return <CopyIcon class="h-4 w-4" aria-hidden="true" />;
case 'export':
return <DownloadIcon class="h-4 w-4" aria-hidden="true" />;
case 'fixture':
return <FlaskConicalIcon class="h-4 w-4" aria-hidden="true" />;
case 'fork':
return <GitForkIcon class="h-4 w-4" aria-hidden="true" />;
case 'undo':
@@ -155,7 +158,9 @@ export function SlashCommandAutocomplete(props: SlashCommandAutocompleteProps) {
type="button"
role="option"
aria-selected={index() === selectedIndex()}
aria-label={`Run /${command.name}: ${command.description}`}
aria-label={`${
command.insertText ? 'Insert' : 'Run'
} /${command.name}: ${command.description}`}
class={`flex w-full items-start gap-3 px-3 py-2.5 text-left transition-colors hover:bg-surface-hover ${
index() === selectedIndex() ? 'bg-surface-hover' : ''
}`}
@@ -906,7 +906,7 @@ describe('AIChat', () => {
role: 'assistant',
content: '',
error:
'Unknown Assistant fixture "/fixture typo-check". Available fixtures: devices, assistant-stream, tool-burst, context-group, pending-tool, long-output, provider-retry, stream-idle, queue-hold, queue-drain, compacted-artifact, skipped-tool.',
'Unknown Assistant fixture "/fixture typo-check". Available fixtures: devices, assistant-stream, send-hold, tool-burst, workflow-burst, context-group, status-boundary, pending-tool, long-output, provider-retry, stream-idle, queue-hold, queue-drain, compacted-artifact, skipped-tool.',
timestamp: new Date('2026-06-05T10:00:00Z'),
model: 'openrouter:qwen/qwen3.7-plus',
streamEvents: [
@@ -1334,9 +1334,7 @@ describe('AIChat', () => {
undefined,
undefined,
);
expect(mockChat.setModel).not.toHaveBeenCalledWith(
'openrouter:deepseek/deepseek-v4-pro',
);
expect(mockChat.setModel).not.toHaveBeenCalledWith('openrouter:deepseek/deepseek-v4-pro');
expect(
screen.queryByRole('status', { name: 'Assistant provider readiness route adopted' }),
).not.toBeInTheDocument();
@@ -1784,6 +1782,38 @@ describe('AIChat', () => {
expect(mockChat.sendMessage).not.toHaveBeenCalled();
});
it('inserts the local fixture command from slash suggestions without sending yet', async () => {
renderChat();
const textarea = screen.getByPlaceholderText(
'Ask about your infrastructure...',
) as HTMLTextAreaElement;
fireEvent.input(textarea, { target: { value: '/fi' } });
expect(screen.getByRole('option', { name: /Insert \/fixture/ })).toBeInTheDocument();
fireEvent.keyDown(textarea, { key: 'Enter' });
await waitFor(() => expect(textarea.value).toBe('/fixture '));
expect(mockChat.sendMessage).not.toHaveBeenCalled();
});
it('runs a local fixture slash command through the normal chat send path', async () => {
renderChat();
const textarea = screen.getByPlaceholderText('Ask about your infrastructure...');
fireEvent.input(textarea, { target: { value: '/fixture provider-retry' } });
fireEvent.keyDown(textarea, { key: 'Enter' });
await waitFor(() => {
expect(mockChat.sendMessage).toHaveBeenCalledWith(
'/fixture provider-retry',
undefined,
undefined,
);
});
});
it('omits unavailable session commands from slash suggestions', () => {
renderChat();
const textarea = screen.getByPlaceholderText('Ask about your infrastructure...');
@@ -5502,16 +5532,12 @@ describe('AIChat', () => {
expect(status).not.toHaveTextContent('Reading current Pulse inventory.');
vi.advanceTimersByTime(1_000);
await waitFor(() =>
expect(status).toHaveTextContent('Reading current Pulse inventory.'),
);
await waitFor(() => expect(status).toHaveTextContent('Reading current Pulse inventory.'));
expect(status).not.toHaveTextContent('Preparing Pulse context.');
expect(status).not.toHaveTextContent('OpenRouter is starting the response.');
vi.advanceTimersByTime(1_000);
await waitFor(() =>
expect(status).toHaveTextContent('OpenRouter is starting the response.'),
);
await waitFor(() => expect(status).toHaveTextContent('OpenRouter is starting the response.'));
expect(status).not.toHaveTextContent('Preparing Pulse context.');
expect(status).not.toHaveTextContent('Reading current Pulse inventory.');
});
@@ -5579,9 +5605,7 @@ describe('AIChat', () => {
vi.advanceTimersByTime(1_000);
await waitFor(() =>
expect(status).toHaveTextContent(
'Reading current Pulse inventory. · 1 follow-up queued',
),
expect(status).toHaveTextContent('Reading current Pulse inventory. · 1 follow-up queued'),
);
expect(status).not.toHaveTextContent('Preparing Pulse context.');
expect(status).not.toHaveTextContent('OpenRouter is starting the response.');
@@ -32,6 +32,17 @@ describe('AssistantCommandHelpDialog', () => {
);
});
it('finds local stream fixtures from command help search', () => {
render(() => <AssistantCommandHelpDialog onClose={vi.fn()} onRunCommand={vi.fn()} />);
fireEvent.input(screen.getByLabelText('Search Assistant commands'), {
target: { value: 'provider-retry' },
});
expect(screen.getByRole('option', { name: /\/fixture/ })).toBeInTheDocument();
expect(screen.getByRole('option', { name: /\/fixture/ })).toHaveTextContent('/fixture');
});
it('moves command selection with arrow keys before running', () => {
const onRunCommand = vi.fn();
render(() => <AssistantCommandHelpDialog onClose={vi.fn()} onRunCommand={onRunCommand} />);
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen } from '@solidjs/testing-library';
import { ASSISTANT_SLASH_COMMANDS } from '../assistantSlashCommands';
import { filterAssistantSlashCommands } from '../assistantSlashCommands';
import { SlashCommandAutocomplete } from '../SlashCommandAutocomplete';
afterEach(cleanup);
@@ -69,6 +69,7 @@ describe('SlashCommandAutocomplete', () => {
it('wraps keyboard selection from the first command to the last command', () => {
const onSelect = vi.fn();
const lastCommand = filterAssistantSlashCommands('').at(-1);
render(() => (
<SlashCommandAutocomplete
query=""
@@ -96,8 +97,8 @@ describe('SlashCommandAutocomplete', () => {
expect(onSelect).toHaveBeenCalledWith(
expect.objectContaining({
action: 'redo',
name: 'redo',
action: lastCommand?.action,
name: lastCommand?.name,
}),
);
});
@@ -113,14 +114,31 @@ describe('SlashCommandAutocomplete', () => {
/>
));
const activeCommands = filterAssistantSlashCommands('');
const options = screen.getAllByRole('option');
expect(options).toHaveLength(ASSISTANT_SLASH_COMMANDS.length);
expect(options).toHaveLength(activeCommands.length);
expect(options.map((option) => option.getAttribute('aria-label'))).toContain(
'Run /compact: Summarize older turns and keep this session moving',
);
expect(container.querySelectorAll('[role="option"] svg')).toHaveLength(
ASSISTANT_SLASH_COMMANDS.length,
);
expect(container.querySelectorAll('[role="option"] svg')).toHaveLength(activeCommands.length);
});
it('surfaces dev stream fixtures through slash command search', () => {
render(() => (
<SlashCommandAutocomplete
query="provider-retry"
visible
position={{ top: 58, left: 0 }}
onClose={vi.fn()}
onSelect={vi.fn()}
/>
));
expect(
screen.getByRole('option', {
name: /Insert \/fixture: Run a local stream fixture by name/,
}),
).toBeInTheDocument();
});
it('renders an empty state when visible command search has no enabled options', () => {
@@ -32,6 +32,8 @@ describe('assistantSlashCommands', () => {
expect(parseAssistantSlashCommand('/fork')).toBe('fork');
expect(parseAssistantSlashCommand('/undo')).toBe('undo');
expect(parseAssistantSlashCommand('/redo')).toBe('redo');
expect(parseAssistantSlashCommand('/fixture')).toBe('fixture');
expect(parseAssistantSlashCommand('/fixtures')).toBe('fixture');
});
it('normalizes casing and surrounding whitespace', () => {
@@ -61,7 +63,19 @@ describe('assistantSlashCommands', () => {
expect(parseAssistantSlashCommand('/model openrouter:qwen/qwen3.7-plus')).toBeNull();
});
it('does not parse arguments for non-model commands', () => {
it('parses dev fixture commands with stream fixture arguments', () => {
expect(parseAssistantSlashCommandInput('/fixture provider-retry')).toEqual({
action: 'fixture',
args: 'provider-retry',
});
expect(parseAssistantSlashCommandInput('/fixtures queued-follow-up')).toEqual({
action: 'fixture',
args: 'queued-follow-up',
});
expect(parseAssistantSlashCommand('/fixture provider-retry')).toBeNull();
});
it('does not parse arguments for commands that do not accept them', () => {
expect(parseAssistantSlashCommandInput('/copy this sentence')).toBeNull();
expect(parseAssistantSlashCommandInput('/status now')).toBeNull();
});
@@ -80,6 +94,7 @@ describe('assistantSlashCommands', () => {
'fork',
'undo',
'redo',
'fixture',
]);
expect(filterAssistantSlashCommands('resume').map((command) => command.name)).toEqual([
'sessions',
@@ -90,6 +105,7 @@ describe('assistantSlashCommands', () => {
expect(filterAssistantSlashCommands('provider').map((command) => command.name)).toEqual([
'providers',
'models',
'fixture',
]);
expect(filterAssistantSlashCommands('connect').map((command) => command.name)).toEqual([
'providers',
@@ -100,6 +116,27 @@ describe('assistantSlashCommands', () => {
expect(filterAssistantSlashCommands('commands').map((command) => command.name)).toEqual([
'help',
]);
expect(filterAssistantSlashCommands('provider-retry').map((command) => command.name)).toEqual([
'fixture',
]);
});
it('keeps dev fixture commands out of production command surfaces', () => {
expect(
filterAssistantSlashCommands('', undefined, {
includeDevCommands: false,
}).map((command) => command.name),
).not.toContain('fixture');
expect(
filterAssistantSlashCommands('provider-retry', undefined, {
includeDevCommands: false,
}),
).toEqual([]);
expect(
parseAssistantSlashCommandInput('/fixture provider-retry', {
includeDevCommands: false,
}),
).toBeNull();
});
it('filters disabled commands from the prompt slash list by default', () => {
@@ -154,5 +191,7 @@ describe('assistantSlashCommands', () => {
]);
const status = filterAssistantSlashCommands('status')[0];
expect(getAssistantSlashCommandTokens(status)).toEqual(['status', 'runtime', 'health']);
const fixture = filterAssistantSlashCommands('fixture')[0];
expect(getAssistantSlashCommandTokens(fixture)).toEqual(['fixture', 'fixtures']);
});
});
@@ -1,7 +1,13 @@
import {
AI_CHAT_DEV_STREAM_FIXTURE_ALIAS_NAMES,
AI_CHAT_DEV_STREAM_FIXTURE_NAMES,
} from '@/api/aiChatDevStreamFixture';
export type AssistantSlashCommandAction =
| 'compact'
| 'copy'
| 'export'
| 'fixture'
| 'fork'
| 'help'
| 'models'
@@ -15,9 +21,12 @@ export type AssistantSlashCommandAction =
export interface AssistantSlashCommand {
action: AssistantSlashCommandAction;
aliases?: string[];
acceptsArgs?: boolean;
disabled?: boolean;
disabledReason?: string;
description: string;
insertText?: string;
keywords?: string[];
name: string;
}
@@ -27,14 +36,44 @@ export type AssistantSlashCommandAvailability = Partial<
export interface AssistantSlashCommandFilterOptions {
availability?: AssistantSlashCommandAvailability;
includeDevCommands?: boolean;
includeDisabled?: boolean;
}
export interface AssistantSlashCommandParseOptions {
includeDevCommands?: boolean;
}
export interface ParsedAssistantSlashCommand {
action: AssistantSlashCommandAction;
args: string;
}
const isAssistantDevCommandSurfaceEnabled = () =>
import.meta.env.DEV || import.meta.env.MODE === 'test';
const includeDevCommandsForOptions = (includeDevCommands?: boolean) =>
includeDevCommands ?? isAssistantDevCommandSurfaceEnabled();
const DEV_ASSISTANT_SLASH_COMMANDS: AssistantSlashCommand[] = [
{
name: 'fixture',
aliases: ['fixtures'],
acceptsArgs: true,
action: 'fixture',
description: 'Run a local stream fixture by name (/fixture provider-retry)',
insertText: '/fixture ',
keywords: [
'dev',
'local',
'stream',
'test',
...AI_CHAT_DEV_STREAM_FIXTURE_NAMES,
...AI_CHAT_DEV_STREAM_FIXTURE_ALIAS_NAMES,
],
},
];
export const ASSISTANT_SLASH_COMMANDS: AssistantSlashCommand[] = [
{
name: 'help',
@@ -63,6 +102,7 @@ export const ASSISTANT_SLASH_COMMANDS: AssistantSlashCommand[] = [
{
name: 'models',
aliases: ['model', 'mo'],
acceptsArgs: true,
action: 'models',
description: 'Open model search or set a route (/model qwen or /model provider:model-id)',
},
@@ -105,15 +145,28 @@ export const ASSISTANT_SLASH_COMMANDS: AssistantSlashCommand[] = [
},
];
const commandByToken = new Map<string, AssistantSlashCommandAction>(
ASSISTANT_SLASH_COMMANDS.flatMap((command) => [
[command.name, command.action],
const assistantSlashCommandsForOptions = (includeDevCommands?: boolean): AssistantSlashCommand[] =>
includeDevCommandsForOptions(includeDevCommands)
? [...ASSISTANT_SLASH_COMMANDS, ...DEV_ASSISTANT_SLASH_COMMANDS]
: ASSISTANT_SLASH_COMMANDS;
const commandEntriesForOptions = (
includeDevCommands?: boolean,
): Array<readonly [string, AssistantSlashCommandAction]> =>
assistantSlashCommandsForOptions(includeDevCommands).flatMap((command) => [
[command.name, command.action] as const,
...(command.aliases || []).map((alias) => [alias, command.action] as const),
]),
);
]);
const commandByToken = (includeDevCommands?: boolean) =>
new Map<string, AssistantSlashCommandAction>(commandEntriesForOptions(includeDevCommands));
const commandForAction = (action: AssistantSlashCommandAction, includeDevCommands?: boolean) =>
assistantSlashCommandsForOptions(includeDevCommands).find((command) => command.action === action);
export const parseAssistantSlashCommandInput = (
input: string,
options: AssistantSlashCommandParseOptions = {},
): ParsedAssistantSlashCommand | null => {
const trimmed = input.trim();
if (!trimmed.startsWith('/')) return null;
@@ -124,17 +177,21 @@ export const parseAssistantSlashCommandInput = (
const token = match[1].toLowerCase();
if (!token) return null;
const action = commandByToken.get(token);
const action = commandByToken(options.includeDevCommands).get(token);
if (!action) return null;
const args = (match[2] || '').trim();
if (args && action !== 'models') return null;
const command = commandForAction(action, options.includeDevCommands);
if (args && !command?.acceptsArgs) return null;
return { action, args };
};
export const parseAssistantSlashCommand = (input: string): AssistantSlashCommandAction | null => {
const parsed = parseAssistantSlashCommandInput(input);
export const parseAssistantSlashCommand = (
input: string,
options: AssistantSlashCommandParseOptions = {},
): AssistantSlashCommandAction | null => {
const parsed = parseAssistantSlashCommandInput(input, options);
if (!parsed || parsed.args) return null;
return parsed.action;
};
@@ -156,11 +213,21 @@ const commandWithAvailability = (
};
};
const keywordMatchesQuery = (keyword: string, query: string) => {
const normalizedKeyword = keyword.toLowerCase();
return (
normalizedKeyword === query ||
normalizedKeyword.startsWith(`${query}-`) ||
normalizedKeyword.startsWith(`${query}_`)
);
};
const commandMatchesQuery = (command: AssistantSlashCommand, query: string) => {
if (!query) return true;
if (command.name.toLowerCase().includes(query)) return true;
if (command.description.toLowerCase().includes(query)) return true;
return commandAliases(command).some((alias) => alias.toLowerCase().includes(query));
if (commandAliases(command).some((alias) => alias.toLowerCase().includes(query))) return true;
return (command.keywords || []).some((keyword) => keywordMatchesQuery(keyword, query));
};
const commandMatchScore = (command: AssistantSlashCommand, query: string) => {
@@ -179,13 +246,13 @@ export const getAssistantSlashCommandTokens = (command: AssistantSlashCommand):
export const filterAssistantSlashCommands = (
query: string,
limit = ASSISTANT_SLASH_COMMANDS.length,
limit?: number,
options: AssistantSlashCommandFilterOptions = {},
): AssistantSlashCommand[] => {
const normalizedQuery = normalizeSlashQuery(query);
return ASSISTANT_SLASH_COMMANDS.map((command) =>
commandWithAvailability(command, options.availability),
)
const commands = assistantSlashCommandsForOptions(options.includeDevCommands);
return commands
.map((command) => commandWithAvailability(command, options.availability))
.filter((command) => options.includeDisabled || !command.disabled)
.filter((command) => commandMatchesQuery(command, normalizedQuery))
.map((command, index) => ({ command, index }))
@@ -195,6 +262,6 @@ export const filterAssistantSlashCommands = (
commandMatchScore(right.command, normalizedQuery);
return scoreDelta || left.index - right.index;
})
.slice(0, limit)
.slice(0, limit ?? commands.length)
.map(({ command }) => command);
};
@@ -2863,6 +2863,43 @@ export const AIChat: Component<AIChatProps> = (props) => {
setShowCommandHelp(true);
};
const insertSlashCommandDraft = (command: AssistantSlashCommand) => {
const nextInput = command.insertText || `/${command.name}`;
setShowCommandHelp(false);
setSlashCommandActive(false);
setSlashCommandQuery('');
setInput(nextInput);
queueMicrotask(() => {
if (textareaRef) {
textareaRef.selectionStart = nextInput.length;
textareaRef.selectionEnd = nextInput.length;
}
resizeTextarea();
focusComposer();
});
};
const runAssistantFixtureSlashCommand = (args: string) => {
const fixturePrompt = args.trim() ? `/fixture ${args.trim()}` : '/fixture';
const submittedMentions: MentionResource[] = [];
const sendPromise = chat.sendMessage(fixturePrompt, undefined, undefined);
addPromptHistoryEntry(fixturePrompt, submittedMentions);
resetPromptHistoryNavigation();
sendPromise
.then((ok) => {
if (!ok) {
restoreFailedSubmitDraft(fixturePrompt, submittedMentions);
}
})
.catch((error) => {
logger.warn('[AIChat] Failed to run Assistant fixture command:', error);
restoreFailedSubmitDraft(fixturePrompt, submittedMentions);
});
clearLocalComposerCommand();
setShowCommandHelp(false);
return true;
};
const executeSlashCommand = (command: AssistantSlashCommandAction, args = '') => {
const commandArgs = args.trim();
const disabledReason = disabledAssistantCommandReason(command);
@@ -2882,6 +2919,10 @@ export const AIChat: Component<AIChatProps> = (props) => {
return consumed;
}
if (command === 'fixture') {
return runAssistantFixtureSlashCommand(commandArgs);
}
clearLocalComposerCommand();
switch (command) {
@@ -3129,12 +3170,20 @@ export const AIChat: Component<AIChatProps> = (props) => {
const handleSlashCommandSelect = (command: AssistantSlashCommand) => {
setSlashCommandActive(false);
setSlashCommandQuery('');
if (command.insertText) {
insertSlashCommandDraft(command);
return;
}
setInput(`/${command.name}`);
executeSlashCommand(command.action);
};
const handleCommandHelpRun = (command: AssistantSlashCommand) => {
setShowCommandHelp(false);
if (command.insertText) {
queueMicrotask(() => insertSlashCommandDraft(command));
return;
}
queueMicrotask(() => executeSlashCommand(command.action));
};
@@ -2860,7 +2860,7 @@ class SubsystemLookupTest(unittest.TestCase):
{
"heading": "## Shared Boundaries",
"path": "internal/api/access_control_handlers.go",
"line": 432,
"line": 436,
"heading_line": 117,
}
],