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.
Files changed (75) hide show
  1. package/dist/ai/conversation/conversation-manager.d.ts +11 -0
  2. package/dist/ai/conversation/conversation-manager.d.ts.map +1 -1
  3. package/dist/ai/conversation/conversation-manager.js +14 -0
  4. package/dist/ai/conversation/conversation-manager.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +4 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/new.d.ts +2 -0
  9. package/dist/commands/new.d.ts.map +1 -1
  10. package/dist/commands/new.js +63 -22
  11. package/dist/commands/new.js.map +1 -1
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +97 -22
  15. package/dist/index.js.map +1 -1
  16. package/dist/tui/app.d.ts +46 -36
  17. package/dist/tui/app.d.ts.map +1 -1
  18. package/dist/tui/app.js +136 -37
  19. package/dist/tui/app.js.map +1 -1
  20. package/dist/tui/components/WiggumBanner.d.ts +30 -0
  21. package/dist/tui/components/WiggumBanner.d.ts.map +1 -0
  22. package/dist/tui/components/WiggumBanner.js +34 -0
  23. package/dist/tui/components/WiggumBanner.js.map +1 -0
  24. package/dist/tui/demo.d.ts +8 -0
  25. package/dist/tui/demo.d.ts.map +1 -0
  26. package/dist/tui/demo.js +69 -0
  27. package/dist/tui/demo.js.map +1 -0
  28. package/dist/tui/hooks/useSpecGenerator.d.ts +16 -0
  29. package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -1
  30. package/dist/tui/hooks/useSpecGenerator.js +47 -0
  31. package/dist/tui/hooks/useSpecGenerator.js.map +1 -1
  32. package/dist/tui/orchestration/index.d.ts +6 -0
  33. package/dist/tui/orchestration/index.d.ts.map +1 -0
  34. package/dist/tui/orchestration/index.js +6 -0
  35. package/dist/tui/orchestration/index.js.map +1 -0
  36. package/dist/tui/orchestration/interview-orchestrator.d.ts +136 -0
  37. package/dist/tui/orchestration/interview-orchestrator.d.ts.map +1 -0
  38. package/dist/tui/orchestration/interview-orchestrator.js +437 -0
  39. package/dist/tui/orchestration/interview-orchestrator.js.map +1 -0
  40. package/dist/tui/screens/InitScreen.d.ts +26 -0
  41. package/dist/tui/screens/InitScreen.d.ts.map +1 -0
  42. package/dist/tui/screens/InitScreen.js +30 -0
  43. package/dist/tui/screens/InitScreen.js.map +1 -0
  44. package/dist/tui/screens/InterviewScreen.d.ts +2 -13
  45. package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
  46. package/dist/tui/screens/InterviewScreen.js +162 -34
  47. package/dist/tui/screens/InterviewScreen.js.map +1 -1
  48. package/dist/tui/screens/MainShell.d.ts +46 -0
  49. package/dist/tui/screens/MainShell.d.ts.map +1 -0
  50. package/dist/tui/screens/MainShell.js +196 -0
  51. package/dist/tui/screens/MainShell.js.map +1 -0
  52. package/dist/tui/screens/WelcomeScreen.d.ts +45 -0
  53. package/dist/tui/screens/WelcomeScreen.d.ts.map +1 -0
  54. package/dist/tui/screens/WelcomeScreen.js +56 -0
  55. package/dist/tui/screens/WelcomeScreen.js.map +1 -0
  56. package/dist/tui/theme.d.ts +4 -0
  57. package/dist/tui/theme.d.ts.map +1 -1
  58. package/dist/tui/theme.js +4 -0
  59. package/dist/tui/theme.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/ai/conversation/conversation-manager.ts +22 -0
  62. package/src/cli.ts +4 -0
  63. package/src/commands/new.ts +79 -27
  64. package/src/index.ts +109 -27
  65. package/src/tui/app.tsx +222 -63
  66. package/src/tui/components/WiggumBanner.tsx +66 -0
  67. package/src/tui/demo.tsx +111 -0
  68. package/src/tui/hooks/useSpecGenerator.ts +73 -0
  69. package/src/tui/orchestration/index.ts +10 -0
  70. package/src/tui/orchestration/interview-orchestrator.ts +559 -0
  71. package/src/tui/screens/InitScreen.tsx +63 -0
  72. package/src/tui/screens/InterviewScreen.tsx +201 -46
  73. package/src/tui/screens/MainShell.tsx +290 -0
  74. package/src/tui/screens/WelcomeScreen.tsx +141 -0
  75. 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
+ }