@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,977 @@
1
+ /**
2
+ * Config Overlay
3
+ *
4
+ * Claude Code-style settings overlay with 3 tabs:
5
+ * - Status: Read-only info (version, model, provider, cwd, session)
6
+ * - Config: Interactive settings (toggle/cycle with Enter/Space)
7
+ * - Usage: Usage statistics with progress bars (mocked)
8
+ *
9
+ * Includes theme selector sub-screen with 418 themes.
10
+ */
11
+ import chalk from 'chalk';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as terminal from './terminal.js';
15
+ import { getCuratedThemes, getCurrentTheme, setCurrentTheme, searchAndFilterThemes, getThemeCount, clearStyleCache, getStyles, } from '../themes/index.js';
16
+ import { getSettings, setSetting, permissionModeToDisplay, displayToPermissionMode, notificationModeToDisplay, displayToNotificationMode, } from '../settings/index.js';
17
+ import { hasApiKey, settingsProviderToCredentialKey } from '../utils/credentials.js';
18
+ // =============================================================================
19
+ // Constants
20
+ // =============================================================================
21
+ const VERSION = '2.0.57';
22
+ const THEME_PAGE_SIZE = 12;
23
+ const TABS = [
24
+ { id: 'status', label: 'Status' },
25
+ { id: 'config', label: 'Config' },
26
+ { id: 'usage', label: 'Usage' },
27
+ ];
28
+ // Provider tabs for model selector
29
+ const MODEL_PROVIDER_TABS = [
30
+ { id: 'claude', label: 'Claude' },
31
+ { id: 'openai', label: 'OpenAI' },
32
+ { id: 'gemini', label: 'Gemini' },
33
+ { id: 'ollama', label: 'Ollama' },
34
+ ];
35
+ const MODEL_OPTIONS = [
36
+ // Claude
37
+ { id: 'claude-sonnet-4-20250514', name: 'Sonnet 4', description: 'Best for everyday tasks', provider: 'claude' },
38
+ { id: 'claude-opus-4-20250514', name: 'Opus 4', description: 'Most capable for complex work', provider: 'claude' },
39
+ { id: 'claude-3-5-haiku-20241022', name: 'Haiku 3.5', description: 'Fastest for quick answers', provider: 'claude' },
40
+ // OpenAI
41
+ { id: 'gpt-4o', name: 'GPT-4o', description: 'Best overall', provider: 'openai' },
42
+ { id: 'gpt-4o-mini', name: 'GPT-4o Mini', description: 'Fast and affordable', provider: 'openai' },
43
+ // Gemini (1.5 models retired April 2025, use 2.0+ only)
44
+ { id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', description: 'Fast multimodal', provider: 'gemini' },
45
+ { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', description: 'Latest stable flash', provider: 'gemini' },
46
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', description: 'Most capable Gemini', provider: 'gemini' },
47
+ // Ollama
48
+ { id: 'llama3.2', name: 'Llama 3.2', description: 'Local model', provider: 'ollama' },
49
+ { id: 'mistral', name: 'Mistral', description: 'Local model', provider: 'ollama' },
50
+ { id: 'codellama', name: 'Code Llama', description: 'Code-focused local model', provider: 'ollama' },
51
+ ];
52
+ function getModelsForProvider(providerId) {
53
+ return MODEL_OPTIONS.filter(m => m.provider === providerId);
54
+ }
55
+ // =============================================================================
56
+ // Config Items (loaded from settings)
57
+ // =============================================================================
58
+ function getConfigItems(currentModel) {
59
+ const currentTheme = getCurrentTheme();
60
+ const settings = getSettings();
61
+ // Find display name for current model
62
+ const modelOption = MODEL_OPTIONS.find(m => m.id === currentModel);
63
+ const modelDisplay = modelOption ? modelOption.name : currentModel || 'Default';
64
+ return [
65
+ { id: 'autoCompact', label: 'Auto-compact', type: 'boolean', value: settings.autoCompact },
66
+ { id: 'showTips', label: 'Show tips', type: 'boolean', value: settings.showTips },
67
+ { id: 'reviseCode', label: 'Revise code (checkpoints)', type: 'boolean', value: settings.reviseCode },
68
+ { id: 'verbose', label: 'Verbose output', type: 'boolean', value: settings.verbose },
69
+ { id: 'progressBar', label: 'Terminal progress bar', type: 'boolean', value: settings.progressBar },
70
+ {
71
+ id: 'permissionMode',
72
+ label: 'Default permission mode',
73
+ type: 'cycle',
74
+ value: permissionModeToDisplay(settings.permissionMode),
75
+ options: ['Default', 'Plan', 'Bypass prompts'],
76
+ },
77
+ { id: 'gitignore', label: 'Respect .gitignore in file picker', type: 'boolean', value: settings.gitignore },
78
+ { id: 'theme', label: 'Theme', type: 'submenu', value: currentTheme.name },
79
+ { id: 'model', label: 'Model', type: 'submenu', value: modelDisplay },
80
+ {
81
+ id: 'notifications',
82
+ label: 'Notifications',
83
+ type: 'cycle',
84
+ value: notificationModeToDisplay(settings.notifications),
85
+ options: ['Auto', 'Enabled', 'Disabled'],
86
+ },
87
+ {
88
+ id: 'outputStyle',
89
+ label: 'Output style',
90
+ type: 'cycle',
91
+ value: settings.outputStyle,
92
+ options: ['default', 'compact', 'verbose'],
93
+ },
94
+ {
95
+ id: 'editorMode',
96
+ label: 'Editor mode',
97
+ type: 'cycle',
98
+ value: settings.editorMode,
99
+ options: ['normal', 'vim', 'emacs'],
100
+ },
101
+ { id: 'autoInstall', label: 'Auto-install IDE extension', type: 'boolean', value: settings.autoInstall },
102
+ ];
103
+ }
104
+ // Unused helper function - removed to fix lint error
105
+ // function getDefaultUsageStats(): UsageStats {
106
+ // return {
107
+ // inputTokens: 0,
108
+ // outputTokens: 0,
109
+ // requests: 0,
110
+ // contextUsed: 0,
111
+ // contextMax: 200000,
112
+ // messageCount: 0,
113
+ // };
114
+ // }
115
+ // =============================================================================
116
+ // Rendering - Header
117
+ // =============================================================================
118
+ function renderHeader(state) {
119
+ const s = getStyles();
120
+ const lines = [];
121
+ const cols = terminal.getTerminalWidth();
122
+ lines.push(s.muted('─'.repeat(Math.max(1, cols - 1))));
123
+ // Tab row
124
+ let tabLine = ' Settings: ';
125
+ for (let i = 0; i < TABS.length; i++) {
126
+ const tab = TABS[i];
127
+ if (i === state.currentTab) {
128
+ tabLine += s.selected(` ${tab.label} `) + ' ';
129
+ }
130
+ else {
131
+ tabLine += s.muted(` ${tab.label} `) + ' ';
132
+ }
133
+ }
134
+ tabLine += s.muted('(tab to cycle)');
135
+ lines.push(tabLine);
136
+ lines.push('');
137
+ return lines;
138
+ }
139
+ // =============================================================================
140
+ // Rendering - Status Tab
141
+ // =============================================================================
142
+ function renderStatusTab(state) {
143
+ const s = getStyles();
144
+ const lines = [];
145
+ const info = state.statusInfo;
146
+ // Calculate uptime
147
+ const uptimeMs = Date.now() - info.startTime.getTime();
148
+ const uptimeSecs = Math.floor(uptimeMs / 1000);
149
+ const uptimeMins = Math.floor(uptimeSecs / 60);
150
+ const uptimeStr = uptimeMins > 0
151
+ ? `${String(uptimeMins)}m ${String(uptimeSecs % 60)}s`
152
+ : `${String(uptimeSecs)}s`;
153
+ lines.push(` Version: ${s.primary(info.version)}`);
154
+ lines.push(` Session: ${s.muted(info.sessionId)}`);
155
+ lines.push(` Uptime: ${s.muted(uptimeStr)}`);
156
+ lines.push('');
157
+ lines.push(` Provider: ${s.primary(info.provider)}`);
158
+ lines.push(` Model: ${s.primary(info.model)}`);
159
+ lines.push(` Tools: ${s.muted(String(info.toolCount))}`);
160
+ lines.push('');
161
+ lines.push(` Cwd: ${s.muted(info.cwd)}`);
162
+ lines.push(` Memory: ${s.muted(info.memoryFile || 'none')}`);
163
+ lines.push(` Settings: ${s.muted('~/.compilr-dev/settings.json')}`);
164
+ return lines;
165
+ }
166
+ // =============================================================================
167
+ // Rendering - Config Tab
168
+ // =============================================================================
169
+ function renderConfigTab(state) {
170
+ const s = getStyles();
171
+ const lines = [];
172
+ lines.push(chalk.bold(' Configure Claude Code preferences'));
173
+ lines.push('');
174
+ for (let i = 0; i < state.configItems.length; i++) {
175
+ const item = state.configItems[i];
176
+ const isSelected = i === state.selectedItem;
177
+ const prefix = isSelected ? s.primary(' ❯ ') : ' ';
178
+ let valueStr;
179
+ if (item.type === 'boolean') {
180
+ valueStr = item.value ? s.success('true') : s.muted('false');
181
+ }
182
+ else if (item.type === 'submenu') {
183
+ valueStr = s.primary(String(item.value));
184
+ }
185
+ else {
186
+ valueStr = s.primary(String(item.value));
187
+ }
188
+ const labelStyled = isSelected
189
+ ? s.primary(item.label.padEnd(35))
190
+ : item.label.padEnd(35);
191
+ lines.push(prefix + labelStyled + valueStr);
192
+ }
193
+ return lines;
194
+ }
195
+ // =============================================================================
196
+ // Rendering - Usage Tab
197
+ // =============================================================================
198
+ function renderUsageTab(state) {
199
+ const s = getStyles();
200
+ const lines = [];
201
+ const stats = state.usageStats;
202
+ // Format numbers with K suffix
203
+ const formatNum = (n) => {
204
+ if (n >= 1000)
205
+ return `${(n / 1000).toFixed(1)}k`;
206
+ return String(n);
207
+ };
208
+ lines.push(chalk.bold(' Session Tokens'));
209
+ lines.push(` Input: ${s.primary(formatNum(stats.inputTokens).padStart(8))}`);
210
+ lines.push(` Output: ${s.primary(formatNum(stats.outputTokens).padStart(8))}`);
211
+ lines.push(` Total: ${s.primary(formatNum(stats.inputTokens + stats.outputTokens).padStart(8))}`);
212
+ lines.push('');
213
+ lines.push(chalk.bold(' Context Window'));
214
+ const contextPct = stats.contextMax > 0 ? stats.contextUsed / stats.contextMax : 0;
215
+ const contextPctDisplay = Math.round(contextPct * 100);
216
+ lines.push(` ${renderProgressBar(contextPct, 30)} ${String(contextPctDisplay)}% used`);
217
+ lines.push(s.muted(` ${formatNum(Math.round(stats.contextUsed))} / ${formatNum(stats.contextMax)} tokens`));
218
+ lines.push('');
219
+ lines.push(chalk.bold(' Session Activity'));
220
+ lines.push(` Requests: ${s.primary(String(stats.requests))}`);
221
+ lines.push(` Messages: ${s.primary(String(stats.messageCount))}`);
222
+ return lines;
223
+ }
224
+ function renderProgressBar(value, width) {
225
+ const s = getStyles();
226
+ const filled = Math.round(value * width);
227
+ const empty = width - filled;
228
+ let color = s.success;
229
+ if (value > 0.8)
230
+ color = s.error;
231
+ else if (value > 0.6)
232
+ color = s.warning;
233
+ return color('█'.repeat(filled)) + s.muted('░'.repeat(empty));
234
+ }
235
+ // =============================================================================
236
+ // Rendering - Footer
237
+ // =============================================================================
238
+ function renderFooter(state) {
239
+ const s = getStyles();
240
+ const lines = [];
241
+ lines.push('');
242
+ if (state.currentTab === 1) {
243
+ // Config tab
244
+ lines.push(s.muted(' Enter/Space to change · Esc to exit'));
245
+ }
246
+ else {
247
+ lines.push(s.muted(' Esc to exit'));
248
+ }
249
+ return lines;
250
+ }
251
+ // =============================================================================
252
+ // Rendering - Theme Selector (Curated)
253
+ // =============================================================================
254
+ function buildThemeCuratedLines(state) {
255
+ const s = getStyles();
256
+ const lines = [];
257
+ const cols = terminal.getTerminalWidth();
258
+ const currentTheme = getCurrentTheme();
259
+ const counts = getThemeCount();
260
+ lines.push(s.muted('─'.repeat(Math.max(1, cols - 1))));
261
+ lines.push('');
262
+ lines.push(chalk.bold(' Theme'));
263
+ lines.push('');
264
+ lines.push(' Choose a theme:');
265
+ lines.push('');
266
+ // Show curated themes
267
+ for (let i = 0; i < state.curatedThemes.length; i++) {
268
+ const theme = state.curatedThemes[i];
269
+ const isSelected = i === state.themeSelectedIndex;
270
+ const isCurrent = theme.id === currentTheme.id;
271
+ const prefix = isSelected ? s.primary(' ❯ ') : ' ';
272
+ const num = `${(i + 1).toString().padStart(2)}. `;
273
+ const icon = theme.type === 'dark' ? '🌙' : '☀️';
274
+ const checkmark = isCurrent ? s.success(' ✓') : '';
275
+ const label = isSelected ? s.primary(theme.name) : theme.name;
276
+ lines.push(prefix + num + label.padEnd(30) + icon + checkmark);
277
+ }
278
+ // Separator
279
+ lines.push(s.muted(' ────────────────────────────────────'));
280
+ // Browse all option
281
+ const browseIndex = state.curatedThemes.length;
282
+ const isBrowseSelected = state.themeSelectedIndex === browseIndex;
283
+ const browsePrefix = isBrowseSelected ? s.primary(' ❯ ') : ' ';
284
+ const browseLabel = isBrowseSelected
285
+ ? s.primary(`Browse all ${String(counts.total)} themes...`)
286
+ : `Browse all ${String(counts.total)} themes...`;
287
+ lines.push(browsePrefix + browseLabel + ' →');
288
+ lines.push('');
289
+ // Preview with theme colors
290
+ const previewTheme = state.themeSelectedIndex < state.curatedThemes.length
291
+ ? state.curatedThemes[state.themeSelectedIndex]
292
+ : currentTheme;
293
+ lines.push(...renderThemePreview(previewTheme));
294
+ lines.push('');
295
+ lines.push(s.muted(' ↑↓ Navigate · 1-9,0 Quick select · Enter Select · Esc Back'));
296
+ return lines;
297
+ }
298
+ // =============================================================================
299
+ // Rendering - Theme Selector (Browse All)
300
+ // =============================================================================
301
+ function buildThemeBrowseLines(state) {
302
+ const s = getStyles();
303
+ const lines = [];
304
+ const cols = terminal.getTerminalWidth();
305
+ const currentTheme = getCurrentTheme();
306
+ const counts = getThemeCount();
307
+ lines.push(s.muted('─'.repeat(Math.max(1, cols - 1))));
308
+ lines.push('');
309
+ lines.push(chalk.bold(` All Themes (${String(counts.total)})`));
310
+ lines.push('');
311
+ // Search box
312
+ const searchBox = ` Search: ${state.browseSearchQuery}█`;
313
+ lines.push(searchBox);
314
+ // Filter tabs
315
+ const filterAll = state.browseFilterType === 'all' ? s.selected(' All ') : s.muted(' All ');
316
+ const filterDark = state.browseFilterType === 'dark' ? s.selected(' Dark ') : s.muted(' Dark ');
317
+ const filterLight = state.browseFilterType === 'light' ? s.selected(' Light ') : s.muted(' Light ');
318
+ lines.push(` Filter: ${filterAll} ${filterDark} ${filterLight} ${s.muted('(Tab to switch)')}`);
319
+ lines.push('');
320
+ // Results
321
+ const totalResults = state.browseResults.length;
322
+ const totalPages = Math.ceil(totalResults / THEME_PAGE_SIZE);
323
+ const startIndex = state.browseCurrentPage * THEME_PAGE_SIZE;
324
+ const endIndex = Math.min(startIndex + THEME_PAGE_SIZE, totalResults);
325
+ const pageResults = state.browseResults.slice(startIndex, endIndex);
326
+ if (totalResults === 0) {
327
+ lines.push(s.muted(' No themes found'));
328
+ }
329
+ else {
330
+ lines.push(s.muted(` Showing ${String(startIndex + 1)}-${String(endIndex)} of ${String(totalResults)}:`));
331
+ lines.push('');
332
+ for (let i = 0; i < pageResults.length; i++) {
333
+ const theme = pageResults[i];
334
+ const isSelected = i === state.browseSelectedIndex;
335
+ const isCurrent = theme.id === currentTheme.id;
336
+ const prefix = isSelected ? s.primary(' ❯ ') : ' ';
337
+ const icon = theme.type === 'dark' ? '🌙' : '☀️';
338
+ const checkmark = isCurrent ? s.success(' ✓') : '';
339
+ const label = isSelected ? s.primary(theme.name) : theme.name;
340
+ lines.push(prefix + label.padEnd(35) + icon + checkmark);
341
+ }
342
+ }
343
+ lines.push('');
344
+ // Pagination
345
+ if (totalPages > 1) {
346
+ const pageInfo = `Page ${String(state.browseCurrentPage + 1)} of ${String(totalPages)}`;
347
+ const prevHint = state.browseCurrentPage > 0 ? '← ' : ' ';
348
+ const nextHint = state.browseCurrentPage < totalPages - 1 ? ' →' : '';
349
+ lines.push(s.muted(` ${prevHint}${pageInfo}${nextHint}`));
350
+ }
351
+ // Separator
352
+ lines.push(s.muted(' ────────────────────────────────────'));
353
+ // Back option
354
+ lines.push(s.muted(' ← Back to curated themes'));
355
+ lines.push('');
356
+ // Preview
357
+ if (pageResults.length > 0 && state.browseSelectedIndex < pageResults.length) {
358
+ lines.push(...renderThemePreview(pageResults[state.browseSelectedIndex]));
359
+ }
360
+ lines.push('');
361
+ lines.push(s.muted(' Type to search · ↑↓ Navigate · ←→ Pages · Tab Filter · Enter Select · Esc Back'));
362
+ return lines;
363
+ }
364
+ // =============================================================================
365
+ // Rendering - Theme Preview
366
+ // =============================================================================
367
+ function renderThemePreview(theme) {
368
+ const s = getStyles();
369
+ const lines = [];
370
+ const colors = theme.colors;
371
+ lines.push(s.muted(' Preview:'));
372
+ // Use chalk for hex colors in preview
373
+ const primary = chalk.hex(colors.primary);
374
+ const secondary = chalk.hex(colors.secondary || colors.primary);
375
+ const muted = chalk.hex(colors.muted);
376
+ const fg = chalk.hex(colors.foreground);
377
+ lines.push(' ' + primary.bold('function') + ' ' + secondary('greet') + fg('() {'));
378
+ lines.push(' ' + muted('// Say hello'));
379
+ lines.push(' ' + fg('console.') + secondary('log') + fg('("Hello, ') + primary('World') + fg('!");'));
380
+ lines.push(' ' + fg('}'));
381
+ return lines;
382
+ }
383
+ // =============================================================================
384
+ // Rendering - Model Selector
385
+ // =============================================================================
386
+ function buildModelSelectorLines(state) {
387
+ const s = getStyles();
388
+ const lines = [];
389
+ const cols = terminal.getTerminalWidth();
390
+ // Header with separator
391
+ lines.push(s.muted('─'.repeat(Math.max(1, cols - 1))));
392
+ // Tab row (similar to main config tabs)
393
+ let tabLine = ' Model: ';
394
+ for (let i = 0; i < MODEL_PROVIDER_TABS.length; i++) {
395
+ const tab = MODEL_PROVIDER_TABS[i];
396
+ const isCurrentProvider = tab.id === state.currentProvider;
397
+ const indicator = isCurrentProvider ? ' ✓' : '';
398
+ if (i === state.modelProviderTab) {
399
+ tabLine += s.selected(` ${tab.label}${indicator} `) + ' ';
400
+ }
401
+ else {
402
+ tabLine += s.muted(` ${tab.label}${indicator} `) + ' ';
403
+ }
404
+ }
405
+ tabLine += s.muted('(tab to switch)');
406
+ lines.push(tabLine);
407
+ lines.push('');
408
+ // Current provider info
409
+ const selectedProviderTab = MODEL_PROVIDER_TABS[state.modelProviderTab];
410
+ const isHotSwappable = selectedProviderTab.id === state.currentProvider;
411
+ if (isHotSwappable) {
412
+ lines.push(s.success(' Current provider - changes apply immediately'));
413
+ }
414
+ else {
415
+ lines.push(s.warning(' Different provider - restart required to apply'));
416
+ }
417
+ // Check if API key is configured for selected provider
418
+ if (selectedProviderTab.id !== 'ollama') {
419
+ const credKey = settingsProviderToCredentialKey(selectedProviderTab.id);
420
+ if (!hasApiKey(credKey)) {
421
+ lines.push(s.error(' ⚠ No API key configured - use /keys to set it'));
422
+ }
423
+ }
424
+ lines.push('');
425
+ // Models for selected provider
426
+ const models = getModelsForProvider(selectedProviderTab.id);
427
+ for (let i = 0; i < models.length; i++) {
428
+ const model = models[i];
429
+ const isSelected = i === state.modelSelectedIndex;
430
+ const isCurrentModel = model.id === state.statusInfo.model;
431
+ const prefix = isSelected ? s.primary(' ❯ ') : ' ';
432
+ const checkmark = isCurrentModel ? s.success(' ✓') : '';
433
+ const label = isSelected ? s.primary(model.name) : model.name;
434
+ const desc = s.muted(` · ${model.description}`);
435
+ lines.push(prefix + label.padEnd(20) + desc + checkmark);
436
+ }
437
+ lines.push('');
438
+ // Footer
439
+ lines.push(s.muted(' Tab Provider · ↑↓ Model · Enter Select · Esc Back'));
440
+ return lines;
441
+ }
442
+ // =============================================================================
443
+ // Unified Rendering
444
+ // =============================================================================
445
+ // Target height for consistent screen sizes (model selector is tallest at ~25 lines)
446
+ const TARGET_HEIGHT = 25;
447
+ function padToHeight(lines, targetHeight) {
448
+ while (lines.length < targetHeight) {
449
+ lines.push('');
450
+ }
451
+ return lines;
452
+ }
453
+ function buildLines(state) {
454
+ if (state.mode === 'theme-curated') {
455
+ return padToHeight(buildThemeCuratedLines(state), TARGET_HEIGHT);
456
+ }
457
+ if (state.mode === 'theme-browse') {
458
+ return padToHeight(buildThemeBrowseLines(state), TARGET_HEIGHT);
459
+ }
460
+ if (state.mode === 'model-selector') {
461
+ return padToHeight(buildModelSelectorLines(state), TARGET_HEIGHT);
462
+ }
463
+ const allLines = [];
464
+ allLines.push(...renderHeader(state));
465
+ switch (TABS[state.currentTab].id) {
466
+ case 'status':
467
+ allLines.push(...renderStatusTab(state));
468
+ break;
469
+ case 'config':
470
+ allLines.push(...renderConfigTab(state));
471
+ break;
472
+ case 'usage':
473
+ allLines.push(...renderUsageTab(state));
474
+ break;
475
+ }
476
+ allLines.push(...renderFooter(state));
477
+ return padToHeight(allLines, TARGET_HEIGHT);
478
+ }
479
+ function render(state, prevLineCount) {
480
+ const lines = buildLines(state);
481
+ // Clear previous content
482
+ terminal.clearLinesAbove(prevLineCount);
483
+ // Write new content
484
+ terminal.write(lines.join('\n'));
485
+ return lines.length;
486
+ }
487
+ // =============================================================================
488
+ // Config Item Actions
489
+ // =============================================================================
490
+ function toggleOrCycleItem(state) {
491
+ const item = state.configItems[state.selectedItem];
492
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
493
+ if (!item)
494
+ return;
495
+ if (item.type === 'boolean') {
496
+ item.value = !item.value;
497
+ // Persist boolean settings - id is always a valid settings key
498
+ switch (item.id) {
499
+ case 'autoCompact':
500
+ setSetting('autoCompact', item.value);
501
+ break;
502
+ case 'showTips':
503
+ setSetting('showTips', item.value);
504
+ break;
505
+ case 'reviseCode':
506
+ setSetting('reviseCode', item.value);
507
+ break;
508
+ case 'verbose':
509
+ setSetting('verbose', item.value);
510
+ break;
511
+ case 'progressBar':
512
+ setSetting('progressBar', item.value);
513
+ break;
514
+ case 'gitignore':
515
+ setSetting('gitignore', item.value);
516
+ break;
517
+ case 'autoInstall':
518
+ setSetting('autoInstall', item.value);
519
+ break;
520
+ }
521
+ }
522
+ else if (item.type === 'cycle' && item.options) {
523
+ const currentIndex = item.options.indexOf(String(item.value));
524
+ const nextIndex = (currentIndex + 1) % item.options.length;
525
+ item.value = item.options[nextIndex];
526
+ // Persist cycle settings
527
+ switch (item.id) {
528
+ case 'permissionMode':
529
+ setSetting('permissionMode', displayToPermissionMode(item.value));
530
+ break;
531
+ case 'notifications':
532
+ setSetting('notifications', displayToNotificationMode(item.value));
533
+ break;
534
+ case 'outputStyle':
535
+ setSetting('outputStyle', item.value);
536
+ break;
537
+ case 'editorMode':
538
+ setSetting('editorMode', item.value);
539
+ break;
540
+ // 'model' is not persisted (would need provider integration)
541
+ }
542
+ }
543
+ else if (item.type === 'submenu' && item.id === 'theme') {
544
+ // Enter theme selector (curated mode)
545
+ state.mode = 'theme-curated';
546
+ state.themeSelectedIndex = 0;
547
+ // Find current theme in curated list
548
+ const currentTheme = getCurrentTheme();
549
+ const curatedIndex = state.curatedThemes.findIndex((t) => t.id === currentTheme.id);
550
+ if (curatedIndex >= 0) {
551
+ state.themeSelectedIndex = curatedIndex;
552
+ }
553
+ }
554
+ else if (item.type === 'submenu' && item.id === 'model') {
555
+ // Enter model selector
556
+ state.mode = 'model-selector';
557
+ state.modelSelectedIndex = 0;
558
+ state.modelProviderTab = 0;
559
+ // Find current model and set provider tab accordingly
560
+ const currentModel = MODEL_OPTIONS.find((m) => m.id === state.statusInfo.model);
561
+ if (currentModel) {
562
+ const providerTabIndex = MODEL_PROVIDER_TABS.findIndex((p) => p.id === currentModel.provider);
563
+ if (providerTabIndex >= 0) {
564
+ state.modelProviderTab = providerTabIndex;
565
+ }
566
+ // Find model index within that provider's models
567
+ const providerModels = getModelsForProvider(currentModel.provider);
568
+ const modelIndex = providerModels.findIndex((m) => m.id === currentModel.id);
569
+ if (modelIndex >= 0) {
570
+ state.modelSelectedIndex = modelIndex;
571
+ }
572
+ }
573
+ }
574
+ }
575
+ // =============================================================================
576
+ // Theme Selection Actions
577
+ // =============================================================================
578
+ function selectTheme(state, theme) {
579
+ setCurrentTheme(theme.id);
580
+ clearStyleCache();
581
+ // Update the config item value
582
+ const themeItem = state.configItems.find((item) => item.id === 'theme');
583
+ if (themeItem) {
584
+ themeItem.value = theme.name;
585
+ }
586
+ }
587
+ function updateBrowseResults(state) {
588
+ state.browseResults = searchAndFilterThemes(state.browseSearchQuery, state.browseFilterType);
589
+ state.browseSelectedIndex = 0;
590
+ state.browseCurrentPage = 0;
591
+ }
592
+ /**
593
+ * Show the config overlay
594
+ */
595
+ export async function showConfigOverlay(options = {}) {
596
+ const curatedThemes = getCuratedThemes();
597
+ const result = {};
598
+ const state = {
599
+ mode: options.initialMode || 'tabs',
600
+ currentTab: 1, // Start on Config tab (like Claude Code)
601
+ selectedItem: 0,
602
+ scrollOffset: 0,
603
+ // Theme curated state
604
+ themeSelectedIndex: 0,
605
+ curatedThemes,
606
+ // Theme browse state
607
+ browseSearchQuery: '',
608
+ browseFilterType: 'all',
609
+ browseCurrentPage: 0,
610
+ browseSelectedIndex: 0,
611
+ browseResults: [],
612
+ // Model selector state
613
+ modelSelectedIndex: 0,
614
+ modelProviderTab: 0,
615
+ currentProvider: options.provider || 'unknown',
616
+ configItems: getConfigItems(options.model),
617
+ statusInfo: {
618
+ version: options.version || VERSION,
619
+ sessionId: options.sessionId || generateSessionId(),
620
+ cwd: options.cwd || process.cwd(),
621
+ model: options.model || 'unknown',
622
+ provider: options.provider || 'unknown',
623
+ toolCount: options.toolCount ?? 0,
624
+ startTime: options.startTime || new Date(),
625
+ memoryFile: findMemoryFile(options.cwd),
626
+ },
627
+ usageStats: {
628
+ inputTokens: options.inputTokens ?? 0,
629
+ outputTokens: options.outputTokens ?? 0,
630
+ requests: options.requests ?? 0,
631
+ contextUsed: options.contextUsed ?? 0,
632
+ contextMax: options.contextMax ?? 200000,
633
+ messageCount: options.messageCount ?? 0,
634
+ },
635
+ onModelChange: options.onModelChange,
636
+ };
637
+ // If starting in model-selector mode, initialize the provider tab and model index
638
+ if (options.initialMode === 'model-selector') {
639
+ // Find current model and set provider tab accordingly
640
+ const currentModel = MODEL_OPTIONS.find((m) => m.id === state.statusInfo.model);
641
+ if (currentModel) {
642
+ const providerTabIndex = MODEL_PROVIDER_TABS.findIndex((p) => p.id === currentModel.provider);
643
+ if (providerTabIndex >= 0) {
644
+ state.modelProviderTab = providerTabIndex;
645
+ }
646
+ // Find model index within that provider's models
647
+ const providerModels = getModelsForProvider(currentModel.provider);
648
+ const modelIndex = providerModels.findIndex((m) => m.id === currentModel.id);
649
+ if (modelIndex >= 0) {
650
+ state.modelSelectedIndex = modelIndex;
651
+ }
652
+ }
653
+ }
654
+ let lineCount = 0;
655
+ terminal.writeLine('');
656
+ terminal.hideCursor();
657
+ const wasRawMode = process.stdin.isRaw;
658
+ terminal.enableRawMode();
659
+ lineCount = render(state, 0);
660
+ return new Promise((resolve) => {
661
+ const cleanup = () => {
662
+ terminal.clearLinesAbove(lineCount);
663
+ terminal.writeLine('');
664
+ terminal.showCursor();
665
+ if (!wasRawMode) {
666
+ terminal.disableRawMode();
667
+ }
668
+ process.stdin.removeListener('data', onData);
669
+ };
670
+ const onData = (data) => {
671
+ const isEscape = data.length === 1 && data[0] === 0x1b;
672
+ const isTab = data.length === 1 && data[0] === 0x09;
673
+ const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
674
+ const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
675
+ const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
676
+ const isRightArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x43;
677
+ const isCtrlC = data.length === 1 && data[0] === 0x03;
678
+ const isEnter = data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
679
+ const isSpace = data.length === 1 && data[0] === 0x20;
680
+ const isBackspace = data.length === 1 && (data[0] === 0x7f || data[0] === 0x08);
681
+ const char = data.toString();
682
+ const isNumberKey = /^[0-9]$/.test(char);
683
+ const isPrintable = data.length === 1 && data[0] >= 32 && data[0] < 127;
684
+ // ===== TABS MODE =====
685
+ if (state.mode === 'tabs') {
686
+ if (isEscape || isCtrlC) {
687
+ cleanup();
688
+ resolve(result);
689
+ return;
690
+ }
691
+ if (isTab) {
692
+ state.currentTab = (state.currentTab + 1) % TABS.length;
693
+ state.selectedItem = 0;
694
+ lineCount = render(state, lineCount);
695
+ return;
696
+ }
697
+ // Navigation only in Config tab
698
+ if (TABS[state.currentTab].id === 'config') {
699
+ if (isUpArrow && state.selectedItem > 0) {
700
+ state.selectedItem--;
701
+ lineCount = render(state, lineCount);
702
+ return;
703
+ }
704
+ if (isDownArrow && state.selectedItem < state.configItems.length - 1) {
705
+ state.selectedItem++;
706
+ lineCount = render(state, lineCount);
707
+ return;
708
+ }
709
+ if (isEnter || isSpace) {
710
+ toggleOrCycleItem(state);
711
+ lineCount = render(state, lineCount);
712
+ return;
713
+ }
714
+ }
715
+ return;
716
+ }
717
+ // ===== THEME CURATED MODE =====
718
+ if (state.mode === 'theme-curated') {
719
+ const maxIndex = state.curatedThemes.length; // includes "Browse all" option
720
+ if (isCtrlC) {
721
+ cleanup();
722
+ resolve(result);
723
+ return;
724
+ }
725
+ if (isEscape) {
726
+ state.mode = 'tabs';
727
+ lineCount = render(state, lineCount);
728
+ return;
729
+ }
730
+ if (isUpArrow && state.themeSelectedIndex > 0) {
731
+ state.themeSelectedIndex--;
732
+ lineCount = render(state, lineCount);
733
+ return;
734
+ }
735
+ if (isDownArrow && state.themeSelectedIndex < maxIndex) {
736
+ state.themeSelectedIndex++;
737
+ lineCount = render(state, lineCount);
738
+ return;
739
+ }
740
+ if (isEnter) {
741
+ if (state.themeSelectedIndex === state.curatedThemes.length) {
742
+ // "Browse all" selected
743
+ state.mode = 'theme-browse';
744
+ state.browseSearchQuery = '';
745
+ state.browseFilterType = 'all';
746
+ updateBrowseResults(state);
747
+ lineCount = render(state, lineCount);
748
+ }
749
+ else {
750
+ // Theme selected
751
+ const theme = state.curatedThemes[state.themeSelectedIndex];
752
+ selectTheme(state, theme);
753
+ state.mode = 'tabs';
754
+ lineCount = render(state, lineCount);
755
+ }
756
+ return;
757
+ }
758
+ // Quick select with number keys (1-9 = themes 1-9, 0 = theme 10)
759
+ if (isNumberKey) {
760
+ const num = char === '0' ? 10 : parseInt(char, 10);
761
+ if (num >= 1 && num <= state.curatedThemes.length) {
762
+ const theme = state.curatedThemes[num - 1];
763
+ selectTheme(state, theme);
764
+ state.mode = 'tabs';
765
+ lineCount = render(state, lineCount);
766
+ }
767
+ return;
768
+ }
769
+ return;
770
+ }
771
+ // ===== THEME BROWSE MODE =====
772
+ if (state.mode === 'theme-browse') {
773
+ const totalPages = Math.ceil(state.browseResults.length / THEME_PAGE_SIZE);
774
+ const pageResults = state.browseResults.slice(state.browseCurrentPage * THEME_PAGE_SIZE, (state.browseCurrentPage + 1) * THEME_PAGE_SIZE);
775
+ if (isCtrlC) {
776
+ cleanup();
777
+ resolve(result);
778
+ return;
779
+ }
780
+ if (isEscape) {
781
+ // Back to curated
782
+ state.mode = 'theme-curated';
783
+ lineCount = render(state, lineCount);
784
+ return;
785
+ }
786
+ // Tab cycles filter
787
+ if (isTab) {
788
+ const filters = ['all', 'dark', 'light'];
789
+ const currentIdx = filters.indexOf(state.browseFilterType);
790
+ state.browseFilterType = filters[(currentIdx + 1) % filters.length];
791
+ updateBrowseResults(state);
792
+ lineCount = render(state, lineCount);
793
+ return;
794
+ }
795
+ // Navigation
796
+ if (isUpArrow && state.browseSelectedIndex > 0) {
797
+ state.browseSelectedIndex--;
798
+ lineCount = render(state, lineCount);
799
+ return;
800
+ }
801
+ if (isDownArrow && state.browseSelectedIndex < pageResults.length - 1) {
802
+ state.browseSelectedIndex++;
803
+ lineCount = render(state, lineCount);
804
+ return;
805
+ }
806
+ // Page navigation
807
+ if (isLeftArrow && state.browseCurrentPage > 0) {
808
+ state.browseCurrentPage--;
809
+ state.browseSelectedIndex = 0;
810
+ lineCount = render(state, lineCount);
811
+ return;
812
+ }
813
+ if (isRightArrow && state.browseCurrentPage < totalPages - 1) {
814
+ state.browseCurrentPage++;
815
+ state.browseSelectedIndex = 0;
816
+ lineCount = render(state, lineCount);
817
+ return;
818
+ }
819
+ // Select theme
820
+ if (isEnter && pageResults.length > 0) {
821
+ const theme = pageResults[state.browseSelectedIndex];
822
+ selectTheme(state, theme);
823
+ state.mode = 'tabs';
824
+ lineCount = render(state, lineCount);
825
+ return;
826
+ }
827
+ // Backspace - delete search char
828
+ if (isBackspace && state.browseSearchQuery.length > 0) {
829
+ state.browseSearchQuery = state.browseSearchQuery.slice(0, -1);
830
+ updateBrowseResults(state);
831
+ lineCount = render(state, lineCount);
832
+ return;
833
+ }
834
+ // Type to search (printable chars except space which might conflict)
835
+ if (isPrintable && !isSpace) {
836
+ state.browseSearchQuery += char;
837
+ updateBrowseResults(state);
838
+ lineCount = render(state, lineCount);
839
+ return;
840
+ }
841
+ return;
842
+ }
843
+ // ===== MODEL SELECTOR MODE =====
844
+ // Mode is always 'model-selector' at this point since we're in the else block
845
+ {
846
+ const currentProviderTab = MODEL_PROVIDER_TABS[state.modelProviderTab];
847
+ const providerModels = getModelsForProvider(currentProviderTab.id);
848
+ const maxModelIndex = providerModels.length - 1;
849
+ if (isCtrlC) {
850
+ cleanup();
851
+ resolve(result);
852
+ return;
853
+ }
854
+ if (isEscape) {
855
+ // If we started in model-selector mode (via /model command), close entirely
856
+ if (options.initialMode === 'model-selector') {
857
+ cleanup();
858
+ resolve(result);
859
+ return;
860
+ }
861
+ state.mode = 'tabs';
862
+ lineCount = render(state, lineCount);
863
+ return;
864
+ }
865
+ // Tab to switch provider tabs
866
+ if (isTab) {
867
+ state.modelProviderTab = (state.modelProviderTab + 1) % MODEL_PROVIDER_TABS.length;
868
+ state.modelSelectedIndex = 0; // Reset model selection when switching tabs
869
+ lineCount = render(state, lineCount);
870
+ return;
871
+ }
872
+ // Left/Right arrows also switch provider tabs
873
+ if (isLeftArrow) {
874
+ state.modelProviderTab = (state.modelProviderTab - 1 + MODEL_PROVIDER_TABS.length) % MODEL_PROVIDER_TABS.length;
875
+ state.modelSelectedIndex = 0;
876
+ lineCount = render(state, lineCount);
877
+ return;
878
+ }
879
+ if (isRightArrow) {
880
+ state.modelProviderTab = (state.modelProviderTab + 1) % MODEL_PROVIDER_TABS.length;
881
+ state.modelSelectedIndex = 0;
882
+ lineCount = render(state, lineCount);
883
+ return;
884
+ }
885
+ if (isUpArrow && state.modelSelectedIndex > 0) {
886
+ state.modelSelectedIndex--;
887
+ lineCount = render(state, lineCount);
888
+ return;
889
+ }
890
+ if (isDownArrow && state.modelSelectedIndex < maxModelIndex) {
891
+ state.modelSelectedIndex++;
892
+ lineCount = render(state, lineCount);
893
+ return;
894
+ }
895
+ if (isEnter) {
896
+ const selectedModel = providerModels[state.modelSelectedIndex];
897
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
898
+ if (selectedModel) {
899
+ const isCurrentProvider = selectedModel.provider === state.currentProvider;
900
+ // Update the config item display
901
+ const modelItem = state.configItems.find((item) => item.id === 'model');
902
+ if (modelItem) {
903
+ modelItem.value = selectedModel.name;
904
+ }
905
+ // Update status info
906
+ state.statusInfo.model = selectedModel.id;
907
+ // Always persist model selection to settings
908
+ setSetting('defaultProvider', selectedModel.provider);
909
+ setSetting('defaultModel', selectedModel.id);
910
+ if (isCurrentProvider) {
911
+ result.modelChanged = selectedModel.id;
912
+ }
913
+ // Different provider: settings are saved, will take effect on restart
914
+ // If we started in model-selector mode (via /model command), close entirely
915
+ if (options.initialMode === 'model-selector') {
916
+ cleanup();
917
+ resolve(result);
918
+ return;
919
+ }
920
+ // Going back to config tabs - don't call onModelChange callback here
921
+ // as the overlay is still open. The model change will be reflected
922
+ // in the config display, and the actual switch happens when overlay closes.
923
+ state.mode = 'tabs';
924
+ lineCount = render(state, lineCount);
925
+ }
926
+ return;
927
+ }
928
+ return;
929
+ }
930
+ };
931
+ process.stdin.on('data', onData);
932
+ });
933
+ }
934
+ // =============================================================================
935
+ // Helpers
936
+ // =============================================================================
937
+ function generateSessionId() {
938
+ const chars = '0123456789abcdef';
939
+ let id = '';
940
+ for (let i = 0; i < 8; i++) {
941
+ id += chars[Math.floor(Math.random() * chars.length)];
942
+ }
943
+ id += '-';
944
+ for (let i = 0; i < 4; i++) {
945
+ id += chars[Math.floor(Math.random() * chars.length)];
946
+ }
947
+ id += '-';
948
+ for (let i = 0; i < 4; i++) {
949
+ id += chars[Math.floor(Math.random() * chars.length)];
950
+ }
951
+ id += '-';
952
+ for (let i = 0; i < 12; i++) {
953
+ id += chars[Math.floor(Math.random() * chars.length)];
954
+ }
955
+ return id;
956
+ }
957
+ /**
958
+ * Find memory/instruction file in cwd
959
+ * Checks for COMPILR.md, .compilr/instructions.md, etc.
960
+ */
961
+ function findMemoryFile(cwd) {
962
+ const dir = cwd || process.cwd();
963
+ const candidates = [
964
+ 'COMPILR.md',
965
+ '.compilr/instructions.md',
966
+ 'CLAUDE.md', // Legacy support
967
+ '.claude/instructions.md', // Legacy support
968
+ 'AGENTS.md',
969
+ ];
970
+ for (const candidate of candidates) {
971
+ const fullPath = path.join(dir, candidate);
972
+ if (fs.existsSync(fullPath)) {
973
+ return fullPath;
974
+ }
975
+ }
976
+ return null;
977
+ }