takt/src/features/interactive/interactive.ts
nrslib 620e384251 interactiveモジュールの分割とタスク再キュー方式への移行
interactive.tsからsummary/runSelector/runSessionReader/selectorUtilsを分離し、
run session参照をrouting層からinstructMode層に移動。instructBranchで新タスク
作成の代わりに既存タスクのrequeueを使用する方式に変更。worktree確認プロンプトを
廃止し常時有効化。
2026-02-18 18:49:21 +09:00

185 lines
5.7 KiB
TypeScript

/**
* Interactive task input mode
*
* Allows users to refine task requirements through conversation with AI
* before executing the task. Uses the same SDK call pattern as piece
* execution (with onStream) to ensure compatibility.
*
* Commands:
* /go - Confirm and execute the task
* /cancel - Cancel and exit
*/
import type { Language } from '../../core/models/index.js';
import {
type SessionState,
} from '../../infra/config/index.js';
import { getLabel, getLabelObject } from '../../shared/i18n/index.js';
import { loadTemplate } from '../../shared/prompts/index.js';
import {
initializeSession,
displayAndClearSessionState,
runConversationLoop,
} from './conversationLoop.js';
import {
type PieceContext,
formatMovementPreviews,
type InteractiveModeAction,
} from './interactive-summary.js';
import { type RunSessionContext, formatRunSessionForPrompt } from './runSessionReader.js';
/** Shape of interactive UI text */
export interface InteractiveUIText {
intro: string;
resume: string;
noConversation: string;
summarizeFailed: string;
continuePrompt: string;
proposed: string;
actionPrompt: string;
actions: {
execute: string;
createIssue: string;
saveTask: string;
continue: string;
};
cancelled: string;
playNoTask: string;
}
/**
* Format session state for display
*/
export function formatSessionStatus(state: SessionState, lang: 'en' | 'ja'): string {
const lines: string[] = [];
// Status line
if (state.status === 'success') {
lines.push(getLabel('interactive.previousTask.success', lang));
} else if (state.status === 'error') {
lines.push(
getLabel('interactive.previousTask.error', lang, {
error: state.errorMessage!,
}),
);
} else if (state.status === 'user_stopped') {
lines.push(getLabel('interactive.previousTask.userStopped', lang));
}
// Piece name
lines.push(
getLabel('interactive.previousTask.piece', lang, {
pieceName: state.pieceName,
}),
);
// Timestamp
const timestamp = new Date(state.timestamp).toLocaleString(lang === 'ja' ? 'ja-JP' : 'en-US');
lines.push(
getLabel('interactive.previousTask.timestamp', lang, {
timestamp,
}),
);
return lines.join('\n');
}
export function resolveLanguage(lang?: Language): 'en' | 'ja' {
return lang === 'ja' ? 'ja' : 'en';
}
/** Default toolset for interactive mode */
export const DEFAULT_INTERACTIVE_TOOLS = ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
/**
* Build the summary prompt (used as both system prompt and user message).
*/
export {
buildSummaryPrompt,
formatMovementPreviews,
type ConversationMessage,
type PieceContext,
type TaskHistorySummaryItem,
} from './interactive-summary.js';
/**
* Run the interactive task input mode.
*
* Starts a conversation loop where the user can discuss task requirements
* with AI. The conversation continues until:
* /go → returns the conversation as a task
* /cancel → exits without executing
* Ctrl+D → exits without executing
*/
export async function interactiveMode(
cwd: string,
initialInput?: string,
pieceContext?: PieceContext,
sessionId?: string,
runSessionContext?: RunSessionContext,
): Promise<InteractiveModeResult> {
const baseCtx = initializeSession(cwd, 'interactive');
const ctx = sessionId ? { ...baseCtx, sessionId } : baseCtx;
displayAndClearSessionState(cwd, ctx.lang);
const hasPreview = !!pieceContext?.movementPreviews?.length;
const hasRunSession = !!runSessionContext;
const runPromptVars = hasRunSession
? formatRunSessionForPrompt(runSessionContext)
: { runTask: '', runPiece: '', runStatus: '', runMovementLogs: '', runReports: '' };
const systemPrompt = loadTemplate('score_interactive_system_prompt', ctx.lang, {
hasPiecePreview: hasPreview,
pieceStructure: pieceContext?.pieceStructure ?? '',
movementDetails: hasPreview ? formatMovementPreviews(pieceContext!.movementPreviews!, ctx.lang) : '',
hasRunSession,
...runPromptVars,
});
const policyContent = loadTemplate('score_interactive_policy', ctx.lang, {});
const ui = getLabelObject<InteractiveUIText>('interactive.ui', ctx.lang);
/**
* Inject policy into user message for AI call.
* Follows the same pattern as piece execution (perform_phase1_message.md).
*/
function injectPolicy(userMessage: string): string {
const policyIntro = ctx.lang === 'ja'
? '以下のポリシーは行動規範です。必ず遵守してください。'
: 'The following policy defines behavioral guidelines. Please follow them.';
const reminderLabel = ctx.lang === 'ja'
? '上記の Policy セクションで定義されたポリシー規範を遵守してください。'
: 'Please follow the policy guidelines defined in the Policy section above.';
return `## Policy\n${policyIntro}\n\n${policyContent}\n\n---\n\n${userMessage}\n\n---\n**Policy Reminder:** ${reminderLabel}`;
}
return runConversationLoop(cwd, ctx, {
systemPrompt,
allowedTools: DEFAULT_INTERACTIVE_TOOLS,
transformPrompt: injectPolicy,
introMessage: ui.intro,
}, pieceContext, initialInput);
}
export {
type InteractiveModeAction,
type InteractiveSummaryUIText,
type PostSummaryAction,
type SummaryActionLabels,
type SummaryActionOption,
type SummaryActionValue,
selectPostSummaryAction,
buildSummaryActionOptions,
selectSummaryAction,
formatTaskHistorySummary,
normalizeTaskHistorySummary,
BASE_SUMMARY_ACTIONS,
} from './interactive-summary.js';
export interface InteractiveModeResult {
/** The action selected by the user */
action: InteractiveModeAction;
/** The assembled task text (only meaningful when action is not 'cancel') */
task: string;
}