feat: インタラクティブモードのスラッシュコマンドを行末でも認識可能にする (#406)
- スラッシュコマンド検出ロジックを commandMatcher.ts に分離 - 行頭・行末の両方でコマンドを認識し、行中は無視する仕様を実装 - conversationLoop を早期リターン + switch ディスパッチにリファクタリング - SlashCommand 定数を shared/constants に追加 - コマンドマッチングのユニットテスト36件を追加 - 行末コマンド・行中非認識のE2Eテスト6件を追加 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
09fda82677
commit
e77cb50ac1
198
src/__tests__/commandMatcher.test.ts
Normal file
198
src/__tests__/commandMatcher.test.ts
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for the slash command parser.
|
||||||
|
*
|
||||||
|
* Verifies command detection at the beginning and end of input,
|
||||||
|
* and ensures commands in the middle of text are not recognized.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { matchSlashCommand } from '../features/interactive/commandMatcher.js';
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Start-of-line detection (existing behavior)
|
||||||
|
// =================================================================
|
||||||
|
describe('start-of-line detection', () => {
|
||||||
|
it('should detect /play with task text', () => {
|
||||||
|
const result = matchSlashCommand('/play fix the login bug');
|
||||||
|
expect(result).toEqual({ command: '/play', text: 'fix the login bug' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /play without task text', () => {
|
||||||
|
const result = matchSlashCommand('/play');
|
||||||
|
expect(result).toEqual({ command: '/play', text: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /go without note', () => {
|
||||||
|
const result = matchSlashCommand('/go');
|
||||||
|
expect(result).toEqual({ command: '/go', text: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /go with user note', () => {
|
||||||
|
const result = matchSlashCommand('/go also check security');
|
||||||
|
expect(result).toEqual({ command: '/go', text: 'also check security' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /cancel', () => {
|
||||||
|
const result = matchSlashCommand('/cancel');
|
||||||
|
expect(result).toEqual({ command: '/cancel', text: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /retry', () => {
|
||||||
|
const result = matchSlashCommand('/retry');
|
||||||
|
expect(result).toEqual({ command: '/retry', text: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /replay', () => {
|
||||||
|
const result = matchSlashCommand('/replay');
|
||||||
|
expect(result).toEqual({ command: '/replay', text: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /resume', () => {
|
||||||
|
const result = matchSlashCommand('/resume');
|
||||||
|
expect(result).toEqual({ command: '/resume', text: '' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// End-of-line detection (new behavior)
|
||||||
|
// =================================================================
|
||||||
|
describe('end-of-line detection', () => {
|
||||||
|
it('should detect /play at the end with preceding text as task', () => {
|
||||||
|
const result = matchSlashCommand('fix the login bug /play');
|
||||||
|
expect(result).toEqual({ command: '/play', text: 'fix the login bug' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /go at the end with preceding text as user note', () => {
|
||||||
|
const result = matchSlashCommand('ここまでの内容で実行して /go');
|
||||||
|
expect(result).toEqual({ command: '/go', text: 'ここまでの内容で実行して' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /go at the end without preceding text', () => {
|
||||||
|
const result = matchSlashCommand('some text /go');
|
||||||
|
expect(result).toEqual({ command: '/go', text: 'some text' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /cancel at the end', () => {
|
||||||
|
const result = matchSlashCommand('やっぱりやめる /cancel');
|
||||||
|
expect(result).toEqual({ command: '/cancel', text: 'やっぱりやめる' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /retry at the end', () => {
|
||||||
|
const result = matchSlashCommand('もう一回 /retry');
|
||||||
|
expect(result).toEqual({ command: '/retry', text: 'もう一回' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /replay at the end', () => {
|
||||||
|
const result = matchSlashCommand('再実行して /replay');
|
||||||
|
expect(result).toEqual({ command: '/replay', text: '再実行して' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect /resume at the end', () => {
|
||||||
|
const result = matchSlashCommand('セッション復元 /resume');
|
||||||
|
expect(result).toEqual({ command: '/resume', text: 'セッション復元' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Middle-of-text: NOT recognized
|
||||||
|
// =================================================================
|
||||||
|
describe('middle-of-text (not recognized)', () => {
|
||||||
|
it('should not detect /go in the middle of text', () => {
|
||||||
|
const result = matchSlashCommand('テキスト中に /go を含むがコマンドではない文');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect /play in the middle of text', () => {
|
||||||
|
const result = matchSlashCommand('I want to /play around with the code later');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect /cancel in the middle of text', () => {
|
||||||
|
const result = matchSlashCommand('we should /cancel the order and redo');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect /retry in the middle of text', () => {
|
||||||
|
const result = matchSlashCommand('lets /retry that approach first');
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect /replay in the middle of text', () => {
|
||||||
|
expect(matchSlashCommand('please /replay the scenario')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect /resume in the middle of text', () => {
|
||||||
|
expect(matchSlashCommand('I want to /resume the work now')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Edge cases
|
||||||
|
// =================================================================
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should return null for empty input', () => {
|
||||||
|
expect(matchSlashCommand('')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for regular text without commands', () => {
|
||||||
|
expect(matchSlashCommand('hello world')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match command without space separator at end', () => {
|
||||||
|
expect(matchSlashCommand('text/go')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match unknown slash command', () => {
|
||||||
|
expect(matchSlashCommand('/unknown')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match unknown slash command at end', () => {
|
||||||
|
expect(matchSlashCommand('text /unknown')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize start-of-line over end-of-line', () => {
|
||||||
|
const result = matchSlashCommand('/go /cancel');
|
||||||
|
expect(result).toEqual({ command: '/go', text: '/cancel' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple spaces between text and end command', () => {
|
||||||
|
const result = matchSlashCommand('text /go');
|
||||||
|
expect(result).toEqual({ command: '/go', text: 'text' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle /play with extra spaces in task', () => {
|
||||||
|
const result = matchSlashCommand('/play fix the bug');
|
||||||
|
expect(result).toEqual({ command: '/play', text: 'fix the bug' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match /go followed by characters without space', () => {
|
||||||
|
expect(matchSlashCommand('/goextra')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match /play as prefix of another word', () => {
|
||||||
|
expect(matchSlashCommand('/playing around')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match partial command at end of input', () => {
|
||||||
|
expect(matchSlashCommand('text /gopher')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match case-insensitive commands', () => {
|
||||||
|
expect(matchSlashCommand('/Go')).toBeNull();
|
||||||
|
expect(matchSlashCommand('/PLAY')).toBeNull();
|
||||||
|
expect(matchSlashCommand('/Cancel')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match slash only', () => {
|
||||||
|
expect(matchSlashCommand('/')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not match slash with space before command name', () => {
|
||||||
|
expect(matchSlashCommand('/ go')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match last command when multiple commands at end', () => {
|
||||||
|
const result = matchSlashCommand('text /go /cancel');
|
||||||
|
expect(result).toEqual({ command: '/cancel', text: 'text /go' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -365,6 +365,67 @@ describe('/play empty task error', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Route H: End-of-line slash commands
|
||||||
|
// =================================================================
|
||||||
|
describe('end-of-line /play command', () => {
|
||||||
|
it('should return execute with preceding text as task', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['fix the login bug /play']));
|
||||||
|
setupProvider([]);
|
||||||
|
|
||||||
|
const result = await runInstruct();
|
||||||
|
|
||||||
|
expect(result.action).toBe('execute');
|
||||||
|
expect(result.task).toBe('fix the login bug');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('end-of-line /go command', () => {
|
||||||
|
it('should use preceding text as user note in summary', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['refactor auth', 'also check security /go']));
|
||||||
|
const capture = setupProvider(['Will do.', 'Refactor auth and check security.']);
|
||||||
|
|
||||||
|
const result = await runInstruct();
|
||||||
|
|
||||||
|
expect(result.action).toBe('execute');
|
||||||
|
expect(result.task).toBe('Refactor auth and check security.');
|
||||||
|
expect(capture.prompts[1]).toContain('also check security');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject end-of-line /go without prior conversation', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['実行して /go', '/cancel']));
|
||||||
|
setupProvider([]);
|
||||||
|
|
||||||
|
const result = await runInstruct();
|
||||||
|
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('end-of-line /cancel command', () => {
|
||||||
|
it('should cancel when /cancel is at the end of input', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['やっぱりやめる /cancel']));
|
||||||
|
setupProvider([]);
|
||||||
|
|
||||||
|
const result = await runInstruct();
|
||||||
|
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
expect(result.task).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('middle-of-text command is not recognized', () => {
|
||||||
|
it('should treat text with /go in the middle as a regular message', async () => {
|
||||||
|
setupRawStdin(toRawInputs(['テキスト中に /go を含むがコマンドではない文', '/cancel']));
|
||||||
|
const capture = setupProvider(['OK.']);
|
||||||
|
|
||||||
|
const result = await runInstruct();
|
||||||
|
|
||||||
|
expect(result.action).toBe('cancel');
|
||||||
|
expect(capture.callCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// Session management: new sessionId propagates across calls
|
// Session management: new sessionId propagates across calls
|
||||||
// =================================================================
|
// =================================================================
|
||||||
|
|||||||
@ -142,7 +142,8 @@ vi.mock('../shared/context.js', () => ({
|
|||||||
isQuietMode: vi.fn(() => false),
|
isQuietMode: vi.fn(() => false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../shared/constants.js', () => ({
|
vi.mock('../shared/constants.js', async (importOriginal) => ({
|
||||||
|
...(await importOriginal<Record<string, unknown>>()),
|
||||||
DEFAULT_PIECE_NAME: 'default',
|
DEFAULT_PIECE_NAME: 'default',
|
||||||
DEFAULT_LANGUAGE: 'en',
|
DEFAULT_LANGUAGE: 'en',
|
||||||
}));
|
}));
|
||||||
|
|||||||
36
src/features/interactive/commandMatcher.ts
Normal file
36
src/features/interactive/commandMatcher.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { SlashCommand } from '../../shared/constants.js';
|
||||||
|
|
||||||
|
const SLASH_COMMAND_VALUES = Object.values(SlashCommand);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slash command parser for interactive mode.
|
||||||
|
*
|
||||||
|
* Detects slash commands at the beginning or end of user input.
|
||||||
|
* Commands in the middle of text are not recognized.
|
||||||
|
*
|
||||||
|
* @param input - User input string.
|
||||||
|
* @returns Parsed command and associated text, or null if no command found.
|
||||||
|
*/
|
||||||
|
export const matchSlashCommand = (input: string): {command: SlashCommand, text: string} | null => {
|
||||||
|
if (!input) return null;
|
||||||
|
|
||||||
|
const prefixMatch = SLASH_COMMAND_VALUES.find((cmd) => {
|
||||||
|
if (!input.startsWith(cmd)) return false;
|
||||||
|
const rest = input.slice(cmd.length);
|
||||||
|
return rest === '' || rest.startsWith(' ');
|
||||||
|
});
|
||||||
|
if (prefixMatch) {
|
||||||
|
const rest = input.slice(prefixMatch.length);
|
||||||
|
return { command: prefixMatch, text: rest.trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffixMatch = SLASH_COMMAND_VALUES.find((cmd) =>
|
||||||
|
input.endsWith(` ${cmd}`),
|
||||||
|
);
|
||||||
|
if (suffixMatch) {
|
||||||
|
const precedingText = input.slice(0, -(suffixMatch.length + 1)).trim();
|
||||||
|
return { command: suffixMatch, text: precedingText };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@ -19,6 +19,8 @@ import { info, error, blankLine } from '../../shared/ui/index.js';
|
|||||||
import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
|
import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
|
||||||
import { readMultilineInput } from './lineEditor.js';
|
import { readMultilineInput } from './lineEditor.js';
|
||||||
import { selectRecentSession } from './sessionSelector.js';
|
import { selectRecentSession } from './sessionSelector.js';
|
||||||
|
import { matchSlashCommand } from './commandMatcher.js';
|
||||||
|
import { SlashCommand } from '../../shared/constants.js';
|
||||||
import {
|
import {
|
||||||
type PieceContext,
|
type PieceContext,
|
||||||
type InteractiveModeResult,
|
type InteractiveModeResult,
|
||||||
@ -157,108 +159,115 @@ export async function runConversationLoop(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimmed.startsWith('/play')) {
|
const match = matchSlashCommand(trimmed);
|
||||||
const task = trimmed.slice(5).trim();
|
|
||||||
if (!task) {
|
|
||||||
info(ui.playNoTask);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
log.info('Play command', { task });
|
|
||||||
return { action: 'execute', task };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed === '/retry') {
|
// No slash command detected, treat as regular message
|
||||||
if (!strategy.enableRetryCommand) {
|
if (!match) {
|
||||||
info(ui.retryUnavailable);
|
history.push({ role: 'user', content: trimmed });
|
||||||
continue;
|
log.debug('Sending to AI', { messageCount: history.length, sessionId });
|
||||||
}
|
process.stdin.pause();
|
||||||
if (!strategy.previousOrderContent) {
|
|
||||||
info(ui.retryNoOrder);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
log.info('Retry command — resubmitting previous order.md');
|
|
||||||
return { action: 'execute', task: strategy.previousOrderContent };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed.startsWith('/go')) {
|
const promptWithTransform = strategy.transformPrompt(trimmed);
|
||||||
const userNote = trimmed.slice(3).trim();
|
const result = await doCallAI(promptWithTransform, strategy.systemPrompt, strategy.allowedTools);
|
||||||
let summaryPrompt = buildSummaryPrompt(
|
if (result) {
|
||||||
history, !!sessionId, ctx.lang, noTranscript, conversationLabel, pieceContext,
|
if (!result.success) {
|
||||||
);
|
error(result.content);
|
||||||
if (!summaryPrompt) {
|
blankLine();
|
||||||
info(ui.noConversation);
|
history.pop();
|
||||||
continue;
|
return { action: 'cancel', task: '' };
|
||||||
}
|
}
|
||||||
if (userNote) {
|
history.push({ role: 'assistant', content: result.content });
|
||||||
summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`;
|
|
||||||
}
|
|
||||||
// Summary AI must not inherit the conversation session to avoid chat-mode behavior.
|
|
||||||
const { result: summaryResult } = await callAIWithRetry(
|
|
||||||
summaryPrompt, summaryPrompt, strategy.allowedTools, cwd,
|
|
||||||
{ ...ctx, sessionId: undefined },
|
|
||||||
);
|
|
||||||
if (!summaryResult) {
|
|
||||||
info(ui.summarizeFailed);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!summaryResult.success) {
|
|
||||||
error(summaryResult.content);
|
|
||||||
blankLine();
|
blankLine();
|
||||||
return { action: 'cancel', task: '' };
|
} else {
|
||||||
}
|
history.pop();
|
||||||
const task = summaryResult.content.trim();
|
|
||||||
const selectedAction = strategy.selectAction
|
|
||||||
? await strategy.selectAction(task, ctx.lang)
|
|
||||||
: await selectPostSummaryAction(task, ui.proposed, ui);
|
|
||||||
if (selectedAction === 'continue' || selectedAction === null) {
|
|
||||||
info(ui.continuePrompt);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
log.info('Conversation action selected', { action: selectedAction, messageCount: history.length });
|
|
||||||
return { action: selectedAction, task };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed === '/replay') {
|
|
||||||
if (!strategy.previousOrderContent) {
|
|
||||||
const replayNoOrder = getLabel('instruct.ui.replayNoOrder', ctx.lang);
|
|
||||||
info(replayNoOrder);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
log.info('Replay command');
|
|
||||||
return { action: 'execute', task: strategy.previousOrderContent };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed === '/cancel') {
|
|
||||||
info(ui.cancelled);
|
|
||||||
return { action: 'cancel', task: '' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed === '/resume') {
|
|
||||||
const selectedId = await selectRecentSession(cwd, ctx.lang);
|
|
||||||
if (selectedId) {
|
|
||||||
sessionId = selectedId;
|
|
||||||
info(getLabel('interactive.resumeSessionLoaded', ctx.lang));
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.push({ role: 'user', content: trimmed });
|
switch (match.command) {
|
||||||
log.debug('Sending to AI', { messageCount: history.length, sessionId });
|
case SlashCommand.Play: {
|
||||||
process.stdin.pause();
|
if (!match.text) {
|
||||||
|
info(ui.playNoTask);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.info('Play command', { task: match.text });
|
||||||
|
return { action: 'execute', task: match.text };
|
||||||
|
}
|
||||||
|
|
||||||
const promptWithTransform = strategy.transformPrompt(trimmed);
|
case SlashCommand.Retry: {
|
||||||
const result = await doCallAI(promptWithTransform, strategy.systemPrompt, strategy.allowedTools);
|
if (!strategy.enableRetryCommand) {
|
||||||
if (result) {
|
info(ui.retryUnavailable);
|
||||||
if (!result.success) {
|
continue;
|
||||||
error(result.content);
|
}
|
||||||
blankLine();
|
if (!strategy.previousOrderContent) {
|
||||||
history.pop();
|
info(ui.retryNoOrder);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.info('Retry command — resubmitting previous order.md');
|
||||||
|
return { action: 'execute', task: strategy.previousOrderContent };
|
||||||
|
}
|
||||||
|
|
||||||
|
case SlashCommand.Go: {
|
||||||
|
const userNote = match.text;
|
||||||
|
let summaryPrompt = buildSummaryPrompt(
|
||||||
|
history, !!sessionId, ctx.lang, noTranscript, conversationLabel, pieceContext,
|
||||||
|
);
|
||||||
|
if (!summaryPrompt) {
|
||||||
|
info(ui.noConversation);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (userNote) {
|
||||||
|
summaryPrompt = `${summaryPrompt}\n\nUser Note:\n${userNote}`;
|
||||||
|
}
|
||||||
|
// Summary AI must not inherit the conversation session to avoid chat-mode behavior.
|
||||||
|
const { result: summaryResult } = await callAIWithRetry(
|
||||||
|
summaryPrompt, summaryPrompt, strategy.allowedTools, cwd,
|
||||||
|
{ ...ctx, sessionId: undefined },
|
||||||
|
);
|
||||||
|
if (!summaryResult) {
|
||||||
|
info(ui.summarizeFailed);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!summaryResult.success) {
|
||||||
|
error(summaryResult.content);
|
||||||
|
blankLine();
|
||||||
|
return { action: 'cancel', task: '' };
|
||||||
|
}
|
||||||
|
const task = summaryResult.content.trim();
|
||||||
|
const selectedAction = strategy.selectAction
|
||||||
|
? await strategy.selectAction(task, ctx.lang)
|
||||||
|
: await selectPostSummaryAction(task, ui.proposed, ui);
|
||||||
|
if (selectedAction === 'continue' || selectedAction === null) {
|
||||||
|
info(ui.continuePrompt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.info('Conversation action selected', { action: selectedAction, messageCount: history.length });
|
||||||
|
return { action: selectedAction, task };
|
||||||
|
}
|
||||||
|
|
||||||
|
case SlashCommand.Replay: {
|
||||||
|
if (!strategy.previousOrderContent) {
|
||||||
|
const replayNoOrder = getLabel('instruct.ui.replayNoOrder', ctx.lang);
|
||||||
|
info(replayNoOrder);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.info('Replay command');
|
||||||
|
return { action: 'execute', task: strategy.previousOrderContent };
|
||||||
|
}
|
||||||
|
|
||||||
|
case SlashCommand.Cancel: {
|
||||||
|
info(ui.cancelled);
|
||||||
return { action: 'cancel', task: '' };
|
return { action: 'cancel', task: '' };
|
||||||
}
|
}
|
||||||
history.push({ role: 'assistant', content: result.content });
|
|
||||||
blankLine();
|
case SlashCommand.Resume: {
|
||||||
} else {
|
const selectedId = await selectRecentSession(cwd, ctx.lang);
|
||||||
history.pop();
|
if (selectedId) {
|
||||||
|
sessionId = selectedId;
|
||||||
|
info(getLabel('interactive.resumeSessionLoaded', ctx.lang));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,3 +10,14 @@ export const DEFAULT_PIECE_NAME = 'default';
|
|||||||
|
|
||||||
/** Default language for new installations */
|
/** Default language for new installations */
|
||||||
export const DEFAULT_LANGUAGE: Language = 'en';
|
export const DEFAULT_LANGUAGE: Language = 'en';
|
||||||
|
|
||||||
|
/** Slash commands recognized in interactive mode */
|
||||||
|
export const SlashCommand = {
|
||||||
|
Play: '/play',
|
||||||
|
Go: '/go',
|
||||||
|
Retry: '/retry',
|
||||||
|
Replay: '/replay',
|
||||||
|
Cancel: '/cancel',
|
||||||
|
Resume: '/resume',
|
||||||
|
} as const;
|
||||||
|
export type SlashCommand = typeof SlashCommand[keyof typeof SlashCommand];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user