@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,21 @@
1
+ /**
2
+ * Iteration Limit Overlay
3
+ *
4
+ * Modal overlay shown when the agent reaches its iteration limit.
5
+ * Asks the user if they want to continue or stop execution.
6
+ */
7
+ export type IterationLimitResult = {
8
+ continue: true;
9
+ additionalIterations: number;
10
+ } | {
11
+ continue: false;
12
+ };
13
+ export interface IterationLimitOverlayOptions {
14
+ iteration: number;
15
+ maxIterations: number;
16
+ toolCallCount: number;
17
+ }
18
+ /**
19
+ * Show the iteration limit overlay
20
+ */
21
+ export declare function showIterationLimitOverlay(options: IterationLimitOverlayOptions): Promise<IterationLimitResult>;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Iteration Limit Overlay
3
+ *
4
+ * Modal overlay shown when the agent reaches its iteration limit.
5
+ * Asks the user if they want to continue or stop execution.
6
+ */
7
+ import chalk from 'chalk';
8
+ import * as terminal from './terminal.js';
9
+ import { getStyles } from '../themes/index.js';
10
+ // =============================================================================
11
+ // Rendering
12
+ // =============================================================================
13
+ function render(options, state, previousLineCount = 0, targetLineCount = 0) {
14
+ const s = getStyles();
15
+ const lines = [];
16
+ const cols = terminal.getTerminalWidth();
17
+ const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
18
+ // Clear previous render
19
+ if (previousLineCount > 0) {
20
+ terminal.clearLinesAbove(previousLineCount);
21
+ }
22
+ // Header
23
+ lines.push(border);
24
+ lines.push(' ' + s.warning('⚠') + ' ' + chalk.bold('Iteration Limit Reached'));
25
+ lines.push('');
26
+ // Stats
27
+ lines.push(' Iterations: ' + s.primary(String(options.iteration)) + ' / ' + s.muted(String(options.maxIterations)));
28
+ lines.push(' Tool calls: ' + s.primary(String(options.toolCallCount)));
29
+ lines.push('');
30
+ lines.push(' ' + s.muted('The agent has reached its iteration limit.'));
31
+ lines.push(' ' + s.muted('Would you like to continue?'));
32
+ lines.push('');
33
+ // Options
34
+ const optionLabels = ['Yes, continue (+10 iterations)', 'Yes, continue (+20 iterations)', 'No, stop here'];
35
+ const optionKeys = ['1', '2', 'n'];
36
+ for (let i = 0; i < optionLabels.length; i++) {
37
+ const isCursor = state.selectedIndex === i;
38
+ const prefix = isCursor ? ' ❯ ' : ' ';
39
+ const key = `[${optionKeys[i]}] `;
40
+ if (isCursor) {
41
+ lines.push(s.primary(prefix + key + optionLabels[i]));
42
+ }
43
+ else {
44
+ lines.push(s.muted(prefix + key + optionLabels[i]));
45
+ }
46
+ }
47
+ // Footer
48
+ lines.push('');
49
+ lines.push(s.muted(' ↑↓ Navigate · Enter Select · 1/2/n Quick select'));
50
+ // Bottom border
51
+ lines.push(border);
52
+ // Pad with empty lines to maintain consistent height
53
+ while (lines.length < targetLineCount) {
54
+ lines.push('');
55
+ }
56
+ // Render all lines
57
+ terminal.write(lines.join('\n'));
58
+ return lines.length;
59
+ }
60
+ // =============================================================================
61
+ // Main Export
62
+ // =============================================================================
63
+ /**
64
+ * Show the iteration limit overlay
65
+ */
66
+ export async function showIterationLimitOverlay(options) {
67
+ const state = {
68
+ selectedIndex: 0, // Default to "Yes, continue (+10)"
69
+ };
70
+ let lineCount = 0;
71
+ let maxLineCount = 0;
72
+ // Ensure we start from a fresh line
73
+ terminal.writeLine('');
74
+ terminal.hideCursor();
75
+ const wasRawMode = process.stdin.isRaw;
76
+ terminal.enableRawMode();
77
+ // Initial render
78
+ lineCount = render(options, state, 0);
79
+ maxLineCount = Math.max(maxLineCount, lineCount);
80
+ return new Promise((resolve) => {
81
+ const cleanup = (result) => {
82
+ terminal.clearLinesAbove(maxLineCount);
83
+ // Show result summary
84
+ const s = getStyles();
85
+ if (result.continue) {
86
+ terminal.writeLine(s.muted(`Iteration limit: `) + s.success(`Continuing (+${String(result.additionalIterations)})`));
87
+ }
88
+ else {
89
+ terminal.writeLine(s.muted(`Iteration limit: `) + s.error(`Stopped`));
90
+ }
91
+ terminal.writeLine(''); // Blank line for separation
92
+ terminal.showCursor();
93
+ if (!wasRawMode) {
94
+ terminal.disableRawMode();
95
+ }
96
+ process.stdin.removeListener('data', handleData);
97
+ resolve(result);
98
+ };
99
+ const handleData = (data) => {
100
+ const isEscape = data.length === 1 && data[0] === 0x1b;
101
+ const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
102
+ const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
103
+ const isCtrlC = data.length === 1 && data[0] === 0x03;
104
+ const isEnter = data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
105
+ // Ctrl+C or Escape = stop
106
+ if (isCtrlC || isEscape) {
107
+ cleanup({ continue: false });
108
+ return;
109
+ }
110
+ // Quick keys
111
+ const key = data.toString();
112
+ if (key === '1') {
113
+ cleanup({ continue: true, additionalIterations: 10 });
114
+ return;
115
+ }
116
+ if (key === '2') {
117
+ cleanup({ continue: true, additionalIterations: 20 });
118
+ return;
119
+ }
120
+ if (key.toLowerCase() === 'n') {
121
+ cleanup({ continue: false });
122
+ return;
123
+ }
124
+ // Arrow navigation
125
+ if (isUpArrow) {
126
+ state.selectedIndex = Math.max(0, state.selectedIndex - 1);
127
+ }
128
+ else if (isDownArrow) {
129
+ state.selectedIndex = Math.min(2, state.selectedIndex + 1);
130
+ }
131
+ else if (isEnter) {
132
+ // Select based on current index
133
+ if (state.selectedIndex === 0) {
134
+ cleanup({ continue: true, additionalIterations: 10 });
135
+ }
136
+ else if (state.selectedIndex === 1) {
137
+ cleanup({ continue: true, additionalIterations: 20 });
138
+ }
139
+ else {
140
+ cleanup({ continue: false });
141
+ }
142
+ return;
143
+ }
144
+ // Re-render
145
+ lineCount = render(options, state, maxLineCount, maxLineCount);
146
+ maxLineCount = Math.max(maxLineCount, lineCount);
147
+ };
148
+ process.stdin.on('data', handleData);
149
+ });
150
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Keys Overlay
3
+ *
4
+ * Modal overlay for managing API keys.
5
+ * Shows status of all providers and allows setting/deleting keys.
6
+ */
7
+ export interface KeysOverlayResult {
8
+ /** Whether any keys were changed */
9
+ changed: boolean;
10
+ }
11
+ /**
12
+ * Show the keys management overlay
13
+ */
14
+ export declare function showKeysOverlay(): Promise<KeysOverlayResult>;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Keys Overlay
3
+ *
4
+ * Modal overlay for managing API keys.
5
+ * Shows status of all providers and allows setting/deleting keys.
6
+ */
7
+ import * as terminal from './terminal.js';
8
+ import { getStyles } from '../themes/index.js';
9
+ import { getAllProviderStatuses, setApiKey, deleteApiKey, } from '../utils/credentials.js';
10
+ // =============================================================================
11
+ // Rendering
12
+ // =============================================================================
13
+ function render(state, previousLineCount = 0) {
14
+ const s = getStyles();
15
+ const lines = [];
16
+ const cols = terminal.getTerminalWidth();
17
+ const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
18
+ // Clear previous render
19
+ if (previousLineCount > 0) {
20
+ terminal.clearLinesAbove(previousLineCount);
21
+ }
22
+ // Header
23
+ lines.push(border);
24
+ lines.push(' ' + s.primaryBold('API Keys'));
25
+ lines.push('');
26
+ if (state.mode === 'list') {
27
+ // List mode - show all providers
28
+ for (let i = 0; i < state.providers.length; i++) {
29
+ const p = state.providers[i];
30
+ const isCursor = state.selectedIndex === i;
31
+ const prefix = isCursor ? ' ❯ ' : ' ';
32
+ // Status indicator
33
+ let status;
34
+ if (p.provider === 'ollama') {
35
+ status = s.secondary('Local (no key needed)');
36
+ }
37
+ else if (p.hasKey) {
38
+ const source = p.fromEnv ? s.muted(' (env)') : '';
39
+ status = s.success(`✓ ${p.masked ?? '***'}`) + source;
40
+ }
41
+ else {
42
+ status = s.warning('○ Not set');
43
+ }
44
+ // Provider name
45
+ const name = isCursor ? s.primary(p.name) : p.name;
46
+ const line = `${prefix}${name.padEnd(22)} ${status}`;
47
+ lines.push(line);
48
+ }
49
+ lines.push('');
50
+ lines.push(s.muted(' ↑↓ Navigate · Enter Edit · d Delete · Esc Close'));
51
+ }
52
+ else {
53
+ // Input mode - show key entry
54
+ const provider = state.providers.find(p => p.provider === state.editingProvider);
55
+ if (provider) {
56
+ lines.push(` Set API key for ${s.primaryBold(provider.name)}:`);
57
+ lines.push('');
58
+ lines.push(` > ${state.inputBuffer}${s.primary('▋')}`);
59
+ lines.push('');
60
+ lines.push(s.muted(` Get key: ${provider.keyUrl}`));
61
+ lines.push('');
62
+ lines.push(s.muted(' Enter Save · Esc Cancel'));
63
+ }
64
+ }
65
+ // Bottom border
66
+ lines.push(border);
67
+ // Render all lines
68
+ terminal.write(lines.join('\n'));
69
+ return lines.length;
70
+ }
71
+ // =============================================================================
72
+ // Main Export
73
+ // =============================================================================
74
+ /**
75
+ * Show the keys management overlay
76
+ */
77
+ export async function showKeysOverlay() {
78
+ const state = {
79
+ mode: 'list',
80
+ selectedIndex: 0,
81
+ editingProvider: null,
82
+ inputBuffer: '',
83
+ cancelled: false,
84
+ providers: getAllProviderStatuses(),
85
+ };
86
+ let lineCount = 0;
87
+ let changed = false;
88
+ // Initial render
89
+ lineCount = render(state, lineCount);
90
+ return new Promise((resolve) => {
91
+ const stdin = process.stdin;
92
+ stdin.setRawMode(true);
93
+ stdin.resume();
94
+ const cleanup = () => {
95
+ stdin.removeListener('data', handleKey);
96
+ stdin.setRawMode(false);
97
+ stdin.pause();
98
+ // Clear the overlay
99
+ terminal.clearLinesAbove(lineCount);
100
+ };
101
+ const handleKey = (data) => {
102
+ const key = data.toString();
103
+ if (state.mode === 'list') {
104
+ // List mode key handling
105
+ switch (key) {
106
+ case '\x1B': // Escape
107
+ case 'q':
108
+ cleanup();
109
+ resolve({ changed });
110
+ return;
111
+ case '\x1B[A': // Up arrow
112
+ if (state.selectedIndex > 0) {
113
+ state.selectedIndex--;
114
+ }
115
+ break;
116
+ case '\x1B[B': // Down arrow
117
+ if (state.selectedIndex < state.providers.length - 1) {
118
+ state.selectedIndex++;
119
+ }
120
+ break;
121
+ case '\r': // Enter - edit selected
122
+ case '\n': {
123
+ const selected = state.providers[state.selectedIndex];
124
+ if (selected.provider !== 'ollama') {
125
+ state.mode = 'input';
126
+ state.editingProvider = selected.provider;
127
+ state.inputBuffer = '';
128
+ }
129
+ break;
130
+ }
131
+ case 'd':
132
+ case 'D': {
133
+ // Delete key for selected provider
134
+ const selected = state.providers[state.selectedIndex];
135
+ if (selected.provider !== 'ollama' && selected.hasKey && !selected.fromEnv) {
136
+ deleteApiKey(selected.provider);
137
+ state.providers = getAllProviderStatuses();
138
+ changed = true;
139
+ }
140
+ break;
141
+ }
142
+ }
143
+ }
144
+ else {
145
+ // Input mode key handling
146
+ if (key === '\x1B') {
147
+ // Escape - cancel input
148
+ state.mode = 'list';
149
+ state.editingProvider = null;
150
+ state.inputBuffer = '';
151
+ }
152
+ else if (key === '\r' || key === '\n') {
153
+ // Enter - save key
154
+ if (state.inputBuffer.trim() && state.editingProvider) {
155
+ setApiKey(state.editingProvider, state.inputBuffer.trim());
156
+ state.providers = getAllProviderStatuses();
157
+ changed = true;
158
+ }
159
+ state.mode = 'list';
160
+ state.editingProvider = null;
161
+ state.inputBuffer = '';
162
+ }
163
+ else if (key === '\x7F' || key === '\b') {
164
+ // Backspace
165
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
166
+ }
167
+ else {
168
+ // Handle paste (multiple characters) or single printable character
169
+ // Filter to only printable ASCII characters
170
+ const printable = key.split('').filter(c => c >= ' ' && c <= '~').join('');
171
+ if (printable.length > 0) {
172
+ state.inputBuffer += printable;
173
+ }
174
+ }
175
+ }
176
+ // Re-render
177
+ lineCount = render(state, lineCount);
178
+ };
179
+ stdin.on('data', handleKey);
180
+ });
181
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Model Warning Overlay
3
+ *
4
+ * Shows a warning when the user runs a command that requires
5
+ * a larger model than they're currently using.
6
+ *
7
+ * Offers options to:
8
+ * - Switch to a recommended model
9
+ * - Use a simpler alternative command
10
+ * - Continue anyway
11
+ * - Cancel
12
+ */
13
+ import type { TierLevel } from '../utils/model-tiers.js';
14
+ export interface ModelWarningOptions {
15
+ /** The command being run (e.g., '/design') */
16
+ command: string;
17
+ /** Current model ID */
18
+ currentModel: string;
19
+ /** Current model's tier */
20
+ currentTier: TierLevel;
21
+ /** Suggested upgrade model (if available) */
22
+ suggestedModel?: string;
23
+ /** Alternative simpler command (e.g., '/sketch') */
24
+ alternativeCommand?: string;
25
+ }
26
+ export type ModelWarningChoice = 'switch' | 'alternative' | 'continue' | 'cancel';
27
+ /**
28
+ * Show the model warning overlay and return the user's choice.
29
+ */
30
+ export declare function showModelWarningOverlay(options: ModelWarningOptions): Promise<ModelWarningChoice>;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Model Warning Overlay
3
+ *
4
+ * Shows a warning when the user runs a command that requires
5
+ * a larger model than they're currently using.
6
+ *
7
+ * Offers options to:
8
+ * - Switch to a recommended model
9
+ * - Use a simpler alternative command
10
+ * - Continue anyway
11
+ * - Cancel
12
+ */
13
+ import * as terminal from './terminal.js';
14
+ import { getStyles } from '../themes/index.js';
15
+ import { pauseForOverlay, resumeAfterOverlay } from './overlay-controller.js';
16
+ // =============================================================================
17
+ // Rendering
18
+ // =============================================================================
19
+ function render(options, selectedIndex, previousLineCount = 0, targetLineCount = 0) {
20
+ const s = getStyles();
21
+ const lines = [];
22
+ const cols = terminal.getTerminalWidth();
23
+ const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
24
+ // Clear previous render
25
+ if (previousLineCount > 0) {
26
+ terminal.clearLinesAbove(previousLineCount);
27
+ }
28
+ // Build options list
29
+ const choiceOptions = [];
30
+ if (options.suggestedModel) {
31
+ choiceOptions.push({
32
+ key: '1',
33
+ label: `Switch to ${options.suggestedModel} and continue`,
34
+ choice: 'switch',
35
+ });
36
+ }
37
+ if (options.alternativeCommand) {
38
+ choiceOptions.push({
39
+ key: String(choiceOptions.length + 1),
40
+ label: `Use ${options.alternativeCommand} instead (simpler, works with any model)`,
41
+ choice: 'alternative',
42
+ });
43
+ }
44
+ choiceOptions.push({
45
+ key: String(choiceOptions.length + 1),
46
+ label: 'Continue with current model anyway',
47
+ choice: 'continue',
48
+ });
49
+ // Header
50
+ lines.push(border);
51
+ lines.push(' ' + s.warning('⚠ Model Recommendation'));
52
+ lines.push('');
53
+ // Description
54
+ lines.push(` ${options.command} works best with larger models for complex`);
55
+ lines.push(' multi-question workflows.');
56
+ lines.push('');
57
+ // Current and recommended
58
+ lines.push(` Current: ${s.muted(options.currentModel)} (${options.currentTier} tier)`);
59
+ if (options.suggestedModel) {
60
+ lines.push(` Recommended: ${s.primary(options.suggestedModel)} (large tier)`);
61
+ }
62
+ lines.push('');
63
+ // Separator
64
+ lines.push(border);
65
+ // Options
66
+ for (let i = 0; i < choiceOptions.length; i++) {
67
+ const opt = choiceOptions[i];
68
+ const isCursor = selectedIndex === i;
69
+ const prefix = isCursor ? ' ❯ ' : ' ';
70
+ if (isCursor) {
71
+ lines.push(s.primary(`${prefix}[${opt.key}] ${opt.label}`));
72
+ }
73
+ else {
74
+ lines.push(s.muted(`${prefix}[${opt.key}] ${opt.label}`));
75
+ }
76
+ }
77
+ // Footer
78
+ lines.push('');
79
+ lines.push(s.muted(' ↑↓ Navigate · Enter Select · Esc Cancel'));
80
+ lines.push(border);
81
+ // Pad with empty lines to maintain consistent height
82
+ while (lines.length < targetLineCount) {
83
+ lines.push('');
84
+ }
85
+ // Render all lines
86
+ terminal.write(lines.join('\n'));
87
+ return lines.length;
88
+ }
89
+ // =============================================================================
90
+ // Main Export
91
+ // =============================================================================
92
+ /**
93
+ * Show the model warning overlay and return the user's choice.
94
+ */
95
+ export async function showModelWarningOverlay(options) {
96
+ // Build choice list to track what each index maps to
97
+ const choices = [];
98
+ if (options.suggestedModel) {
99
+ choices.push('switch');
100
+ }
101
+ if (options.alternativeCommand) {
102
+ choices.push('alternative');
103
+ }
104
+ choices.push('continue');
105
+ let selectedIndex = 0;
106
+ let lineCount = 0;
107
+ let maxLineCount = 0;
108
+ // Pause footer animation
109
+ pauseForOverlay();
110
+ // Clear and prepare
111
+ terminal.clearToEndOfScreen();
112
+ terminal.writeLine('');
113
+ terminal.hideCursor();
114
+ const wasRawMode = process.stdin.isRaw;
115
+ terminal.enableRawMode();
116
+ // Initial render
117
+ lineCount = render(options, selectedIndex, 0);
118
+ maxLineCount = Math.max(maxLineCount, lineCount);
119
+ // Re-render to stabilize
120
+ lineCount = render(options, selectedIndex, lineCount, lineCount);
121
+ return new Promise((resolve) => {
122
+ const cleanup = () => {
123
+ terminal.clearLinesAbove(maxLineCount);
124
+ terminal.writeLine('');
125
+ terminal.showCursor();
126
+ if (!wasRawMode) {
127
+ terminal.disableRawMode();
128
+ }
129
+ process.stdin.removeListener('data', handleData);
130
+ resumeAfterOverlay();
131
+ };
132
+ const handleData = (data) => {
133
+ const isEscape = data.length === 1 && data[0] === 0x1b;
134
+ const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
135
+ const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
136
+ const isCtrlC = data.length === 1 && data[0] === 0x03;
137
+ const isEnter = data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
138
+ // Ctrl+C or Escape cancels
139
+ if (isCtrlC || isEscape) {
140
+ cleanup();
141
+ resolve('cancel');
142
+ return;
143
+ }
144
+ // Navigation
145
+ if (isUpArrow) {
146
+ selectedIndex = Math.max(0, selectedIndex - 1);
147
+ }
148
+ else if (isDownArrow) {
149
+ selectedIndex = Math.min(choices.length - 1, selectedIndex + 1);
150
+ }
151
+ else if (isEnter) {
152
+ cleanup();
153
+ resolve(choices[selectedIndex]);
154
+ return;
155
+ }
156
+ else if (data.length === 1 && data[0] >= 0x31 && data[0] <= 0x39) {
157
+ // Number keys 1-9
158
+ const numIndex = data[0] - 0x31; // 0-indexed
159
+ if (numIndex < choices.length) {
160
+ cleanup();
161
+ resolve(choices[numIndex]);
162
+ return;
163
+ }
164
+ }
165
+ // Re-render
166
+ lineCount = render(options, selectedIndex, maxLineCount, maxLineCount);
167
+ maxLineCount = Math.max(maxLineCount, lineCount);
168
+ };
169
+ process.stdin.on('data', handleData);
170
+ });
171
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Overlay Controller
3
+ *
4
+ * Global controller for managing overlay display and footer coordination.
5
+ * Tools and overlays can use this to pause/resume the footer animation
6
+ * and take exclusive control of the terminal.
7
+ */
8
+ type PauseCallback = () => void;
9
+ type ResumeCallback = () => void;
10
+ /**
11
+ * Register the footer pause/resume callbacks.
12
+ * Called once during REPL initialization.
13
+ */
14
+ export declare function registerFooterCallbacks(pause: PauseCallback, resume: ResumeCallback): void;
15
+ /**
16
+ * Pause footer animation and clear it for overlay use.
17
+ * Call this before displaying an overlay.
18
+ */
19
+ export declare function pauseForOverlay(): void;
20
+ /**
21
+ * Resume footer animation after overlay is done.
22
+ * Call this after the overlay closes.
23
+ */
24
+ export declare function resumeAfterOverlay(): void;
25
+ export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Overlay Controller
3
+ *
4
+ * Global controller for managing overlay display and footer coordination.
5
+ * Tools and overlays can use this to pause/resume the footer animation
6
+ * and take exclusive control of the terminal.
7
+ */
8
+ let pauseFooter = null;
9
+ let resumeFooter = null;
10
+ /**
11
+ * Register the footer pause/resume callbacks.
12
+ * Called once during REPL initialization.
13
+ */
14
+ export function registerFooterCallbacks(pause, resume) {
15
+ pauseFooter = pause;
16
+ resumeFooter = resume;
17
+ }
18
+ /**
19
+ * Pause footer animation and clear it for overlay use.
20
+ * Call this before displaying an overlay.
21
+ */
22
+ export function pauseForOverlay() {
23
+ if (pauseFooter) {
24
+ pauseFooter();
25
+ }
26
+ }
27
+ /**
28
+ * Resume footer animation after overlay is done.
29
+ * Call this after the overlay closes.
30
+ */
31
+ export function resumeAfterOverlay() {
32
+ if (resumeFooter) {
33
+ resumeFooter();
34
+ }
35
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Overlays
3
+ *
4
+ * Modal overlays that temporarily take over the screen.
5
+ * - Help menu
6
+ * - Confirmation dialogs
7
+ * - Permission prompts
8
+ * - Context stats
9
+ */
10
+ import type { ContextStats, PermissionResult } from './types.js';
11
+ /**
12
+ * Show the tabbed help menu
13
+ */
14
+ export declare function showHelp(): Promise<void>;
15
+ export interface ConfirmationOptions {
16
+ confirmText?: string;
17
+ cancelText?: string;
18
+ }
19
+ /**
20
+ * Show a confirmation dialog
21
+ */
22
+ export declare function showConfirmation(message: string, options?: ConfirmationOptions): Promise<boolean>;
23
+ /**
24
+ * Show permission prompt for tool execution
25
+ */
26
+ export declare function showPermissionPrompt(tool: string, args: string): Promise<PermissionResult>;
27
+ /**
28
+ * Show context window usage stats
29
+ */
30
+ export declare function showContextStats(stats: ContextStats): void;
31
+ export interface TokenUsage {
32
+ inputTokens: number;
33
+ outputTokens: number;
34
+ totalTokens: number;
35
+ estimatedCost?: number;
36
+ }
37
+ /**
38
+ * Show session token usage
39
+ */
40
+ export declare function showTokenUsage(usage: TokenUsage): void;
41
+ /**
42
+ * Show available tools in compact column format
43
+ */
44
+ export declare function showTools(tools: Array<{
45
+ name: string;
46
+ description?: string;
47
+ }>): void;