threewzrd 1.0.5 → 1.0.6
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 +14 -6
- package/dist/core/ThreeJsWizard.d.ts +1 -0
- package/dist/core/ThreeJsWizard.js +10 -2
- package/dist/core/types.d.ts +2 -0
- package/dist/tools/ToolExecutor.js +21 -25
- package/dist/ui/TerminalUI.d.ts +13 -2
- package/dist/ui/TerminalUI.js +137 -3
- package/dist/ui/onboarding.js +10 -4
- package/package.json +1 -1
package/dist/core/AgentEngine.js
CHANGED
|
@@ -58,14 +58,17 @@ export class AgentEngine {
|
|
|
58
58
|
}
|
|
59
59
|
async runAgentLoop() {
|
|
60
60
|
let continueLoop = true;
|
|
61
|
+
let turn = 1;
|
|
61
62
|
while (continueLoop) {
|
|
62
|
-
continueLoop = await this.runSingleTurn();
|
|
63
|
+
continueLoop = await this.runSingleTurn(turn);
|
|
64
|
+
turn++;
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
|
-
async runSingleTurn(retryCount = 0) {
|
|
67
|
+
async runSingleTurn(turn, retryCount = 0) {
|
|
66
68
|
try {
|
|
67
|
-
// Show thinking indicator
|
|
68
|
-
|
|
69
|
+
// Show thinking indicator with turn info
|
|
70
|
+
const thinkingMessage = turn === 1 ? 'Thinking' : 'Processing';
|
|
71
|
+
this.ui.startThinking(thinkingMessage, turn);
|
|
69
72
|
// Create the API request with streaming
|
|
70
73
|
const stream = this.client.messages.stream({
|
|
71
74
|
model: MODEL_MAP[this.model],
|
|
@@ -110,8 +113,13 @@ export class AgentEngine {
|
|
|
110
113
|
}
|
|
111
114
|
// Execute all tool calls
|
|
112
115
|
const toolResults = [];
|
|
113
|
-
|
|
116
|
+
const totalTools = toolUseBlocks.length;
|
|
117
|
+
for (let i = 0; i < toolUseBlocks.length; i++) {
|
|
118
|
+
const toolUse = toolUseBlocks[i];
|
|
119
|
+
const toolProgress = totalTools > 1 ? ` (${i + 1}/${totalTools})` : '';
|
|
120
|
+
this.ui.startThinking(`Executing ${toolUse.name}${toolProgress}`, turn);
|
|
114
121
|
const result = await this.toolExecutor.execute(toolUse.name, toolUse.input);
|
|
122
|
+
this.ui.stopThinking();
|
|
115
123
|
// Truncate large outputs to save tokens
|
|
116
124
|
let output = result.success ? result.output : `Error: ${result.error}`;
|
|
117
125
|
if (output.length > 2000) {
|
|
@@ -140,7 +148,7 @@ export class AgentEngine {
|
|
|
140
148
|
const delay = RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
141
149
|
this.ui.printWarning(`Rate limited. Retrying in ${delay / 1000}s...`);
|
|
142
150
|
await this.sleep(delay);
|
|
143
|
-
return this.runSingleTurn(retryCount + 1);
|
|
151
|
+
return this.runSingleTurn(turn, retryCount + 1);
|
|
144
152
|
}
|
|
145
153
|
this.ui.printError('Rate limit exceeded. Please wait a moment and try again.');
|
|
146
154
|
return false;
|
|
@@ -9,6 +9,7 @@ export class ThreeJsWizard {
|
|
|
9
9
|
workingDirectory;
|
|
10
10
|
isRunning = false;
|
|
11
11
|
hasOnboarded = false;
|
|
12
|
+
currentMode = 'single-shot';
|
|
12
13
|
constructor(options) {
|
|
13
14
|
this.workingDirectory = process.cwd();
|
|
14
15
|
this.ui = new TerminalUI();
|
|
@@ -42,6 +43,7 @@ export class ThreeJsWizard {
|
|
|
42
43
|
if (!this.hasOnboarded && isEmptyDir) {
|
|
43
44
|
const preferences = await runOnboarding(this.ui);
|
|
44
45
|
this.hasOnboarded = true;
|
|
46
|
+
this.currentMode = preferences.mode;
|
|
45
47
|
// Process the initial project request
|
|
46
48
|
const contextMessage = buildContextMessage(preferences);
|
|
47
49
|
await this.engine.processMessage(contextMessage);
|
|
@@ -53,7 +55,8 @@ export class ThreeJsWizard {
|
|
|
53
55
|
// Main REPL loop
|
|
54
56
|
while (this.isRunning) {
|
|
55
57
|
try {
|
|
56
|
-
const input = await this.ui.
|
|
58
|
+
const { text: input, mode } = await this.ui.promptWithMode(this.currentMode);
|
|
59
|
+
this.currentMode = mode;
|
|
57
60
|
if (!input) {
|
|
58
61
|
continue;
|
|
59
62
|
}
|
|
@@ -62,8 +65,13 @@ export class ThreeJsWizard {
|
|
|
62
65
|
await this.handleCommand(input);
|
|
63
66
|
continue;
|
|
64
67
|
}
|
|
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;
|
|
65
73
|
// Process user message through agent
|
|
66
|
-
await this.engine.processMessage(
|
|
74
|
+
await this.engine.processMessage(fullMessage);
|
|
67
75
|
// Track created files
|
|
68
76
|
for (const file of this.engine.getCreatedFiles()) {
|
|
69
77
|
this.projectManager.addFile(file);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4,10 +4,12 @@ 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';
|
|
7
8
|
export interface ProjectPreferences {
|
|
8
9
|
language: ProjectLanguage;
|
|
9
10
|
target: ProjectTarget;
|
|
10
11
|
description: string;
|
|
12
|
+
mode: ExecutionMode;
|
|
11
13
|
}
|
|
12
14
|
export interface ProjectConfig {
|
|
13
15
|
name: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
4
5
|
import { shouldValidate, validate } from './CodeValidator.js';
|
|
5
6
|
// Whitelist of allowed commands for security
|
|
6
7
|
const ALLOWED_COMMANDS = new Set([
|
|
@@ -232,6 +233,7 @@ export class ToolExecutor {
|
|
|
232
233
|
try {
|
|
233
234
|
// Validate input structure
|
|
234
235
|
const validatedInput = this.validateWriteFileInput(input);
|
|
236
|
+
this.ui.startToolSpinner('write_file', `Writing ${validatedInput.path}`);
|
|
235
237
|
// Validate path doesn't escape working directory
|
|
236
238
|
const fullPath = this.validatePath(validatedInput.path);
|
|
237
239
|
const dir = path.dirname(fullPath);
|
|
@@ -241,8 +243,7 @@ export class ToolExecutor {
|
|
|
241
243
|
// If there are errors, don't write the file
|
|
242
244
|
if (!validationResult.valid) {
|
|
243
245
|
const errorDetails = validationResult.errors.join('\n - ');
|
|
244
|
-
this.ui.
|
|
245
|
-
this.ui.printToolResult(false, 'Syntax validation failed');
|
|
246
|
+
this.ui.failToolSpinner('Syntax validation failed');
|
|
246
247
|
return {
|
|
247
248
|
success: false,
|
|
248
249
|
output: '',
|
|
@@ -261,8 +262,7 @@ export class ToolExecutor {
|
|
|
261
262
|
// Write the file
|
|
262
263
|
await fs.writeFile(fullPath, validatedInput.content, 'utf-8');
|
|
263
264
|
this.createdFiles.add(validatedInput.path);
|
|
264
|
-
this.ui.
|
|
265
|
-
this.ui.printToolResult(true, '');
|
|
265
|
+
this.ui.succeedToolSpinner(`Wrote ${validatedInput.path}`);
|
|
266
266
|
return {
|
|
267
267
|
success: true,
|
|
268
268
|
output: `Successfully wrote ${validatedInput.path}`,
|
|
@@ -270,9 +270,7 @@ export class ToolExecutor {
|
|
|
270
270
|
}
|
|
271
271
|
catch (error) {
|
|
272
272
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
273
|
-
|
|
274
|
-
this.ui.printToolCall('write_file', `Writing: ${displayPath}`);
|
|
275
|
-
this.ui.printToolResult(false, errorMessage);
|
|
273
|
+
this.ui.failToolSpinner(`Failed to write: ${errorMessage}`);
|
|
276
274
|
return {
|
|
277
275
|
success: false,
|
|
278
276
|
output: '',
|
|
@@ -284,11 +282,11 @@ export class ToolExecutor {
|
|
|
284
282
|
try {
|
|
285
283
|
// Validate input structure
|
|
286
284
|
const validatedInput = this.validateReadFileInput(input);
|
|
285
|
+
this.ui.startToolSpinner('read_file', `Reading ${validatedInput.path}`);
|
|
287
286
|
// Validate path doesn't escape working directory
|
|
288
287
|
const fullPath = this.validatePath(validatedInput.path);
|
|
289
288
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
290
|
-
this.ui.
|
|
291
|
-
this.ui.printToolResult(true, '');
|
|
289
|
+
this.ui.succeedToolSpinner(`Read ${validatedInput.path}`);
|
|
292
290
|
return {
|
|
293
291
|
success: true,
|
|
294
292
|
output: content,
|
|
@@ -296,9 +294,7 @@ export class ToolExecutor {
|
|
|
296
294
|
}
|
|
297
295
|
catch (error) {
|
|
298
296
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
299
|
-
|
|
300
|
-
this.ui.printToolCall('read_file', `Reading: ${displayPath}`);
|
|
301
|
-
this.ui.printToolResult(false, errorMessage);
|
|
297
|
+
this.ui.failToolSpinner(`Failed to read: ${errorMessage}`);
|
|
302
298
|
return {
|
|
303
299
|
success: false,
|
|
304
300
|
output: '',
|
|
@@ -318,17 +314,20 @@ export class ToolExecutor {
|
|
|
318
314
|
if (validatedInput.cwd) {
|
|
319
315
|
cwd = this.validatePath(validatedInput.cwd);
|
|
320
316
|
}
|
|
321
|
-
|
|
317
|
+
// Show command for user confirmation
|
|
318
|
+
console.log();
|
|
319
|
+
console.log(chalk.yellow(`[Command] `) + chalk.gray(validatedInput.command));
|
|
322
320
|
// Ask for user confirmation before running any command
|
|
323
321
|
const approved = await this.ui.confirm(`Run this command?`);
|
|
324
322
|
if (!approved) {
|
|
325
|
-
|
|
323
|
+
console.log(chalk.red(' ✗ Declined'));
|
|
326
324
|
return {
|
|
327
325
|
success: false,
|
|
328
326
|
output: '',
|
|
329
327
|
error: 'User declined to run this command',
|
|
330
328
|
};
|
|
331
329
|
}
|
|
330
|
+
this.ui.startToolSpinner('run_command', `Running: ${validatedInput.command}`);
|
|
332
331
|
// For piped commands, use shell with pre-validated command string
|
|
333
332
|
// For non-piped commands, use spawn without shell for security
|
|
334
333
|
return new Promise((resolve) => {
|
|
@@ -355,14 +354,14 @@ export class ToolExecutor {
|
|
|
355
354
|
child.on('close', (code) => {
|
|
356
355
|
const output = stdout + (stderr ? `\nStderr: ${stderr}` : '');
|
|
357
356
|
if (code === 0) {
|
|
358
|
-
this.ui.
|
|
357
|
+
this.ui.succeedToolSpinner('Command completed');
|
|
359
358
|
resolve({
|
|
360
359
|
success: true,
|
|
361
360
|
output: output || 'Command completed successfully',
|
|
362
361
|
});
|
|
363
362
|
}
|
|
364
363
|
else {
|
|
365
|
-
this.ui.
|
|
364
|
+
this.ui.failToolSpinner(`Exit code: ${code}`);
|
|
366
365
|
resolve({
|
|
367
366
|
success: false,
|
|
368
367
|
output: '',
|
|
@@ -371,7 +370,7 @@ export class ToolExecutor {
|
|
|
371
370
|
}
|
|
372
371
|
});
|
|
373
372
|
child.on('error', (error) => {
|
|
374
|
-
this.ui.
|
|
373
|
+
this.ui.failToolSpinner(error.message);
|
|
375
374
|
resolve({
|
|
376
375
|
success: false,
|
|
377
376
|
output: '',
|
|
@@ -382,9 +381,7 @@ export class ToolExecutor {
|
|
|
382
381
|
}
|
|
383
382
|
catch (error) {
|
|
384
383
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
385
|
-
|
|
386
|
-
this.ui.printToolCall('run_command', `Command: ${displayCmd}`);
|
|
387
|
-
this.ui.printToolResult(false, errorMessage);
|
|
384
|
+
this.ui.failToolSpinner(`Failed: ${errorMessage}`);
|
|
388
385
|
return {
|
|
389
386
|
success: false,
|
|
390
387
|
output: '',
|
|
@@ -396,15 +393,16 @@ export class ToolExecutor {
|
|
|
396
393
|
try {
|
|
397
394
|
// Validate input structure
|
|
398
395
|
const validatedInput = this.validateListFilesInput(input);
|
|
396
|
+
const displayPath = validatedInput.path || '.';
|
|
397
|
+
this.ui.startToolSpinner('list_files', `Listing ${displayPath}`);
|
|
399
398
|
// Validate path doesn't escape working directory
|
|
400
399
|
const targetPath = validatedInput.path
|
|
401
400
|
? this.validatePath(validatedInput.path)
|
|
402
401
|
: this.workingDirectory;
|
|
403
|
-
this.ui.printToolCall('list_files', `Listing: ${validatedInput.path || '.'}`);
|
|
404
402
|
const files = await this.listFilesRecursive(targetPath, validatedInput.recursive ?? false);
|
|
405
403
|
// Format output
|
|
406
404
|
const relativePaths = files.map(f => path.relative(this.workingDirectory, f));
|
|
407
|
-
this.ui.
|
|
405
|
+
this.ui.succeedToolSpinner(`Listed ${relativePaths.length} items`);
|
|
408
406
|
return {
|
|
409
407
|
success: true,
|
|
410
408
|
output: relativePaths.length > 0
|
|
@@ -414,9 +412,7 @@ export class ToolExecutor {
|
|
|
414
412
|
}
|
|
415
413
|
catch (error) {
|
|
416
414
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
417
|
-
|
|
418
|
-
this.ui.printToolCall('list_files', `Listing: ${displayPath}`);
|
|
419
|
-
this.ui.printToolResult(false, errorMessage);
|
|
415
|
+
this.ui.failToolSpinner(`Failed to list: ${errorMessage}`);
|
|
420
416
|
return {
|
|
421
417
|
success: false,
|
|
422
418
|
output: '',
|
package/dist/ui/TerminalUI.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModelId } from '../core/types.js';
|
|
1
|
+
import { ModelId, ExecutionMode } from '../core/types.js';
|
|
2
2
|
export interface SelectOption {
|
|
3
3
|
label: string;
|
|
4
4
|
value: string;
|
|
@@ -7,9 +7,15 @@ export declare class TerminalUI {
|
|
|
7
7
|
private rl;
|
|
8
8
|
private isStreaming;
|
|
9
9
|
private thinkingSpinner;
|
|
10
|
+
private toolSpinner;
|
|
10
11
|
constructor();
|
|
11
|
-
startThinking(message?: string): void;
|
|
12
|
+
startThinking(message?: string, turn?: number): void;
|
|
13
|
+
updateThinking(message: string, turn?: number): void;
|
|
12
14
|
stopThinking(): void;
|
|
15
|
+
startToolSpinner(toolName: string, detail: string): void;
|
|
16
|
+
succeedToolSpinner(message?: string): void;
|
|
17
|
+
failToolSpinner(message: string): void;
|
|
18
|
+
stopToolSpinner(): void;
|
|
13
19
|
confirm(message: string): Promise<boolean>;
|
|
14
20
|
select(question: string, options: SelectOption[]): Promise<string>;
|
|
15
21
|
printBanner(): void;
|
|
@@ -31,6 +37,11 @@ export declare class TerminalUI {
|
|
|
31
37
|
streamText(text: string): void;
|
|
32
38
|
endStreaming(): void;
|
|
33
39
|
prompt(): Promise<string>;
|
|
40
|
+
promptWithMode(defaultMode?: ExecutionMode): Promise<{
|
|
41
|
+
text: string;
|
|
42
|
+
mode: ExecutionMode;
|
|
43
|
+
}>;
|
|
44
|
+
printModeInfo(mode: ExecutionMode): void;
|
|
34
45
|
close(): void;
|
|
35
46
|
clearScreen(): void;
|
|
36
47
|
}
|
package/dist/ui/TerminalUI.js
CHANGED
|
@@ -5,20 +5,28 @@ export class TerminalUI {
|
|
|
5
5
|
rl;
|
|
6
6
|
isStreaming = false;
|
|
7
7
|
thinkingSpinner = null;
|
|
8
|
+
toolSpinner = null;
|
|
8
9
|
constructor() {
|
|
9
10
|
this.rl = readline.createInterface({
|
|
10
11
|
input: process.stdin,
|
|
11
12
|
output: process.stdout,
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
|
-
// Thinking indicator
|
|
15
|
-
startThinking(message = 'Thinking') {
|
|
15
|
+
// Thinking indicator with turn info
|
|
16
|
+
startThinking(message = 'Thinking', turn) {
|
|
17
|
+
const turnInfo = turn ? chalk.gray(` [Turn ${turn}]`) : '';
|
|
16
18
|
this.thinkingSpinner = ora({
|
|
17
|
-
text: chalk.cyan(message),
|
|
19
|
+
text: chalk.cyan(message) + turnInfo,
|
|
18
20
|
spinner: 'dots',
|
|
19
21
|
discardStdin: false, // Don't interfere with readline
|
|
20
22
|
}).start();
|
|
21
23
|
}
|
|
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
|
+
}
|
|
22
30
|
stopThinking() {
|
|
23
31
|
if (this.thinkingSpinner) {
|
|
24
32
|
this.thinkingSpinner.stop();
|
|
@@ -29,6 +37,32 @@ export class TerminalUI {
|
|
|
29
37
|
process.stdin.resume();
|
|
30
38
|
}
|
|
31
39
|
}
|
|
40
|
+
// Tool execution spinner
|
|
41
|
+
startToolSpinner(toolName, detail) {
|
|
42
|
+
this.toolSpinner = ora({
|
|
43
|
+
text: chalk.yellow(`${toolName}: `) + chalk.gray(detail),
|
|
44
|
+
spinner: 'dots',
|
|
45
|
+
discardStdin: false,
|
|
46
|
+
}).start();
|
|
47
|
+
}
|
|
48
|
+
succeedToolSpinner(message) {
|
|
49
|
+
if (this.toolSpinner) {
|
|
50
|
+
this.toolSpinner.succeed(message ? chalk.green(message) : undefined);
|
|
51
|
+
this.toolSpinner = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
failToolSpinner(message) {
|
|
55
|
+
if (this.toolSpinner) {
|
|
56
|
+
this.toolSpinner.fail(chalk.red(message));
|
|
57
|
+
this.toolSpinner = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
stopToolSpinner() {
|
|
61
|
+
if (this.toolSpinner) {
|
|
62
|
+
this.toolSpinner.stop();
|
|
63
|
+
this.toolSpinner = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
32
66
|
// Confirmation prompt for dangerous actions
|
|
33
67
|
async confirm(message) {
|
|
34
68
|
// Ensure any spinner is stopped
|
|
@@ -78,12 +112,18 @@ export class TerminalUI {
|
|
|
78
112
|
console.log(chalk.gray(' lighting, animations, and more - just describe what'));
|
|
79
113
|
console.log(chalk.gray(' you want in plain English.'));
|
|
80
114
|
console.log();
|
|
115
|
+
console.log(chalk.gray(' Modes: ') + chalk.cyan('⚡ Quick') + chalk.gray(' | ') + chalk.magenta('📋 Plan') + chalk.gray(' (Tab to switch)'));
|
|
81
116
|
console.log(chalk.gray(' Commands: ') + chalk.yellow('/help') + chalk.gray(' | ') + chalk.yellow('/clear') + chalk.gray(' | ') + chalk.yellow('/exit'));
|
|
82
117
|
console.log();
|
|
83
118
|
console.log(chalk.gray(' ─────────────────────────────────────────'));
|
|
84
119
|
console.log();
|
|
85
120
|
}
|
|
86
121
|
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'));
|
|
87
127
|
console.log();
|
|
88
128
|
console.log(chalk.yellow('Commands:'));
|
|
89
129
|
console.log(chalk.cyan(' /help') + chalk.gray(' - Show this help message'));
|
|
@@ -176,6 +216,100 @@ export class TerminalUI {
|
|
|
176
216
|
});
|
|
177
217
|
});
|
|
178
218
|
}
|
|
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
|
+
}
|
|
179
313
|
close() {
|
|
180
314
|
this.rl.close();
|
|
181
315
|
}
|
package/dist/ui/onboarding.js
CHANGED
|
@@ -12,27 +12,33 @@ 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 with mode toggle
|
|
16
16
|
console.log();
|
|
17
|
-
console.log(chalk.cyan('
|
|
17
|
+
console.log(chalk.cyan(' 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 description = await ui.
|
|
20
|
+
const { text: description, mode } = await ui.promptWithMode('single-shot');
|
|
21
21
|
console.log();
|
|
22
|
+
ui.printModeInfo(mode);
|
|
22
23
|
console.log(chalk.gray(' ─────────────────────────────────────────'));
|
|
23
24
|
console.log();
|
|
24
25
|
return {
|
|
25
26
|
language,
|
|
26
27
|
target,
|
|
27
28
|
description,
|
|
29
|
+
mode,
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
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.`;
|
|
31
36
|
return `The user wants to create a Three.js project with these preferences:
|
|
32
37
|
- Language: ${prefs.language}
|
|
33
38
|
- Target platform: ${prefs.target}
|
|
39
|
+
- Execution mode: ${prefs.mode === 'planning' ? 'Planning Mode (detailed plan first)' : 'Single-Shot Mode (direct implementation)'}
|
|
34
40
|
|
|
35
|
-
Their project description: ${prefs.description}
|
|
41
|
+
Their project description: ${prefs.description}${modeInstruction}
|
|
36
42
|
|
|
37
43
|
Please create the project structure and initial files based on these requirements. Start by setting up the basic Three.js scene.`;
|
|
38
44
|
}
|