@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.
Files changed (152) hide show
  1. package/README.md +110 -0
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agent.js +317 -0
  4. package/dist/agents/registry.d.ts +66 -0
  5. package/dist/agents/registry.js +238 -0
  6. package/dist/agents/types.d.ts +40 -0
  7. package/dist/agents/types.js +94 -0
  8. package/dist/commands/custom-registry.d.ts +69 -0
  9. package/dist/commands/custom-registry.js +246 -0
  10. package/dist/commands/index.d.ts +7 -0
  11. package/dist/commands/index.js +7 -0
  12. package/dist/commands/types.d.ts +31 -0
  13. package/dist/commands/types.js +26 -0
  14. package/dist/commands.d.ts +63 -0
  15. package/dist/commands.js +324 -0
  16. package/dist/db/index.d.ts +42 -0
  17. package/dist/db/index.js +146 -0
  18. package/dist/db/repositories/document-repository.d.ts +63 -0
  19. package/dist/db/repositories/document-repository.js +184 -0
  20. package/dist/db/repositories/index.d.ts +9 -0
  21. package/dist/db/repositories/index.js +6 -0
  22. package/dist/db/repositories/project-repository.d.ts +132 -0
  23. package/dist/db/repositories/project-repository.js +337 -0
  24. package/dist/db/repositories/work-item-repository.d.ts +115 -0
  25. package/dist/db/repositories/work-item-repository.js +389 -0
  26. package/dist/db/schema.d.ts +83 -0
  27. package/dist/db/schema.js +143 -0
  28. package/dist/debug.d.ts +8 -0
  29. package/dist/debug.js +48 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +348 -0
  32. package/dist/index.old.d.ts +7 -0
  33. package/dist/index.old.js +1014 -0
  34. package/dist/repl.d.ts +121 -0
  35. package/dist/repl.js +1878 -0
  36. package/dist/settings/index.d.ts +80 -0
  37. package/dist/settings/index.js +195 -0
  38. package/dist/shared-handlers.d.ts +63 -0
  39. package/dist/shared-handlers.js +57 -0
  40. package/dist/slash-autocomplete.d.ts +41 -0
  41. package/dist/slash-autocomplete.js +638 -0
  42. package/dist/state.d.ts +75 -0
  43. package/dist/state.js +130 -0
  44. package/dist/tabbed-menu.d.ts +11 -0
  45. package/dist/tabbed-menu.js +328 -0
  46. package/dist/templates/backlog-md.d.ts +7 -0
  47. package/dist/templates/backlog-md.js +94 -0
  48. package/dist/templates/claude-md.d.ts +7 -0
  49. package/dist/templates/claude-md.js +189 -0
  50. package/dist/templates/coding-standards.d.ts +7 -0
  51. package/dist/templates/coding-standards.js +299 -0
  52. package/dist/templates/compilr-md.d.ts +7 -0
  53. package/dist/templates/compilr-md.js +189 -0
  54. package/dist/templates/config-json.d.ts +38 -0
  55. package/dist/templates/config-json.js +39 -0
  56. package/dist/templates/gitignore.d.ts +7 -0
  57. package/dist/templates/gitignore.js +85 -0
  58. package/dist/templates/index.d.ts +19 -0
  59. package/dist/templates/index.js +302 -0
  60. package/dist/templates/package-json.d.ts +7 -0
  61. package/dist/templates/package-json.js +111 -0
  62. package/dist/templates/readme-md.d.ts +7 -0
  63. package/dist/templates/readme-md.js +161 -0
  64. package/dist/templates/tsconfig.d.ts +7 -0
  65. package/dist/templates/tsconfig.js +61 -0
  66. package/dist/templates/types.d.ts +33 -0
  67. package/dist/templates/types.js +24 -0
  68. package/dist/test-autocomplete.d.ts +7 -0
  69. package/dist/test-autocomplete.js +85 -0
  70. package/dist/test-tabbed-menu.d.ts +7 -0
  71. package/dist/test-tabbed-menu.js +25 -0
  72. package/dist/themes/colors.d.ts +49 -0
  73. package/dist/themes/colors.js +135 -0
  74. package/dist/themes/index.d.ts +23 -0
  75. package/dist/themes/index.js +24 -0
  76. package/dist/themes/registry.d.ts +60 -0
  77. package/dist/themes/registry.js +195 -0
  78. package/dist/themes/types.d.ts +82 -0
  79. package/dist/themes/types.js +7 -0
  80. package/dist/tool-selector.d.ts +71 -0
  81. package/dist/tool-selector.js +184 -0
  82. package/dist/tools/ask-user-simple.d.ts +19 -0
  83. package/dist/tools/ask-user-simple.js +86 -0
  84. package/dist/tools/ask-user.d.ts +32 -0
  85. package/dist/tools/ask-user.js +113 -0
  86. package/dist/tools/backlog.d.ts +53 -0
  87. package/dist/tools/backlog.js +709 -0
  88. package/dist/tools.d.ts +15 -0
  89. package/dist/tools.js +121 -0
  90. package/dist/ui/agents-overlay.d.ts +12 -0
  91. package/dist/ui/agents-overlay.js +501 -0
  92. package/dist/ui/arch-type-overlay.d.ts +20 -0
  93. package/dist/ui/arch-type-overlay.js +229 -0
  94. package/dist/ui/ask-user-overlay.d.ts +26 -0
  95. package/dist/ui/ask-user-overlay.js +647 -0
  96. package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
  97. package/dist/ui/ask-user-simple-overlay.js +242 -0
  98. package/dist/ui/backlog-overlay.d.ts +17 -0
  99. package/dist/ui/backlog-overlay.js +786 -0
  100. package/dist/ui/commands-overlay.d.ts +11 -0
  101. package/dist/ui/commands-overlay.js +410 -0
  102. package/dist/ui/config-overlay.d.ts +34 -0
  103. package/dist/ui/config-overlay.js +977 -0
  104. package/dist/ui/conversation.d.ts +82 -0
  105. package/dist/ui/conversation.js +508 -0
  106. package/dist/ui/diff.d.ts +38 -0
  107. package/dist/ui/diff.js +182 -0
  108. package/dist/ui/ephemeral.d.ts +111 -0
  109. package/dist/ui/ephemeral.js +413 -0
  110. package/dist/ui/file-autocomplete.d.ts +45 -0
  111. package/dist/ui/file-autocomplete.js +237 -0
  112. package/dist/ui/footer.d.ts +153 -0
  113. package/dist/ui/footer.js +422 -0
  114. package/dist/ui/index.d.ts +12 -0
  115. package/dist/ui/index.js +15 -0
  116. package/dist/ui/init-overlay.d.ts +24 -0
  117. package/dist/ui/init-overlay.js +525 -0
  118. package/dist/ui/input-prompt-v2.d.ts +179 -0
  119. package/dist/ui/input-prompt-v2.js +991 -0
  120. package/dist/ui/input-prompt.d.ts +97 -0
  121. package/dist/ui/input-prompt.js +800 -0
  122. package/dist/ui/iteration-limit-overlay.d.ts +21 -0
  123. package/dist/ui/iteration-limit-overlay.js +150 -0
  124. package/dist/ui/keys-overlay.d.ts +14 -0
  125. package/dist/ui/keys-overlay.js +181 -0
  126. package/dist/ui/model-warning-overlay.d.ts +30 -0
  127. package/dist/ui/model-warning-overlay.js +171 -0
  128. package/dist/ui/overlay-controller.d.ts +25 -0
  129. package/dist/ui/overlay-controller.js +35 -0
  130. package/dist/ui/overlays.d.ts +47 -0
  131. package/dist/ui/overlays.js +627 -0
  132. package/dist/ui/permission-overlay.d.ts +16 -0
  133. package/dist/ui/permission-overlay.js +494 -0
  134. package/dist/ui/terminal.d.ts +117 -0
  135. package/dist/ui/terminal.js +237 -0
  136. package/dist/ui/todo-zone.d.ts +112 -0
  137. package/dist/ui/todo-zone.js +353 -0
  138. package/dist/ui/tools-overlay.d.ts +26 -0
  139. package/dist/ui/tools-overlay.js +278 -0
  140. package/dist/ui/tutorial-overlay.d.ts +10 -0
  141. package/dist/ui/tutorial-overlay.js +936 -0
  142. package/dist/ui/types.d.ts +103 -0
  143. package/dist/ui/types.js +33 -0
  144. package/dist/utils/credentials.d.ts +55 -0
  145. package/dist/utils/credentials.js +268 -0
  146. package/dist/utils/model-tiers.d.ts +37 -0
  147. package/dist/utils/model-tiers.js +118 -0
  148. package/dist/utils/project-memory.d.ts +47 -0
  149. package/dist/utils/project-memory.js +117 -0
  150. package/dist/utils/project-status.d.ts +56 -0
  151. package/dist/utils/project-status.js +237 -0
  152. 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
+ }