threewzrd 1.0.4 → 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 +41 -28
- 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([
|
|
@@ -64,12 +65,29 @@ export class ToolExecutor {
|
|
|
64
65
|
if (typeof obj.path !== 'string' || !obj.path.trim()) {
|
|
65
66
|
throw new Error('Invalid input: path must be a non-empty string');
|
|
66
67
|
}
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
// Coerce content to string - handle various types the model might return
|
|
69
|
+
let content;
|
|
70
|
+
if (typeof obj.content === 'string') {
|
|
71
|
+
content = obj.content;
|
|
72
|
+
}
|
|
73
|
+
else if (obj.content === null || obj.content === undefined) {
|
|
74
|
+
content = '';
|
|
75
|
+
}
|
|
76
|
+
else if (Array.isArray(obj.content)) {
|
|
77
|
+
// Model sometimes returns content as an array of strings
|
|
78
|
+
content = obj.content.map(item => String(item)).join('\n');
|
|
79
|
+
}
|
|
80
|
+
else if (typeof obj.content === 'object') {
|
|
81
|
+
// Fallback: stringify objects
|
|
82
|
+
content = JSON.stringify(obj.content, null, 2);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Fallback for numbers, booleans, etc.
|
|
86
|
+
content = String(obj.content);
|
|
69
87
|
}
|
|
70
88
|
return {
|
|
71
89
|
path: obj.path.trim(),
|
|
72
|
-
content
|
|
90
|
+
content,
|
|
73
91
|
skipValidation: obj.skipValidation === true
|
|
74
92
|
};
|
|
75
93
|
}
|
|
@@ -215,6 +233,7 @@ export class ToolExecutor {
|
|
|
215
233
|
try {
|
|
216
234
|
// Validate input structure
|
|
217
235
|
const validatedInput = this.validateWriteFileInput(input);
|
|
236
|
+
this.ui.startToolSpinner('write_file', `Writing ${validatedInput.path}`);
|
|
218
237
|
// Validate path doesn't escape working directory
|
|
219
238
|
const fullPath = this.validatePath(validatedInput.path);
|
|
220
239
|
const dir = path.dirname(fullPath);
|
|
@@ -224,8 +243,7 @@ export class ToolExecutor {
|
|
|
224
243
|
// If there are errors, don't write the file
|
|
225
244
|
if (!validationResult.valid) {
|
|
226
245
|
const errorDetails = validationResult.errors.join('\n - ');
|
|
227
|
-
this.ui.
|
|
228
|
-
this.ui.printToolResult(false, 'Syntax validation failed');
|
|
246
|
+
this.ui.failToolSpinner('Syntax validation failed');
|
|
229
247
|
return {
|
|
230
248
|
success: false,
|
|
231
249
|
output: '',
|
|
@@ -244,8 +262,7 @@ export class ToolExecutor {
|
|
|
244
262
|
// Write the file
|
|
245
263
|
await fs.writeFile(fullPath, validatedInput.content, 'utf-8');
|
|
246
264
|
this.createdFiles.add(validatedInput.path);
|
|
247
|
-
this.ui.
|
|
248
|
-
this.ui.printToolResult(true, '');
|
|
265
|
+
this.ui.succeedToolSpinner(`Wrote ${validatedInput.path}`);
|
|
249
266
|
return {
|
|
250
267
|
success: true,
|
|
251
268
|
output: `Successfully wrote ${validatedInput.path}`,
|
|
@@ -253,9 +270,7 @@ export class ToolExecutor {
|
|
|
253
270
|
}
|
|
254
271
|
catch (error) {
|
|
255
272
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
256
|
-
|
|
257
|
-
this.ui.printToolCall('write_file', `Writing: ${displayPath}`);
|
|
258
|
-
this.ui.printToolResult(false, errorMessage);
|
|
273
|
+
this.ui.failToolSpinner(`Failed to write: ${errorMessage}`);
|
|
259
274
|
return {
|
|
260
275
|
success: false,
|
|
261
276
|
output: '',
|
|
@@ -267,11 +282,11 @@ export class ToolExecutor {
|
|
|
267
282
|
try {
|
|
268
283
|
// Validate input structure
|
|
269
284
|
const validatedInput = this.validateReadFileInput(input);
|
|
285
|
+
this.ui.startToolSpinner('read_file', `Reading ${validatedInput.path}`);
|
|
270
286
|
// Validate path doesn't escape working directory
|
|
271
287
|
const fullPath = this.validatePath(validatedInput.path);
|
|
272
288
|
const content = await fs.readFile(fullPath, 'utf-8');
|
|
273
|
-
this.ui.
|
|
274
|
-
this.ui.printToolResult(true, '');
|
|
289
|
+
this.ui.succeedToolSpinner(`Read ${validatedInput.path}`);
|
|
275
290
|
return {
|
|
276
291
|
success: true,
|
|
277
292
|
output: content,
|
|
@@ -279,9 +294,7 @@ export class ToolExecutor {
|
|
|
279
294
|
}
|
|
280
295
|
catch (error) {
|
|
281
296
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
282
|
-
|
|
283
|
-
this.ui.printToolCall('read_file', `Reading: ${displayPath}`);
|
|
284
|
-
this.ui.printToolResult(false, errorMessage);
|
|
297
|
+
this.ui.failToolSpinner(`Failed to read: ${errorMessage}`);
|
|
285
298
|
return {
|
|
286
299
|
success: false,
|
|
287
300
|
output: '',
|
|
@@ -301,17 +314,20 @@ export class ToolExecutor {
|
|
|
301
314
|
if (validatedInput.cwd) {
|
|
302
315
|
cwd = this.validatePath(validatedInput.cwd);
|
|
303
316
|
}
|
|
304
|
-
|
|
317
|
+
// Show command for user confirmation
|
|
318
|
+
console.log();
|
|
319
|
+
console.log(chalk.yellow(`[Command] `) + chalk.gray(validatedInput.command));
|
|
305
320
|
// Ask for user confirmation before running any command
|
|
306
321
|
const approved = await this.ui.confirm(`Run this command?`);
|
|
307
322
|
if (!approved) {
|
|
308
|
-
|
|
323
|
+
console.log(chalk.red(' ✗ Declined'));
|
|
309
324
|
return {
|
|
310
325
|
success: false,
|
|
311
326
|
output: '',
|
|
312
327
|
error: 'User declined to run this command',
|
|
313
328
|
};
|
|
314
329
|
}
|
|
330
|
+
this.ui.startToolSpinner('run_command', `Running: ${validatedInput.command}`);
|
|
315
331
|
// For piped commands, use shell with pre-validated command string
|
|
316
332
|
// For non-piped commands, use spawn without shell for security
|
|
317
333
|
return new Promise((resolve) => {
|
|
@@ -338,14 +354,14 @@ export class ToolExecutor {
|
|
|
338
354
|
child.on('close', (code) => {
|
|
339
355
|
const output = stdout + (stderr ? `\nStderr: ${stderr}` : '');
|
|
340
356
|
if (code === 0) {
|
|
341
|
-
this.ui.
|
|
357
|
+
this.ui.succeedToolSpinner('Command completed');
|
|
342
358
|
resolve({
|
|
343
359
|
success: true,
|
|
344
360
|
output: output || 'Command completed successfully',
|
|
345
361
|
});
|
|
346
362
|
}
|
|
347
363
|
else {
|
|
348
|
-
this.ui.
|
|
364
|
+
this.ui.failToolSpinner(`Exit code: ${code}`);
|
|
349
365
|
resolve({
|
|
350
366
|
success: false,
|
|
351
367
|
output: '',
|
|
@@ -354,7 +370,7 @@ export class ToolExecutor {
|
|
|
354
370
|
}
|
|
355
371
|
});
|
|
356
372
|
child.on('error', (error) => {
|
|
357
|
-
this.ui.
|
|
373
|
+
this.ui.failToolSpinner(error.message);
|
|
358
374
|
resolve({
|
|
359
375
|
success: false,
|
|
360
376
|
output: '',
|
|
@@ -365,9 +381,7 @@ export class ToolExecutor {
|
|
|
365
381
|
}
|
|
366
382
|
catch (error) {
|
|
367
383
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
368
|
-
|
|
369
|
-
this.ui.printToolCall('run_command', `Command: ${displayCmd}`);
|
|
370
|
-
this.ui.printToolResult(false, errorMessage);
|
|
384
|
+
this.ui.failToolSpinner(`Failed: ${errorMessage}`);
|
|
371
385
|
return {
|
|
372
386
|
success: false,
|
|
373
387
|
output: '',
|
|
@@ -379,15 +393,16 @@ export class ToolExecutor {
|
|
|
379
393
|
try {
|
|
380
394
|
// Validate input structure
|
|
381
395
|
const validatedInput = this.validateListFilesInput(input);
|
|
396
|
+
const displayPath = validatedInput.path || '.';
|
|
397
|
+
this.ui.startToolSpinner('list_files', `Listing ${displayPath}`);
|
|
382
398
|
// Validate path doesn't escape working directory
|
|
383
399
|
const targetPath = validatedInput.path
|
|
384
400
|
? this.validatePath(validatedInput.path)
|
|
385
401
|
: this.workingDirectory;
|
|
386
|
-
this.ui.printToolCall('list_files', `Listing: ${validatedInput.path || '.'}`);
|
|
387
402
|
const files = await this.listFilesRecursive(targetPath, validatedInput.recursive ?? false);
|
|
388
403
|
// Format output
|
|
389
404
|
const relativePaths = files.map(f => path.relative(this.workingDirectory, f));
|
|
390
|
-
this.ui.
|
|
405
|
+
this.ui.succeedToolSpinner(`Listed ${relativePaths.length} items`);
|
|
391
406
|
return {
|
|
392
407
|
success: true,
|
|
393
408
|
output: relativePaths.length > 0
|
|
@@ -397,9 +412,7 @@ export class ToolExecutor {
|
|
|
397
412
|
}
|
|
398
413
|
catch (error) {
|
|
399
414
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
400
|
-
|
|
401
|
-
this.ui.printToolCall('list_files', `Listing: ${displayPath}`);
|
|
402
|
-
this.ui.printToolResult(false, errorMessage);
|
|
415
|
+
this.ui.failToolSpinner(`Failed to list: ${errorMessage}`);
|
|
403
416
|
return {
|
|
404
417
|
success: false,
|
|
405
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
|
}
|