@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
package/dist/repl.js ADDED
@@ -0,0 +1,1878 @@
1
+ /**
2
+ * REPL (Read-Eval-Print Loop)
3
+ *
4
+ * Main orchestration for the interactive agent session.
5
+ * Coordinates between input, agent execution, and output.
6
+ *
7
+ * Uses event-driven architecture with Footer component for:
8
+ * - Persistent todo list while agent runs
9
+ * - Persistent input prompt (always visible)
10
+ * - Input queueing during agent execution
11
+ * - Esc to cancel running agent
12
+ */
13
+ import { builtinSkills } from '@compilr-dev/agents';
14
+ import { exec } from 'child_process';
15
+ import { getToolPermissionInfo } from './agent.js';
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import pc from 'picocolors';
19
+ import { debug, debugError } from './debug.js';
20
+ import { getNextMode, MODE_INFO } from './ui/types.js';
21
+ import { createInitialState, updateTodos, setAgentRunning } from './state.js';
22
+ import { Footer } from './ui/footer.js';
23
+ import * as conversation from './ui/conversation.js';
24
+ import * as overlays from './ui/overlays.js';
25
+ import * as terminal from './ui/terminal.js';
26
+ import { resolveCommand } from './commands.js';
27
+ import { showAgentsOverlay } from './ui/agents-overlay.js';
28
+ import { showCommandsOverlay } from './ui/commands-overlay.js';
29
+ import { showConfigOverlay } from './ui/config-overlay.js';
30
+ import { showInitOverlay } from './ui/init-overlay.js';
31
+ import { showBacklogOverlay } from './ui/backlog-overlay.js';
32
+ import { showKeysOverlay } from './ui/keys-overlay.js';
33
+ import { showTutorialOverlay } from './ui/tutorial-overlay.js';
34
+ import { showToolsOverlay } from './ui/tools-overlay.js';
35
+ import { showModelWarningOverlay } from './ui/model-warning-overlay.js';
36
+ import { showArchTypeOverlay } from './ui/arch-type-overlay.js';
37
+ import { registerFooterCallbacks } from './ui/overlay-controller.js';
38
+ import { getCustomCommandRegistry } from './commands/index.js';
39
+ import { getPermissionMode } from './settings/index.js';
40
+ import { getModelTier, modelMeetsTier } from './utils/model-tiers.js';
41
+ import { findBacklogPath, parseBacklogItems } from './tools/backlog.js';
42
+ import { isOverlayActive } from './shared-handlers.js';
43
+ import { loadCompilrConfig } from './utils/project-status.js';
44
+ // =============================================================================
45
+ // Tool Intent Detection
46
+ // =============================================================================
47
+ // Keyword patterns for tool selection
48
+ const TOOL_KEYWORDS = {
49
+ read_file: ['read', 'show', 'display', 'look at', 'view', 'what is in', 'content of'],
50
+ write_file: ['write', 'create', 'save', 'make a file'],
51
+ edit: ['edit', 'modify', 'change', 'update', 'fix', 'replace'],
52
+ bash: ['run', 'execute', 'command', 'shell', 'npm', 'git', 'pip', 'ls', 'pwd', 'mkdir', 'build', 'install'],
53
+ grep: ['search', 'find', 'grep', 'look for', 'where is', 'occurrences'],
54
+ glob: ['list files', 'find files', 'what files', 'show files', 'directory', 'pattern'],
55
+ todo_write: ['todo', 'task', 'plan', 'steps'],
56
+ todo_read: ['todo', 'task', 'plan', 'steps'],
57
+ backlog_read: ['backlog', 'requirements', 'features', 'bugs', 'items'],
58
+ backlog_write: ['backlog', 'requirements', 'features', 'bugs', 'add item', 'add feature'],
59
+ git_status: ['git status', 'changes', 'modified', 'staged'],
60
+ git_diff: ['git diff', 'differences', 'what changed'],
61
+ git_log: ['git log', 'history', 'commits'],
62
+ git_commit: ['commit', 'save changes'],
63
+ git_branch: ['branch', 'branches'],
64
+ detect_project: ['project type', 'what kind of project', 'framework'],
65
+ find_project_root: ['project root', 'root directory'],
66
+ run_tests: ['test', 'tests', 'run tests', 'check tests'],
67
+ run_lint: ['lint', 'linting', 'check code', 'style'],
68
+ };
69
+ /**
70
+ * Select relevant tool names based on user input intent
71
+ */
72
+ function selectToolNamesByIntent(input, allToolNames) {
73
+ const lower = input.toLowerCase();
74
+ // If input is very short or a question, use all tools
75
+ if (input.length < 10 || input.includes('?')) {
76
+ return allToolNames;
77
+ }
78
+ const selected = new Set();
79
+ // Add tools based on keyword matches
80
+ for (const [tool, keywords] of Object.entries(TOOL_KEYWORDS)) {
81
+ if (keywords.some((kw) => lower.includes(kw)) && allToolNames.includes(tool)) {
82
+ selected.add(tool);
83
+ }
84
+ }
85
+ // Always include todo tools (they're used implicitly)
86
+ if (allToolNames.includes('todo_write'))
87
+ selected.add('todo_write');
88
+ if (allToolNames.includes('todo_read'))
89
+ selected.add('todo_read');
90
+ // If nothing matched, use all tools
91
+ if (selected.size === 0) {
92
+ return allToolNames;
93
+ }
94
+ return Array.from(selected);
95
+ }
96
+ // =============================================================================
97
+ // Build Command Types and Constants
98
+ // =============================================================================
99
+ const PRIORITY_ORDER = ['critical', 'high', 'medium', 'low'];
100
+ // =============================================================================
101
+ // Project Detection Helpers
102
+ // =============================================================================
103
+ /**
104
+ * Detect if we're in a compilr project (has .compilr folder or -docs sibling)
105
+ * Returns project path from config.json if available
106
+ */
107
+ function detectCompilrProject() {
108
+ const cwd = process.cwd();
109
+ // First try to load config and get paths from there
110
+ const config = loadCompilrConfig(cwd);
111
+ if (config) {
112
+ return {
113
+ found: true,
114
+ backlogPath: findBacklogPath(cwd),
115
+ projectPath: config.paths.project || null,
116
+ docsPath: config.paths.docs,
117
+ };
118
+ }
119
+ // Check for .compilr folder in current directory
120
+ const compilrDir = path.join(cwd, '.compilr');
121
+ if (fs.existsSync(compilrDir)) {
122
+ const backlogPath = path.join(compilrDir, 'backlog.md');
123
+ return {
124
+ found: true,
125
+ backlogPath: fs.existsSync(backlogPath) ? backlogPath : null,
126
+ projectPath: cwd,
127
+ docsPath: null,
128
+ };
129
+ }
130
+ // Check for -docs sibling folder (two-repo pattern)
131
+ const parentDir = path.dirname(cwd);
132
+ const projectName = path.basename(cwd);
133
+ const docsDir = path.join(parentDir, `${projectName}-docs`);
134
+ const docsBacklogPath = path.join(docsDir, '01-planning', 'backlog.md');
135
+ if (fs.existsSync(docsBacklogPath)) {
136
+ return {
137
+ found: true,
138
+ backlogPath: docsBacklogPath,
139
+ projectPath: cwd,
140
+ docsPath: docsDir,
141
+ };
142
+ }
143
+ // Check if we're in the docs repo itself
144
+ const inDocsBacklog = path.join(cwd, '01-planning', 'backlog.md');
145
+ if (fs.existsSync(inDocsBacklog)) {
146
+ // We're in -docs folder, try to find sibling code folder
147
+ const baseName = projectName.replace(/-docs$/, '');
148
+ const codeDir = path.join(parentDir, baseName);
149
+ return {
150
+ found: true,
151
+ backlogPath: inDocsBacklog,
152
+ projectPath: fs.existsSync(codeDir) ? codeDir : null,
153
+ docsPath: cwd,
154
+ };
155
+ }
156
+ // Check for project subfolders (running from parent folder after /init)
157
+ try {
158
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
159
+ for (const entry of entries) {
160
+ if (!entry.isDirectory())
161
+ continue;
162
+ // Check for .compilr in subfolder (single-repo pattern)
163
+ const subCompilrDir = path.join(cwd, entry.name, '.compilr');
164
+ if (fs.existsSync(subCompilrDir)) {
165
+ const backlogPath = path.join(subCompilrDir, 'backlog.md');
166
+ return {
167
+ found: true,
168
+ backlogPath: fs.existsSync(backlogPath) ? backlogPath : null,
169
+ projectPath: path.join(cwd, entry.name),
170
+ docsPath: null,
171
+ };
172
+ }
173
+ // Check for -docs subfolder (two-repo pattern)
174
+ if (entry.name.endsWith('-docs')) {
175
+ const docsBacklog = path.join(cwd, entry.name, '01-planning', 'backlog.md');
176
+ if (fs.existsSync(docsBacklog)) {
177
+ const baseName = entry.name.replace(/-docs$/, '');
178
+ const codeDir = path.join(cwd, baseName);
179
+ return {
180
+ found: true,
181
+ backlogPath: docsBacklog,
182
+ projectPath: fs.existsSync(codeDir) ? codeDir : null,
183
+ docsPath: path.join(cwd, entry.name),
184
+ };
185
+ }
186
+ }
187
+ }
188
+ }
189
+ catch {
190
+ // Ignore read errors
191
+ }
192
+ return { found: false, backlogPath: null, projectPath: null, docsPath: null };
193
+ }
194
+ /**
195
+ * Check if backlog has any items using the shared parser
196
+ */
197
+ function hasBacklogItems(backlogPath) {
198
+ if (!backlogPath || !fs.existsSync(backlogPath)) {
199
+ return false;
200
+ }
201
+ try {
202
+ const content = fs.readFileSync(backlogPath, 'utf-8');
203
+ const items = parseBacklogItems(content);
204
+ return items.length > 0;
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ }
210
+ /**
211
+ * Get skill prompt by name from builtinSkills
212
+ */
213
+ function getSkillPrompt(name) {
214
+ const skill = builtinSkills.find((s) => s.name === name);
215
+ return skill?.prompt ?? null;
216
+ }
217
+ // =============================================================================
218
+ // Build Command Helpers
219
+ // =============================================================================
220
+ /**
221
+ * Select the best backlog item to build.
222
+ * If requestedId is provided, finds that specific item.
223
+ * Otherwise, auto-picks the highest priority 📋 item.
224
+ */
225
+ function selectBuildItem(items, requestedId) {
226
+ // If specific ID requested, find it
227
+ if (requestedId && requestedId.toLowerCase() !== 'scaffold') {
228
+ const searchId = requestedId.toUpperCase();
229
+ const item = items.find(i => i.id === searchId);
230
+ if (!item)
231
+ return null;
232
+ if (item.status !== '📋') {
233
+ // Item exists but not in planned status
234
+ return null; // Handler will show appropriate message
235
+ }
236
+ return item;
237
+ }
238
+ // Filter to only 📋 (planned) items
239
+ const planned = items.filter(i => i.status === '📋');
240
+ if (planned.length === 0)
241
+ return null;
242
+ // Sort by priority, then by ID
243
+ planned.sort((a, b) => {
244
+ const prioA = PRIORITY_ORDER.indexOf(a.priority.toLowerCase());
245
+ const prioB = PRIORITY_ORDER.indexOf(b.priority.toLowerCase());
246
+ if (prioA !== prioB)
247
+ return prioA - prioB;
248
+ return a.id.localeCompare(b.id); // REQ-001 before REQ-002
249
+ });
250
+ return planned[0];
251
+ }
252
+ /**
253
+ * Find dependency IDs mentioned in item title/description.
254
+ * Looks for patterns like "depends on REQ-001", "blocked by BUG-002", etc.
255
+ */
256
+ function findDependencies(item) {
257
+ const text = `${item.title} ${item.description}`;
258
+ const patterns = [
259
+ /depends\s+on\s+([A-Z]+-\d+)/gi,
260
+ /blocked\s+by\s+([A-Z]+-\d+)/gi,
261
+ /requires\s+([A-Z]+-\d+)/gi,
262
+ /after\s+([A-Z]+-\d+)/gi,
263
+ /needs\s+([A-Z]+-\d+)/gi,
264
+ ];
265
+ const deps = [];
266
+ for (const pattern of patterns) {
267
+ const matches = text.matchAll(pattern);
268
+ for (const match of matches) {
269
+ deps.push(match[1].toUpperCase());
270
+ }
271
+ }
272
+ return [...new Set(deps)];
273
+ }
274
+ /**
275
+ * Get list of dependencies that are not yet completed.
276
+ */
277
+ function getUnmetDependencies(item, allItems) {
278
+ const depIds = findDependencies(item);
279
+ return allItems.filter(i => depIds.includes(i.id) && i.status !== '✅');
280
+ }
281
+ /**
282
+ * Check if the project has a code foundation (src/, package.json, etc.)
283
+ */
284
+ function hasProjectFoundation() {
285
+ const cwd = process.cwd();
286
+ const indicators = [
287
+ 'src',
288
+ 'lib',
289
+ 'app',
290
+ 'package.json',
291
+ 'requirements.txt',
292
+ 'Cargo.toml',
293
+ 'go.mod',
294
+ 'pom.xml',
295
+ 'build.gradle',
296
+ 'setup.py',
297
+ 'pyproject.toml',
298
+ ];
299
+ for (const indicator of indicators) {
300
+ const fullPath = path.join(cwd, indicator);
301
+ if (fs.existsSync(fullPath)) {
302
+ return true;
303
+ }
304
+ }
305
+ // Also check in project subfolders (for two-repo pattern)
306
+ try {
307
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
308
+ for (const entry of entries) {
309
+ if (!entry.isDirectory())
310
+ continue;
311
+ // Skip docs folder and hidden folders
312
+ if (entry.name.endsWith('-docs') || entry.name.startsWith('.'))
313
+ continue;
314
+ for (const indicator of indicators) {
315
+ const subPath = path.join(cwd, entry.name, indicator);
316
+ if (fs.existsSync(subPath)) {
317
+ return true;
318
+ }
319
+ }
320
+ }
321
+ }
322
+ catch {
323
+ // Ignore read errors
324
+ }
325
+ return false;
326
+ }
327
+ // =============================================================================
328
+ // REPL Class
329
+ // =============================================================================
330
+ export class REPL {
331
+ agent;
332
+ model;
333
+ currentModel; // Can be hot-switched
334
+ provider;
335
+ version;
336
+ showFiltering;
337
+ onModeChange;
338
+ onAgentFinish;
339
+ startTime;
340
+ state;
341
+ footer;
342
+ // Session stats
343
+ sessionInputTokens = 0;
344
+ sessionOutputTokens = 0;
345
+ sessionRequests = 0;
346
+ // Running state
347
+ isRunning = false;
348
+ agentRunning = false;
349
+ abortController = null;
350
+ // Promise for waiting on REPL exit
351
+ exitResolve = null;
352
+ constructor(options) {
353
+ this.agent = options.agent;
354
+ this.model = options.model;
355
+ this.currentModel = options.model; // Start with same model, can be hot-switched
356
+ this.provider = options.provider ?? 'unknown';
357
+ this.version = options.version ?? '0.0.1';
358
+ this.showFiltering = options.showFiltering ?? false;
359
+ this.onModeChange = options.onModeChange;
360
+ this.onAgentFinish = options.onAgentFinish;
361
+ this.startTime = new Date();
362
+ this.state = createInitialState(this.model);
363
+ // Map permission mode setting to AgentMode
364
+ const permissionMode = getPermissionMode();
365
+ const initialMode = permissionMode === 'bypass' ? 'auto-accept' :
366
+ permissionMode === 'plan' ? 'plan' : 'normal';
367
+ this.footer = new Footer({
368
+ showSeparators: true,
369
+ initialMode,
370
+ });
371
+ // Notify external state handler of initial mode
372
+ if (this.onModeChange && initialMode !== 'normal') {
373
+ this.onModeChange(initialMode);
374
+ }
375
+ // Set up event handlers
376
+ this.setupEventHandlers();
377
+ // Notify that footer is ready with pause/resume/setSuggestion functions
378
+ if (options.onFooterReady) {
379
+ options.onFooterReady(() => { this.footer.pauseAnimation(); }, () => { this.footer.resumeAnimation(); }, (action) => { this.footer.setSuggestion(action); });
380
+ }
381
+ // Register footer callbacks for overlay controller (used by tools like ask_user)
382
+ registerFooterCallbacks(() => { this.footer.pauseAnimation(); }, () => { this.footer.resumeAnimation(); });
383
+ }
384
+ /**
385
+ * Set up Footer event handlers
386
+ */
387
+ setupEventHandlers() {
388
+ // Handle submit - process message or it gets queued by Footer
389
+ this.footer.on('submit', (input) => {
390
+ if (!this.agentRunning && input.trim()) {
391
+ void this.handleSubmit(input);
392
+ }
393
+ // If agent is running, Footer automatically queues the input
394
+ });
395
+ // Handle slash commands
396
+ this.footer.on('command', (command, args) => {
397
+ // Pause animation for overlays
398
+ this.footer.pauseAnimation();
399
+ void this.handleCommand(command, args).then((shouldContinue) => {
400
+ this.footer.resumeAnimation();
401
+ if (!shouldContinue) {
402
+ this.isRunning = false;
403
+ if (this.exitResolve) {
404
+ this.exitResolve();
405
+ }
406
+ }
407
+ });
408
+ });
409
+ // Handle Esc - abort running agent
410
+ this.footer.on('escape', () => {
411
+ if (this.agentRunning && this.abortController) {
412
+ this.abortController.abort();
413
+ conversation.printWarning('Agent execution cancelled');
414
+ }
415
+ });
416
+ // Handle Ctrl+C - exit REPL
417
+ this.footer.on('cancel', () => {
418
+ this.isRunning = false;
419
+ if (this.exitResolve) {
420
+ this.exitResolve();
421
+ }
422
+ });
423
+ // Handle Shift+Tab - cycle modes
424
+ this.footer.on('modeChange', () => {
425
+ this.cycleMode();
426
+ });
427
+ }
428
+ /**
429
+ * Cycle through agent modes (normal -> auto-accept -> plan -> normal)
430
+ */
431
+ cycleMode() {
432
+ const currentMode = this.footer.getMode();
433
+ const nextMode = getNextMode(currentMode);
434
+ this.footer.setMode(nextMode);
435
+ // Notify external state handler
436
+ if (this.onModeChange) {
437
+ this.onModeChange(nextMode);
438
+ }
439
+ // Show feedback
440
+ const modeInfo = MODE_INFO[nextMode];
441
+ this.footer.clearForOutput();
442
+ conversation.printInfo(`Mode: ${modeInfo.label} - ${modeInfo.description}`);
443
+ this.footer.forceRender();
444
+ }
445
+ // ===========================================================================
446
+ // Public API
447
+ // ===========================================================================
448
+ /**
449
+ * Start the REPL (event-driven)
450
+ */
451
+ async run() {
452
+ this.isRunning = true;
453
+ // Set terminal title and clear screen
454
+ terminal.setTitle('compilr');
455
+ terminal.clearScreen();
456
+ conversation.printWelcome(this.model, this.version);
457
+ // Start footer (begins render loop and input capture)
458
+ this.footer.start();
459
+ // Wait for exit signal
460
+ await this.waitForExit();
461
+ // Cleanup
462
+ this.footer.stop();
463
+ terminal.writeLine('');
464
+ conversation.printInfo('Goodbye!');
465
+ }
466
+ /**
467
+ * Wait for REPL exit
468
+ */
469
+ waitForExit() {
470
+ return new Promise((resolve) => {
471
+ this.exitResolve = resolve;
472
+ });
473
+ }
474
+ /**
475
+ * Get current model
476
+ */
477
+ getCurrentModel() {
478
+ return this.currentModel;
479
+ }
480
+ /**
481
+ * Get current provider
482
+ */
483
+ getProvider() {
484
+ return this.provider;
485
+ }
486
+ /**
487
+ * Set model for hot-switching (within same provider)
488
+ */
489
+ setModel(model) {
490
+ this.currentModel = model;
491
+ conversation.printInfo(`Model switched to: ${model}`);
492
+ }
493
+ /**
494
+ * Set the agent mode programmatically (e.g., from permission handler)
495
+ */
496
+ setMode(mode) {
497
+ this.footer.setMode(mode);
498
+ // Notify external state handler
499
+ if (this.onModeChange) {
500
+ this.onModeChange(mode);
501
+ }
502
+ // Show feedback
503
+ const modeInfo = MODE_INFO[mode];
504
+ this.footer.clearForOutput();
505
+ conversation.printInfo(`Mode: ${modeInfo.label} - ${modeInfo.description}`);
506
+ this.footer.forceRender();
507
+ }
508
+ /**
509
+ * Stop the REPL
510
+ */
511
+ stop() {
512
+ this.isRunning = false;
513
+ this.footer.stop();
514
+ if (this.exitResolve) {
515
+ this.exitResolve();
516
+ }
517
+ }
518
+ /**
519
+ * Handle submitted input (processes message and queued inputs)
520
+ */
521
+ async handleSubmit(input) {
522
+ // Clear any previous suggestion when new input is submitted
523
+ this.footer.setSuggestion(null);
524
+ await this.processMessage(input);
525
+ // Process queued inputs (FIFO)
526
+ while (this.footer.hasQueuedInput() && this.isRunning) {
527
+ const queued = this.footer.popQueuedInput();
528
+ if (queued) {
529
+ await this.processMessage(queued);
530
+ }
531
+ }
532
+ }
533
+ // ===========================================================================
534
+ // Command Handling
535
+ // ===========================================================================
536
+ /**
537
+ * Handle slash command
538
+ * Returns false if REPL should exit
539
+ */
540
+ async handleCommand(command, args) {
541
+ // Resolve aliases to canonical command name
542
+ const resolved = resolveCommand(command);
543
+ if (!resolved) {
544
+ // Check if it's a custom command
545
+ const customRegistry = getCustomCommandRegistry();
546
+ if (customRegistry.has(command)) {
547
+ // Expand custom command and send as message
548
+ const customArgs = args ? args.split(' ').filter(a => a.trim()) : [];
549
+ const expanded = customRegistry.expand(command, customArgs);
550
+ if (expanded) {
551
+ // Resume animation before processing (it was paused for overlay commands)
552
+ this.footer.resumeAnimation();
553
+ // Process as a regular message
554
+ await this.processMessage(expanded);
555
+ return true;
556
+ }
557
+ }
558
+ conversation.printWarning(`Unknown command: /${command}`);
559
+ conversation.printInfo('Type /help to see available commands');
560
+ return true;
561
+ }
562
+ switch (resolved) {
563
+ case 'agents':
564
+ await showAgentsOverlay();
565
+ return true;
566
+ case 'backlog': {
567
+ const backlogResult = await showBacklogOverlay();
568
+ if (backlogResult.modified) {
569
+ conversation.printSuccess('Backlog updated.');
570
+ }
571
+ return true;
572
+ }
573
+ case 'commands':
574
+ await showCommandsOverlay();
575
+ return true;
576
+ case 'exit':
577
+ return false;
578
+ case 'help':
579
+ await overlays.showHelp();
580
+ return true;
581
+ case 'init': {
582
+ const initResult = await showInitOverlay();
583
+ if (initResult.created) {
584
+ // Show success message with created files info
585
+ conversation.printSuccess(`Project created: ${initResult.projectPath ?? ''}`);
586
+ if (initResult.docsPath) {
587
+ conversation.printSuccess(`Docs repo created: ${initResult.docsPath}`);
588
+ }
589
+ conversation.printInfo('');
590
+ conversation.printInfo('Next steps:');
591
+ conversation.printInfo(` cd ${initResult.projectPath?.split('/').pop() ?? 'project'}`);
592
+ conversation.printInfo(' npm install');
593
+ conversation.printInfo(' npm run dev');
594
+ conversation.printInfo('');
595
+ // Resume footer animation before processing message
596
+ this.footer.resumeAnimation();
597
+ // Ask agent to read the COMPILR.md and understand the project
598
+ const compilrMdPath = initResult.docsPath
599
+ ? `${initResult.docsPath}/COMPILR.md`
600
+ : `${initResult.projectPath ?? '.'}/COMPILR.md`;
601
+ await this.processMessage(`I just created a new project using /init. Please read ${compilrMdPath} to understand the project context and confirm you're ready to help me build it.
602
+
603
+ (Note to user: When you want to define requirements and populate the backlog, type /design)`);
604
+ }
605
+ return true;
606
+ }
607
+ case 'keys': {
608
+ const keysResult = await showKeysOverlay();
609
+ if (keysResult.changed) {
610
+ conversation.printSuccess('API keys updated.');
611
+ conversation.printInfo('Restart the CLI to use the new keys.');
612
+ }
613
+ return true;
614
+ }
615
+ case 'clear':
616
+ terminal.clearScreen();
617
+ conversation.printWelcome(this.model, this.version);
618
+ return true;
619
+ case 'compact':
620
+ await this.handleCompact();
621
+ return true;
622
+ case 'config': {
623
+ // Get context stats for usage tab
624
+ const contextManager = this.agent.getContextManager();
625
+ const history = this.agent.getHistory();
626
+ let contextUsed = 0;
627
+ let contextMax = 200000;
628
+ if (contextManager) {
629
+ const stats = contextManager.getStats(history.length);
630
+ contextUsed = stats.currentTokens;
631
+ contextMax = stats.maxTokens;
632
+ }
633
+ const configResult = await showConfigOverlay({
634
+ version: this.version,
635
+ cwd: process.cwd(),
636
+ model: this.currentModel,
637
+ provider: this.provider,
638
+ toolCount: this.agent.getToolDefinitions().length,
639
+ startTime: this.startTime,
640
+ // Usage stats
641
+ inputTokens: this.sessionInputTokens,
642
+ outputTokens: this.sessionOutputTokens,
643
+ requests: this.sessionRequests,
644
+ contextUsed,
645
+ contextMax,
646
+ messageCount: history.length,
647
+ // Model change callback for hot-switch
648
+ onModelChange: (model) => {
649
+ this.setModel(model);
650
+ },
651
+ });
652
+ // Handle model change result (for hot-switch)
653
+ if (configResult.modelChanged) {
654
+ this.currentModel = configResult.modelChanged;
655
+ }
656
+ // Refresh prompt in case theme changed
657
+ this.footer.refreshPrompt();
658
+ return true;
659
+ }
660
+ case 'tools':
661
+ await this.showTools();
662
+ return true;
663
+ case 'tokens':
664
+ this.showTokens();
665
+ return true;
666
+ case 'context':
667
+ this.showContext();
668
+ return true;
669
+ case 'export':
670
+ await this.handleExport(args);
671
+ return true;
672
+ case 'model': {
673
+ const modelResult = await showConfigOverlay({
674
+ model: this.currentModel,
675
+ provider: this.provider,
676
+ initialMode: 'model-selector',
677
+ // Don't print in callback - overlay cleanup would clear it
678
+ // We'll print after overlay closes instead
679
+ });
680
+ // Handle model change result
681
+ if (modelResult.modelChanged) {
682
+ this.currentModel = modelResult.modelChanged;
683
+ // Clear footer, print message, re-render footer
684
+ this.footer.clearForOutput();
685
+ conversation.printSuccess(`Model switched to: ${modelResult.modelChanged}`);
686
+ this.footer.forceRender();
687
+ }
688
+ return true;
689
+ }
690
+ case 'status':
691
+ this.showStatus();
692
+ return true;
693
+ case 'todos':
694
+ this.showTodos();
695
+ return true;
696
+ case 'plan':
697
+ // Switch to plan mode
698
+ this.footer.setMode('plan');
699
+ if (this.onModeChange) {
700
+ this.onModeChange('plan');
701
+ }
702
+ conversation.printInfo('📋 Switched to Plan mode');
703
+ conversation.printInfo('The agent will create execution plans without performing actions.');
704
+ conversation.printInfo('Use Shift+Tab to switch back to Normal or Auto-accept mode.');
705
+ return true;
706
+ case 'design': {
707
+ // Check if project is initialized
708
+ const designProject = detectCompilrProject();
709
+ if (!designProject.found) {
710
+ conversation.printError('No .compilr project found. Run /init first.');
711
+ return true;
712
+ }
713
+ // Check model tier - /design works best with large models
714
+ if (!modelMeetsTier(this.currentModel, '/design')) {
715
+ const modelTier = getModelTier(this.currentModel);
716
+ // Show warning overlay
717
+ const choice = await showModelWarningOverlay({
718
+ command: '/design',
719
+ currentModel: this.currentModel,
720
+ currentTier: modelTier.tier,
721
+ suggestedModel: modelTier.suggestedUpgrade,
722
+ alternativeCommand: '/sketch',
723
+ });
724
+ switch (choice) {
725
+ case 'cancel':
726
+ return true;
727
+ case 'alternative':
728
+ // Run /sketch instead (command name without leading slash)
729
+ return this.handleCommand('sketch', '');
730
+ case 'switch':
731
+ // Hot-switch to the suggested model
732
+ if (modelTier.suggestedUpgrade) {
733
+ this.currentModel = modelTier.suggestedUpgrade;
734
+ conversation.printSuccess(`Switched to ${modelTier.suggestedUpgrade}`);
735
+ conversation.printInfo('');
736
+ }
737
+ else {
738
+ conversation.printWarning('No suggested upgrade available. Continuing with current model.');
739
+ conversation.printInfo('');
740
+ }
741
+ break;
742
+ case 'continue':
743
+ // Proceed with current model
744
+ break;
745
+ }
746
+ }
747
+ // Get design skill prompt
748
+ const designPrompt = getSkillPrompt('design');
749
+ if (!designPrompt) {
750
+ conversation.printError('Design skill not found.');
751
+ return true;
752
+ }
753
+ // Note: Plan mode is not yet implemented, so we run in normal mode
754
+ // The skill prompt guides the agent's behavior
755
+ conversation.printInfo('📋 Starting project design...');
756
+ conversation.printInfo('');
757
+ // Resume footer animation before processing message
758
+ this.footer.resumeAnimation();
759
+ // Inject design skill and send initial message
760
+ // Show short message to user, but send full skill prompt to agent
761
+ await this.processMessage(`I want to design my project and create the backlog.
762
+
763
+ ${designPrompt}
764
+
765
+ Please start by using todo_write to track the design phases, then begin with Phase 1: Vision. Use the ask_user tool to gather information efficiently.`, {
766
+ displayMessage: 'Start the design process for my project.',
767
+ });
768
+ return true;
769
+ }
770
+ case 'sketch': {
771
+ // Check if project is initialized
772
+ const sketchProject = detectCompilrProject();
773
+ if (!sketchProject.found) {
774
+ conversation.printError('No .compilr project found. Run /init first.');
775
+ return true;
776
+ }
777
+ // Get sketch skill prompt
778
+ const sketchPrompt = getSkillPrompt('sketch');
779
+ if (!sketchPrompt) {
780
+ conversation.printError('Sketch skill not found.');
781
+ return true;
782
+ }
783
+ conversation.printInfo('✏️ Starting quick project sketch...');
784
+ conversation.printInfo('');
785
+ // Resume footer animation before processing message
786
+ this.footer.resumeAnimation();
787
+ // Inject sketch skill and send initial message
788
+ // Uses ask_user_simple for simpler questions (one at a time)
789
+ await this.processMessage(`I want to quickly outline my project.
790
+
791
+ ${sketchPrompt}
792
+
793
+ Please start by asking me about the type of application I'm building using the ask_user_simple tool.`, {
794
+ displayMessage: 'Quick project outline.',
795
+ });
796
+ return true;
797
+ }
798
+ case 'refine': {
799
+ // Check if project is initialized - use same path detection as /backlog
800
+ const refineBacklogPath = findBacklogPath();
801
+ if (!refineBacklogPath) {
802
+ conversation.printError('No backlog found. Run /init first to create a project.');
803
+ return true;
804
+ }
805
+ // Check if backlog has items
806
+ if (!hasBacklogItems(refineBacklogPath)) {
807
+ conversation.printError('No backlog items found. Run /design first to create initial requirements.');
808
+ return true;
809
+ }
810
+ // Check if a specific item ID was provided (e.g., /refine REQ-001)
811
+ const itemId = args.trim().toUpperCase();
812
+ const isFocusedRefine = itemId && /^[A-Z]+-\d{3}$/.test(itemId);
813
+ // For full refine mode (no item ID), check model tier and warn
814
+ if (!isFocusedRefine && !modelMeetsTier(this.currentModel, '/refine')) {
815
+ const modelTier = getModelTier(this.currentModel);
816
+ const choice = await showModelWarningOverlay({
817
+ command: '/refine',
818
+ currentModel: this.currentModel,
819
+ currentTier: modelTier.tier,
820
+ suggestedModel: modelTier.suggestedUpgrade,
821
+ alternativeCommand: '/refine <ITEM-ID>',
822
+ });
823
+ switch (choice) {
824
+ case 'cancel':
825
+ return true;
826
+ case 'alternative':
827
+ // Show hint about using specific item ID
828
+ conversation.printInfo('💡 Tip: Use /refine <ITEM-ID> for focused refinement with smaller models.');
829
+ conversation.printInfo(' Example: /refine REQ-001');
830
+ conversation.printInfo('');
831
+ return true;
832
+ case 'switch':
833
+ if (modelTier.suggestedUpgrade) {
834
+ this.currentModel = modelTier.suggestedUpgrade;
835
+ conversation.printSuccess(`Switched to ${this.currentModel}`);
836
+ }
837
+ break;
838
+ case 'continue':
839
+ // Continue with current model
840
+ break;
841
+ }
842
+ }
843
+ // Get appropriate skill prompt
844
+ const skillName = isFocusedRefine ? 'refine-item' : 'refine';
845
+ const refinePrompt = getSkillPrompt(skillName);
846
+ if (!refinePrompt) {
847
+ conversation.printError(`${skillName} skill not found.`);
848
+ return true;
849
+ }
850
+ if (isFocusedRefine) {
851
+ conversation.printInfo(`🔄 Refining item ${itemId}...`);
852
+ }
853
+ else {
854
+ conversation.printInfo('🔄 Starting requirements refinement...');
855
+ }
856
+ conversation.printInfo('');
857
+ // Resume footer animation before processing message
858
+ this.footer.resumeAnimation();
859
+ // Build message based on mode
860
+ const userIntent = isFocusedRefine
861
+ ? `I want to refine backlog item ${itemId}.`
862
+ : 'I want to refine my project requirements.';
863
+ const agentInstructions = isFocusedRefine
864
+ ? `Please use backlog_read with id:"${itemId}" to get the item details, then guide me through refining it.`
865
+ : 'Please start by using backlog_read with limit:10 to get an overview, then ask me what I\'d like to focus on using ask_user_simple.';
866
+ await this.processMessage(`${userIntent}
867
+
868
+ ${refinePrompt}
869
+
870
+ ${agentInstructions}`, {
871
+ displayMessage: userIntent,
872
+ });
873
+ return true;
874
+ }
875
+ case 'arch': {
876
+ // Check if project is initialized
877
+ const archProject = detectCompilrProject();
878
+ if (!archProject.found) {
879
+ conversation.printError('No .compilr project found. Run /init first.');
880
+ return true;
881
+ }
882
+ // Check model tier - /arch requires large models
883
+ if (!modelMeetsTier(this.currentModel, '/arch')) {
884
+ const modelTier = getModelTier(this.currentModel);
885
+ const choice = await showModelWarningOverlay({
886
+ command: '/arch',
887
+ currentModel: this.currentModel,
888
+ currentTier: modelTier.tier,
889
+ suggestedModel: modelTier.suggestedUpgrade,
890
+ // No alternative command for /arch - it's inherently complex
891
+ });
892
+ switch (choice) {
893
+ case 'cancel':
894
+ return true;
895
+ case 'switch':
896
+ if (modelTier.suggestedUpgrade) {
897
+ this.currentModel = modelTier.suggestedUpgrade;
898
+ conversation.printSuccess(`Switched to ${modelTier.suggestedUpgrade}`);
899
+ conversation.printInfo('');
900
+ }
901
+ else {
902
+ conversation.printWarning('No suggested upgrade available. Continuing with current model.');
903
+ conversation.printInfo('');
904
+ }
905
+ break;
906
+ case 'continue':
907
+ // Proceed with current model
908
+ break;
909
+ }
910
+ }
911
+ // Show type selection overlay
912
+ const archChoice = await showArchTypeOverlay();
913
+ if (!archChoice) {
914
+ // User cancelled
915
+ return true;
916
+ }
917
+ // Get architecture skill prompt
918
+ const archPrompt = getSkillPrompt('architecture');
919
+ if (!archPrompt) {
920
+ conversation.printError('Architecture skill not found.');
921
+ return true;
922
+ }
923
+ // Build the document type description for display
924
+ const docTypeLabels = {
925
+ 'adr': 'Architecture Decision Record',
926
+ 'diagram': 'System Diagram',
927
+ 'data-model': 'Data Model',
928
+ 'api': 'API Design',
929
+ 'custom': 'Custom Documentation',
930
+ };
931
+ const displayType = archChoice.customTopic
932
+ ? `Custom: ${archChoice.customTopic}`
933
+ : docTypeLabels[archChoice.type];
934
+ conversation.printInfo(`📐 Creating ${displayType}...`);
935
+ conversation.printInfo('');
936
+ // Resume footer animation before processing message
937
+ this.footer.resumeAnimation();
938
+ // Replace placeholders in skill prompt
939
+ const finalPrompt = archPrompt
940
+ .replace('{{doc_type}}', archChoice.type)
941
+ .replace('{{#if custom_topic}}Custom Topic: {{custom_topic}}{{/if}}', archChoice.customTopic ? `Custom Topic: ${archChoice.customTopic}` : '');
942
+ await this.processMessage(`I want to create architecture documentation.
943
+
944
+ ${finalPrompt}
945
+
946
+ Please start by reading any existing PRD.md and using backlog_read to understand the project context. Then use ask_user to gather the information needed for this ${archChoice.type} document.`, {
947
+ displayMessage: `Create ${displayType} documentation.`,
948
+ });
949
+ return true;
950
+ }
951
+ case 'note': {
952
+ // Check if project is initialized
953
+ const noteProject = detectCompilrProject();
954
+ if (!noteProject.found) {
955
+ conversation.printError('No .compilr project found. Run /init first.');
956
+ return true;
957
+ }
958
+ // Get session-notes skill prompt
959
+ const notePrompt = getSkillPrompt('session-notes');
960
+ if (!notePrompt) {
961
+ conversation.printError('Session-notes skill not found.');
962
+ return true;
963
+ }
964
+ // Check for optional title argument
965
+ const noteTitle = args.trim();
966
+ conversation.printInfo('📝 Creating session note...');
967
+ conversation.printInfo('');
968
+ // Resume footer animation before processing message
969
+ this.footer.resumeAnimation();
970
+ // Build message based on whether title was provided
971
+ const titleInstruction = noteTitle
972
+ ? `The session title is: "${noteTitle}"`
973
+ : 'Please ask me for a title using ask_user_simple, or generate one from the session summary.';
974
+ await this.processMessage(`I want to create a session note capturing what we've done.
975
+
976
+ ${notePrompt}
977
+
978
+ ${titleInstruction}
979
+
980
+ Review the conversation context to understand what was accomplished, then create the session note file.`, {
981
+ displayMessage: noteTitle
982
+ ? `Create session note: "${noteTitle}"`
983
+ : 'Create a session note for this session.',
984
+ });
985
+ return true;
986
+ }
987
+ case 'prd': {
988
+ // Check if project is initialized
989
+ const prdProject = detectCompilrProject();
990
+ if (!prdProject.found) {
991
+ conversation.printError('No .compilr project found. Run /init first.');
992
+ return true;
993
+ }
994
+ // Get prd skill prompt
995
+ const prdPrompt = getSkillPrompt('prd');
996
+ if (!prdPrompt) {
997
+ conversation.printError('PRD skill not found.');
998
+ return true;
999
+ }
1000
+ // Check for optional section argument
1001
+ const prdSection = args.trim().toLowerCase();
1002
+ const validSections = ['vision', 'scope', 'technical', 'success'];
1003
+ const sectionInstruction = validSections.includes(prdSection)
1004
+ ? `The user wants to update the "${prdSection}" section specifically.`
1005
+ : 'Ask the user which section they want to update using ask_user_simple.';
1006
+ conversation.printInfo('📄 Opening PRD for updates...');
1007
+ conversation.printInfo('');
1008
+ // Resume footer animation before processing message
1009
+ this.footer.resumeAnimation();
1010
+ await this.processMessage(`I want to update the Product Requirements Document.
1011
+
1012
+ ${prdPrompt}
1013
+
1014
+ ${sectionInstruction}
1015
+
1016
+ Start by reading the existing PRD.md file to understand current state.`, {
1017
+ displayMessage: prdSection
1018
+ ? `Update PRD: ${prdSection} section`
1019
+ : 'Update the Product Requirements Document',
1020
+ });
1021
+ return true;
1022
+ }
1023
+ case 'build': {
1024
+ const itemId = args.trim() || undefined;
1025
+ // Handle /build scaffold - redirect to scaffold handler
1026
+ if (itemId?.toLowerCase() === 'scaffold') {
1027
+ return this.handleScaffoldCommand();
1028
+ }
1029
+ // Check if project is initialized
1030
+ const buildProject = detectCompilrProject();
1031
+ if (!buildProject.found) {
1032
+ conversation.printError('No .compilr project found. Run /init first.');
1033
+ return true;
1034
+ }
1035
+ // Read backlog using the shared path detection
1036
+ const buildBacklogPath = findBacklogPath();
1037
+ if (!buildBacklogPath) {
1038
+ conversation.printError('No backlog found. Run /design or /sketch first.');
1039
+ return true;
1040
+ }
1041
+ // Parse backlog items
1042
+ let buildItems = [];
1043
+ try {
1044
+ const backlogContent = fs.readFileSync(buildBacklogPath, 'utf-8');
1045
+ buildItems = parseBacklogItems(backlogContent);
1046
+ }
1047
+ catch {
1048
+ conversation.printError('Failed to read backlog file.');
1049
+ return true;
1050
+ }
1051
+ if (buildItems.length === 0) {
1052
+ conversation.printError('No backlog items found. Run /design or /sketch first.');
1053
+ return true;
1054
+ }
1055
+ // Get project path for context
1056
+ const projectPathForBuild = buildProject.projectPath || process.cwd();
1057
+ const projectPathContext = `**IMPORTANT: Project Directory**
1058
+ All code files MUST be created in: ${projectPathForBuild}
1059
+ Do NOT create files in the current working directory if it differs from the project path.
1060
+ `;
1061
+ // Check foundation
1062
+ const foundationExists = hasProjectFoundation();
1063
+ if (!foundationExists) {
1064
+ // Let agent handle foundation check via skill
1065
+ conversation.printInfo('🔍 Checking project foundation...');
1066
+ conversation.printInfo('');
1067
+ this.footer.resumeAnimation();
1068
+ await this.processMessage(`I want to build a feature from the backlog, but first check if the project has a foundation.
1069
+
1070
+ ${projectPathContext}
1071
+
1072
+ Use detect_project to check the current state. If there's no code foundation (no src/, no package.json or equivalent), use ask_user_simple to ask:
1073
+ "No project foundation detected. Would you like to:"
1074
+ - "Create scaffold first (recommended)"
1075
+ - "Proceed with feature anyway"
1076
+ - "Cancel"
1077
+
1078
+ If user chooses scaffold, read COMPILR.md and PRD.md for tech stack info, then create the project scaffold following these guidelines:
1079
+
1080
+ ${getSkillPrompt('scaffold') ?? ''}
1081
+
1082
+ After scaffold is done (or if user chose to proceed anyway), continue with building the feature.`, { displayMessage: 'Check project foundation before building.' });
1083
+ return true;
1084
+ }
1085
+ // Select item
1086
+ const item = selectBuildItem(buildItems, itemId);
1087
+ if (!item) {
1088
+ if (itemId) {
1089
+ const existingItem = buildItems.find(i => i.id.toUpperCase() === itemId.toUpperCase());
1090
+ if (existingItem) {
1091
+ conversation.printError(`Item ${itemId} is already ${existingItem.status === '✅' ? 'completed' : 'in progress'}.`);
1092
+ }
1093
+ else {
1094
+ conversation.printError(`Item ${itemId} not found in backlog.`);
1095
+ }
1096
+ }
1097
+ else {
1098
+ conversation.printInfo('No pending items in backlog. All done! 🎉');
1099
+ }
1100
+ return true;
1101
+ }
1102
+ // Check dependencies
1103
+ const unmetDeps = getUnmetDependencies(item, buildItems);
1104
+ const depsWarning = unmetDeps.length > 0
1105
+ ? `\n\n**⚠️ UNMET DEPENDENCIES:**\n${unmetDeps.map(d => `- ${d.id}: ${d.title} (${d.status})`).join('\n')}\n\nThese items are not yet completed. Ask user to confirm before proceeding.`
1106
+ : '';
1107
+ // Get build skill prompt and replace placeholders
1108
+ const buildPromptTemplate = getSkillPrompt('build');
1109
+ if (!buildPromptTemplate) {
1110
+ conversation.printError('Build skill not found.');
1111
+ return true;
1112
+ }
1113
+ const buildPrompt = buildPromptTemplate
1114
+ .replace(/\{\{item_id\}\}/g, item.id)
1115
+ .replace(/\{\{item_title\}\}/g, item.title)
1116
+ .replace(/\{\{item_description\}\}/g, item.description)
1117
+ .replace(/\{\{item_type\}\}/g, item.type)
1118
+ .replace(/\{\{item_priority\}\}/g, item.priority);
1119
+ conversation.printInfo(`🔨 Building ${item.id}: ${item.title}`);
1120
+ conversation.printInfo('');
1121
+ this.footer.resumeAnimation();
1122
+ await this.processMessage(`I want to implement backlog item ${item.id}.
1123
+
1124
+ ${projectPathContext}
1125
+
1126
+ ${buildPrompt}${depsWarning}`, { displayMessage: `Build ${item.id}: ${item.title}` });
1127
+ return true;
1128
+ }
1129
+ case 'scaffold': {
1130
+ return this.handleScaffoldCommand();
1131
+ }
1132
+ case 'tutorial': {
1133
+ await showTutorialOverlay();
1134
+ return true;
1135
+ }
1136
+ default:
1137
+ // This shouldn't happen if resolveCommand works correctly
1138
+ conversation.printWarning(`Unknown command: /${command}`);
1139
+ return true;
1140
+ }
1141
+ }
1142
+ /**
1143
+ * Handle /scaffold command (shared between /scaffold and /build scaffold)
1144
+ */
1145
+ async handleScaffoldCommand() {
1146
+ // Check if project is initialized
1147
+ const scaffoldProject = detectCompilrProject();
1148
+ if (!scaffoldProject.found) {
1149
+ conversation.printError('No .compilr project found. Run /init first.');
1150
+ return true;
1151
+ }
1152
+ // Get project path for context
1153
+ const projectPathForScaffold = scaffoldProject.projectPath || process.cwd();
1154
+ const scaffoldPathContext = `**IMPORTANT: Project Directory**
1155
+ All code files MUST be created in: ${projectPathForScaffold}
1156
+ Do NOT create files in the current working directory if it differs from the project path.
1157
+ `;
1158
+ // Check if foundation already exists
1159
+ const foundationExists = hasProjectFoundation();
1160
+ if (foundationExists) {
1161
+ // Let agent confirm with user
1162
+ conversation.printInfo('🔍 Project files detected...');
1163
+ conversation.printInfo('');
1164
+ this.footer.resumeAnimation();
1165
+ await this.processMessage(`User wants to create a project scaffold, but a foundation may already exist.
1166
+
1167
+ ${scaffoldPathContext}
1168
+
1169
+ Use detect_project to analyze the current state. Then use ask_user_simple:
1170
+ "Project files already exist. What would you like to do?"
1171
+ - "Continue anyway (may overwrite)"
1172
+ - "Cancel"
1173
+
1174
+ If continuing, read COMPILR.md and PRD.md for tech stack info, then create the scaffold:
1175
+
1176
+ ${getSkillPrompt('scaffold') ?? ''}`, { displayMessage: 'Create project scaffold (foundation exists).' });
1177
+ return true;
1178
+ }
1179
+ // Get scaffold skill prompt
1180
+ const scaffoldPrompt = getSkillPrompt('scaffold');
1181
+ if (!scaffoldPrompt) {
1182
+ conversation.printError('Scaffold skill not found.');
1183
+ return true;
1184
+ }
1185
+ conversation.printInfo('🏗️ Creating project scaffold...');
1186
+ conversation.printInfo('');
1187
+ this.footer.resumeAnimation();
1188
+ await this.processMessage(`I want to create the project scaffold (foundation).
1189
+
1190
+ ${scaffoldPathContext}
1191
+
1192
+ ${scaffoldPrompt}
1193
+
1194
+ Read COMPILR.md and PRD.md first to understand the tech stack, then create the appropriate scaffold.`, { displayMessage: 'Create project scaffold.' });
1195
+ return true;
1196
+ }
1197
+ // ===========================================================================
1198
+ // Message Processing
1199
+ // ===========================================================================
1200
+ /**
1201
+ * Strip '@' prefix from @path mentions before sending to agent
1202
+ * The '@' is UI syntax for path autocomplete, not part of the actual path
1203
+ * Only strips @ when it's at start of a word (not in middle like email@domain.com)
1204
+ */
1205
+ stripAtMentions(input) {
1206
+ // Match @ at start of string or after whitespace, followed by non-whitespace
1207
+ return input.replace(/(^|\s)@(\S+)/g, '$1$2');
1208
+ }
1209
+ /**
1210
+ * Process a message to the agent
1211
+ * @param input - The full message to send to the agent
1212
+ * @param options - Optional settings
1213
+ * @param options.displayMessage - What to show the user (defaults to input)
1214
+ * @param options.skipPlanModeCheck - Skip plan mode handling (for /design, /refine)
1215
+ */
1216
+ async processMessage(input, options) {
1217
+ // Print user message to conversation (show displayMessage or original)
1218
+ this.footer.clearForOutput();
1219
+ conversation.printUserMessage(options?.displayMessage ?? input);
1220
+ this.footer.forceRender();
1221
+ // Strip @ prefix from path mentions before sending to agent
1222
+ const cleanedInput = this.stripAtMentions(input);
1223
+ // Check current mode (unless skipped for skill-based commands)
1224
+ const mode = this.footer.getMode();
1225
+ // Plan mode - placeholder for now (skip for /design, /refine which handle their own flow)
1226
+ if (mode === 'plan' && !options?.skipPlanModeCheck) {
1227
+ this.footer.clearForOutput();
1228
+ conversation.printInfo('📋 Plan Mode (coming soon)');
1229
+ conversation.printInfo('In plan mode, the agent will analyze your request and create');
1230
+ conversation.printInfo('a detailed execution plan without performing any actions.');
1231
+ conversation.printInfo('');
1232
+ conversation.printInfo('For now, switch to Normal or Auto-accept mode with Shift+Tab.');
1233
+ terminal.writeLine('');
1234
+ this.footer.forceRender();
1235
+ return;
1236
+ }
1237
+ // Get all tool names and filter based on intent (use cleaned input)
1238
+ const allToolNames = this.agent.getToolDefinitions().map((t) => t.name);
1239
+ const selectedNames = selectToolNamesByIntent(cleanedInput, allToolNames);
1240
+ // Show filtering analysis if enabled
1241
+ if (this.showFiltering) {
1242
+ const saved = allToolNames.length - selectedNames.length;
1243
+ const pct = Math.round((saved / allToolNames.length) * 100);
1244
+ this.footer.clearForOutput();
1245
+ conversation.printInfo(`[Tool Filtering] ${String(selectedNames.length)}/${String(allToolNames.length)} tools, ~${String(pct)}% reduction`);
1246
+ this.footer.forceRender();
1247
+ }
1248
+ // Start agent running state
1249
+ this.agentRunning = true;
1250
+ setAgentRunning(this.state, true);
1251
+ this.footer.setAgentRunning(true);
1252
+ this.abortController = new AbortController();
1253
+ let totalInputTokens = 0;
1254
+ let totalOutputTokens = 0;
1255
+ let llmCalls = 0;
1256
+ let hasTextOutput = false;
1257
+ let usedTools = false;
1258
+ let lastToolInput = null;
1259
+ // Event-based rendering: accumulate text, render complete blocks
1260
+ let textAccumulator = '';
1261
+ debug('processMessage', 'Starting agent stream', {
1262
+ inputLength: cleanedInput.length,
1263
+ toolCount: selectedNames.length,
1264
+ model: this.currentModel,
1265
+ });
1266
+ try {
1267
+ for await (const event of this.agent.stream(cleanedInput, {
1268
+ toolFilter: selectedNames,
1269
+ signal: this.abortController.signal,
1270
+ chatOptions: { model: this.currentModel },
1271
+ })) {
1272
+ debug('processMessage', `Event received: ${event.type}`, event);
1273
+ // Check if aborted
1274
+ if (this.abortController.signal.aborted) {
1275
+ debug('processMessage', 'Aborted');
1276
+ break;
1277
+ }
1278
+ if (event.type === 'llm_chunk') {
1279
+ if (event.chunk.type === 'text' && event.chunk.text) {
1280
+ // Event-based rendering: accumulate text until tool_start or stream end
1281
+ // This allows complete code blocks to be rendered with syntax highlighting
1282
+ textAccumulator += event.chunk.text;
1283
+ hasTextOutput = true;
1284
+ }
1285
+ else if (event.chunk.type === 'done' && event.chunk.usage) {
1286
+ const tokens = event.chunk.usage.inputTokens + event.chunk.usage.outputTokens;
1287
+ this.footer.addTokens(tokens);
1288
+ totalInputTokens += event.chunk.usage.inputTokens;
1289
+ totalOutputTokens += event.chunk.usage.outputTokens;
1290
+ llmCalls++;
1291
+ }
1292
+ }
1293
+ else if (event.type === 'tool_start') {
1294
+ // Event-based rendering: flush accumulated text before tool output
1295
+ // Exception: For ask_user/ask_user_simple tools, we'll buffer the text and print after overlay closes
1296
+ // to avoid the text interfering with the overlay rendering
1297
+ // Also skip if an overlay is currently active (e.g., permission overlay)
1298
+ const isAskUserTool = event.name === 'ask_user' || event.name === 'ask_user_simple';
1299
+ const shouldBuffer = isAskUserTool || isOverlayActive();
1300
+ if (textAccumulator.trim() && !shouldBuffer) {
1301
+ this.footer.clearForOutput();
1302
+ conversation.printAssistantResponse(textAccumulator.trim());
1303
+ terminal.writeLine('');
1304
+ this.footer.forceRender();
1305
+ textAccumulator = '';
1306
+ }
1307
+ usedTools = true;
1308
+ lastToolInput = event.input;
1309
+ // Show tool in spinner (but not silent tools)
1310
+ if (event.name !== 'todo_read' && event.name !== 'todo_write' && event.name !== 'suggest') {
1311
+ // For task tool, show subagent type
1312
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1313
+ if (event.name === 'task' && event.input) {
1314
+ const inputObj = event.input;
1315
+ const subagentType = typeof inputObj.subagent_type === 'string' ? inputObj.subagent_type : 'general';
1316
+ const description = typeof inputObj.description === 'string' ? inputObj.description : '';
1317
+ this.footer.setCurrentTool(`task(${subagentType}): ${description}`);
1318
+ }
1319
+ else {
1320
+ this.footer.setCurrentTool(event.name);
1321
+ }
1322
+ }
1323
+ // Note: Diff is shown in the permission handler (index.ts) BEFORE the prompt
1324
+ }
1325
+ else if (event.type === 'tool_end') {
1326
+ const toolName = event.name;
1327
+ const toolInput = lastToolInput;
1328
+ const result = event.result;
1329
+ lastToolInput = null;
1330
+ // Handle todo_write - update state and render
1331
+ if (toolName === 'todo_write' && toolInput && Array.isArray(toolInput.todos)) {
1332
+ const todos = toolInput.todos.map((t) => {
1333
+ const content = typeof t.content === 'string' ? t.content :
1334
+ typeof t.title === 'string' ? t.title :
1335
+ typeof t.task === 'string' ? t.task :
1336
+ typeof t.description === 'string' ? t.description :
1337
+ typeof t.text === 'string' ? t.text : 'Untitled task';
1338
+ const status = (typeof t.status === 'string' && ['pending', 'in_progress', 'completed'].includes(t.status)
1339
+ ? t.status
1340
+ : 'pending');
1341
+ const activeForm = typeof t.activeForm === 'string' ? t.activeForm : undefined;
1342
+ return { content, status, activeForm };
1343
+ });
1344
+ updateTodos(this.state, todos);
1345
+ this.footer.setTodos(todos);
1346
+ this.footer.setCurrentTool(null);
1347
+ continue;
1348
+ }
1349
+ // Skip todo_read display
1350
+ if (toolName === 'todo_read') {
1351
+ this.footer.setCurrentTool(null);
1352
+ continue;
1353
+ }
1354
+ // Skip suggest tool display (suggestion is shown in input prompt)
1355
+ if (toolName === 'suggest') {
1356
+ this.footer.setCurrentTool(null);
1357
+ this.footer.forceRender();
1358
+ continue;
1359
+ }
1360
+ // Handle ask_user/ask_user_simple tools - print buffered text now that overlay is done
1361
+ if (toolName === 'ask_user' || toolName === 'ask_user_simple') {
1362
+ // Print any accumulated text that was buffered before the overlay
1363
+ if (textAccumulator.trim()) {
1364
+ this.footer.clearForOutput();
1365
+ conversation.printAssistantResponse(textAccumulator.trim());
1366
+ terminal.writeLine('');
1367
+ textAccumulator = '';
1368
+ }
1369
+ this.footer.setCurrentTool(null);
1370
+ this.footer.forceRender();
1371
+ continue;
1372
+ }
1373
+ // Clear footer for output
1374
+ this.footer.clearForOutput();
1375
+ // Handle edit tool - diff was already shown on tool_start
1376
+ if (toolName === 'edit') {
1377
+ // Just clear the tool indicator, diff was shown before permission prompt
1378
+ this.footer.setCurrentTool(null);
1379
+ this.footer.forceRender();
1380
+ continue;
1381
+ }
1382
+ // Handle task tool (subagent) - show subagent info
1383
+ if (toolName === 'task' && toolInput) {
1384
+ const subagentType = typeof toolInput.subagent_type === 'string' ? toolInput.subagent_type : 'general';
1385
+ const description = typeof toolInput.description === 'string' ? toolInput.description : '';
1386
+ // Format result summary for subagent
1387
+ let resultSummary = '';
1388
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1389
+ if (result && typeof result === 'object') {
1390
+ const resultObj = result;
1391
+ if (resultObj.error) {
1392
+ const errorObj = resultObj.error;
1393
+ const errorMsg = typeof errorObj.message === 'string' ? errorObj.message : JSON.stringify(resultObj.error);
1394
+ resultSummary = pc.red(`Error: ${errorMsg.slice(0, 60)}`);
1395
+ }
1396
+ else if (resultObj.result && typeof resultObj.result === 'object') {
1397
+ const r = resultObj.result;
1398
+ const iterations = typeof r.iterations === 'number' ? r.iterations : 1;
1399
+ // Try to get a summary from the result
1400
+ if (typeof r.result === 'string') {
1401
+ const preview = r.result.slice(0, 60);
1402
+ resultSummary = pc.dim(`${String(iterations)} iteration(s) - ${preview}${r.result.length > 60 ? '...' : ''}`);
1403
+ }
1404
+ else {
1405
+ resultSummary = pc.dim(`${String(iterations)} iteration(s)`);
1406
+ }
1407
+ }
1408
+ }
1409
+ // Print as "task(explore): Search for files" format
1410
+ conversation.printToolExecution(`task(${pc.cyan(subagentType)})`, description, resultSummary);
1411
+ this.footer.setCurrentTool(null);
1412
+ this.footer.forceRender();
1413
+ continue;
1414
+ }
1415
+ // Build argument summary for other tools
1416
+ let argSummary = '';
1417
+ if (toolInput) {
1418
+ const path = toolInput.path ?? toolInput.file_path ?? toolInput.filePath;
1419
+ const command = toolInput.command;
1420
+ const pattern = toolInput.pattern;
1421
+ if (typeof path === 'string') {
1422
+ argSummary = path.split('/').pop() ?? path;
1423
+ }
1424
+ else if (typeof command === 'string') {
1425
+ argSummary = command.length > 40 ? command.slice(0, 40) + '...' : command;
1426
+ }
1427
+ else if (typeof pattern === 'string') {
1428
+ argSummary = pattern;
1429
+ }
1430
+ }
1431
+ // Format result summary
1432
+ let resultSummary = '';
1433
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1434
+ if (result && typeof result === 'object') {
1435
+ const resultObj = result;
1436
+ if (resultObj.error) {
1437
+ const errorObj = resultObj.error;
1438
+ const errorMsg = typeof errorObj.message === 'string' ? errorObj.message : JSON.stringify(resultObj.error);
1439
+ resultSummary = pc.red(`Error: ${errorMsg.slice(0, 60)}`);
1440
+ }
1441
+ else if (resultObj.result !== undefined) {
1442
+ resultSummary = conversation.formatToolResult(resultObj.result);
1443
+ }
1444
+ }
1445
+ // Print tool log
1446
+ conversation.printToolExecution(toolName, argSummary, resultSummary);
1447
+ // Re-render footer after output
1448
+ this.footer.setCurrentTool(null);
1449
+ this.footer.forceRender();
1450
+ }
1451
+ }
1452
+ debug('processMessage', 'Stream completed', {
1453
+ hasTextOutput,
1454
+ usedTools,
1455
+ totalInputTokens,
1456
+ totalOutputTokens,
1457
+ llmCalls,
1458
+ textAccumulatorLength: textAccumulator.length,
1459
+ });
1460
+ // Skip stats if aborted
1461
+ if (this.abortController.signal.aborted) {
1462
+ this.agentRunning = false;
1463
+ setAgentRunning(this.state, false);
1464
+ this.footer.setAgentRunning(false);
1465
+ this.abortController = null;
1466
+ return;
1467
+ }
1468
+ // Event-based rendering: flush any remaining accumulated text
1469
+ if (textAccumulator.trim()) {
1470
+ this.footer.clearForOutput();
1471
+ conversation.printAssistantResponse(textAccumulator.trim());
1472
+ terminal.writeLine('');
1473
+ textAccumulator = '';
1474
+ }
1475
+ // Warn if tools used but no text
1476
+ if (usedTools && !hasTextOutput) {
1477
+ this.footer.clearForOutput();
1478
+ conversation.printWarning('No text response from model - try a more capable model');
1479
+ }
1480
+ // Update session stats
1481
+ this.sessionInputTokens += totalInputTokens;
1482
+ this.sessionOutputTokens += totalOutputTokens;
1483
+ this.sessionRequests++;
1484
+ // Show token usage
1485
+ if (totalInputTokens > 0 || totalOutputTokens > 0) {
1486
+ this.footer.clearForOutput();
1487
+ const total = totalInputTokens + totalOutputTokens;
1488
+ const sessionTotal = this.sessionInputTokens + this.sessionOutputTokens;
1489
+ conversation.printInfo(`[Tokens: ${total.toLocaleString()} (${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out) - ${String(llmCalls)} LLM call(s)]`);
1490
+ conversation.printInfo(`[Session: ${sessionTotal.toLocaleString()} total (${String(this.sessionRequests)} requests)]`);
1491
+ terminal.writeLine('');
1492
+ }
1493
+ // Stop spinner and re-render footer (AFTER all output is printed)
1494
+ this.agentRunning = false;
1495
+ setAgentRunning(this.state, false);
1496
+ this.footer.setAgentRunning(false);
1497
+ }
1498
+ catch (error) {
1499
+ debugError('processMessage', error);
1500
+ if (error.name !== 'AbortError') {
1501
+ this.footer.clearForOutput();
1502
+ conversation.printError(error.message);
1503
+ this.footer.forceRender();
1504
+ }
1505
+ }
1506
+ finally {
1507
+ // Ensure agent is stopped even on error
1508
+ this.agentRunning = false;
1509
+ setAgentRunning(this.state, false);
1510
+ this.footer.setAgentRunning(false);
1511
+ this.abortController = null;
1512
+ // Notify that agent has finished (for applying deferred suggestions)
1513
+ this.onAgentFinish?.();
1514
+ }
1515
+ }
1516
+ // ===========================================================================
1517
+ // Helper Commands
1518
+ // ===========================================================================
1519
+ async handleCompact() {
1520
+ const contextManager = this.agent.getContextManager();
1521
+ if (!contextManager) {
1522
+ conversation.printWarning('Context manager not available');
1523
+ return;
1524
+ }
1525
+ const history = this.agent.getHistory();
1526
+ if (history.length === 0) {
1527
+ conversation.printInfo('No conversation history to compact');
1528
+ return;
1529
+ }
1530
+ const stats = contextManager.getStats(history.length);
1531
+ this.footer.clearForOutput();
1532
+ conversation.printInfo(`Current: ${stats.currentTokens.toLocaleString()} tokens, ${String(history.length)} messages`);
1533
+ const confirmed = await overlays.showConfirmation('Compact conversation history?');
1534
+ if (!confirmed) {
1535
+ this.footer.forceRender();
1536
+ return;
1537
+ }
1538
+ // Show spinner during compaction
1539
+ this.footer.setSpinnerText('Compacting context');
1540
+ setAgentRunning(this.state, true);
1541
+ this.footer.setAgentRunning(true);
1542
+ this.footer.forceRender(); // Render immediately before blocking operation
1543
+ try {
1544
+ // Use the agent's compact() method - handles summarization, tool pairing, and hints
1545
+ const result = (await this.agent.compact());
1546
+ // Stop spinner and clear for output
1547
+ setAgentRunning(this.state, false);
1548
+ this.footer.setAgentRunning(false);
1549
+ this.footer.setSpinnerText(null);
1550
+ this.footer.clearForOutput();
1551
+ if (!result.success) {
1552
+ conversation.printWarning('Compaction not available (no context manager)');
1553
+ this.footer.forceRender();
1554
+ return;
1555
+ }
1556
+ // Build status message
1557
+ const newHistory = this.agent.getHistory();
1558
+ const statusParts = [
1559
+ `Compacted: ${String(result.originalTokens ?? 0).replace(/\B(?=(\d{3})+(?!\d))/g, ',')} → ${String(result.summaryTokens ?? 0).replace(/\B(?=(\d{3})+(?!\d))/g, ',')} tokens`,
1560
+ `(${String(history.length)} → ${String(newHistory.length)} messages, ${String(result.messagesPreserved ?? 0)} preserved)`,
1561
+ ];
1562
+ // Add smart compaction category info
1563
+ if (result.categoryStats) {
1564
+ const actions = [];
1565
+ if (result.categoryStats.toolResults.action === 'compacted') {
1566
+ actions.push('tool results → files');
1567
+ }
1568
+ if (result.categoryStats.history.action === 'summarized') {
1569
+ actions.push('history → summary');
1570
+ }
1571
+ if (actions.length > 0) {
1572
+ statusParts.push(`[${actions.join(', ')}]`);
1573
+ }
1574
+ }
1575
+ // Add files created info
1576
+ if (result.filesCreated && result.filesCreated.length > 0) {
1577
+ statusParts.push(`[${String(result.filesCreated.length)} file(s) created]`);
1578
+ }
1579
+ // Add file tracking info
1580
+ if (result.restorationHintsInjected) {
1581
+ const fileTracker = this.agent.getFileTracker();
1582
+ const trackerStats = fileTracker?.getStats();
1583
+ const filesTracked = trackerStats ? trackerStats.total : 0;
1584
+ statusParts.push(`[${String(filesTracked)} files tracked]`);
1585
+ }
1586
+ // Add repair info if tool_results were fixed
1587
+ if (result.toolResultsRepaired && result.toolResultsRepaired > 0) {
1588
+ statusParts.push(`[${String(result.toolResultsRepaired)} orphaned tool results removed]`);
1589
+ }
1590
+ conversation.printSuccess(statusParts.join(' '));
1591
+ }
1592
+ catch (error) {
1593
+ // Stop spinner on error too
1594
+ setAgentRunning(this.state, false);
1595
+ this.footer.setAgentRunning(false);
1596
+ this.footer.setSpinnerText(null);
1597
+ this.footer.clearForOutput();
1598
+ conversation.printError(`Compaction failed: ${error.message}`);
1599
+ }
1600
+ this.footer.forceRender();
1601
+ }
1602
+ async showTools() {
1603
+ const tools = this.agent.getToolDefinitions().map((t) => {
1604
+ // Extract parameters from inputSchema
1605
+ const parameters = [];
1606
+ const props = t.inputSchema.properties;
1607
+ const required = t.inputSchema.required ?? [];
1608
+ for (const [name, schema] of Object.entries(props)) {
1609
+ parameters.push({
1610
+ name,
1611
+ type: schema.type ?? 'unknown',
1612
+ description: schema.description,
1613
+ required: required.includes(name),
1614
+ });
1615
+ }
1616
+ // Look up permission info
1617
+ const permissionInfo = getToolPermissionInfo(t.name);
1618
+ const permission = permissionInfo
1619
+ ? { level: permissionInfo.level, description: permissionInfo.description }
1620
+ : undefined;
1621
+ // For task tool, extract just the intro (before verbose agent types list)
1622
+ let description = t.description;
1623
+ if (t.name === 'task' && description.includes('Available agent types:')) {
1624
+ description = description.split('Available agent types:')[0].trim();
1625
+ }
1626
+ return {
1627
+ name: t.name,
1628
+ description,
1629
+ parameters,
1630
+ permission,
1631
+ };
1632
+ });
1633
+ // Pause footer during overlay
1634
+ this.footer.pauseAnimation();
1635
+ try {
1636
+ await showToolsOverlay(tools);
1637
+ }
1638
+ finally {
1639
+ this.footer.resumeAnimation();
1640
+ this.footer.forceRender();
1641
+ }
1642
+ }
1643
+ showTokens() {
1644
+ this.footer.clearForOutput();
1645
+ overlays.showTokenUsage({
1646
+ inputTokens: this.sessionInputTokens,
1647
+ outputTokens: this.sessionOutputTokens,
1648
+ totalTokens: this.sessionInputTokens + this.sessionOutputTokens,
1649
+ });
1650
+ this.footer.forceRender();
1651
+ }
1652
+ showContext() {
1653
+ const contextManager = this.agent.getContextManager();
1654
+ const history = this.agent.getHistory();
1655
+ this.footer.clearForOutput();
1656
+ if (contextManager) {
1657
+ const stats = contextManager.getStats(history.length);
1658
+ overlays.showContextStats({
1659
+ tokens: stats.currentTokens,
1660
+ maxTokens: stats.maxTokens,
1661
+ messages: stats.messageCount,
1662
+ turns: stats.turnCount,
1663
+ utilization: stats.utilization,
1664
+ });
1665
+ }
1666
+ else {
1667
+ // Fallback to session tracking
1668
+ overlays.showContextStats({
1669
+ tokens: this.sessionInputTokens + this.sessionOutputTokens,
1670
+ maxTokens: 200000,
1671
+ messages: history.length,
1672
+ turns: Math.floor(history.filter((m) => m.role === 'user').length),
1673
+ utilization: (this.sessionInputTokens + this.sessionOutputTokens) / 200000,
1674
+ });
1675
+ }
1676
+ this.footer.forceRender();
1677
+ }
1678
+ showStatus() {
1679
+ const contextManager = this.agent.getContextManager();
1680
+ const history = this.agent.getHistory();
1681
+ const mode = this.footer.getMode();
1682
+ const modeInfo = MODE_INFO[mode];
1683
+ this.footer.clearForOutput();
1684
+ terminal.writeLine('');
1685
+ terminal.writeLine(pc.bold('Status'));
1686
+ terminal.writeLine(pc.dim('─'.repeat(40)));
1687
+ terminal.writeLine(` Version: ${pc.cyan(this.version)}`);
1688
+ terminal.writeLine(` Model: ${pc.cyan(this.model)}`);
1689
+ terminal.writeLine(` Mode: ${pc.cyan(modeInfo.label)}`);
1690
+ if (contextManager) {
1691
+ const stats = contextManager.getStats(history.length);
1692
+ const pct = (stats.utilization * 100).toFixed(1);
1693
+ const bar = this.renderProgressBar(stats.utilization, 20);
1694
+ terminal.writeLine(` Context: ${bar} ${pct}%`);
1695
+ terminal.writeLine(` Messages: ${pc.cyan(String(stats.messageCount))}`);
1696
+ }
1697
+ else {
1698
+ const total = this.sessionInputTokens + this.sessionOutputTokens;
1699
+ terminal.writeLine(` Tokens: ${pc.cyan(total.toLocaleString())}`);
1700
+ terminal.writeLine(` Messages: ${pc.cyan(String(history.length))}`);
1701
+ }
1702
+ terminal.writeLine(` Requests: ${pc.cyan(String(this.sessionRequests))}`);
1703
+ terminal.writeLine('');
1704
+ this.footer.forceRender();
1705
+ }
1706
+ renderProgressBar(ratio, width) {
1707
+ const filled = Math.round(ratio * width);
1708
+ const empty = width - filled;
1709
+ const color = ratio > 0.9 ? pc.red : ratio > 0.7 ? pc.yellow : pc.green;
1710
+ return color('█'.repeat(filled)) + pc.dim('░'.repeat(empty));
1711
+ }
1712
+ showTodos() {
1713
+ const todos = this.state.todos;
1714
+ this.footer.clearForOutput();
1715
+ terminal.writeLine('');
1716
+ if (todos.length === 0) {
1717
+ terminal.writeLine(pc.dim('No todos. The agent will create todos when working on tasks.'));
1718
+ terminal.writeLine('');
1719
+ this.footer.forceRender();
1720
+ return;
1721
+ }
1722
+ terminal.writeLine(pc.bold('Todos'));
1723
+ terminal.writeLine(pc.dim('─'.repeat(40)));
1724
+ for (const todo of todos) {
1725
+ let icon;
1726
+ let style;
1727
+ switch (todo.status) {
1728
+ case 'completed':
1729
+ icon = pc.green('✓');
1730
+ style = pc.strikethrough;
1731
+ break;
1732
+ case 'in_progress':
1733
+ icon = pc.yellow('●');
1734
+ style = pc.bold;
1735
+ break;
1736
+ default:
1737
+ icon = pc.dim('○');
1738
+ style = (s) => s;
1739
+ }
1740
+ terminal.writeLine(` ${icon} ${style(todo.content)}`);
1741
+ }
1742
+ terminal.writeLine('');
1743
+ this.footer.forceRender();
1744
+ }
1745
+ async handleExport(args) {
1746
+ const history = this.agent.getHistory();
1747
+ if (history.length === 0) {
1748
+ this.footer.clearForOutput();
1749
+ conversation.printWarning('No conversation to export');
1750
+ this.footer.forceRender();
1751
+ return;
1752
+ }
1753
+ // Format conversation as markdown
1754
+ const markdown = this.formatConversationAsMarkdown(history);
1755
+ if (args.trim()) {
1756
+ // Export to file
1757
+ const filename = args.trim();
1758
+ const filepath = path.isAbsolute(filename) ? filename : path.join(process.cwd(), filename);
1759
+ try {
1760
+ fs.writeFileSync(filepath, markdown, 'utf-8');
1761
+ this.footer.clearForOutput();
1762
+ conversation.printSuccess(`Exported to ${filepath}`);
1763
+ this.footer.forceRender();
1764
+ }
1765
+ catch (error) {
1766
+ this.footer.clearForOutput();
1767
+ conversation.printError(`Failed to export: ${error.message}`);
1768
+ this.footer.forceRender();
1769
+ }
1770
+ }
1771
+ else {
1772
+ // Export to clipboard
1773
+ try {
1774
+ await this.copyToClipboard(markdown);
1775
+ this.footer.clearForOutput();
1776
+ conversation.printSuccess(`Copied ${String(history.length)} messages to clipboard`);
1777
+ this.footer.forceRender();
1778
+ }
1779
+ catch (error) {
1780
+ this.footer.clearForOutput();
1781
+ conversation.printError(`Failed to copy to clipboard: ${error.message}`);
1782
+ this.footer.forceRender();
1783
+ }
1784
+ }
1785
+ }
1786
+ formatConversationAsMarkdown(history) {
1787
+ const lines = [];
1788
+ const timestamp = new Date().toISOString().split('T')[0];
1789
+ lines.push(`# Conversation Export`);
1790
+ lines.push('');
1791
+ lines.push(`**Model:** ${this.model}`);
1792
+ lines.push(`**Date:** ${timestamp}`);
1793
+ lines.push(`**Messages:** ${String(history.length)}`);
1794
+ lines.push('');
1795
+ lines.push('---');
1796
+ lines.push('');
1797
+ for (const msg of history) {
1798
+ const role = msg.role === 'user' ? '👤 User' : '🤖 Assistant';
1799
+ lines.push(`## ${role}`);
1800
+ lines.push('');
1801
+ if (typeof msg.content === 'string') {
1802
+ lines.push(msg.content);
1803
+ }
1804
+ else if (Array.isArray(msg.content)) {
1805
+ // Handle content blocks (tool calls, etc.)
1806
+ for (const block of msg.content) {
1807
+ if (typeof block === 'object' && block !== null) {
1808
+ const b = block;
1809
+ if (b.type === 'text' && typeof b.text === 'string') {
1810
+ lines.push(b.text);
1811
+ }
1812
+ else if (b.type === 'tool_use') {
1813
+ const toolName = typeof b.name === 'string' ? b.name : String(b.name);
1814
+ lines.push(`**Tool:** \`${toolName}\``);
1815
+ lines.push('```json');
1816
+ lines.push(JSON.stringify(b.input, null, 2));
1817
+ lines.push('```');
1818
+ }
1819
+ else if (b.type === 'tool_result') {
1820
+ lines.push(`**Tool Result:**`);
1821
+ const content = typeof b.content === 'string' ? b.content : JSON.stringify(b.content);
1822
+ if (content.length > 500) {
1823
+ lines.push('```');
1824
+ lines.push(content.slice(0, 500) + '...[truncated]');
1825
+ lines.push('```');
1826
+ }
1827
+ else {
1828
+ lines.push('```');
1829
+ lines.push(content);
1830
+ lines.push('```');
1831
+ }
1832
+ }
1833
+ }
1834
+ }
1835
+ }
1836
+ else {
1837
+ lines.push(JSON.stringify(msg.content, null, 2));
1838
+ }
1839
+ lines.push('');
1840
+ lines.push('---');
1841
+ lines.push('');
1842
+ }
1843
+ return lines.join('\n');
1844
+ }
1845
+ async copyToClipboard(text) {
1846
+ const platform = process.platform;
1847
+ let command;
1848
+ if (platform === 'darwin') {
1849
+ command = 'pbcopy';
1850
+ }
1851
+ else if (platform === 'linux') {
1852
+ // Try xclip first, fall back to xsel
1853
+ command = 'xclip -selection clipboard';
1854
+ }
1855
+ else if (platform === 'win32') {
1856
+ command = 'clip';
1857
+ }
1858
+ else {
1859
+ throw new Error(`Unsupported platform: ${platform}`);
1860
+ }
1861
+ const child = exec(command);
1862
+ if (child.stdin) {
1863
+ child.stdin.write(text);
1864
+ child.stdin.end();
1865
+ }
1866
+ await new Promise((resolve, reject) => {
1867
+ child.on('exit', (code) => {
1868
+ if (code === 0) {
1869
+ resolve();
1870
+ }
1871
+ else {
1872
+ reject(new Error(`Clipboard command exited with code ${String(code ?? 'unknown')}`));
1873
+ }
1874
+ });
1875
+ child.on('error', reject);
1876
+ });
1877
+ }
1878
+ }