threewzrd 1.0.6 → 1.0.8
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/core/AgentEngine.js +25 -17
- package/dist/core/ThreeJsWizard.d.ts +0 -1
- package/dist/core/ThreeJsWizard.js +2 -10
- package/dist/core/types.d.ts +0 -2
- package/dist/tools/ToolExecutor.js +35 -26
- package/dist/ui/TerminalUI.d.ts +5 -12
- package/dist/ui/TerminalUI.js +10 -123
- package/dist/ui/onboarding.js +4 -10
- package/package.json +1 -1
package/dist/core/AgentEngine.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ToolExecutor } from '../tools/ToolExecutor.js';
|
|
|
5
5
|
import { THREEJS_SYSTEM_PROMPT } from '../prompts/system.js';
|
|
6
6
|
// Limits to prevent hitting rate limits
|
|
7
7
|
const MAX_HISTORY_MESSAGES = 20; // Keep last N messages
|
|
8
|
-
const MAX_TOKENS =
|
|
8
|
+
const MAX_TOKENS = 16384; // Needs to be large enough for file contents in tool calls
|
|
9
9
|
const MAX_RETRIES = 3;
|
|
10
10
|
const RETRY_DELAY_MS = 5000; // 5 seconds base delay
|
|
11
11
|
export class AgentEngine {
|
|
@@ -58,17 +58,14 @@ export class AgentEngine {
|
|
|
58
58
|
}
|
|
59
59
|
async runAgentLoop() {
|
|
60
60
|
let continueLoop = true;
|
|
61
|
-
let turn = 1;
|
|
62
61
|
while (continueLoop) {
|
|
63
|
-
continueLoop = await this.runSingleTurn(
|
|
64
|
-
turn++;
|
|
62
|
+
continueLoop = await this.runSingleTurn();
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
|
-
async runSingleTurn(
|
|
65
|
+
async runSingleTurn(retryCount = 0) {
|
|
68
66
|
try {
|
|
69
|
-
// Show thinking indicator
|
|
70
|
-
|
|
71
|
-
this.ui.startThinking(thinkingMessage, turn);
|
|
67
|
+
// Show thinking indicator
|
|
68
|
+
this.ui.startThinking('Thinking');
|
|
72
69
|
// Create the API request with streaming
|
|
73
70
|
const stream = this.client.messages.stream({
|
|
74
71
|
model: MODEL_MAP[this.model],
|
|
@@ -96,6 +93,10 @@ export class AgentEngine {
|
|
|
96
93
|
if (!isFirstText) {
|
|
97
94
|
this.ui.endStreaming();
|
|
98
95
|
}
|
|
96
|
+
// Check for truncated response
|
|
97
|
+
if (response.stop_reason === 'max_tokens') {
|
|
98
|
+
this.ui.printWarning('Response was truncated due to length limit. Some tool calls may be incomplete.');
|
|
99
|
+
}
|
|
99
100
|
// Process all content blocks
|
|
100
101
|
for (const block of response.content) {
|
|
101
102
|
contentBlocks.push(block);
|
|
@@ -111,15 +112,16 @@ export class AgentEngine {
|
|
|
111
112
|
// No tool calls, we're done
|
|
112
113
|
return false;
|
|
113
114
|
}
|
|
115
|
+
// Show tool processing spinner
|
|
116
|
+
this.ui.startToolProcessing(toolUseBlocks.length);
|
|
114
117
|
// Execute all tool calls
|
|
115
118
|
const toolResults = [];
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.ui.
|
|
119
|
+
for (const toolUse of toolUseBlocks) {
|
|
120
|
+
// Update spinner with current tool name
|
|
121
|
+
this.ui.updateToolProcessing(toolUse.name);
|
|
122
|
+
// Stop spinner before tool execution (which prints its own output)
|
|
123
|
+
this.ui.stopToolProcessing();
|
|
121
124
|
const result = await this.toolExecutor.execute(toolUse.name, toolUse.input);
|
|
122
|
-
this.ui.stopThinking();
|
|
123
125
|
// Truncate large outputs to save tokens
|
|
124
126
|
let output = result.success ? result.output : `Error: ${result.error}`;
|
|
125
127
|
if (output.length > 2000) {
|
|
@@ -131,24 +133,30 @@ export class AgentEngine {
|
|
|
131
133
|
content: output,
|
|
132
134
|
is_error: !result.success,
|
|
133
135
|
});
|
|
136
|
+
// Restart spinner if more tools remain
|
|
137
|
+
const remainingTools = toolUseBlocks.length - toolResults.length;
|
|
138
|
+
if (remainingTools > 0) {
|
|
139
|
+
this.ui.startToolProcessing(remainingTools);
|
|
140
|
+
}
|
|
134
141
|
}
|
|
135
142
|
// Add tool results to history
|
|
136
143
|
this.conversationHistory.push({
|
|
137
144
|
role: 'user',
|
|
138
145
|
content: toolResults,
|
|
139
146
|
});
|
|
140
|
-
//
|
|
141
|
-
return
|
|
147
|
+
// Always continue after tool execution - model must see results
|
|
148
|
+
return true;
|
|
142
149
|
}
|
|
143
150
|
catch (error) {
|
|
144
151
|
this.ui.stopThinking();
|
|
152
|
+
this.ui.stopToolProcessing();
|
|
145
153
|
// Handle rate limit errors with retry
|
|
146
154
|
if (error instanceof Anthropic.RateLimitError) {
|
|
147
155
|
if (retryCount < MAX_RETRIES) {
|
|
148
156
|
const delay = RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
149
157
|
this.ui.printWarning(`Rate limited. Retrying in ${delay / 1000}s...`);
|
|
150
158
|
await this.sleep(delay);
|
|
151
|
-
return this.runSingleTurn(
|
|
159
|
+
return this.runSingleTurn(retryCount + 1);
|
|
152
160
|
}
|
|
153
161
|
this.ui.printError('Rate limit exceeded. Please wait a moment and try again.');
|
|
154
162
|
return false;
|
|
@@ -9,7 +9,6 @@ export class ThreeJsWizard {
|
|
|
9
9
|
workingDirectory;
|
|
10
10
|
isRunning = false;
|
|
11
11
|
hasOnboarded = false;
|
|
12
|
-
currentMode = 'single-shot';
|
|
13
12
|
constructor(options) {
|
|
14
13
|
this.workingDirectory = process.cwd();
|
|
15
14
|
this.ui = new TerminalUI();
|
|
@@ -43,7 +42,6 @@ export class ThreeJsWizard {
|
|
|
43
42
|
if (!this.hasOnboarded && isEmptyDir) {
|
|
44
43
|
const preferences = await runOnboarding(this.ui);
|
|
45
44
|
this.hasOnboarded = true;
|
|
46
|
-
this.currentMode = preferences.mode;
|
|
47
45
|
// Process the initial project request
|
|
48
46
|
const contextMessage = buildContextMessage(preferences);
|
|
49
47
|
await this.engine.processMessage(contextMessage);
|
|
@@ -55,8 +53,7 @@ export class ThreeJsWizard {
|
|
|
55
53
|
// Main REPL loop
|
|
56
54
|
while (this.isRunning) {
|
|
57
55
|
try {
|
|
58
|
-
const
|
|
59
|
-
this.currentMode = mode;
|
|
56
|
+
const input = await this.ui.prompt();
|
|
60
57
|
if (!input) {
|
|
61
58
|
continue;
|
|
62
59
|
}
|
|
@@ -65,13 +62,8 @@ export class ThreeJsWizard {
|
|
|
65
62
|
await this.handleCommand(input);
|
|
66
63
|
continue;
|
|
67
64
|
}
|
|
68
|
-
// Build message with mode context
|
|
69
|
-
const modePrefix = mode === 'planning'
|
|
70
|
-
? '[PLANNING MODE] Output a detailed implementation plan before coding.\n\n'
|
|
71
|
-
: '';
|
|
72
|
-
const fullMessage = modePrefix + input;
|
|
73
65
|
// Process user message through agent
|
|
74
|
-
await this.engine.processMessage(
|
|
66
|
+
await this.engine.processMessage(input);
|
|
75
67
|
// Track created files
|
|
76
68
|
for (const file of this.engine.getCreatedFiles()) {
|
|
77
69
|
this.projectManager.addFile(file);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4,12 +4,10 @@ export declare const MODEL_MAP: Record<ModelId, string>;
|
|
|
4
4
|
export declare const DEFAULT_MODEL: ModelId;
|
|
5
5
|
export type ProjectLanguage = 'javascript' | 'typescript';
|
|
6
6
|
export type ProjectTarget = 'browser' | 'mobile' | 'desktop';
|
|
7
|
-
export type ExecutionMode = 'single-shot' | 'planning';
|
|
8
7
|
export interface ProjectPreferences {
|
|
9
8
|
language: ProjectLanguage;
|
|
10
9
|
target: ProjectTarget;
|
|
11
10
|
description: string;
|
|
12
|
-
mode: ExecutionMode;
|
|
13
11
|
}
|
|
14
12
|
export interface ProjectConfig {
|
|
15
13
|
name: string;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs/promises';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
4
|
import { shouldValidate, validate } from './CodeValidator.js';
|
|
6
5
|
// Whitelist of allowed commands for security
|
|
7
6
|
const ALLOWED_COMMANDS = new Set([
|
|
@@ -59,20 +58,21 @@ export class ToolExecutor {
|
|
|
59
58
|
*/
|
|
60
59
|
validateWriteFileInput(input) {
|
|
61
60
|
if (!input || typeof input !== 'object') {
|
|
62
|
-
throw new Error(
|
|
61
|
+
throw new Error(`Invalid input: expected object, got ${typeof input}: ${JSON.stringify(input)}`);
|
|
63
62
|
}
|
|
64
63
|
const obj = input;
|
|
65
64
|
if (typeof obj.path !== 'string' || !obj.path.trim()) {
|
|
66
|
-
throw new Error(
|
|
65
|
+
throw new Error(`Invalid input: path must be a non-empty string. Received keys: ${Object.keys(obj).join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
// Content is required - reject null/undefined
|
|
68
|
+
if (obj.content === null || obj.content === undefined) {
|
|
69
|
+
throw new Error(`Invalid input: content is required. Received keys: [${Object.keys(obj).join(', ')}], content type: ${typeof obj.content}`);
|
|
67
70
|
}
|
|
68
71
|
// Coerce content to string - handle various types the model might return
|
|
69
72
|
let content;
|
|
70
73
|
if (typeof obj.content === 'string') {
|
|
71
74
|
content = obj.content;
|
|
72
75
|
}
|
|
73
|
-
else if (obj.content === null || obj.content === undefined) {
|
|
74
|
-
content = '';
|
|
75
|
-
}
|
|
76
76
|
else if (Array.isArray(obj.content)) {
|
|
77
77
|
// Model sometimes returns content as an array of strings
|
|
78
78
|
content = obj.content.map(item => String(item)).join('\n');
|
|
@@ -85,6 +85,10 @@ export class ToolExecutor {
|
|
|
85
85
|
// Fallback for numbers, booleans, etc.
|
|
86
86
|
content = String(obj.content);
|
|
87
87
|
}
|
|
88
|
+
// Reject empty content - likely a model error
|
|
89
|
+
if (!content.trim()) {
|
|
90
|
+
throw new Error('Invalid input: content cannot be empty');
|
|
91
|
+
}
|
|
88
92
|
return {
|
|
89
93
|
path: obj.path.trim(),
|
|
90
94
|
content,
|
|
@@ -233,7 +237,6 @@ export class ToolExecutor {
|
|
|
233
237
|
try {
|
|
234
238
|
// Validate input structure
|
|
235
239
|
const validatedInput = this.validateWriteFileInput(input);
|
|
236
|
-
this.ui.startToolSpinner('write_file', `Writing ${validatedInput.path}`);
|
|
237
240
|
// Validate path doesn't escape working directory
|
|
238
241
|
const fullPath = this.validatePath(validatedInput.path);
|
|
239
242
|
const dir = path.dirname(fullPath);
|
|
@@ -243,7 +246,8 @@ export class ToolExecutor {
|
|
|
243
246
|
// If there are errors, don't write the file
|
|
244
247
|
if (!validationResult.valid) {
|
|
245
248
|
const errorDetails = validationResult.errors.join('\n - ');
|
|
246
|
-
this.ui.
|
|
249
|
+
this.ui.printToolCall('write_file', `Writing: ${validatedInput.path}`);
|
|
250
|
+
this.ui.printToolResult(false, 'Syntax validation failed');
|
|
247
251
|
return {
|
|
248
252
|
success: false,
|
|
249
253
|
output: '',
|
|
@@ -262,7 +266,8 @@ export class ToolExecutor {
|
|
|
262
266
|
// Write the file
|
|
263
267
|
await fs.writeFile(fullPath, validatedInput.content, 'utf-8');
|
|
264
268
|
this.createdFiles.add(validatedInput.path);
|
|
265
|
-
this.ui.
|
|
269
|
+
this.ui.printToolCall('write_file', `Writing: ${validatedInput.path}`);
|
|
270
|
+
this.ui.printToolResult(true, '');
|
|
266
271
|
return {
|
|
267
272
|
success: true,
|
|
268
273
|
output: `Successfully wrote ${validatedInput.path}`,
|
|
@@ -270,7 +275,9 @@ export class ToolExecutor {
|
|
|
270
275
|
}
|
|
271
276
|
catch (error) {
|
|
272
277
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
273
|
-
|
|
278
|
+
const displayPath = input?.path || 'unknown';
|
|
279
|
+
this.ui.printToolCall('write_file', `Writing: ${displayPath}`);
|
|
280
|
+
this.ui.printToolResult(false, errorMessage);
|
|
274
281
|
return {
|
|
275
282
|
success: false,
|
|
276
283
|
output: '',
|
|
@@ -282,11 +289,11 @@ export class ToolExecutor {
|
|
|
282
289
|
try {
|
|
283
290
|
// Validate input structure
|
|
284
291
|
const validatedInput = this.validateReadFileInput(input);
|
|
285
|
-
this.ui.startToolSpinner('read_file', `Reading ${validatedInput.path}`);
|
|
286
292
|
// Validate path doesn't escape working directory
|
|
287
293
|
const fullPath = this.validatePath(validatedInput.path);
|
|
288
294
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
289
|
-
this.ui.
|
|
295
|
+
this.ui.printToolCall('read_file', `Reading: ${validatedInput.path}`);
|
|
296
|
+
this.ui.printToolResult(true, '');
|
|
290
297
|
return {
|
|
291
298
|
success: true,
|
|
292
299
|
output: content,
|
|
@@ -294,7 +301,9 @@ export class ToolExecutor {
|
|
|
294
301
|
}
|
|
295
302
|
catch (error) {
|
|
296
303
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
297
|
-
|
|
304
|
+
const displayPath = input?.path || 'unknown';
|
|
305
|
+
this.ui.printToolCall('read_file', `Reading: ${displayPath}`);
|
|
306
|
+
this.ui.printToolResult(false, errorMessage);
|
|
298
307
|
return {
|
|
299
308
|
success: false,
|
|
300
309
|
output: '',
|
|
@@ -314,20 +323,17 @@ export class ToolExecutor {
|
|
|
314
323
|
if (validatedInput.cwd) {
|
|
315
324
|
cwd = this.validatePath(validatedInput.cwd);
|
|
316
325
|
}
|
|
317
|
-
|
|
318
|
-
console.log();
|
|
319
|
-
console.log(chalk.yellow(`[Command] `) + chalk.gray(validatedInput.command));
|
|
326
|
+
this.ui.printToolCall('run_command', `Command: ${validatedInput.command}`);
|
|
320
327
|
// Ask for user confirmation before running any command
|
|
321
328
|
const approved = await this.ui.confirm(`Run this command?`);
|
|
322
329
|
if (!approved) {
|
|
323
|
-
|
|
330
|
+
this.ui.printToolResult(false, 'User declined');
|
|
324
331
|
return {
|
|
325
332
|
success: false,
|
|
326
333
|
output: '',
|
|
327
334
|
error: 'User declined to run this command',
|
|
328
335
|
};
|
|
329
336
|
}
|
|
330
|
-
this.ui.startToolSpinner('run_command', `Running: ${validatedInput.command}`);
|
|
331
337
|
// For piped commands, use shell with pre-validated command string
|
|
332
338
|
// For non-piped commands, use spawn without shell for security
|
|
333
339
|
return new Promise((resolve) => {
|
|
@@ -354,14 +360,14 @@ export class ToolExecutor {
|
|
|
354
360
|
child.on('close', (code) => {
|
|
355
361
|
const output = stdout + (stderr ? `\nStderr: ${stderr}` : '');
|
|
356
362
|
if (code === 0) {
|
|
357
|
-
this.ui.
|
|
363
|
+
this.ui.printToolResult(true, '');
|
|
358
364
|
resolve({
|
|
359
365
|
success: true,
|
|
360
366
|
output: output || 'Command completed successfully',
|
|
361
367
|
});
|
|
362
368
|
}
|
|
363
369
|
else {
|
|
364
|
-
this.ui.
|
|
370
|
+
this.ui.printToolResult(false, `Exit code: ${code}`);
|
|
365
371
|
resolve({
|
|
366
372
|
success: false,
|
|
367
373
|
output: '',
|
|
@@ -370,7 +376,7 @@ export class ToolExecutor {
|
|
|
370
376
|
}
|
|
371
377
|
});
|
|
372
378
|
child.on('error', (error) => {
|
|
373
|
-
this.ui.
|
|
379
|
+
this.ui.printToolResult(false, error.message);
|
|
374
380
|
resolve({
|
|
375
381
|
success: false,
|
|
376
382
|
output: '',
|
|
@@ -381,7 +387,9 @@ export class ToolExecutor {
|
|
|
381
387
|
}
|
|
382
388
|
catch (error) {
|
|
383
389
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
384
|
-
|
|
390
|
+
const displayCmd = input?.command || 'unknown';
|
|
391
|
+
this.ui.printToolCall('run_command', `Command: ${displayCmd}`);
|
|
392
|
+
this.ui.printToolResult(false, errorMessage);
|
|
385
393
|
return {
|
|
386
394
|
success: false,
|
|
387
395
|
output: '',
|
|
@@ -393,16 +401,15 @@ export class ToolExecutor {
|
|
|
393
401
|
try {
|
|
394
402
|
// Validate input structure
|
|
395
403
|
const validatedInput = this.validateListFilesInput(input);
|
|
396
|
-
const displayPath = validatedInput.path || '.';
|
|
397
|
-
this.ui.startToolSpinner('list_files', `Listing ${displayPath}`);
|
|
398
404
|
// Validate path doesn't escape working directory
|
|
399
405
|
const targetPath = validatedInput.path
|
|
400
406
|
? this.validatePath(validatedInput.path)
|
|
401
407
|
: this.workingDirectory;
|
|
408
|
+
this.ui.printToolCall('list_files', `Listing: ${validatedInput.path || '.'}`);
|
|
402
409
|
const files = await this.listFilesRecursive(targetPath, validatedInput.recursive ?? false);
|
|
403
410
|
// Format output
|
|
404
411
|
const relativePaths = files.map(f => path.relative(this.workingDirectory, f));
|
|
405
|
-
this.ui.
|
|
412
|
+
this.ui.printToolResult(true, '');
|
|
406
413
|
return {
|
|
407
414
|
success: true,
|
|
408
415
|
output: relativePaths.length > 0
|
|
@@ -412,7 +419,9 @@ export class ToolExecutor {
|
|
|
412
419
|
}
|
|
413
420
|
catch (error) {
|
|
414
421
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
415
|
-
|
|
422
|
+
const displayPath = input?.path || '.';
|
|
423
|
+
this.ui.printToolCall('list_files', `Listing: ${displayPath}`);
|
|
424
|
+
this.ui.printToolResult(false, errorMessage);
|
|
416
425
|
return {
|
|
417
426
|
success: false,
|
|
418
427
|
output: '',
|
package/dist/ui/TerminalUI.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModelId
|
|
1
|
+
import { ModelId } from '../core/types.js';
|
|
2
2
|
export interface SelectOption {
|
|
3
3
|
label: string;
|
|
4
4
|
value: string;
|
|
@@ -9,13 +9,11 @@ export declare class TerminalUI {
|
|
|
9
9
|
private thinkingSpinner;
|
|
10
10
|
private toolSpinner;
|
|
11
11
|
constructor();
|
|
12
|
-
startThinking(message?: string
|
|
13
|
-
updateThinking(message: string, turn?: number): void;
|
|
12
|
+
startThinking(message?: string): void;
|
|
14
13
|
stopThinking(): void;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
stopToolSpinner(): void;
|
|
14
|
+
startToolProcessing(toolCount: number): void;
|
|
15
|
+
updateToolProcessing(toolName: string): void;
|
|
16
|
+
stopToolProcessing(): void;
|
|
19
17
|
confirm(message: string): Promise<boolean>;
|
|
20
18
|
select(question: string, options: SelectOption[]): Promise<string>;
|
|
21
19
|
printBanner(): void;
|
|
@@ -37,11 +35,6 @@ export declare class TerminalUI {
|
|
|
37
35
|
streamText(text: string): void;
|
|
38
36
|
endStreaming(): void;
|
|
39
37
|
prompt(): Promise<string>;
|
|
40
|
-
promptWithMode(defaultMode?: ExecutionMode): Promise<{
|
|
41
|
-
text: string;
|
|
42
|
-
mode: ExecutionMode;
|
|
43
|
-
}>;
|
|
44
|
-
printModeInfo(mode: ExecutionMode): void;
|
|
45
38
|
close(): void;
|
|
46
39
|
clearScreen(): void;
|
|
47
40
|
}
|
package/dist/ui/TerminalUI.js
CHANGED
|
@@ -12,21 +12,14 @@ export class TerminalUI {
|
|
|
12
12
|
output: process.stdout,
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
// Thinking indicator
|
|
16
|
-
startThinking(message = 'Thinking'
|
|
17
|
-
const turnInfo = turn ? chalk.gray(` [Turn ${turn}]`) : '';
|
|
15
|
+
// Thinking indicator
|
|
16
|
+
startThinking(message = 'Thinking') {
|
|
18
17
|
this.thinkingSpinner = ora({
|
|
19
|
-
text: chalk.cyan(message)
|
|
18
|
+
text: chalk.cyan(message),
|
|
20
19
|
spinner: 'dots',
|
|
21
20
|
discardStdin: false, // Don't interfere with readline
|
|
22
21
|
}).start();
|
|
23
22
|
}
|
|
24
|
-
updateThinking(message, turn) {
|
|
25
|
-
if (this.thinkingSpinner) {
|
|
26
|
-
const turnInfo = turn ? chalk.gray(` [Turn ${turn}]`) : '';
|
|
27
|
-
this.thinkingSpinner.text = chalk.cyan(message) + turnInfo;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
23
|
stopThinking() {
|
|
31
24
|
if (this.thinkingSpinner) {
|
|
32
25
|
this.thinkingSpinner.stop();
|
|
@@ -37,27 +30,21 @@ export class TerminalUI {
|
|
|
37
30
|
process.stdin.resume();
|
|
38
31
|
}
|
|
39
32
|
}
|
|
40
|
-
// Tool
|
|
41
|
-
|
|
33
|
+
// Tool processing spinner
|
|
34
|
+
startToolProcessing(toolCount) {
|
|
35
|
+
const plural = toolCount > 1 ? 's' : '';
|
|
42
36
|
this.toolSpinner = ora({
|
|
43
|
-
text: chalk.yellow(
|
|
37
|
+
text: chalk.yellow(`Executing ${toolCount} tool${plural}...`),
|
|
44
38
|
spinner: 'dots',
|
|
45
39
|
discardStdin: false,
|
|
46
40
|
}).start();
|
|
47
41
|
}
|
|
48
|
-
|
|
49
|
-
if (this.toolSpinner) {
|
|
50
|
-
this.toolSpinner.succeed(message ? chalk.green(message) : undefined);
|
|
51
|
-
this.toolSpinner = null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
failToolSpinner(message) {
|
|
42
|
+
updateToolProcessing(toolName) {
|
|
55
43
|
if (this.toolSpinner) {
|
|
56
|
-
this.toolSpinner.
|
|
57
|
-
this.toolSpinner = null;
|
|
44
|
+
this.toolSpinner.text = chalk.yellow(`Executing: ${toolName}...`);
|
|
58
45
|
}
|
|
59
46
|
}
|
|
60
|
-
|
|
47
|
+
stopToolProcessing() {
|
|
61
48
|
if (this.toolSpinner) {
|
|
62
49
|
this.toolSpinner.stop();
|
|
63
50
|
this.toolSpinner = null;
|
|
@@ -112,18 +99,12 @@ export class TerminalUI {
|
|
|
112
99
|
console.log(chalk.gray(' lighting, animations, and more - just describe what'));
|
|
113
100
|
console.log(chalk.gray(' you want in plain English.'));
|
|
114
101
|
console.log();
|
|
115
|
-
console.log(chalk.gray(' Modes: ') + chalk.cyan('⚡ Quick') + chalk.gray(' | ') + chalk.magenta('📋 Plan') + chalk.gray(' (Tab to switch)'));
|
|
116
102
|
console.log(chalk.gray(' Commands: ') + chalk.yellow('/help') + chalk.gray(' | ') + chalk.yellow('/clear') + chalk.gray(' | ') + chalk.yellow('/exit'));
|
|
117
103
|
console.log();
|
|
118
104
|
console.log(chalk.gray(' ─────────────────────────────────────────'));
|
|
119
105
|
console.log();
|
|
120
106
|
}
|
|
121
107
|
printHelp() {
|
|
122
|
-
console.log();
|
|
123
|
-
console.log(chalk.yellow('Execution Modes:'));
|
|
124
|
-
console.log(chalk.cyan(' ⚡ Quick Mode') + chalk.gray(' - Direct implementation (default)'));
|
|
125
|
-
console.log(chalk.magenta(' 📋 Plan Mode') + chalk.gray(' - Detailed architecture plan first'));
|
|
126
|
-
console.log(chalk.gray(' Press Tab while typing to switch modes'));
|
|
127
108
|
console.log();
|
|
128
109
|
console.log(chalk.yellow('Commands:'));
|
|
129
110
|
console.log(chalk.cyan(' /help') + chalk.gray(' - Show this help message'));
|
|
@@ -216,100 +197,6 @@ export class TerminalUI {
|
|
|
216
197
|
});
|
|
217
198
|
});
|
|
218
199
|
}
|
|
219
|
-
// Mode-aware prompt with Tab toggle for execution mode
|
|
220
|
-
async promptWithMode(defaultMode = 'single-shot') {
|
|
221
|
-
let currentMode = defaultMode;
|
|
222
|
-
// Print mode indicator and instructions
|
|
223
|
-
const printModeBar = () => {
|
|
224
|
-
const singleShot = currentMode === 'single-shot'
|
|
225
|
-
? chalk.bgCyan.black(' ⚡ Quick ')
|
|
226
|
-
: chalk.gray(' ⚡ Quick ');
|
|
227
|
-
const planning = currentMode === 'planning'
|
|
228
|
-
? chalk.bgMagenta.white(' 📋 Plan ')
|
|
229
|
-
: chalk.gray(' 📋 Plan ');
|
|
230
|
-
// Clear line and reprint
|
|
231
|
-
process.stdout.write('\r\x1b[K');
|
|
232
|
-
process.stdout.write(` ${singleShot} ${planning} ${chalk.gray('Tab to switch')}\n`);
|
|
233
|
-
};
|
|
234
|
-
return new Promise((resolve) => {
|
|
235
|
-
printModeBar();
|
|
236
|
-
console.log();
|
|
237
|
-
// Close the existing readline interface to fully release stdin
|
|
238
|
-
this.rl.close();
|
|
239
|
-
// Store the current input
|
|
240
|
-
let inputBuffer = '';
|
|
241
|
-
// Set up raw mode for keypress detection
|
|
242
|
-
if (process.stdin.isTTY) {
|
|
243
|
-
process.stdin.setRawMode(true);
|
|
244
|
-
}
|
|
245
|
-
process.stdin.resume();
|
|
246
|
-
const promptPrefix = chalk.magenta(' › ');
|
|
247
|
-
process.stdout.write(promptPrefix);
|
|
248
|
-
const handleKeypress = (chunk) => {
|
|
249
|
-
const key = chunk.toString();
|
|
250
|
-
// Tab key - toggle mode
|
|
251
|
-
if (key === '\t') {
|
|
252
|
-
currentMode = currentMode === 'single-shot' ? 'planning' : 'single-shot';
|
|
253
|
-
// Move cursor up, clear the mode bar, reprint it, move back down
|
|
254
|
-
process.stdout.write('\x1b[2A'); // Move up 2 lines
|
|
255
|
-
printModeBar();
|
|
256
|
-
console.log();
|
|
257
|
-
process.stdout.write(promptPrefix + inputBuffer);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// Enter key - submit
|
|
261
|
-
if (key === '\r' || key === '\n') {
|
|
262
|
-
process.stdout.write('\n');
|
|
263
|
-
cleanup();
|
|
264
|
-
resolve({ text: inputBuffer.trim(), mode: currentMode });
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
// Ctrl+C - exit
|
|
268
|
-
if (key === '\x03') {
|
|
269
|
-
cleanup();
|
|
270
|
-
process.exit(0);
|
|
271
|
-
}
|
|
272
|
-
// Backspace
|
|
273
|
-
if (key === '\x7f' || key === '\b') {
|
|
274
|
-
if (inputBuffer.length > 0) {
|
|
275
|
-
inputBuffer = inputBuffer.slice(0, -1);
|
|
276
|
-
process.stdout.write('\b \b');
|
|
277
|
-
}
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
// Escape sequences (arrow keys, etc.) - ignore for simplicity
|
|
281
|
-
if (key.startsWith('\x1b')) {
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
// Regular character
|
|
285
|
-
if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
286
|
-
inputBuffer += key;
|
|
287
|
-
process.stdout.write(key);
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
const cleanup = () => {
|
|
291
|
-
process.stdin.removeListener('data', handleKeypress);
|
|
292
|
-
if (process.stdin.isTTY) {
|
|
293
|
-
process.stdin.setRawMode(false);
|
|
294
|
-
}
|
|
295
|
-
// Recreate readline interface
|
|
296
|
-
this.rl = readline.createInterface({
|
|
297
|
-
input: process.stdin,
|
|
298
|
-
output: process.stdout,
|
|
299
|
-
});
|
|
300
|
-
};
|
|
301
|
-
process.stdin.on('data', handleKeypress);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
printModeInfo(mode) {
|
|
305
|
-
if (mode === 'single-shot') {
|
|
306
|
-
console.log(chalk.cyan(' ⚡ Quick Mode: ') + chalk.gray('Direct implementation without formal planning'));
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
console.log(chalk.magenta(' 📋 Plan Mode: ') + chalk.gray('Detailed architecture plan before implementation'));
|
|
310
|
-
}
|
|
311
|
-
console.log();
|
|
312
|
-
}
|
|
313
200
|
close() {
|
|
314
201
|
this.rl.close();
|
|
315
202
|
}
|
package/dist/ui/onboarding.js
CHANGED
|
@@ -12,33 +12,27 @@ export async function runOnboarding(ui) {
|
|
|
12
12
|
{ label: 'Mobile (React Native, etc.)', value: 'mobile' },
|
|
13
13
|
{ label: 'Desktop (Electron, etc.)', value: 'desktop' },
|
|
14
14
|
]);
|
|
15
|
-
// Ask for project description
|
|
15
|
+
// Ask for project description
|
|
16
16
|
console.log();
|
|
17
|
-
console.log(chalk.cyan('
|
|
17
|
+
console.log(chalk.cyan(' Now describe what you\'d like to build:'));
|
|
18
18
|
console.log(chalk.gray(' (e.g., "A 3D solar system with orbiting planets")'));
|
|
19
19
|
console.log();
|
|
20
|
-
const
|
|
20
|
+
const description = await ui.prompt();
|
|
21
21
|
console.log();
|
|
22
|
-
ui.printModeInfo(mode);
|
|
23
22
|
console.log(chalk.gray(' ─────────────────────────────────────────'));
|
|
24
23
|
console.log();
|
|
25
24
|
return {
|
|
26
25
|
language,
|
|
27
26
|
target,
|
|
28
27
|
description,
|
|
29
|
-
mode,
|
|
30
28
|
};
|
|
31
29
|
}
|
|
32
30
|
export function buildContextMessage(prefs) {
|
|
33
|
-
const modeInstruction = prefs.mode === 'planning'
|
|
34
|
-
? `\n\nIMPORTANT: The user has requested Planning Mode. You MUST output a detailed implementation plan with Architecture Overview, Dependencies, File Structure, and Execution Steps BEFORE writing any code.`
|
|
35
|
-
: `\n\nThe user has requested Single-Shot Mode. Plan internally and proceed directly to implementation without outputting a formal plan.`;
|
|
36
31
|
return `The user wants to create a Three.js project with these preferences:
|
|
37
32
|
- Language: ${prefs.language}
|
|
38
33
|
- Target platform: ${prefs.target}
|
|
39
|
-
- Execution mode: ${prefs.mode === 'planning' ? 'Planning Mode (detailed plan first)' : 'Single-Shot Mode (direct implementation)'}
|
|
40
34
|
|
|
41
|
-
Their project description: ${prefs.description}
|
|
35
|
+
Their project description: ${prefs.description}
|
|
42
36
|
|
|
43
37
|
Please create the project structure and initial files based on these requirements. Start by setting up the basic Three.js scene.`;
|
|
44
38
|
}
|