@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
package/dist/ui/diff.js
ADDED
|
@@ -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
|
+
}
|