@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,182 @@
1
+ /**
2
+ * Diff Formatting Utility
3
+ *
4
+ * Generates colorized diff output for file edits, similar to Claude Code's display.
5
+ * Shows removed lines with red background and added lines with green background.
6
+ */
7
+ import * as fs from 'fs';
8
+ import chalk from 'chalk';
9
+ import { getStyles } from '../themes/index.js';
10
+ // =============================================================================
11
+ // Configuration
12
+ // =============================================================================
13
+ /** Number of context lines to show before and after changes */
14
+ const CONTEXT_LINES = 3;
15
+ // =============================================================================
16
+ // Diff Generation
17
+ // =============================================================================
18
+ /**
19
+ * Generate a formatted diff for an edit operation.
20
+ * Supports both single replacement and replaceAll.
21
+ * Shows full lines being modified with proper addition/removal counts.
22
+ *
23
+ * @param filePath - Path to the file being edited
24
+ * @param oldText - Text being replaced
25
+ * @param newText - Replacement text
26
+ * @param replaceAll - Whether all occurrences are being replaced
27
+ * @returns Formatted diff result, or null if file can't be read
28
+ */
29
+ export function generateEditDiff(filePath, oldText, newText, replaceAll = false) {
30
+ // Try to read the current file content
31
+ let fileContent;
32
+ try {
33
+ fileContent = fs.readFileSync(filePath, 'utf-8');
34
+ }
35
+ catch {
36
+ // File doesn't exist yet or can't be read
37
+ return null;
38
+ }
39
+ // Split file into lines
40
+ const lines = fileContent.split('\n');
41
+ // Find all line numbers that contain the old text
42
+ const affectedLines = [];
43
+ for (let i = 0; i < lines.length; i++) {
44
+ if (lines[i].includes(oldText)) {
45
+ affectedLines.push(i);
46
+ if (!replaceAll)
47
+ break; // Only find first occurrence
48
+ }
49
+ }
50
+ if (affectedLines.length === 0) {
51
+ // Old text not found
52
+ return null;
53
+ }
54
+ // Build the diff output
55
+ const diffLines = [];
56
+ let additions = 0;
57
+ let removals = 0;
58
+ // Track which context lines have been shown
59
+ const shownContextLines = new Set();
60
+ let lastShownLine = -999;
61
+ for (let idx = 0; idx < affectedLines.length; idx++) {
62
+ const lineNum = affectedLines[idx];
63
+ // Calculate context range for this change
64
+ const contextStart = Math.max(0, lineNum - CONTEXT_LINES);
65
+ const contextEnd = Math.min(lines.length - 1, lineNum + CONTEXT_LINES);
66
+ // Add separator if there's a gap from the last shown line
67
+ const s = getStyles();
68
+ if (lastShownLine >= 0 && contextStart > lastShownLine + 1) {
69
+ diffLines.push(s.muted(' ...'));
70
+ }
71
+ // Show context lines before the change
72
+ for (let i = contextStart; i < lineNum; i++) {
73
+ if (!shownContextLines.has(i)) {
74
+ const lineNumStr = String(i + 1).padStart(6);
75
+ diffLines.push(formatContextLine(lineNumStr, lines[i]));
76
+ shownContextLines.add(i);
77
+ lastShownLine = i;
78
+ }
79
+ }
80
+ // Show the changed line (removal + addition)
81
+ const lineNumStr = String(lineNum + 1).padStart(6);
82
+ const originalLine = lines[lineNum];
83
+ const modifiedLine = replaceAll
84
+ ? originalLine.split(oldText).join(newText) // Replace all in line
85
+ : originalLine.replace(oldText, newText); // Replace first in line
86
+ diffLines.push(formatRemovedLine(lineNumStr, originalLine));
87
+ diffLines.push(formatAddedLine(lineNumStr, modifiedLine));
88
+ removals++;
89
+ additions++;
90
+ shownContextLines.add(lineNum);
91
+ lastShownLine = lineNum;
92
+ // Show context lines after the change (but only if not overlapping with next change)
93
+ const nextChangeLineNum = idx < affectedLines.length - 1 ? affectedLines[idx + 1] : Infinity;
94
+ for (let i = lineNum + 1; i <= contextEnd && i < nextChangeLineNum; i++) {
95
+ if (!shownContextLines.has(i)) {
96
+ const ctxLineNumStr = String(i + 1).padStart(6);
97
+ diffLines.push(formatContextLine(ctxLineNumStr, lines[i]));
98
+ shownContextLines.add(i);
99
+ lastShownLine = i;
100
+ }
101
+ }
102
+ }
103
+ return {
104
+ lines: diffLines,
105
+ additions,
106
+ removals,
107
+ };
108
+ }
109
+ /**
110
+ * Format a diff header showing what changed
111
+ */
112
+ export function formatDiffHeader(filePath, additions, removals) {
113
+ const s = getStyles();
114
+ const fileName = filePath.split('/').pop() || filePath;
115
+ const stats = [];
116
+ if (additions > 0) {
117
+ stats.push(s.success(`${String(additions)} addition${additions !== 1 ? 's' : ''}`));
118
+ }
119
+ if (removals > 0) {
120
+ stats.push(s.error(`${String(removals)} removal${removals !== 1 ? 's' : ''}`));
121
+ }
122
+ const statsStr = stats.length > 0 ? ` with ${stats.join(' and ')}` : '';
123
+ return ` ${s.muted('⎿')} Updated ${s.primary(fileName)}${statsStr}`;
124
+ }
125
+ // =============================================================================
126
+ // Line Formatting
127
+ // =============================================================================
128
+ /**
129
+ * Format a context line (unchanged)
130
+ */
131
+ function formatContextLine(lineNum, content) {
132
+ const s = getStyles();
133
+ return `${s.muted(lineNum)} ${content}`;
134
+ }
135
+ /**
136
+ * Format a removed line (red background)
137
+ */
138
+ function formatRemovedLine(lineNum, content) {
139
+ const s = getStyles();
140
+ return `${s.muted(lineNum)} ${chalk.bgRed.white('-')} ${chalk.bgRed.white(content)}`;
141
+ }
142
+ /**
143
+ * Format an added line (green background)
144
+ */
145
+ function formatAddedLine(lineNum, content) {
146
+ const s = getStyles();
147
+ return `${s.muted(lineNum)} ${chalk.bgGreen.black('+')} ${chalk.bgGreen.black(content)}`;
148
+ }
149
+ // =============================================================================
150
+ // Write File Diff (for new files or complete overwrites)
151
+ // =============================================================================
152
+ /**
153
+ * Generate a simple diff for write_file operations (new content only)
154
+ */
155
+ export function generateWriteDiff(filePath, content, _isNew) {
156
+ const lines = content.split('\n');
157
+ const diffLines = [];
158
+ // Show first few lines as preview
159
+ const previewLines = Math.min(lines.length, 10);
160
+ for (let i = 0; i < previewLines; i++) {
161
+ const lineNum = String(i + 1).padStart(6);
162
+ diffLines.push(formatAddedLine(lineNum, lines[i]));
163
+ }
164
+ if (lines.length > previewLines) {
165
+ const s = getStyles();
166
+ diffLines.push(s.muted(` ... ${String(lines.length - previewLines)} more lines`));
167
+ }
168
+ return {
169
+ lines: diffLines,
170
+ additions: lines.length,
171
+ removals: 0,
172
+ };
173
+ }
174
+ /**
175
+ * Format header for write_file operations
176
+ */
177
+ export function formatWriteHeader(filePath, lineCount, isNew) {
178
+ const s = getStyles();
179
+ const fileName = filePath.split('/').pop() || filePath;
180
+ const action = isNew ? 'Created' : 'Wrote';
181
+ return ` ${s.muted('⎿')} ${action} ${s.primary(fileName)} (${String(lineCount)} lines)`;
182
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Ephemeral Zone
3
+ *
4
+ * @deprecated This module is superseded by the new Footer architecture.
5
+ * Use todo-zone.ts, input-prompt-v2.ts, and footer.ts instead.
6
+ * This file is kept for reference and may be removed in a future cleanup.
7
+ *
8
+ * Manages the ephemeral area that is cleared and redrawn while agent works.
9
+ * Contains: status line (spinner/tool), todo list, pending messages.
10
+ */
11
+ import type { SpinnerState, TodoItem } from './types.js';
12
+ export interface EphemeralState {
13
+ spinner: SpinnerState | null;
14
+ todos: TodoItem[];
15
+ pendingMessages: string[];
16
+ }
17
+ export declare class EphemeralZone {
18
+ private renderedLines;
19
+ private animationInterval;
20
+ private animationPosition;
21
+ private animationDirection;
22
+ private thinkingWord;
23
+ private state;
24
+ private abortController;
25
+ /**
26
+ * Start the ephemeral zone with spinner animation
27
+ * Returns AbortController that can be used to interrupt the agent
28
+ */
29
+ start(): AbortController;
30
+ /**
31
+ * Stop the animation and clear the zone
32
+ */
33
+ stop(wasAborted?: boolean): void;
34
+ /**
35
+ * Pause the animation (for permission prompts, etc.)
36
+ */
37
+ pause(): void;
38
+ /**
39
+ * Resume the animation after pause
40
+ */
41
+ resume(): void;
42
+ /**
43
+ * Update the ephemeral state
44
+ */
45
+ update(state: Partial<EphemeralState>): void;
46
+ /**
47
+ * Update token count
48
+ */
49
+ addTokens(count: number): void;
50
+ /**
51
+ * Set current tool being executed
52
+ */
53
+ setTool(toolName: string | null): void;
54
+ /**
55
+ * Update todos
56
+ */
57
+ setTodos(todos: TodoItem[]): void;
58
+ /**
59
+ * Add a pending message
60
+ */
61
+ addPendingMessage(message: string): void;
62
+ /**
63
+ * Get and clear pending messages
64
+ */
65
+ getPendingMessages(): string[];
66
+ /**
67
+ * Clear all rendered lines
68
+ */
69
+ clear(): void;
70
+ /**
71
+ * Clear for external output (tool results, etc.)
72
+ * After calling this, use forceRender() to redraw
73
+ */
74
+ clearForOutput(): void;
75
+ /**
76
+ * Force an immediate render
77
+ */
78
+ forceRender(): void;
79
+ /**
80
+ * Get number of rendered lines
81
+ */
82
+ getRenderedLines(): number;
83
+ /**
84
+ * Check if zone is active (animation running)
85
+ */
86
+ isActive(): boolean;
87
+ /**
88
+ * Get the abort controller
89
+ */
90
+ getAbortController(): AbortController | null;
91
+ /**
92
+ * Get the currently active todo
93
+ */
94
+ private getActiveTodo;
95
+ /**
96
+ * Get the text to display (active todo or thinking word)
97
+ */
98
+ private getDisplayText;
99
+ /**
100
+ * Render the ephemeral zone
101
+ */
102
+ private render;
103
+ }
104
+ /**
105
+ * Get the singleton ephemeral zone instance
106
+ */
107
+ export declare function getEphemeralZone(): EphemeralZone;
108
+ /**
109
+ * Get the current ephemeral instance (may be null if not active)
110
+ */
111
+ export declare function getCurrentEphemeral(): EphemeralZone | null;
@@ -0,0 +1,413 @@
1
+ /**
2
+ * Ephemeral Zone
3
+ *
4
+ * @deprecated This module is superseded by the new Footer architecture.
5
+ * Use todo-zone.ts, input-prompt-v2.ts, and footer.ts instead.
6
+ * This file is kept for reference and may be removed in a future cleanup.
7
+ *
8
+ * Manages the ephemeral area that is cleared and redrawn while agent works.
9
+ * Contains: status line (spinner/tool), todo list, pending messages.
10
+ */
11
+ import chalk from 'chalk';
12
+ import { getStyles } from '../themes/index.js';
13
+ import * as terminal from './terminal.js';
14
+ import { formatPendingMessage } from './conversation.js';
15
+ // Note: This file is deprecated but kept for reference.
16
+ // All pc.* calls below have been converted to use getStyles() for theme support.
17
+ // =============================================================================
18
+ // Thinking Words (for random selection)
19
+ // =============================================================================
20
+ const THINKING_WORDS = [
21
+ 'Thinking',
22
+ 'Pondering',
23
+ 'Contemplating',
24
+ 'Ruminating',
25
+ 'Cogitating',
26
+ 'Deliberating',
27
+ 'Musing',
28
+ 'Reflecting',
29
+ 'Considering',
30
+ 'Analyzing',
31
+ 'Processing',
32
+ 'Computing',
33
+ 'Synthesizing',
34
+ 'Extrapolating',
35
+ 'Hypothesizing',
36
+ 'Discombobulating',
37
+ 'Nebulizing',
38
+ 'Percolating',
39
+ 'Marinating',
40
+ 'Simmering',
41
+ 'Brewing',
42
+ 'Fermenting',
43
+ 'Distilling',
44
+ 'Crystallizing',
45
+ 'Transmuting',
46
+ ];
47
+ // =============================================================================
48
+ // Formatting Helpers
49
+ // =============================================================================
50
+ /**
51
+ * Format duration in human-readable format (e.g., "1m 23s", "45s")
52
+ */
53
+ function formatDuration(ms) {
54
+ const seconds = Math.floor(ms / 1000);
55
+ if (seconds < 60) {
56
+ return `${String(seconds)}s`;
57
+ }
58
+ const minutes = Math.floor(seconds / 60);
59
+ const remainingSeconds = seconds % 60;
60
+ return `${String(minutes)}m ${String(remainingSeconds)}s`;
61
+ }
62
+ /**
63
+ * Format token count (e.g., "1.2k", "500")
64
+ */
65
+ function formatTokens(tokens) {
66
+ if (tokens >= 1000) {
67
+ return `${(tokens / 1000).toFixed(1)}k`;
68
+ }
69
+ return tokens.toString();
70
+ }
71
+ /**
72
+ * Render todo list with visual styling
73
+ */
74
+ function renderTodoList(todos) {
75
+ if (todos.length === 0) {
76
+ return '';
77
+ }
78
+ const s = getStyles();
79
+ const lines = [];
80
+ for (const todo of todos) {
81
+ let checkbox;
82
+ let text;
83
+ switch (todo.status) {
84
+ case 'completed':
85
+ checkbox = s.muted('☒');
86
+ text = chalk.strikethrough(s.muted(todo.content));
87
+ break;
88
+ case 'in_progress':
89
+ checkbox = s.primary('☐');
90
+ text = chalk.bold(todo.content);
91
+ break;
92
+ case 'pending':
93
+ default:
94
+ checkbox = s.muted('☐');
95
+ text = s.muted(todo.content);
96
+ break;
97
+ }
98
+ lines.push(`${checkbox} ${text}`);
99
+ }
100
+ return lines.join('\n');
101
+ }
102
+ export class EphemeralZone {
103
+ renderedLines = 0;
104
+ animationInterval = null;
105
+ animationPosition = 0;
106
+ animationDirection = 1; // 1 = right, -1 = left
107
+ thinkingWord = '';
108
+ // Current state
109
+ state = {
110
+ spinner: null,
111
+ todos: [],
112
+ pendingMessages: [],
113
+ };
114
+ // Abort controller for interrupting agent
115
+ abortController = null;
116
+ /**
117
+ * Start the ephemeral zone with spinner animation
118
+ * Returns AbortController that can be used to interrupt the agent
119
+ */
120
+ start() {
121
+ this.abortController = new AbortController();
122
+ this.animationPosition = 0;
123
+ this.animationDirection = 1;
124
+ this.renderedLines = 0;
125
+ // Pick a random thinking word
126
+ this.thinkingWord =
127
+ THINKING_WORDS[Math.floor(Math.random() * THINKING_WORDS.length)] + '...';
128
+ // Initialize spinner state
129
+ this.state.spinner = {
130
+ text: this.thinkingWord,
131
+ tokens: 0,
132
+ startTime: Date.now(),
133
+ };
134
+ // Start animation
135
+ this.render();
136
+ this.animationInterval = setInterval(() => {
137
+ // Move position
138
+ this.animationPosition += this.animationDirection;
139
+ // Get current display text
140
+ const displayText = this.getDisplayText();
141
+ // Bounce at edges
142
+ if (this.animationPosition >= displayText.length - 1) {
143
+ this.animationDirection = -1;
144
+ }
145
+ else if (this.animationPosition <= 0) {
146
+ this.animationDirection = 1;
147
+ }
148
+ this.render();
149
+ }, 150); // Smooth scanning speed
150
+ return this.abortController;
151
+ }
152
+ /**
153
+ * Stop the animation and clear the zone
154
+ */
155
+ stop(wasAborted = false) {
156
+ if (this.animationInterval) {
157
+ clearInterval(this.animationInterval);
158
+ this.animationInterval = null;
159
+ }
160
+ this.abortController = null;
161
+ // Clear all rendered lines
162
+ this.clear();
163
+ if (wasAborted) {
164
+ const s = getStyles();
165
+ console.log(s.warning('Interrupted by user.\n'));
166
+ }
167
+ // Reset state
168
+ this.state = {
169
+ spinner: null,
170
+ todos: [],
171
+ pendingMessages: [],
172
+ };
173
+ }
174
+ /**
175
+ * Pause the animation (for permission prompts, etc.)
176
+ */
177
+ pause() {
178
+ if (this.animationInterval) {
179
+ clearInterval(this.animationInterval);
180
+ this.animationInterval = null;
181
+ }
182
+ this.clear();
183
+ }
184
+ /**
185
+ * Resume the animation after pause
186
+ */
187
+ resume() {
188
+ if (!this.animationInterval && this.abortController) {
189
+ this.render();
190
+ this.animationInterval = setInterval(() => {
191
+ this.animationPosition += this.animationDirection;
192
+ const displayText = this.getDisplayText();
193
+ if (this.animationPosition >= displayText.length - 1) {
194
+ this.animationDirection = -1;
195
+ }
196
+ else if (this.animationPosition <= 0) {
197
+ this.animationDirection = 1;
198
+ }
199
+ this.render();
200
+ }, 150);
201
+ }
202
+ }
203
+ /**
204
+ * Update the ephemeral state
205
+ */
206
+ update(state) {
207
+ if (state.spinner !== undefined) {
208
+ this.state.spinner = state.spinner;
209
+ }
210
+ if (state.todos !== undefined) {
211
+ this.state.todos = state.todos;
212
+ }
213
+ if (state.pendingMessages !== undefined) {
214
+ this.state.pendingMessages = state.pendingMessages;
215
+ }
216
+ // Force render if animation is running
217
+ if (this.animationInterval) {
218
+ this.render();
219
+ }
220
+ }
221
+ /**
222
+ * Update token count
223
+ */
224
+ addTokens(count) {
225
+ if (this.state.spinner) {
226
+ this.state.spinner.tokens += count;
227
+ }
228
+ }
229
+ /**
230
+ * Set current tool being executed
231
+ */
232
+ setTool(toolName) {
233
+ if (this.state.spinner) {
234
+ this.state.spinner.tool = toolName ?? undefined;
235
+ // Re-render immediately to show tool
236
+ if (this.animationInterval) {
237
+ this.render();
238
+ }
239
+ }
240
+ }
241
+ /**
242
+ * Update todos
243
+ */
244
+ setTodos(todos) {
245
+ this.state.todos = todos;
246
+ if (this.animationInterval) {
247
+ this.render();
248
+ }
249
+ }
250
+ /**
251
+ * Add a pending message
252
+ */
253
+ addPendingMessage(message) {
254
+ this.state.pendingMessages.push(message);
255
+ if (this.animationInterval) {
256
+ this.render();
257
+ }
258
+ }
259
+ /**
260
+ * Get and clear pending messages
261
+ */
262
+ getPendingMessages() {
263
+ const messages = [...this.state.pendingMessages];
264
+ this.state.pendingMessages = [];
265
+ return messages;
266
+ }
267
+ /**
268
+ * Clear all rendered lines
269
+ */
270
+ clear() {
271
+ if (this.renderedLines > 0) {
272
+ terminal.moveCursorToLineStart();
273
+ if (this.renderedLines > 1) {
274
+ terminal.moveCursorUp(this.renderedLines - 1);
275
+ }
276
+ terminal.clearToEndOfScreen();
277
+ }
278
+ else {
279
+ terminal.clearLine();
280
+ }
281
+ this.renderedLines = 0;
282
+ }
283
+ /**
284
+ * Clear for external output (tool results, etc.)
285
+ * After calling this, use forceRender() to redraw
286
+ */
287
+ clearForOutput() {
288
+ this.clear();
289
+ }
290
+ /**
291
+ * Force an immediate render
292
+ */
293
+ forceRender() {
294
+ if (this.abortController) {
295
+ this.render();
296
+ }
297
+ }
298
+ /**
299
+ * Get number of rendered lines
300
+ */
301
+ getRenderedLines() {
302
+ return this.renderedLines;
303
+ }
304
+ /**
305
+ * Check if zone is active (animation running)
306
+ */
307
+ isActive() {
308
+ return this.abortController !== null;
309
+ }
310
+ /**
311
+ * Get the abort controller
312
+ */
313
+ getAbortController() {
314
+ return this.abortController;
315
+ }
316
+ // =============================================================================
317
+ // Private Methods
318
+ // =============================================================================
319
+ /**
320
+ * Get the currently active todo
321
+ */
322
+ getActiveTodo() {
323
+ return this.state.todos.find((t) => t.status === 'in_progress');
324
+ }
325
+ /**
326
+ * Get the text to display (active todo or thinking word)
327
+ */
328
+ getDisplayText() {
329
+ const activeTodo = this.getActiveTodo();
330
+ return activeTodo
331
+ ? (activeTodo.activeForm || activeTodo.content) + '...'
332
+ : this.thinkingWord;
333
+ }
334
+ /**
335
+ * Render the ephemeral zone
336
+ */
337
+ render() {
338
+ if (!this.state.spinner)
339
+ return;
340
+ const s = getStyles();
341
+ const elapsed = formatDuration(Date.now() - this.state.spinner.startTime);
342
+ const tokenStr = formatTokens(this.state.spinner.tokens);
343
+ // Clear previous render
344
+ this.clear();
345
+ // Build stats string
346
+ const stats = s.muted(` (Esc to interrupt · ${elapsed} · ↓ ${tokenStr} tokens)`);
347
+ // If a tool is running, show that instead of the thinking word
348
+ if (this.state.spinner.tool) {
349
+ const toolDisplay = s.warning(`● ${this.state.spinner.tool}`);
350
+ terminal.write(`${toolDisplay}${stats}`);
351
+ }
352
+ else {
353
+ // Show animated thinking text with knight rider effect
354
+ const displayText = this.getDisplayText();
355
+ const activeTodo = this.getActiveTodo();
356
+ let result = '';
357
+ for (let i = 0; i < displayText.length; i++) {
358
+ const distance = Math.abs(i - (this.animationPosition % displayText.length));
359
+ const char = displayText[i];
360
+ if (distance === 0) {
361
+ result += chalk.bold.white(char);
362
+ }
363
+ else if (distance === 1) {
364
+ result += s.primary(char);
365
+ }
366
+ else if (distance === 2) {
367
+ result += s.info(char);
368
+ }
369
+ else {
370
+ result += s.muted(char);
371
+ }
372
+ }
373
+ // If we have an active todo, show the indicator
374
+ if (activeTodo) {
375
+ result = s.primary('✱ ') + result;
376
+ }
377
+ terminal.write(`${result}${stats}`);
378
+ }
379
+ // Count lines rendered
380
+ this.renderedLines = 1;
381
+ // Render todo list if we have todos
382
+ if (this.state.todos.length > 0) {
383
+ terminal.write('\n' + renderTodoList(this.state.todos));
384
+ this.renderedLines += this.state.todos.length;
385
+ }
386
+ // Render pending messages if any
387
+ if (this.state.pendingMessages.length > 0) {
388
+ for (const msg of this.state.pendingMessages) {
389
+ terminal.write('\n' + formatPendingMessage(msg));
390
+ this.renderedLines++;
391
+ }
392
+ }
393
+ }
394
+ }
395
+ // =============================================================================
396
+ // Singleton Instance
397
+ // =============================================================================
398
+ let ephemeralInstance = null;
399
+ /**
400
+ * Get the singleton ephemeral zone instance
401
+ */
402
+ export function getEphemeralZone() {
403
+ if (!ephemeralInstance) {
404
+ ephemeralInstance = new EphemeralZone();
405
+ }
406
+ return ephemeralInstance;
407
+ }
408
+ /**
409
+ * Get the current ephemeral instance (may be null if not active)
410
+ */
411
+ export function getCurrentEphemeral() {
412
+ return ephemeralInstance;
413
+ }