wiggum-cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/conversation/conversation-manager.d.ts +11 -0
- package/dist/ai/conversation/conversation-manager.d.ts.map +1 -1
- package/dist/ai/conversation/conversation-manager.js +14 -0
- package/dist/ai/conversation/conversation-manager.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +63 -22
- package/dist/commands/new.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -22
- package/dist/index.js.map +1 -1
- package/dist/tui/app.d.ts +46 -36
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +136 -37
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/WiggumBanner.d.ts +30 -0
- package/dist/tui/components/WiggumBanner.d.ts.map +1 -0
- package/dist/tui/components/WiggumBanner.js +34 -0
- package/dist/tui/components/WiggumBanner.js.map +1 -0
- package/dist/tui/demo.d.ts +8 -0
- package/dist/tui/demo.d.ts.map +1 -0
- package/dist/tui/demo.js +69 -0
- package/dist/tui/demo.js.map +1 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts +16 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -1
- package/dist/tui/hooks/useSpecGenerator.js +47 -0
- package/dist/tui/hooks/useSpecGenerator.js.map +1 -1
- package/dist/tui/orchestration/index.d.ts +6 -0
- package/dist/tui/orchestration/index.d.ts.map +1 -0
- package/dist/tui/orchestration/index.js +6 -0
- package/dist/tui/orchestration/index.js.map +1 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts +136 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts.map +1 -0
- package/dist/tui/orchestration/interview-orchestrator.js +437 -0
- package/dist/tui/orchestration/interview-orchestrator.js.map +1 -0
- package/dist/tui/screens/InitScreen.d.ts +26 -0
- package/dist/tui/screens/InitScreen.d.ts.map +1 -0
- package/dist/tui/screens/InitScreen.js +30 -0
- package/dist/tui/screens/InitScreen.js.map +1 -0
- package/dist/tui/screens/InterviewScreen.d.ts +2 -13
- package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
- package/dist/tui/screens/InterviewScreen.js +162 -34
- package/dist/tui/screens/InterviewScreen.js.map +1 -1
- package/dist/tui/screens/MainShell.d.ts +46 -0
- package/dist/tui/screens/MainShell.d.ts.map +1 -0
- package/dist/tui/screens/MainShell.js +196 -0
- package/dist/tui/screens/MainShell.js.map +1 -0
- package/dist/tui/screens/WelcomeScreen.d.ts +45 -0
- package/dist/tui/screens/WelcomeScreen.d.ts.map +1 -0
- package/dist/tui/screens/WelcomeScreen.js +56 -0
- package/dist/tui/screens/WelcomeScreen.js.map +1 -0
- package/dist/tui/theme.d.ts +4 -0
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +4 -0
- package/dist/tui/theme.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/conversation/conversation-manager.ts +22 -0
- package/src/cli.ts +4 -0
- package/src/commands/new.ts +79 -27
- package/src/index.ts +109 -27
- package/src/tui/app.tsx +222 -63
- package/src/tui/components/WiggumBanner.tsx +66 -0
- package/src/tui/demo.tsx +111 -0
- package/src/tui/hooks/useSpecGenerator.ts +73 -0
- package/src/tui/orchestration/index.ts +10 -0
- package/src/tui/orchestration/interview-orchestrator.ts +559 -0
- package/src/tui/screens/InitScreen.tsx +63 -0
- package/src/tui/screens/InterviewScreen.tsx +201 -46
- package/src/tui/screens/MainShell.tsx +290 -0
- package/src/tui/screens/WelcomeScreen.tsx +141 -0
- package/src/tui/theme.ts +4 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interview Orchestrator
|
|
3
|
+
* Bridges the event-based TUI hook to ConversationManager
|
|
4
|
+
*
|
|
5
|
+
* This class manages the interview flow for spec generation,
|
|
6
|
+
* translating ConversationManager events into TUI-friendly callbacks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ConversationManager } from '../../ai/conversation/conversation-manager.js';
|
|
10
|
+
import { fetchContent } from '../../ai/conversation/url-fetcher.js';
|
|
11
|
+
import { createInterviewTools } from '../../ai/conversation/interview-tools.js';
|
|
12
|
+
import { createTavilySearchTool, canUseTavily } from '../../ai/tools/tavily.js';
|
|
13
|
+
import { createContext7Tools, canUseContext7 } from '../../ai/tools/context7.js';
|
|
14
|
+
import type { AIProvider } from '../../ai/providers.js';
|
|
15
|
+
import type { ScanResult } from '../../scanner/types.js';
|
|
16
|
+
import type { GeneratorPhase } from '../hooks/useSpecGenerator.js';
|
|
17
|
+
|
|
18
|
+
/** Maximum number of interview questions before auto-completing */
|
|
19
|
+
const MAX_INTERVIEW_QUESTIONS = 10;
|
|
20
|
+
|
|
21
|
+
/** Minimum number of questions before AI can indicate "enough information" */
|
|
22
|
+
const MIN_INTERVIEW_QUESTIONS = 2;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Session context from /init analysis
|
|
26
|
+
*/
|
|
27
|
+
export interface SessionContext {
|
|
28
|
+
entryPoints?: string[];
|
|
29
|
+
keyDirectories?: Record<string, string>;
|
|
30
|
+
commands?: { build?: string; dev?: string; test?: string };
|
|
31
|
+
namingConventions?: string;
|
|
32
|
+
implementationGuidelines?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for the InterviewOrchestrator
|
|
37
|
+
*/
|
|
38
|
+
export interface InterviewOrchestratorOptions {
|
|
39
|
+
/** Name of the feature being specified */
|
|
40
|
+
featureName: string;
|
|
41
|
+
/** Project root directory path */
|
|
42
|
+
projectRoot: string;
|
|
43
|
+
/** AI provider to use */
|
|
44
|
+
provider: AIProvider;
|
|
45
|
+
/** Model ID to use */
|
|
46
|
+
model: string;
|
|
47
|
+
/** Optional scan result with detected tech stack */
|
|
48
|
+
scanResult?: ScanResult;
|
|
49
|
+
/** Rich session context from /init */
|
|
50
|
+
sessionContext?: SessionContext;
|
|
51
|
+
/** Tavily API key for web search */
|
|
52
|
+
tavilyApiKey?: string;
|
|
53
|
+
/** Context7 API key for docs lookup */
|
|
54
|
+
context7ApiKey?: string;
|
|
55
|
+
|
|
56
|
+
// Event callbacks for TUI
|
|
57
|
+
/** Called when a message should be added to the conversation */
|
|
58
|
+
onMessage: (role: 'user' | 'assistant' | 'system', content: string) => void;
|
|
59
|
+
/** Called when streaming text should be appended */
|
|
60
|
+
onStreamChunk: (chunk: string) => void;
|
|
61
|
+
/** Called when streaming is complete */
|
|
62
|
+
onStreamComplete: () => void;
|
|
63
|
+
/** Called when a tool starts executing */
|
|
64
|
+
onToolStart: (toolName: string, input: Record<string, unknown>) => string;
|
|
65
|
+
/** Called when a tool completes */
|
|
66
|
+
onToolEnd: (toolId: string, output?: string, error?: string) => void;
|
|
67
|
+
/** Called when the phase changes */
|
|
68
|
+
onPhaseChange: (phase: GeneratorPhase) => void;
|
|
69
|
+
/** Called when spec generation is complete */
|
|
70
|
+
onComplete: (spec: string) => void;
|
|
71
|
+
/** Called when an error occurs */
|
|
72
|
+
onError: (error: string) => void;
|
|
73
|
+
/** Called when working state changes */
|
|
74
|
+
onWorkingChange: (isWorking: boolean, status: string) => void;
|
|
75
|
+
/** Called when ready for user input */
|
|
76
|
+
onReady: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build enhanced system prompt with project context and tool awareness
|
|
81
|
+
*/
|
|
82
|
+
function buildSystemPrompt(
|
|
83
|
+
sessionContext?: SessionContext,
|
|
84
|
+
hasTools?: { codebase: boolean; tavily: boolean; context7: boolean }
|
|
85
|
+
): string {
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
|
|
88
|
+
// Base prompt
|
|
89
|
+
parts.push(`You are an expert product manager and technical writer helping to create detailed feature specifications.
|
|
90
|
+
|
|
91
|
+
Your role is to:
|
|
92
|
+
1. Understand the user's feature goals through targeted questions
|
|
93
|
+
2. Identify edge cases and potential issues
|
|
94
|
+
3. Generate a comprehensive, actionable specification
|
|
95
|
+
|
|
96
|
+
When interviewing:
|
|
97
|
+
- Ask one focused question at a time
|
|
98
|
+
- Acknowledge answers before asking the next question
|
|
99
|
+
- Stop asking when you have enough information (usually 3-5 questions)
|
|
100
|
+
- Say "I have enough information to generate the spec" when ready`);
|
|
101
|
+
|
|
102
|
+
// Add tool awareness
|
|
103
|
+
if (hasTools) {
|
|
104
|
+
const toolList: string[] = [];
|
|
105
|
+
if (hasTools.codebase) {
|
|
106
|
+
toolList.push('- read_file: Read project files to understand existing code');
|
|
107
|
+
toolList.push('- search_codebase: Search for patterns, functions, or imports');
|
|
108
|
+
toolList.push('- list_directory: Explore project structure');
|
|
109
|
+
}
|
|
110
|
+
if (hasTools.tavily) {
|
|
111
|
+
toolList.push('- tavily_search: Search the web for best practices and documentation');
|
|
112
|
+
}
|
|
113
|
+
if (hasTools.context7) {
|
|
114
|
+
toolList.push('- resolveLibraryId/queryDocs: Look up library documentation');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (toolList.length > 0) {
|
|
118
|
+
parts.push(`
|
|
119
|
+
## Available Tools
|
|
120
|
+
You have access to the following tools to help understand the project and gather information:
|
|
121
|
+
${toolList.join('\n')}
|
|
122
|
+
|
|
123
|
+
USE THESE TOOLS PROACTIVELY:
|
|
124
|
+
- When the user describes a feature, read relevant files to understand existing patterns
|
|
125
|
+
- When unsure about implementation, search the codebase for similar code
|
|
126
|
+
- When discussing best practices, search the web for current recommendations
|
|
127
|
+
- Don't ask the user to paste code - read it yourself`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Add emphatic web search guidance when Tavily is available
|
|
131
|
+
if (hasTools.tavily) {
|
|
132
|
+
parts.push(`
|
|
133
|
+
## IMPORTANT: Web Search Available
|
|
134
|
+
You have tavily_search to look up current best practices and documentation.
|
|
135
|
+
|
|
136
|
+
WHEN YOU MUST USE WEB SEARCH:
|
|
137
|
+
- User mentions a library, framework, or API you should verify
|
|
138
|
+
- User asks about "best practices" or "how to" patterns
|
|
139
|
+
- You need current (2026+) information not in your training data
|
|
140
|
+
- Discussing implementation approaches for modern libraries`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add project context from /init
|
|
145
|
+
if (sessionContext) {
|
|
146
|
+
const contextParts: string[] = ['## Project Context (from analysis)'];
|
|
147
|
+
|
|
148
|
+
if (sessionContext.entryPoints && sessionContext.entryPoints.length > 0) {
|
|
149
|
+
contextParts.push(`\nEntry Points:\n${sessionContext.entryPoints.map(e => `- ${e}`).join('\n')}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (sessionContext.keyDirectories && Object.keys(sessionContext.keyDirectories).length > 0) {
|
|
153
|
+
contextParts.push(`\nKey Directories:`);
|
|
154
|
+
for (const [dir, purpose] of Object.entries(sessionContext.keyDirectories)) {
|
|
155
|
+
contextParts.push(`- ${dir}: ${purpose}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (sessionContext.commands) {
|
|
160
|
+
const cmds = sessionContext.commands;
|
|
161
|
+
const cmdList = Object.entries(cmds).filter(([_, v]) => v);
|
|
162
|
+
if (cmdList.length > 0) {
|
|
163
|
+
contextParts.push(`\nCommands:`);
|
|
164
|
+
for (const [name, cmd] of cmdList) {
|
|
165
|
+
contextParts.push(`- ${name}: ${cmd}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (contextParts.length > 1) {
|
|
171
|
+
parts.push(contextParts.join('\n'));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Spec format
|
|
176
|
+
parts.push(`
|
|
177
|
+
## Spec Format
|
|
178
|
+
When generating the spec, use this format:
|
|
179
|
+
|
|
180
|
+
# [Feature Name] Feature Specification
|
|
181
|
+
|
|
182
|
+
**Status:** Planned
|
|
183
|
+
**Version:** 1.0
|
|
184
|
+
**Last Updated:** [date]
|
|
185
|
+
|
|
186
|
+
## Purpose
|
|
187
|
+
[Brief description]
|
|
188
|
+
|
|
189
|
+
## User Stories
|
|
190
|
+
- As a [user], I want [action] so that [benefit]
|
|
191
|
+
|
|
192
|
+
## Requirements
|
|
193
|
+
|
|
194
|
+
### Functional Requirements
|
|
195
|
+
- [ ] Requirement with clear acceptance criteria
|
|
196
|
+
|
|
197
|
+
### Non-Functional Requirements
|
|
198
|
+
- [ ] Performance, security, accessibility requirements
|
|
199
|
+
|
|
200
|
+
## Technical Notes
|
|
201
|
+
- Implementation approach
|
|
202
|
+
- Key dependencies
|
|
203
|
+
- Database changes if needed
|
|
204
|
+
|
|
205
|
+
## Acceptance Criteria
|
|
206
|
+
- [ ] Specific, testable conditions
|
|
207
|
+
|
|
208
|
+
## Out of Scope
|
|
209
|
+
- Items explicitly not included`);
|
|
210
|
+
|
|
211
|
+
return parts.join('\n\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Extract session context from EnhancedScanResult
|
|
216
|
+
*/
|
|
217
|
+
function extractSessionContext(scanResult: ScanResult): SessionContext | undefined {
|
|
218
|
+
// Check if this is an EnhancedScanResult with aiAnalysis
|
|
219
|
+
const enhanced = scanResult as ScanResult & { aiAnalysis?: { projectContext?: SessionContext; commands?: SessionContext['commands']; implementationGuidelines?: string[] } };
|
|
220
|
+
if (!enhanced.aiAnalysis) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ai = enhanced.aiAnalysis;
|
|
225
|
+
return {
|
|
226
|
+
entryPoints: ai.projectContext?.entryPoints,
|
|
227
|
+
keyDirectories: ai.projectContext?.keyDirectories,
|
|
228
|
+
commands: ai.commands,
|
|
229
|
+
namingConventions: ai.projectContext?.namingConventions,
|
|
230
|
+
implementationGuidelines: ai.implementationGuidelines,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* InterviewOrchestrator
|
|
236
|
+
*
|
|
237
|
+
* Manages the interview flow for spec generation, bridging
|
|
238
|
+
* the ConversationManager to TUI callbacks.
|
|
239
|
+
*/
|
|
240
|
+
export class InterviewOrchestrator {
|
|
241
|
+
private conversation: ConversationManager;
|
|
242
|
+
private phase: GeneratorPhase = 'context';
|
|
243
|
+
private readonly featureName: string;
|
|
244
|
+
private readonly projectRoot: string;
|
|
245
|
+
private generatedSpec: string = '';
|
|
246
|
+
private questionCount: number = 0;
|
|
247
|
+
private readonly hasTools: { codebase: boolean; tavily: boolean; context7: boolean };
|
|
248
|
+
private readonly sessionContext?: SessionContext;
|
|
249
|
+
|
|
250
|
+
// Callbacks
|
|
251
|
+
private readonly onMessage: InterviewOrchestratorOptions['onMessage'];
|
|
252
|
+
private readonly onStreamChunk: InterviewOrchestratorOptions['onStreamChunk'];
|
|
253
|
+
private readonly onStreamComplete: InterviewOrchestratorOptions['onStreamComplete'];
|
|
254
|
+
private readonly onToolStart: InterviewOrchestratorOptions['onToolStart'];
|
|
255
|
+
private readonly onToolEnd: InterviewOrchestratorOptions['onToolEnd'];
|
|
256
|
+
private readonly onPhaseChange: InterviewOrchestratorOptions['onPhaseChange'];
|
|
257
|
+
private readonly onComplete: InterviewOrchestratorOptions['onComplete'];
|
|
258
|
+
private readonly onError: InterviewOrchestratorOptions['onError'];
|
|
259
|
+
private readonly onWorkingChange: InterviewOrchestratorOptions['onWorkingChange'];
|
|
260
|
+
private readonly onReady: InterviewOrchestratorOptions['onReady'];
|
|
261
|
+
|
|
262
|
+
// Track active tool calls for result mapping
|
|
263
|
+
// Uses a queue per tool name to handle multiple calls of the same tool
|
|
264
|
+
private activeToolCalls: Map<string, string[]> = new Map();
|
|
265
|
+
|
|
266
|
+
constructor(options: InterviewOrchestratorOptions) {
|
|
267
|
+
this.featureName = options.featureName;
|
|
268
|
+
this.projectRoot = options.projectRoot;
|
|
269
|
+
|
|
270
|
+
// Store callbacks
|
|
271
|
+
this.onMessage = options.onMessage;
|
|
272
|
+
this.onStreamChunk = options.onStreamChunk;
|
|
273
|
+
this.onStreamComplete = options.onStreamComplete;
|
|
274
|
+
this.onToolStart = options.onToolStart;
|
|
275
|
+
this.onToolEnd = options.onToolEnd;
|
|
276
|
+
this.onPhaseChange = options.onPhaseChange;
|
|
277
|
+
this.onComplete = options.onComplete;
|
|
278
|
+
this.onError = options.onError;
|
|
279
|
+
this.onWorkingChange = options.onWorkingChange;
|
|
280
|
+
this.onReady = options.onReady;
|
|
281
|
+
|
|
282
|
+
// Get API keys from options or environment
|
|
283
|
+
const tavilyApiKey = options.tavilyApiKey || process.env.TAVILY_API_KEY;
|
|
284
|
+
const context7ApiKey = options.context7ApiKey || process.env.CONTEXT7_API_KEY;
|
|
285
|
+
|
|
286
|
+
// Track which tools are available
|
|
287
|
+
this.hasTools = {
|
|
288
|
+
codebase: true, // Always available
|
|
289
|
+
tavily: canUseTavily(tavilyApiKey),
|
|
290
|
+
context7: canUseContext7(context7ApiKey),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Build tools object
|
|
294
|
+
const tools: Record<string, unknown> = {};
|
|
295
|
+
|
|
296
|
+
// Add codebase tools
|
|
297
|
+
const codebaseTools = createInterviewTools(options.projectRoot);
|
|
298
|
+
Object.assign(tools, codebaseTools);
|
|
299
|
+
|
|
300
|
+
// Add Tavily search if available
|
|
301
|
+
if (this.hasTools.tavily && tavilyApiKey) {
|
|
302
|
+
tools.tavily_search = createTavilySearchTool(tavilyApiKey);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Add Context7 tools if available
|
|
306
|
+
if (this.hasTools.context7 && context7ApiKey) {
|
|
307
|
+
const context7Tools = createContext7Tools(context7ApiKey);
|
|
308
|
+
Object.assign(tools, context7Tools);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Extract session context from scan result or use provided
|
|
312
|
+
this.sessionContext = options.sessionContext || (
|
|
313
|
+
options.scanResult ? extractSessionContext(options.scanResult) : undefined
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Build enhanced system prompt
|
|
317
|
+
const systemPrompt = buildSystemPrompt(this.sessionContext, this.hasTools);
|
|
318
|
+
|
|
319
|
+
// Create conversation manager with tools
|
|
320
|
+
this.conversation = new ConversationManager({
|
|
321
|
+
provider: options.provider,
|
|
322
|
+
model: options.model,
|
|
323
|
+
systemPrompt,
|
|
324
|
+
tools: tools as Record<string, never>,
|
|
325
|
+
onToolUse: (toolName, args) => {
|
|
326
|
+
// Create tool ID and notify TUI
|
|
327
|
+
const toolId = this.onToolStart(toolName, args);
|
|
328
|
+
// Store in queue for this tool name (handles multiple concurrent calls)
|
|
329
|
+
const queue = this.activeToolCalls.get(toolName) || [];
|
|
330
|
+
queue.push(toolId);
|
|
331
|
+
this.activeToolCalls.set(toolName, queue);
|
|
332
|
+
},
|
|
333
|
+
onToolResult: (toolName, result) => {
|
|
334
|
+
const queue = this.activeToolCalls.get(toolName);
|
|
335
|
+
// Get the first (oldest) tool ID from the queue (FIFO order)
|
|
336
|
+
const toolId = queue?.shift();
|
|
337
|
+
if (toolId) {
|
|
338
|
+
// Format result for display
|
|
339
|
+
const output = typeof result === 'string'
|
|
340
|
+
? result
|
|
341
|
+
: JSON.stringify(result, null, 2).slice(0, 200);
|
|
342
|
+
this.onToolEnd(toolId, output);
|
|
343
|
+
// Clean up empty queues
|
|
344
|
+
if (queue && queue.length === 0) {
|
|
345
|
+
this.activeToolCalls.delete(toolName);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
maxToolSteps: 8,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (options.scanResult) {
|
|
353
|
+
this.conversation.setCodebaseContext(options.scanResult);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Start the interview flow
|
|
359
|
+
* Called after the component mounts
|
|
360
|
+
*/
|
|
361
|
+
async start(): Promise<void> {
|
|
362
|
+
try {
|
|
363
|
+
// Set initial phase
|
|
364
|
+
this.onPhaseChange('context');
|
|
365
|
+
this.onMessage('system', `Spec Generator initialized for feature: ${this.featureName}`);
|
|
366
|
+
|
|
367
|
+
// Ready for context input
|
|
368
|
+
this.onReady();
|
|
369
|
+
} catch (error) {
|
|
370
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Add a reference URL or file path
|
|
376
|
+
*/
|
|
377
|
+
async addReference(refUrl: string): Promise<void> {
|
|
378
|
+
try {
|
|
379
|
+
this.onWorkingChange(true, 'Fetching reference...');
|
|
380
|
+
|
|
381
|
+
const result = await fetchContent(refUrl, this.projectRoot);
|
|
382
|
+
|
|
383
|
+
if (result.error) {
|
|
384
|
+
this.onMessage('system', `Error: ${result.error}`);
|
|
385
|
+
} else {
|
|
386
|
+
this.conversation.addReference(result.content, result.source);
|
|
387
|
+
const preview = result.content.slice(0, 100).replace(/\n/g, ' ').trim();
|
|
388
|
+
this.onMessage('system', `Added reference from ${result.source}: "${preview}..."`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.onReady();
|
|
392
|
+
} catch (error) {
|
|
393
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
394
|
+
this.onReady();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Advance to the goals phase
|
|
400
|
+
* Called when user is done adding context
|
|
401
|
+
*/
|
|
402
|
+
async advanceToGoals(): Promise<void> {
|
|
403
|
+
this.phase = 'goals';
|
|
404
|
+
this.onPhaseChange('goals');
|
|
405
|
+
this.onMessage('system', 'Phase 2: Goals - Describe what you want to build');
|
|
406
|
+
this.onReady();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Submit user's goals and start the interview
|
|
411
|
+
*/
|
|
412
|
+
async submitGoals(goals: string): Promise<void> {
|
|
413
|
+
try {
|
|
414
|
+
this.onWorkingChange(true, 'Exploring project...');
|
|
415
|
+
|
|
416
|
+
// Add user message to conversation
|
|
417
|
+
const userMessage = goals
|
|
418
|
+
? `I want to create a feature called "${this.featureName}". Here's what I'm thinking:\n\n${goals}`
|
|
419
|
+
: `I want to create a feature called "${this.featureName}".`;
|
|
420
|
+
|
|
421
|
+
this.conversation.addToHistory({ role: 'user', content: userMessage });
|
|
422
|
+
|
|
423
|
+
// Phase 2a: Explore project silently
|
|
424
|
+
const explorePrompt = `Explore the codebase to understand the project structure for the feature "${this.featureName}".
|
|
425
|
+
Use your tools to read key files that are relevant to this feature.
|
|
426
|
+
DO NOT ask any questions yet - just gather information silently.
|
|
427
|
+
Respond with a VERY brief (1-2 sentence) summary of what you found relevant to this feature.`;
|
|
428
|
+
|
|
429
|
+
const summary = await this.conversation.chat(explorePrompt);
|
|
430
|
+
const shortSummary = summary.slice(0, 120).replace(/\n/g, ' ');
|
|
431
|
+
this.onMessage('system', `Context: ${shortSummary}${summary.length > 120 ? '...' : ''}`);
|
|
432
|
+
|
|
433
|
+
// Phase 2b: Start interview with first question
|
|
434
|
+
this.onWorkingChange(true, 'Preparing first question...');
|
|
435
|
+
|
|
436
|
+
const interviewPrompt = `Based on what you learned about the project, briefly acknowledge the user's goals for "${this.featureName}" and ask your FIRST clarifying question.
|
|
437
|
+
Ask only ONE question. Be concise.`;
|
|
438
|
+
|
|
439
|
+
const response = await this.conversation.chat(interviewPrompt);
|
|
440
|
+
this.onMessage('assistant', response);
|
|
441
|
+
|
|
442
|
+
// Transition to interview phase
|
|
443
|
+
this.phase = 'interview';
|
|
444
|
+
this.onPhaseChange('interview');
|
|
445
|
+
this.onReady();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
448
|
+
this.onReady();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Submit an answer during the interview phase
|
|
454
|
+
*/
|
|
455
|
+
async submitAnswer(answer: string): Promise<void> {
|
|
456
|
+
try {
|
|
457
|
+
this.onWorkingChange(true, 'Thinking...');
|
|
458
|
+
|
|
459
|
+
const response = await this.conversation.chat(answer);
|
|
460
|
+
this.onMessage('assistant', response);
|
|
461
|
+
|
|
462
|
+
this.questionCount++;
|
|
463
|
+
|
|
464
|
+
// Check if AI indicates it has enough information
|
|
465
|
+
if (this.questionCount >= MIN_INTERVIEW_QUESTIONS) {
|
|
466
|
+
const lowerResponse = response.toLowerCase();
|
|
467
|
+
if (
|
|
468
|
+
lowerResponse.includes('enough information') ||
|
|
469
|
+
lowerResponse.includes('ready to generate') ||
|
|
470
|
+
lowerResponse.includes("let me generate") ||
|
|
471
|
+
lowerResponse.includes("i'll now generate") ||
|
|
472
|
+
lowerResponse.includes("i will now generate")
|
|
473
|
+
) {
|
|
474
|
+
await this.generateSpec();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check if max questions reached
|
|
480
|
+
if (this.questionCount >= MAX_INTERVIEW_QUESTIONS) {
|
|
481
|
+
await this.generateSpec();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.onReady();
|
|
486
|
+
} catch (error) {
|
|
487
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
488
|
+
this.onReady();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Skip to generation phase
|
|
494
|
+
*/
|
|
495
|
+
async skipToGeneration(): Promise<void> {
|
|
496
|
+
await this.generateSpec();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Generate the specification
|
|
501
|
+
*/
|
|
502
|
+
private async generateSpec(): Promise<void> {
|
|
503
|
+
try {
|
|
504
|
+
this.phase = 'generation';
|
|
505
|
+
this.onPhaseChange('generation');
|
|
506
|
+
this.onWorkingChange(true, 'Generating specification...');
|
|
507
|
+
|
|
508
|
+
const prompt = `Based on our conversation, generate a complete feature specification for "${this.featureName}".
|
|
509
|
+
|
|
510
|
+
Use the format from your instructions. Be specific and actionable. Include:
|
|
511
|
+
- Clear user stories
|
|
512
|
+
- Detailed requirements with acceptance criteria
|
|
513
|
+
- Technical notes based on the project's tech stack
|
|
514
|
+
- Specific acceptance criteria that can be tested
|
|
515
|
+
|
|
516
|
+
Today's date is ${new Date().toISOString().split('T')[0]}.`;
|
|
517
|
+
|
|
518
|
+
// Use streaming for generation
|
|
519
|
+
const stream = this.conversation.chatStream(prompt);
|
|
520
|
+
let fullSpec = '';
|
|
521
|
+
|
|
522
|
+
for await (const chunk of stream) {
|
|
523
|
+
fullSpec += chunk;
|
|
524
|
+
this.onStreamChunk(chunk);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
this.onStreamComplete();
|
|
528
|
+
this.generatedSpec = fullSpec;
|
|
529
|
+
|
|
530
|
+
// Complete
|
|
531
|
+
this.phase = 'complete';
|
|
532
|
+
this.onPhaseChange('complete');
|
|
533
|
+
this.onComplete(fullSpec);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get current phase
|
|
541
|
+
*/
|
|
542
|
+
getPhase(): GeneratorPhase {
|
|
543
|
+
return this.phase;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Get question count
|
|
548
|
+
*/
|
|
549
|
+
getQuestionCount(): number {
|
|
550
|
+
return this.questionCount;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Get generated spec
|
|
555
|
+
*/
|
|
556
|
+
getSpec(): string {
|
|
557
|
+
return this.generatedSpec;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InitScreen - Screen for the /init command workflow
|
|
3
|
+
*
|
|
4
|
+
* Handles project initialization within the TUI context.
|
|
5
|
+
* Since the init workflow uses readline-based prompts, this screen
|
|
6
|
+
* signals that init should run outside of Ink.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useEffect } from 'react';
|
|
10
|
+
import { Box, Text } from 'ink';
|
|
11
|
+
import { colors } from '../theme.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Props for the InitScreen component
|
|
15
|
+
*/
|
|
16
|
+
export interface InitScreenProps {
|
|
17
|
+
/** Called to trigger the init workflow (runs outside Ink) */
|
|
18
|
+
onRunInit: () => void;
|
|
19
|
+
/** Called when user cancels */
|
|
20
|
+
onCancel: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* InitScreen component
|
|
25
|
+
*
|
|
26
|
+
* Displays a message and triggers the init workflow.
|
|
27
|
+
* The actual init workflow runs outside of Ink because it uses
|
|
28
|
+
* readline-based interactive prompts.
|
|
29
|
+
*/
|
|
30
|
+
export function InitScreen({
|
|
31
|
+
onRunInit,
|
|
32
|
+
onCancel: _onCancel,
|
|
33
|
+
}: InitScreenProps): React.ReactElement {
|
|
34
|
+
// Trigger init workflow on mount
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
// Small delay to allow the screen to render before unmounting
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
onRunInit();
|
|
39
|
+
}, 100);
|
|
40
|
+
|
|
41
|
+
return () => clearTimeout(timer);
|
|
42
|
+
}, [onRunInit]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Box flexDirection="column" padding={1}>
|
|
46
|
+
<Box marginBottom={1}>
|
|
47
|
+
<Text color={colors.yellow} bold>
|
|
48
|
+
Initializing Project
|
|
49
|
+
</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
|
|
52
|
+
<Box marginBottom={1}>
|
|
53
|
+
<Text>Starting initialization workflow...</Text>
|
|
54
|
+
</Box>
|
|
55
|
+
|
|
56
|
+
<Box>
|
|
57
|
+
<Text dimColor>
|
|
58
|
+
Press Ctrl+C to cancel
|
|
59
|
+
</Text>
|
|
60
|
+
</Box>
|
|
61
|
+
</Box>
|
|
62
|
+
);
|
|
63
|
+
}
|