@compilr-dev/cli 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +317 -0
- package/dist/agents/registry.d.ts +66 -0
- package/dist/agents/registry.js +238 -0
- package/dist/agents/types.d.ts +40 -0
- package/dist/agents/types.js +94 -0
- package/dist/commands/custom-registry.d.ts +69 -0
- package/dist/commands/custom-registry.js +246 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/types.d.ts +31 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +324 -0
- package/dist/db/index.d.ts +42 -0
- package/dist/db/index.js +146 -0
- package/dist/db/repositories/document-repository.d.ts +63 -0
- package/dist/db/repositories/document-repository.js +184 -0
- package/dist/db/repositories/index.d.ts +9 -0
- package/dist/db/repositories/index.js +6 -0
- package/dist/db/repositories/project-repository.d.ts +132 -0
- package/dist/db/repositories/project-repository.js +337 -0
- package/dist/db/repositories/work-item-repository.d.ts +115 -0
- package/dist/db/repositories/work-item-repository.js +389 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +143 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +348 -0
- package/dist/index.old.d.ts +7 -0
- package/dist/index.old.js +1014 -0
- package/dist/repl.d.ts +121 -0
- package/dist/repl.js +1878 -0
- package/dist/settings/index.d.ts +80 -0
- package/dist/settings/index.js +195 -0
- package/dist/shared-handlers.d.ts +63 -0
- package/dist/shared-handlers.js +57 -0
- package/dist/slash-autocomplete.d.ts +41 -0
- package/dist/slash-autocomplete.js +638 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.js +130 -0
- package/dist/tabbed-menu.d.ts +11 -0
- package/dist/tabbed-menu.js +328 -0
- package/dist/templates/backlog-md.d.ts +7 -0
- package/dist/templates/backlog-md.js +94 -0
- package/dist/templates/claude-md.d.ts +7 -0
- package/dist/templates/claude-md.js +189 -0
- package/dist/templates/coding-standards.d.ts +7 -0
- package/dist/templates/coding-standards.js +299 -0
- package/dist/templates/compilr-md.d.ts +7 -0
- package/dist/templates/compilr-md.js +189 -0
- package/dist/templates/config-json.d.ts +38 -0
- package/dist/templates/config-json.js +39 -0
- package/dist/templates/gitignore.d.ts +7 -0
- package/dist/templates/gitignore.js +85 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +302 -0
- package/dist/templates/package-json.d.ts +7 -0
- package/dist/templates/package-json.js +111 -0
- package/dist/templates/readme-md.d.ts +7 -0
- package/dist/templates/readme-md.js +161 -0
- package/dist/templates/tsconfig.d.ts +7 -0
- package/dist/templates/tsconfig.js +61 -0
- package/dist/templates/types.d.ts +33 -0
- package/dist/templates/types.js +24 -0
- package/dist/test-autocomplete.d.ts +7 -0
- package/dist/test-autocomplete.js +85 -0
- package/dist/test-tabbed-menu.d.ts +7 -0
- package/dist/test-tabbed-menu.js +25 -0
- package/dist/themes/colors.d.ts +49 -0
- package/dist/themes/colors.js +135 -0
- package/dist/themes/index.d.ts +23 -0
- package/dist/themes/index.js +24 -0
- package/dist/themes/registry.d.ts +60 -0
- package/dist/themes/registry.js +195 -0
- package/dist/themes/types.d.ts +82 -0
- package/dist/themes/types.js +7 -0
- package/dist/tool-selector.d.ts +71 -0
- package/dist/tool-selector.js +184 -0
- package/dist/tools/ask-user-simple.d.ts +19 -0
- package/dist/tools/ask-user-simple.js +86 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +113 -0
- package/dist/tools/backlog.d.ts +53 -0
- package/dist/tools/backlog.js +709 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +121 -0
- package/dist/ui/agents-overlay.d.ts +12 -0
- package/dist/ui/agents-overlay.js +501 -0
- package/dist/ui/arch-type-overlay.d.ts +20 -0
- package/dist/ui/arch-type-overlay.js +229 -0
- package/dist/ui/ask-user-overlay.d.ts +26 -0
- package/dist/ui/ask-user-overlay.js +647 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay.js +242 -0
- package/dist/ui/backlog-overlay.d.ts +17 -0
- package/dist/ui/backlog-overlay.js +786 -0
- package/dist/ui/commands-overlay.d.ts +11 -0
- package/dist/ui/commands-overlay.js +410 -0
- package/dist/ui/config-overlay.d.ts +34 -0
- package/dist/ui/config-overlay.js +977 -0
- package/dist/ui/conversation.d.ts +82 -0
- package/dist/ui/conversation.js +508 -0
- package/dist/ui/diff.d.ts +38 -0
- package/dist/ui/diff.js +182 -0
- package/dist/ui/ephemeral.d.ts +111 -0
- package/dist/ui/ephemeral.js +413 -0
- package/dist/ui/file-autocomplete.d.ts +45 -0
- package/dist/ui/file-autocomplete.js +237 -0
- package/dist/ui/footer.d.ts +153 -0
- package/dist/ui/footer.js +422 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/init-overlay.d.ts +24 -0
- package/dist/ui/init-overlay.js +525 -0
- package/dist/ui/input-prompt-v2.d.ts +179 -0
- package/dist/ui/input-prompt-v2.js +991 -0
- package/dist/ui/input-prompt.d.ts +97 -0
- package/dist/ui/input-prompt.js +800 -0
- package/dist/ui/iteration-limit-overlay.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay.js +150 -0
- package/dist/ui/keys-overlay.d.ts +14 -0
- package/dist/ui/keys-overlay.js +181 -0
- package/dist/ui/model-warning-overlay.d.ts +30 -0
- package/dist/ui/model-warning-overlay.js +171 -0
- package/dist/ui/overlay-controller.d.ts +25 -0
- package/dist/ui/overlay-controller.js +35 -0
- package/dist/ui/overlays.d.ts +47 -0
- package/dist/ui/overlays.js +627 -0
- package/dist/ui/permission-overlay.d.ts +16 -0
- package/dist/ui/permission-overlay.js +494 -0
- package/dist/ui/terminal.d.ts +117 -0
- package/dist/ui/terminal.js +237 -0
- package/dist/ui/todo-zone.d.ts +112 -0
- package/dist/ui/todo-zone.js +353 -0
- package/dist/ui/tools-overlay.d.ts +26 -0
- package/dist/ui/tools-overlay.js +278 -0
- package/dist/ui/tutorial-overlay.d.ts +10 -0
- package/dist/ui/tutorial-overlay.js +936 -0
- package/dist/ui/types.d.ts +103 -0
- package/dist/ui/types.js +33 -0
- package/dist/utils/credentials.d.ts +55 -0
- package/dist/utils/credentials.js +268 -0
- package/dist/utils/model-tiers.d.ts +37 -0
- package/dist/utils/model-tiers.js +118 -0
- package/dist/utils/project-memory.d.ts +47 -0
- package/dist/utils/project-memory.js +117 -0
- package/dist/utils/project-status.d.ts +56 -0
- package/dist/utils/project-status.js +237 -0
- package/package.json +66 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal Utilities
|
|
3
|
+
*
|
|
4
|
+
* Low-level terminal operations using ANSI escape codes.
|
|
5
|
+
* Pure functions with no state.
|
|
6
|
+
*/
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Terminal Title
|
|
9
|
+
// =============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Set terminal window title
|
|
12
|
+
*/
|
|
13
|
+
export function setTitle(title) {
|
|
14
|
+
process.stdout.write(`\x1b]0;${title}\x07`);
|
|
15
|
+
}
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Terminal Dimensions
|
|
18
|
+
// =============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Get terminal width (columns)
|
|
21
|
+
*/
|
|
22
|
+
export function getTerminalWidth() {
|
|
23
|
+
return process.stdout.columns || 80;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get terminal height (rows)
|
|
27
|
+
*/
|
|
28
|
+
export function getTerminalHeight() {
|
|
29
|
+
return process.stdout.rows || 24;
|
|
30
|
+
}
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Cursor Movement
|
|
33
|
+
// =============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* Move cursor up N lines
|
|
36
|
+
*/
|
|
37
|
+
export function moveCursorUp(n) {
|
|
38
|
+
if (n > 0) {
|
|
39
|
+
process.stdout.write(`\x1b[${String(n)}A`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Move cursor down N lines
|
|
44
|
+
*/
|
|
45
|
+
export function moveCursorDown(n) {
|
|
46
|
+
if (n > 0) {
|
|
47
|
+
process.stdout.write(`\x1b[${String(n)}B`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Move cursor to column (1-indexed)
|
|
52
|
+
*/
|
|
53
|
+
export function moveCursorToColumn(col) {
|
|
54
|
+
process.stdout.write(`\x1b[${String(col)}G`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Move cursor to beginning of line
|
|
58
|
+
*/
|
|
59
|
+
export function moveCursorToLineStart() {
|
|
60
|
+
process.stdout.write('\r');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Save cursor position
|
|
64
|
+
*/
|
|
65
|
+
export function saveCursor() {
|
|
66
|
+
process.stdout.write('\x1b[s');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Restore cursor position
|
|
70
|
+
*/
|
|
71
|
+
export function restoreCursor() {
|
|
72
|
+
process.stdout.write('\x1b[u');
|
|
73
|
+
}
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// Cursor Visibility
|
|
76
|
+
// =============================================================================
|
|
77
|
+
/**
|
|
78
|
+
* Hide cursor
|
|
79
|
+
*/
|
|
80
|
+
export function hideCursor() {
|
|
81
|
+
process.stdout.write('\x1b[?25l');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Show cursor
|
|
85
|
+
*/
|
|
86
|
+
export function showCursor() {
|
|
87
|
+
process.stdout.write('\x1b[?25h');
|
|
88
|
+
}
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// Clearing
|
|
91
|
+
// =============================================================================
|
|
92
|
+
/**
|
|
93
|
+
* Clear current line
|
|
94
|
+
*/
|
|
95
|
+
export function clearLine() {
|
|
96
|
+
process.stdout.write('\r\x1b[K');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Clear from cursor to end of screen
|
|
100
|
+
*/
|
|
101
|
+
export function clearToEndOfScreen() {
|
|
102
|
+
process.stdout.write('\x1b[J');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Clear N lines above cursor (including current line)
|
|
106
|
+
* Moves cursor up, clears to end of screen, cursor ends at top
|
|
107
|
+
*/
|
|
108
|
+
export function clearLinesAbove(count) {
|
|
109
|
+
if (count <= 0)
|
|
110
|
+
return;
|
|
111
|
+
if (count > 1) {
|
|
112
|
+
moveCursorUp(count - 1);
|
|
113
|
+
}
|
|
114
|
+
moveCursorToLineStart();
|
|
115
|
+
clearToEndOfScreen();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Clear entire screen
|
|
119
|
+
*/
|
|
120
|
+
export function clearScreen() {
|
|
121
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
122
|
+
}
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// Line Calculations
|
|
125
|
+
// =============================================================================
|
|
126
|
+
/**
|
|
127
|
+
* Calculate how many physical (visual) lines a string occupies
|
|
128
|
+
* given terminal width and starting column.
|
|
129
|
+
*
|
|
130
|
+
* @param text - The text to measure (may contain newlines)
|
|
131
|
+
* @param termWidth - Terminal width in columns
|
|
132
|
+
* @param startCol - Starting column position (0-indexed)
|
|
133
|
+
* @returns Number of physical lines occupied
|
|
134
|
+
*/
|
|
135
|
+
export function calculatePhysicalLines(text, termWidth, startCol = 0) {
|
|
136
|
+
if (text.length === 0)
|
|
137
|
+
return 1;
|
|
138
|
+
let lines = 0;
|
|
139
|
+
let col = startCol;
|
|
140
|
+
for (const char of text) {
|
|
141
|
+
if (char === '\n') {
|
|
142
|
+
lines++;
|
|
143
|
+
col = 0;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
col++;
|
|
147
|
+
if (col >= termWidth) {
|
|
148
|
+
lines++;
|
|
149
|
+
col = 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Count the final line (even if it doesn't end with newline)
|
|
154
|
+
lines++;
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Calculate physical layout of text with cursor position
|
|
159
|
+
*
|
|
160
|
+
* @param text - The text to analyze
|
|
161
|
+
* @param cursorPos - Cursor position in the text (character index)
|
|
162
|
+
* @param termWidth - Terminal width
|
|
163
|
+
* @param startCol - Starting column (for prompt prefix)
|
|
164
|
+
*/
|
|
165
|
+
export function calculateCursorPosition(text, cursorPos, termWidth, startCol = 0) {
|
|
166
|
+
let row = 0;
|
|
167
|
+
let col = startCol;
|
|
168
|
+
for (let i = 0; i < cursorPos && i < text.length; i++) {
|
|
169
|
+
if (text[i] === '\n') {
|
|
170
|
+
row++;
|
|
171
|
+
col = 0;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
col++;
|
|
175
|
+
if (col >= termWidth) {
|
|
176
|
+
row++;
|
|
177
|
+
col = 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { row, col };
|
|
182
|
+
}
|
|
183
|
+
// =============================================================================
|
|
184
|
+
// Horizontal Lines
|
|
185
|
+
// =============================================================================
|
|
186
|
+
/**
|
|
187
|
+
* Create a horizontal line spanning terminal width
|
|
188
|
+
*
|
|
189
|
+
* @param char - Character to use (default: '─')
|
|
190
|
+
* @returns String of repeated characters
|
|
191
|
+
*/
|
|
192
|
+
export function horizontalLine(char = '─') {
|
|
193
|
+
return char.repeat(getTerminalWidth());
|
|
194
|
+
}
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Raw Mode
|
|
197
|
+
// =============================================================================
|
|
198
|
+
/**
|
|
199
|
+
* Enable raw mode for stdin
|
|
200
|
+
* In raw mode, input is available character by character
|
|
201
|
+
*/
|
|
202
|
+
export function enableRawMode() {
|
|
203
|
+
if (process.stdin.isTTY) {
|
|
204
|
+
process.stdin.setRawMode(true);
|
|
205
|
+
}
|
|
206
|
+
process.stdin.resume();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Disable raw mode for stdin and pause it
|
|
210
|
+
*/
|
|
211
|
+
export function disableRawMode() {
|
|
212
|
+
if (process.stdin.isTTY) {
|
|
213
|
+
process.stdin.setRawMode(false);
|
|
214
|
+
}
|
|
215
|
+
process.stdin.pause();
|
|
216
|
+
}
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// Output Helpers
|
|
219
|
+
// =============================================================================
|
|
220
|
+
/**
|
|
221
|
+
* Write to stdout without newline
|
|
222
|
+
*/
|
|
223
|
+
export function write(text) {
|
|
224
|
+
process.stdout.write(text);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Write line to stdout with newline
|
|
228
|
+
*/
|
|
229
|
+
export function writeLine(text = '') {
|
|
230
|
+
console.log(text);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Ring terminal bell
|
|
234
|
+
*/
|
|
235
|
+
export function bell() {
|
|
236
|
+
process.stdout.write('\x07');
|
|
237
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo Zone
|
|
3
|
+
*
|
|
4
|
+
* Renders the spinner and todo list as a unified component.
|
|
5
|
+
* - When agent running: Shows animated spinner with active task name
|
|
6
|
+
* - When agent idle: Shows "Todos" header with todo list
|
|
7
|
+
*
|
|
8
|
+
* This is a pure rendering component - it returns strings/lines,
|
|
9
|
+
* the Footer orchestrator handles actual terminal output.
|
|
10
|
+
*/
|
|
11
|
+
import type { TodoItem, SpinnerState } from './types.js';
|
|
12
|
+
export interface TodoZoneOptions {
|
|
13
|
+
/** Width of the knight rider scanner (default: auto from text) */
|
|
14
|
+
spinnerWidth?: number;
|
|
15
|
+
/** Animation update interval in ms (default: 150) */
|
|
16
|
+
animationInterval?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class TodoZone {
|
|
19
|
+
private readonly animationInterval;
|
|
20
|
+
private agentRunning;
|
|
21
|
+
private todos;
|
|
22
|
+
private currentTool;
|
|
23
|
+
private spinnerState;
|
|
24
|
+
private animationPosition;
|
|
25
|
+
private animationDirection;
|
|
26
|
+
private thinkingWord;
|
|
27
|
+
private customSpinnerText;
|
|
28
|
+
private animationTimer;
|
|
29
|
+
private onAnimationUpdate;
|
|
30
|
+
constructor(options?: TodoZoneOptions);
|
|
31
|
+
/**
|
|
32
|
+
* Set whether the agent is running
|
|
33
|
+
*/
|
|
34
|
+
setAgentRunning(running: boolean): void;
|
|
35
|
+
/**
|
|
36
|
+
* Update the todo list
|
|
37
|
+
*/
|
|
38
|
+
setTodos(todos: TodoItem[]): void;
|
|
39
|
+
/**
|
|
40
|
+
* Set the current tool being executed
|
|
41
|
+
*/
|
|
42
|
+
setCurrentTool(tool: string | null): void;
|
|
43
|
+
/**
|
|
44
|
+
* Set custom spinner text (overrides todo activeForm and random thinking word)
|
|
45
|
+
* Pass null to clear and use default behavior.
|
|
46
|
+
*/
|
|
47
|
+
setSpinnerText(text: string | null): void;
|
|
48
|
+
/**
|
|
49
|
+
* Add tokens to the counter
|
|
50
|
+
*/
|
|
51
|
+
addTokens(count: number): void;
|
|
52
|
+
/**
|
|
53
|
+
* Set callback for animation updates
|
|
54
|
+
*/
|
|
55
|
+
setAnimationCallback(callback: (() => void) | null): void;
|
|
56
|
+
/**
|
|
57
|
+
* Pause the animation (for overlays, permission prompts)
|
|
58
|
+
*/
|
|
59
|
+
pauseAnimation(): void;
|
|
60
|
+
/**
|
|
61
|
+
* Resume the animation after pause
|
|
62
|
+
*/
|
|
63
|
+
resumeAnimation(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Check if zone is running
|
|
66
|
+
*/
|
|
67
|
+
isRunning(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Get spinner state (for external access to tokens, etc.)
|
|
70
|
+
*/
|
|
71
|
+
getSpinnerState(): SpinnerState | null;
|
|
72
|
+
/**
|
|
73
|
+
* Render the zone - returns array of lines to display
|
|
74
|
+
*/
|
|
75
|
+
render(): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Get the height (number of lines) this zone will render
|
|
78
|
+
*/
|
|
79
|
+
getHeight(): number;
|
|
80
|
+
/**
|
|
81
|
+
* Cleanup resources
|
|
82
|
+
*/
|
|
83
|
+
dispose(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Start the spinner animation
|
|
86
|
+
*/
|
|
87
|
+
private startSpinner;
|
|
88
|
+
/**
|
|
89
|
+
* Stop the spinner animation
|
|
90
|
+
*/
|
|
91
|
+
private stopSpinner;
|
|
92
|
+
/**
|
|
93
|
+
* Start the animation interval loop
|
|
94
|
+
*/
|
|
95
|
+
private startAnimationLoop;
|
|
96
|
+
/**
|
|
97
|
+
* Get the currently active todo (in_progress)
|
|
98
|
+
*/
|
|
99
|
+
private getActiveTodo;
|
|
100
|
+
/**
|
|
101
|
+
* Get the text to display in spinner (custom text, active todo, or thinking word)
|
|
102
|
+
*/
|
|
103
|
+
private getDisplayText;
|
|
104
|
+
/**
|
|
105
|
+
* Render the spinner line (when agent running)
|
|
106
|
+
*/
|
|
107
|
+
private renderSpinnerLine;
|
|
108
|
+
/**
|
|
109
|
+
* Render the todo list items
|
|
110
|
+
*/
|
|
111
|
+
private renderTodoList;
|
|
112
|
+
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Todo Zone
|
|
3
|
+
*
|
|
4
|
+
* Renders the spinner and todo list as a unified component.
|
|
5
|
+
* - When agent running: Shows animated spinner with active task name
|
|
6
|
+
* - When agent idle: Shows "Todos" header with todo list
|
|
7
|
+
*
|
|
8
|
+
* This is a pure rendering component - it returns strings/lines,
|
|
9
|
+
* the Footer orchestrator handles actual terminal output.
|
|
10
|
+
*/
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { getStyles } from '../themes/index.js';
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Thinking Words (for random selection when no active todo)
|
|
15
|
+
// =============================================================================
|
|
16
|
+
const THINKING_WORDS = [
|
|
17
|
+
'Thinking',
|
|
18
|
+
'Pondering',
|
|
19
|
+
'Contemplating',
|
|
20
|
+
'Ruminating',
|
|
21
|
+
'Cogitating',
|
|
22
|
+
'Deliberating',
|
|
23
|
+
'Musing',
|
|
24
|
+
'Reflecting',
|
|
25
|
+
'Considering',
|
|
26
|
+
'Analyzing',
|
|
27
|
+
'Processing',
|
|
28
|
+
'Computing',
|
|
29
|
+
'Synthesizing',
|
|
30
|
+
'Extrapolating',
|
|
31
|
+
'Hypothesizing',
|
|
32
|
+
'Discombobulating',
|
|
33
|
+
'Nebulizing',
|
|
34
|
+
'Percolating',
|
|
35
|
+
'Marinating',
|
|
36
|
+
'Simmering',
|
|
37
|
+
'Brewing',
|
|
38
|
+
'Fermenting',
|
|
39
|
+
'Distilling',
|
|
40
|
+
'Crystallizing',
|
|
41
|
+
'Transmuting',
|
|
42
|
+
];
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Formatting Helpers
|
|
45
|
+
// =============================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Format duration in human-readable format (e.g., "1m 23s", "45s")
|
|
48
|
+
*/
|
|
49
|
+
function formatDuration(ms) {
|
|
50
|
+
const seconds = Math.floor(ms / 1000);
|
|
51
|
+
if (seconds < 60) {
|
|
52
|
+
return `${String(seconds)}s`;
|
|
53
|
+
}
|
|
54
|
+
const minutes = Math.floor(seconds / 60);
|
|
55
|
+
const remainingSeconds = seconds % 60;
|
|
56
|
+
return `${String(minutes)}m ${String(remainingSeconds)}s`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Format token count (e.g., "1.2k", "500")
|
|
60
|
+
*/
|
|
61
|
+
function formatTokens(tokens) {
|
|
62
|
+
if (tokens >= 1000) {
|
|
63
|
+
return `${(tokens / 1000).toFixed(1)}k`;
|
|
64
|
+
}
|
|
65
|
+
return tokens.toString();
|
|
66
|
+
}
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Todo Zone Class
|
|
69
|
+
// =============================================================================
|
|
70
|
+
export class TodoZone {
|
|
71
|
+
// Configuration
|
|
72
|
+
animationInterval;
|
|
73
|
+
// State
|
|
74
|
+
agentRunning = false;
|
|
75
|
+
todos = [];
|
|
76
|
+
currentTool = null;
|
|
77
|
+
// Spinner state
|
|
78
|
+
spinnerState = null;
|
|
79
|
+
animationPosition = 0;
|
|
80
|
+
animationDirection = 1; // 1 = right, -1 = left
|
|
81
|
+
thinkingWord = '';
|
|
82
|
+
customSpinnerText = null;
|
|
83
|
+
animationTimer = null;
|
|
84
|
+
// Callback for when animation updates (Footer will re-render)
|
|
85
|
+
onAnimationUpdate = null;
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
this.animationInterval = options.animationInterval ?? 150;
|
|
88
|
+
}
|
|
89
|
+
// ===========================================================================
|
|
90
|
+
// Public API
|
|
91
|
+
// ===========================================================================
|
|
92
|
+
/**
|
|
93
|
+
* Set whether the agent is running
|
|
94
|
+
*/
|
|
95
|
+
setAgentRunning(running) {
|
|
96
|
+
const wasRunning = this.agentRunning;
|
|
97
|
+
this.agentRunning = running;
|
|
98
|
+
if (running && !wasRunning) {
|
|
99
|
+
// Starting - initialize spinner
|
|
100
|
+
this.startSpinner();
|
|
101
|
+
}
|
|
102
|
+
else if (!running && wasRunning) {
|
|
103
|
+
// Stopping - clear spinner
|
|
104
|
+
this.stopSpinner();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Update the todo list
|
|
109
|
+
*/
|
|
110
|
+
setTodos(todos) {
|
|
111
|
+
this.todos = todos;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Set the current tool being executed
|
|
115
|
+
*/
|
|
116
|
+
setCurrentTool(tool) {
|
|
117
|
+
this.currentTool = tool;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Set custom spinner text (overrides todo activeForm and random thinking word)
|
|
121
|
+
* Pass null to clear and use default behavior.
|
|
122
|
+
*/
|
|
123
|
+
setSpinnerText(text) {
|
|
124
|
+
this.customSpinnerText = text;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Add tokens to the counter
|
|
128
|
+
*/
|
|
129
|
+
addTokens(count) {
|
|
130
|
+
if (this.spinnerState) {
|
|
131
|
+
this.spinnerState.tokens += count;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Set callback for animation updates
|
|
136
|
+
*/
|
|
137
|
+
setAnimationCallback(callback) {
|
|
138
|
+
this.onAnimationUpdate = callback;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Pause the animation (for overlays, permission prompts)
|
|
142
|
+
*/
|
|
143
|
+
pauseAnimation() {
|
|
144
|
+
if (this.animationTimer) {
|
|
145
|
+
clearInterval(this.animationTimer);
|
|
146
|
+
this.animationTimer = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Resume the animation after pause
|
|
151
|
+
*/
|
|
152
|
+
resumeAnimation() {
|
|
153
|
+
if (this.agentRunning && !this.animationTimer) {
|
|
154
|
+
this.startAnimationLoop();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if zone is running
|
|
159
|
+
*/
|
|
160
|
+
isRunning() {
|
|
161
|
+
return this.agentRunning;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get spinner state (for external access to tokens, etc.)
|
|
165
|
+
*/
|
|
166
|
+
getSpinnerState() {
|
|
167
|
+
return this.spinnerState;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Render the zone - returns array of lines to display
|
|
171
|
+
*/
|
|
172
|
+
render() {
|
|
173
|
+
const lines = [];
|
|
174
|
+
if (this.agentRunning && this.spinnerState) {
|
|
175
|
+
// Running mode: always show spinner, optionally with todo list
|
|
176
|
+
lines.push(this.renderSpinnerLine());
|
|
177
|
+
if (this.todos.length > 0) {
|
|
178
|
+
lines.push(...this.renderTodoList(true));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (this.todos.length > 0) {
|
|
182
|
+
// Idle mode with todos: "Todos" header + todo list
|
|
183
|
+
lines.push(chalk.bold('Todos'));
|
|
184
|
+
lines.push(...this.renderTodoList(false));
|
|
185
|
+
}
|
|
186
|
+
// Idle mode without todos: render nothing
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Get the height (number of lines) this zone will render
|
|
191
|
+
*/
|
|
192
|
+
getHeight() {
|
|
193
|
+
if (this.agentRunning && this.spinnerState) {
|
|
194
|
+
// Spinner line + optional todo items
|
|
195
|
+
return 1 + this.todos.length;
|
|
196
|
+
}
|
|
197
|
+
else if (this.todos.length > 0) {
|
|
198
|
+
// "Todos" header + todo items
|
|
199
|
+
return 1 + this.todos.length;
|
|
200
|
+
}
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Cleanup resources
|
|
205
|
+
*/
|
|
206
|
+
dispose() {
|
|
207
|
+
this.stopSpinner();
|
|
208
|
+
}
|
|
209
|
+
// ===========================================================================
|
|
210
|
+
// Private Methods
|
|
211
|
+
// ===========================================================================
|
|
212
|
+
/**
|
|
213
|
+
* Start the spinner animation
|
|
214
|
+
*/
|
|
215
|
+
startSpinner() {
|
|
216
|
+
// Pick a random thinking word
|
|
217
|
+
this.thinkingWord =
|
|
218
|
+
THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)] + '...';
|
|
219
|
+
// Initialize spinner state
|
|
220
|
+
this.spinnerState = {
|
|
221
|
+
text: this.thinkingWord,
|
|
222
|
+
tokens: 0,
|
|
223
|
+
startTime: Date.now(),
|
|
224
|
+
};
|
|
225
|
+
// Reset animation
|
|
226
|
+
this.animationPosition = 0;
|
|
227
|
+
this.animationDirection = 1;
|
|
228
|
+
// Start animation loop
|
|
229
|
+
this.startAnimationLoop();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Stop the spinner animation
|
|
233
|
+
*/
|
|
234
|
+
stopSpinner() {
|
|
235
|
+
if (this.animationTimer) {
|
|
236
|
+
clearInterval(this.animationTimer);
|
|
237
|
+
this.animationTimer = null;
|
|
238
|
+
}
|
|
239
|
+
this.spinnerState = null;
|
|
240
|
+
this.currentTool = null;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Start the animation interval loop
|
|
244
|
+
*/
|
|
245
|
+
startAnimationLoop() {
|
|
246
|
+
this.animationTimer = setInterval(() => {
|
|
247
|
+
// Move position
|
|
248
|
+
this.animationPosition += this.animationDirection;
|
|
249
|
+
// Get current display text for bounds
|
|
250
|
+
const displayText = this.getDisplayText();
|
|
251
|
+
// Bounce at edges
|
|
252
|
+
if (this.animationPosition >= displayText.length - 1) {
|
|
253
|
+
this.animationDirection = -1;
|
|
254
|
+
}
|
|
255
|
+
else if (this.animationPosition <= 0) {
|
|
256
|
+
this.animationDirection = 1;
|
|
257
|
+
}
|
|
258
|
+
// Notify Footer to re-render
|
|
259
|
+
if (this.onAnimationUpdate) {
|
|
260
|
+
this.onAnimationUpdate();
|
|
261
|
+
}
|
|
262
|
+
}, this.animationInterval);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get the currently active todo (in_progress)
|
|
266
|
+
*/
|
|
267
|
+
getActiveTodo() {
|
|
268
|
+
return this.todos.find((t) => t.status === 'in_progress');
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the text to display in spinner (custom text, active todo, or thinking word)
|
|
272
|
+
*/
|
|
273
|
+
getDisplayText() {
|
|
274
|
+
// Custom text takes priority
|
|
275
|
+
if (this.customSpinnerText) {
|
|
276
|
+
return this.customSpinnerText + '...';
|
|
277
|
+
}
|
|
278
|
+
const activeTodo = this.getActiveTodo();
|
|
279
|
+
return activeTodo
|
|
280
|
+
? (activeTodo.activeForm || activeTodo.content) + '...'
|
|
281
|
+
: this.thinkingWord;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Render the spinner line (when agent running)
|
|
285
|
+
*/
|
|
286
|
+
renderSpinnerLine() {
|
|
287
|
+
if (!this.spinnerState) {
|
|
288
|
+
return '';
|
|
289
|
+
}
|
|
290
|
+
const s = getStyles();
|
|
291
|
+
const elapsed = formatDuration(Date.now() - this.spinnerState.startTime);
|
|
292
|
+
const tokenStr = formatTokens(this.spinnerState.tokens);
|
|
293
|
+
const stats = s.muted(` (esc to cancel · ${elapsed} · ↓ ${tokenStr} tokens)`);
|
|
294
|
+
// Determine display text - tool name or thinking word
|
|
295
|
+
const displayText = this.currentTool
|
|
296
|
+
? `● ${this.currentTool}`
|
|
297
|
+
: this.getDisplayText();
|
|
298
|
+
const activeTodo = this.getActiveTodo();
|
|
299
|
+
let result = '';
|
|
300
|
+
// Apply knight rider animation to the display text
|
|
301
|
+
// Uses primary color family: bold primary -> primary -> muted
|
|
302
|
+
for (let i = 0; i < displayText.length; i++) {
|
|
303
|
+
const distance = Math.abs(i - (this.animationPosition % displayText.length));
|
|
304
|
+
const char = displayText[i];
|
|
305
|
+
if (distance === 0) {
|
|
306
|
+
// Brightest - bold primary
|
|
307
|
+
result += s.primaryBold(char);
|
|
308
|
+
}
|
|
309
|
+
else if (distance === 1) {
|
|
310
|
+
// Medium - normal primary
|
|
311
|
+
result += s.primary(char);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
// Dimmed - muted
|
|
315
|
+
result += s.muted(char);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// If we have an active todo (and no tool running), show the indicator
|
|
319
|
+
if (activeTodo && !this.currentTool) {
|
|
320
|
+
result = s.primary('✱ ') + result;
|
|
321
|
+
}
|
|
322
|
+
return `${result}${stats}`;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Render the todo list items
|
|
326
|
+
*/
|
|
327
|
+
renderTodoList(indented) {
|
|
328
|
+
const s = getStyles();
|
|
329
|
+
const lines = [];
|
|
330
|
+
const indent = indented ? ' ' : '';
|
|
331
|
+
for (const todo of this.todos) {
|
|
332
|
+
let checkbox;
|
|
333
|
+
let text;
|
|
334
|
+
switch (todo.status) {
|
|
335
|
+
case 'completed':
|
|
336
|
+
checkbox = s.muted('☒');
|
|
337
|
+
text = chalk.strikethrough(s.muted(todo.content));
|
|
338
|
+
break;
|
|
339
|
+
case 'in_progress':
|
|
340
|
+
checkbox = s.primary('☐');
|
|
341
|
+
text = chalk.bold(todo.content);
|
|
342
|
+
break;
|
|
343
|
+
case 'pending':
|
|
344
|
+
default:
|
|
345
|
+
checkbox = s.muted('☐');
|
|
346
|
+
text = s.muted(todo.content);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
lines.push(`${indent}${checkbox} ${text}`);
|
|
350
|
+
}
|
|
351
|
+
return lines;
|
|
352
|
+
}
|
|
353
|
+
}
|