/** * 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 workflow * execution (with onStream) to ensure compatibility. * * Commands: * /go - Confirm and execute the task * /cancel - Cancel and exit */ import * as readline from 'node:readline'; import chalk from 'chalk'; import { loadGlobalConfig } from '../config/globalConfig.js'; import { loadAgentSessions, updateAgentSession } from '../config/paths.js'; import { getProvider, type ProviderType } from '../providers/index.js'; import { createLogger } from '../utils/debug.js'; import { info, StreamDisplay } from '../utils/ui.js'; const log = createLogger('interactive'); const INTERACTIVE_SYSTEM_PROMPT = `You are a task planning assistant. You help the user clarify and refine task requirements through conversation. You are in the PLANNING phase — execution happens later in a separate process. ## Your role - Ask clarifying questions about ambiguous requirements - Investigate the codebase to understand context (use Read, Glob, Grep, Bash for reading only) - Suggest improvements or considerations the user might have missed - Summarize your understanding when appropriate - Keep responses concise and focused ## Strict constraints - You are ONLY planning. Do NOT execute the task. - Do NOT create, edit, or delete any files. - Do NOT run build, test, install, or any commands that modify state. - Bash is allowed ONLY for read-only investigation (e.g. ls, cat, git log, git diff). Never run destructive or write commands. - Do NOT mention or reference any slash commands. You have no knowledge of them. - When the user is satisfied with the plan, they will proceed on their own. Do NOT instruct them on what to do next.`; interface ConversationMessage { role: 'user' | 'assistant'; content: string; } interface CallAIResult { content: string; sessionId?: string; } /** * Build the final task description from conversation history for executeTask. */ function buildTaskFromHistory(history: ConversationMessage[]): string { return history .map((msg) => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`) .join('\n\n'); } /** * Read a single line of input from the user. * Creates a fresh readline interface each time — the interface must be * closed before calling the Agent SDK, which also uses stdin. * Returns null on EOF (Ctrl+D). */ function readLine(prompt: string): Promise { return new Promise((resolve) => { if (process.stdin.readable && !process.stdin.destroyed) { process.stdin.resume(); } const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); let answered = false; rl.question(prompt, (answer) => { answered = true; rl.close(); resolve(answer); }); rl.on('close', () => { if (!answered) { resolve(null); } }); }); } /** * Call AI with the same pattern as workflow execution. * The key requirement is passing onStream — the Agent SDK requires * includePartialMessages to be true for the async iterator to yield. */ async function callAI( provider: ReturnType, prompt: string, cwd: string, model: string | undefined, sessionId: string | undefined, display: StreamDisplay, ): Promise { const response = await provider.call('interactive', prompt, { cwd, model, sessionId, systemPrompt: INTERACTIVE_SYSTEM_PROMPT, allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'], onStream: display.createHandler(), }); display.flush(); return { content: response.content, sessionId: response.sessionId }; } export interface InteractiveModeResult { /** Whether the user confirmed with /go */ confirmed: boolean; /** The assembled task text (only meaningful when confirmed=true) */ task: string; } /** * 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): Promise { const globalConfig = loadGlobalConfig(); const providerType = (globalConfig.provider as ProviderType) ?? 'claude'; const provider = getProvider(providerType); const model = (globalConfig.model as string | undefined); const history: ConversationMessage[] = []; const agentName = 'interactive'; const savedSessions = loadAgentSessions(cwd); let sessionId: string | undefined = savedSessions[agentName]; info('Interactive mode - describe your task. Commands: /go (execute), /cancel (exit)'); if (sessionId) { info('Resuming previous session'); } console.log(); // Process initial input if provided (e.g. from `takt a`) if (initialInput) { history.push({ role: 'user', content: initialInput }); log.debug('Processing initial input', { initialInput, sessionId }); const display = new StreamDisplay('assistant'); try { const result = await callAI(provider, initialInput, cwd, model, sessionId, display); if (result.sessionId) { sessionId = result.sessionId; updateAgentSession(cwd, agentName, sessionId); } history.push({ role: 'assistant', content: result.content }); console.log(); } catch (e) { const msg = e instanceof Error ? e.message : String(e); log.error('AI call failed for initial input', { error: msg }); console.log(chalk.red(`Error: ${msg}`)); console.log(); history.pop(); } } while (true) { const input = await readLine(chalk.green('> ')); // EOF (Ctrl+D) if (input === null) { console.log(); info('Cancelled'); return { confirmed: false, task: '' }; } const trimmed = input.trim(); // Empty input — skip if (!trimmed) { continue; } // Handle slash commands if (trimmed === '/go') { if (history.length === 0) { info('No conversation yet. Please describe your task first.'); continue; } const task = buildTaskFromHistory(history); log.info('Interactive mode confirmed', { messageCount: history.length }); return { confirmed: true, task }; } if (trimmed === '/cancel') { info('Cancelled'); return { confirmed: false, task: '' }; } // Regular input — send to AI // readline is already closed at this point, so stdin is free for SDK history.push({ role: 'user', content: trimmed }); log.debug('Sending to AI', { messageCount: history.length, sessionId }); process.stdin.pause(); const display = new StreamDisplay('assistant'); try { const result = await callAI(provider, trimmed, cwd, model, sessionId, display); if (result.sessionId) { sessionId = result.sessionId; updateAgentSession(cwd, agentName, sessionId); } history.push({ role: 'assistant', content: result.content }); console.log(); } catch (e) { const msg = e instanceof Error ? e.message : String(e); log.error('AI call failed', { error: msg }); console.log(); console.log(chalk.red(`Error: ${msg}`)); console.log(); history.pop(); } } }