@compilr-dev/cli 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +317 -0
- package/dist/agents/registry.d.ts +66 -0
- package/dist/agents/registry.js +238 -0
- package/dist/agents/types.d.ts +40 -0
- package/dist/agents/types.js +94 -0
- package/dist/commands/custom-registry.d.ts +69 -0
- package/dist/commands/custom-registry.js +246 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/types.d.ts +31 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +324 -0
- package/dist/db/index.d.ts +42 -0
- package/dist/db/index.js +146 -0
- package/dist/db/repositories/document-repository.d.ts +63 -0
- package/dist/db/repositories/document-repository.js +184 -0
- package/dist/db/repositories/index.d.ts +9 -0
- package/dist/db/repositories/index.js +6 -0
- package/dist/db/repositories/project-repository.d.ts +132 -0
- package/dist/db/repositories/project-repository.js +337 -0
- package/dist/db/repositories/work-item-repository.d.ts +115 -0
- package/dist/db/repositories/work-item-repository.js +389 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +143 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +348 -0
- package/dist/index.old.d.ts +7 -0
- package/dist/index.old.js +1014 -0
- package/dist/repl.d.ts +121 -0
- package/dist/repl.js +1878 -0
- package/dist/settings/index.d.ts +80 -0
- package/dist/settings/index.js +195 -0
- package/dist/shared-handlers.d.ts +63 -0
- package/dist/shared-handlers.js +57 -0
- package/dist/slash-autocomplete.d.ts +41 -0
- package/dist/slash-autocomplete.js +638 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.js +130 -0
- package/dist/tabbed-menu.d.ts +11 -0
- package/dist/tabbed-menu.js +328 -0
- package/dist/templates/backlog-md.d.ts +7 -0
- package/dist/templates/backlog-md.js +94 -0
- package/dist/templates/claude-md.d.ts +7 -0
- package/dist/templates/claude-md.js +189 -0
- package/dist/templates/coding-standards.d.ts +7 -0
- package/dist/templates/coding-standards.js +299 -0
- package/dist/templates/compilr-md.d.ts +7 -0
- package/dist/templates/compilr-md.js +189 -0
- package/dist/templates/config-json.d.ts +38 -0
- package/dist/templates/config-json.js +39 -0
- package/dist/templates/gitignore.d.ts +7 -0
- package/dist/templates/gitignore.js +85 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +302 -0
- package/dist/templates/package-json.d.ts +7 -0
- package/dist/templates/package-json.js +111 -0
- package/dist/templates/readme-md.d.ts +7 -0
- package/dist/templates/readme-md.js +161 -0
- package/dist/templates/tsconfig.d.ts +7 -0
- package/dist/templates/tsconfig.js +61 -0
- package/dist/templates/types.d.ts +33 -0
- package/dist/templates/types.js +24 -0
- package/dist/test-autocomplete.d.ts +7 -0
- package/dist/test-autocomplete.js +85 -0
- package/dist/test-tabbed-menu.d.ts +7 -0
- package/dist/test-tabbed-menu.js +25 -0
- package/dist/themes/colors.d.ts +49 -0
- package/dist/themes/colors.js +135 -0
- package/dist/themes/index.d.ts +23 -0
- package/dist/themes/index.js +24 -0
- package/dist/themes/registry.d.ts +60 -0
- package/dist/themes/registry.js +195 -0
- package/dist/themes/types.d.ts +82 -0
- package/dist/themes/types.js +7 -0
- package/dist/tool-selector.d.ts +71 -0
- package/dist/tool-selector.js +184 -0
- package/dist/tools/ask-user-simple.d.ts +19 -0
- package/dist/tools/ask-user-simple.js +86 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +113 -0
- package/dist/tools/backlog.d.ts +53 -0
- package/dist/tools/backlog.js +709 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +121 -0
- package/dist/ui/agents-overlay.d.ts +12 -0
- package/dist/ui/agents-overlay.js +501 -0
- package/dist/ui/arch-type-overlay.d.ts +20 -0
- package/dist/ui/arch-type-overlay.js +229 -0
- package/dist/ui/ask-user-overlay.d.ts +26 -0
- package/dist/ui/ask-user-overlay.js +647 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay.js +242 -0
- package/dist/ui/backlog-overlay.d.ts +17 -0
- package/dist/ui/backlog-overlay.js +786 -0
- package/dist/ui/commands-overlay.d.ts +11 -0
- package/dist/ui/commands-overlay.js +410 -0
- package/dist/ui/config-overlay.d.ts +34 -0
- package/dist/ui/config-overlay.js +977 -0
- package/dist/ui/conversation.d.ts +82 -0
- package/dist/ui/conversation.js +508 -0
- package/dist/ui/diff.d.ts +38 -0
- package/dist/ui/diff.js +182 -0
- package/dist/ui/ephemeral.d.ts +111 -0
- package/dist/ui/ephemeral.js +413 -0
- package/dist/ui/file-autocomplete.d.ts +45 -0
- package/dist/ui/file-autocomplete.js +237 -0
- package/dist/ui/footer.d.ts +153 -0
- package/dist/ui/footer.js +422 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/init-overlay.d.ts +24 -0
- package/dist/ui/init-overlay.js +525 -0
- package/dist/ui/input-prompt-v2.d.ts +179 -0
- package/dist/ui/input-prompt-v2.js +991 -0
- package/dist/ui/input-prompt.d.ts +97 -0
- package/dist/ui/input-prompt.js +800 -0
- package/dist/ui/iteration-limit-overlay.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay.js +150 -0
- package/dist/ui/keys-overlay.d.ts +14 -0
- package/dist/ui/keys-overlay.js +181 -0
- package/dist/ui/model-warning-overlay.d.ts +30 -0
- package/dist/ui/model-warning-overlay.js +171 -0
- package/dist/ui/overlay-controller.d.ts +25 -0
- package/dist/ui/overlay-controller.js +35 -0
- package/dist/ui/overlays.d.ts +47 -0
- package/dist/ui/overlays.js +627 -0
- package/dist/ui/permission-overlay.d.ts +16 -0
- package/dist/ui/permission-overlay.js +494 -0
- package/dist/ui/terminal.d.ts +117 -0
- package/dist/ui/terminal.js +237 -0
- package/dist/ui/todo-zone.d.ts +112 -0
- package/dist/ui/todo-zone.js +353 -0
- package/dist/ui/tools-overlay.d.ts +26 -0
- package/dist/ui/tools-overlay.js +278 -0
- package/dist/ui/tutorial-overlay.d.ts +10 -0
- package/dist/ui/tutorial-overlay.js +936 -0
- package/dist/ui/types.d.ts +103 -0
- package/dist/ui/types.js +33 -0
- package/dist/utils/credentials.d.ts +55 -0
- package/dist/utils/credentials.js +268 -0
- package/dist/utils/model-tiers.d.ts +37 -0
- package/dist/utils/model-tiers.js +118 -0
- package/dist/utils/project-memory.d.ts +47 -0
- package/dist/utils/project-memory.js +117 -0
- package/dist/utils/project-status.d.ts +56 -0
- package/dist/utils/project-status.js +237 -0
- package/package.json +66 -0
|
@@ -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
|
+
}
|