agent-tool-forge 0.3.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/LICENSE +21 -0
- package/README.md +209 -0
- package/lib/agent-registry.js +170 -0
- package/lib/api-client.js +792 -0
- package/lib/api-loader.js +260 -0
- package/lib/auth.d.ts +25 -0
- package/lib/auth.js +158 -0
- package/lib/checks/check-adapter.js +172 -0
- package/lib/checks/compose.js +42 -0
- package/lib/checks/content-match.js +14 -0
- package/lib/checks/cost-budget.js +11 -0
- package/lib/checks/index.js +18 -0
- package/lib/checks/json-valid.js +15 -0
- package/lib/checks/latency.js +11 -0
- package/lib/checks/length-bounds.js +17 -0
- package/lib/checks/negative-match.js +14 -0
- package/lib/checks/no-hallucinated-numbers.js +63 -0
- package/lib/checks/non-empty.js +34 -0
- package/lib/checks/regex-match.js +12 -0
- package/lib/checks/run-checks.js +84 -0
- package/lib/checks/schema-match.js +26 -0
- package/lib/checks/tool-call-count.js +16 -0
- package/lib/checks/tool-selection.js +34 -0
- package/lib/checks/types.js +45 -0
- package/lib/comparison/compare.js +86 -0
- package/lib/comparison/format.js +104 -0
- package/lib/comparison/index.js +6 -0
- package/lib/comparison/statistics.js +59 -0
- package/lib/comparison/types.js +41 -0
- package/lib/config-schema.js +200 -0
- package/lib/config.d.ts +66 -0
- package/lib/conversation-store.d.ts +77 -0
- package/lib/conversation-store.js +443 -0
- package/lib/db.d.ts +6 -0
- package/lib/db.js +1112 -0
- package/lib/dep-check.js +99 -0
- package/lib/drift-background.js +61 -0
- package/lib/drift-monitor.js +187 -0
- package/lib/eval-runner.js +566 -0
- package/lib/fixtures/fixture-store.js +161 -0
- package/lib/fixtures/index.js +11 -0
- package/lib/forge-engine.js +982 -0
- package/lib/forge-eval-generator.js +417 -0
- package/lib/forge-file-writer.js +386 -0
- package/lib/forge-service-client.js +190 -0
- package/lib/forge-service.d.ts +4 -0
- package/lib/forge-service.js +655 -0
- package/lib/forge-verifier-generator.js +271 -0
- package/lib/handlers/admin.js +151 -0
- package/lib/handlers/agents.js +229 -0
- package/lib/handlers/chat-resume.js +334 -0
- package/lib/handlers/chat-sync.js +320 -0
- package/lib/handlers/chat.js +320 -0
- package/lib/handlers/conversations.js +92 -0
- package/lib/handlers/preferences.js +88 -0
- package/lib/handlers/tools-list.js +58 -0
- package/lib/hitl-engine.d.ts +60 -0
- package/lib/hitl-engine.js +261 -0
- package/lib/http-utils.js +92 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +141 -0
- package/lib/init.js +636 -0
- package/lib/manual-entry.js +59 -0
- package/lib/mcp-server.js +252 -0
- package/lib/output-groups.js +54 -0
- package/lib/postgres-store.d.ts +31 -0
- package/lib/postgres-store.js +465 -0
- package/lib/preference-store.d.ts +47 -0
- package/lib/preference-store.js +79 -0
- package/lib/prompt-store.d.ts +42 -0
- package/lib/prompt-store.js +60 -0
- package/lib/rate-limiter.d.ts +30 -0
- package/lib/rate-limiter.js +104 -0
- package/lib/react-engine.d.ts +110 -0
- package/lib/react-engine.js +337 -0
- package/lib/runner/cli.js +156 -0
- package/lib/runner/cost-estimator.js +71 -0
- package/lib/runner/gate.js +46 -0
- package/lib/runner/index.js +165 -0
- package/lib/sidecar.d.ts +83 -0
- package/lib/sidecar.js +161 -0
- package/lib/sse.d.ts +15 -0
- package/lib/sse.js +30 -0
- package/lib/tools-scanner.js +91 -0
- package/lib/tui.js +253 -0
- package/lib/verifier-report.js +78 -0
- package/lib/verifier-runner.js +338 -0
- package/lib/verifier-scanner.js +70 -0
- package/lib/verifier-worker-pool.js +196 -0
- package/lib/views/chat.js +340 -0
- package/lib/views/endpoints.js +203 -0
- package/lib/views/eval-run.js +206 -0
- package/lib/views/forge-agent.js +538 -0
- package/lib/views/forge.js +410 -0
- package/lib/views/main-menu.js +275 -0
- package/lib/views/mediation.js +381 -0
- package/lib/views/model-compare.js +430 -0
- package/lib/views/model-comparison.js +333 -0
- package/lib/views/onboarding.js +470 -0
- package/lib/views/performance.js +237 -0
- package/lib/views/run-evals.js +205 -0
- package/lib/views/settings.js +829 -0
- package/lib/views/tools-evals.js +514 -0
- package/lib/views/verifier-coverage.js +617 -0
- package/lib/workers/verifier-worker.js +52 -0
- package/package.json +123 -0
- package/widget/forge-chat.js +789 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings View — Model selector, system prompt editor, forge skill prompt, .env manager.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import blessed from 'blessed';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { resolve, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PROJECT_ROOT = resolve(__dirname, '../..');
|
|
12
|
+
const CONFIG_FILE = resolve(PROJECT_ROOT, 'forge.config.json');
|
|
13
|
+
const ENV_FILE = resolve(PROJECT_ROOT, '.env');
|
|
14
|
+
const SKILL_FILE = resolve(PROJECT_ROOT, 'skills/forge-tool/SKILL.md');
|
|
15
|
+
|
|
16
|
+
const CLAUDE_MODELS = [
|
|
17
|
+
'claude-opus-4-6',
|
|
18
|
+
'claude-sonnet-4-6',
|
|
19
|
+
'claude-haiku-4-5-20251001'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const OPENAI_MODELS = [
|
|
23
|
+
'gpt-4o',
|
|
24
|
+
'gpt-4o-mini',
|
|
25
|
+
'o1',
|
|
26
|
+
'o3-mini'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const GOOGLE_MODELS = [
|
|
30
|
+
'gemini-2.0-flash',
|
|
31
|
+
'gemini-2.5-pro-exp'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const DEEPSEEK_MODELS = [
|
|
35
|
+
'deepseek-chat',
|
|
36
|
+
'deepseek-reasoner'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the model list based on which API keys are present in .env.
|
|
41
|
+
* Always includes Claude models (forge-tool always uses Claude).
|
|
42
|
+
* Adds provider sections when matching keys are detected.
|
|
43
|
+
* Always ends with a "Custom..." entry.
|
|
44
|
+
*/
|
|
45
|
+
function buildModelList(envMap) {
|
|
46
|
+
const sections = [
|
|
47
|
+
{ header: '── Anthropic (forge-tool) ──────────────', models: CLAUDE_MODELS }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const hasOpenAI = Object.keys(envMap).some((k) => /OPENAI/i.test(k));
|
|
51
|
+
const hasGoogle = Object.keys(envMap).some((k) => /GOOGLE|GEMINI/i.test(k));
|
|
52
|
+
const hasDeepSeek = Object.keys(envMap).some((k) => /DEEPSEEK/i.test(k));
|
|
53
|
+
|
|
54
|
+
if (hasOpenAI) sections.push({ header: '── OpenAI ──────────────────────────────', models: OPENAI_MODELS });
|
|
55
|
+
if (hasGoogle) sections.push({ header: '── Google ──────────────────────────────', models: GOOGLE_MODELS });
|
|
56
|
+
if (hasDeepSeek) sections.push({ header: '── DeepSeek ────────────────────────────', models: DEEPSEEK_MODELS });
|
|
57
|
+
|
|
58
|
+
// Flat list with divider labels (not selectable) interleaved
|
|
59
|
+
const items = []; // display strings
|
|
60
|
+
const values = []; // corresponding model strings (null for divider rows)
|
|
61
|
+
|
|
62
|
+
for (const section of sections) {
|
|
63
|
+
items.push(`{#555555-fg}${section.header}{/#555555-fg}`);
|
|
64
|
+
values.push(null); // divider — not selectable
|
|
65
|
+
for (const m of section.models) {
|
|
66
|
+
items.push(m);
|
|
67
|
+
values.push(m);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
items.push('{#555555-fg}────────────────────────────────────────{/#555555-fg}');
|
|
72
|
+
values.push(null);
|
|
73
|
+
items.push(' Custom model...');
|
|
74
|
+
values.push('__custom__');
|
|
75
|
+
|
|
76
|
+
return { items, values };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function loadConfig() {
|
|
80
|
+
try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch (_) { return {}; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function saveConfig(cfg) {
|
|
84
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), 'utf-8');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function loadEnv() {
|
|
88
|
+
if (!existsSync(ENV_FILE)) return {};
|
|
89
|
+
const lines = readFileSync(ENV_FILE, 'utf-8').split('\n');
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const trimmed = line.trim();
|
|
93
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
94
|
+
const eqIdx = trimmed.indexOf('=');
|
|
95
|
+
if (eqIdx === -1) continue;
|
|
96
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
97
|
+
const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
98
|
+
out[key] = val;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function saveEnv(envMap) {
|
|
104
|
+
const lines = Object.entries(envMap).map(([k, v]) => `${k}=${v}`);
|
|
105
|
+
writeFileSync(ENV_FILE, lines.join('\n') + '\n', 'utf-8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function maskValue(val) {
|
|
109
|
+
if (!val || val.length <= 4) return '****';
|
|
110
|
+
return val.slice(0, 3) + '****';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createView({ screen, content, config, navigate, setFooter, screenKey, openPopup, closePopup, startService }) {
|
|
114
|
+
const list = blessed.list({
|
|
115
|
+
top: 0,
|
|
116
|
+
left: 0,
|
|
117
|
+
width: '100%',
|
|
118
|
+
height: '100%',
|
|
119
|
+
tags: true,
|
|
120
|
+
keys: true,
|
|
121
|
+
vi: true,
|
|
122
|
+
mouse: true,
|
|
123
|
+
style: {
|
|
124
|
+
selected: { bg: 'blue', fg: 'white', bold: true },
|
|
125
|
+
item: { fg: 'white' }
|
|
126
|
+
},
|
|
127
|
+
padding: { top: 1, left: 2 }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
setFooter(' {bold}↑↓{/bold} navigate {bold}Enter{/bold} select section {bold}r{/bold} refresh {bold}b{/bold} back');
|
|
131
|
+
|
|
132
|
+
function afterPopupClose() {
|
|
133
|
+
list.refresh();
|
|
134
|
+
list.focus();
|
|
135
|
+
screen.render();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
list.on('select', (item, index) => {
|
|
139
|
+
switch (index) {
|
|
140
|
+
case 0: showModelSelector(screen, config, openPopup, closePopup, 'generation', afterPopupClose); break;
|
|
141
|
+
case 1: showModelSelector(screen, config, openPopup, closePopup, 'eval', afterPopupClose); break;
|
|
142
|
+
case 2: showModelSelector(screen, config, openPopup, closePopup, 'verifier', afterPopupClose); break;
|
|
143
|
+
case 3: showModelSelector(screen, config, openPopup, closePopup, 'secondary', afterPopupClose); break;
|
|
144
|
+
case 4: showPromptEditor(screen, config, 'agent', openPopup, closePopup, afterPopupClose); break;
|
|
145
|
+
case 5: showPromptEditor(screen, config, 'skill', openPopup, closePopup, afterPopupClose); break;
|
|
146
|
+
case 6: showApiSourceEditor(screen, config, openPopup, closePopup, afterPopupClose); break;
|
|
147
|
+
case 7: showEnvManager(screen, config, openPopup, closePopup, afterPopupClose); break;
|
|
148
|
+
case 8: showModelMatrixEditor(screen, config, openPopup, closePopup, afterPopupClose); break;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
let lastSettingsJson = '';
|
|
153
|
+
|
|
154
|
+
list.refresh = () => {
|
|
155
|
+
const cfg = loadConfig();
|
|
156
|
+
const envMap = loadEnv();
|
|
157
|
+
|
|
158
|
+
// Quick change detection — skip render if nothing changed
|
|
159
|
+
const snapshot = JSON.stringify({ cfg, envKeys: Object.keys(envMap) });
|
|
160
|
+
if (snapshot === lastSettingsJson) return;
|
|
161
|
+
lastSettingsJson = snapshot;
|
|
162
|
+
const envCount = Object.keys(envMap).length;
|
|
163
|
+
|
|
164
|
+
const systemPromptStatus = cfg.systemPromptPath
|
|
165
|
+
? `{green-fg}${cfg.systemPromptPath}{/green-fg}`
|
|
166
|
+
: '{#888888-fg}not configured{/#888888-fg}';
|
|
167
|
+
|
|
168
|
+
const skillStatus = existsSync(SKILL_FILE)
|
|
169
|
+
? '{green-fg}skills/forge-tool/SKILL.md{/green-fg}'
|
|
170
|
+
: '{red-fg}not found{/red-fg}';
|
|
171
|
+
|
|
172
|
+
const models = cfg.models || {};
|
|
173
|
+
const generationModelDisplay = (models.generation || cfg.model)
|
|
174
|
+
? `{cyan-fg}${models.generation || cfg.model}{/cyan-fg}`
|
|
175
|
+
: '{yellow-fg}not set (default: claude-sonnet-4-6){/yellow-fg}';
|
|
176
|
+
const evalModelDisplay = models.eval
|
|
177
|
+
? `{cyan-fg}${models.eval}{/cyan-fg}`
|
|
178
|
+
: '{#888888-fg}default (claude-sonnet-4-6){/#888888-fg}';
|
|
179
|
+
const verifierModelDisplay = models.verifier
|
|
180
|
+
? `{cyan-fg}${models.verifier}{/cyan-fg}`
|
|
181
|
+
: '{#888888-fg}default (claude-sonnet-4-6){/#888888-fg}';
|
|
182
|
+
const secondaryModelDisplay = models.secondary
|
|
183
|
+
? `{cyan-fg}${models.secondary}{/cyan-fg}`
|
|
184
|
+
: '{#888888-fg}not set (model comparison disabled){/#888888-fg}';
|
|
185
|
+
|
|
186
|
+
// Detect which API providers have keys
|
|
187
|
+
const providers = [];
|
|
188
|
+
if (Object.keys(envMap).some((k) => /ANTHROPIC/i.test(k))) providers.push('{green-fg}Anthropic{/green-fg}');
|
|
189
|
+
if (Object.keys(envMap).some((k) => /OPENAI/i.test(k))) providers.push('{green-fg}OpenAI{/green-fg}');
|
|
190
|
+
if (Object.keys(envMap).some((k) => /GOOGLE|GEMINI/i.test(k))) providers.push('{green-fg}Google{/green-fg}');
|
|
191
|
+
if (Object.keys(envMap).some((k) => /DEEPSEEK/i.test(k))) providers.push('{green-fg}DeepSeek{/green-fg}');
|
|
192
|
+
const keysSummary = envCount === 0
|
|
193
|
+
? '{#888888-fg}no keys{/#888888-fg}'
|
|
194
|
+
: `${envCount} key(s)${providers.length ? ' ' + providers.join(' ') : ''}`;
|
|
195
|
+
|
|
196
|
+
// API source status
|
|
197
|
+
const apiSource = cfg.api?.manifestPath || cfg.api?.discovery?.url || cfg.api?.discovery?.file;
|
|
198
|
+
const apiSourceStatus = apiSource
|
|
199
|
+
? `{green-fg}${apiSource}{/green-fg}`
|
|
200
|
+
: '{yellow-fg}not configured{/yellow-fg}';
|
|
201
|
+
|
|
202
|
+
const matrix = cfg.modelMatrix || [];
|
|
203
|
+
const matrixDisplay = matrix.length > 0
|
|
204
|
+
? `{cyan-fg}${matrix.length} model(s): ${matrix.join(', ')}{/cyan-fg}`
|
|
205
|
+
: '{#888888-fg}not configured (add models to compare){/#888888-fg}';
|
|
206
|
+
|
|
207
|
+
const row = (num, label, val) =>
|
|
208
|
+
` {bold}${num}.{/bold} {white-fg}${label.padEnd(22)}{/white-fg}${val}`;
|
|
209
|
+
|
|
210
|
+
list.setItems([
|
|
211
|
+
row('1', 'Generation Model', generationModelDisplay),
|
|
212
|
+
row('2', 'Eval Model', evalModelDisplay),
|
|
213
|
+
row('3', 'Verifier Model', verifierModelDisplay),
|
|
214
|
+
row('4', 'Secondary Model', secondaryModelDisplay),
|
|
215
|
+
row('5', 'Agent System Prompt', systemPromptStatus),
|
|
216
|
+
row('6', 'Forge Skill Prompt', skillStatus),
|
|
217
|
+
row('7', 'API Source', apiSourceStatus),
|
|
218
|
+
row('8', 'API Keys / Secrets', keysSummary),
|
|
219
|
+
row('9', 'Model Matrix', matrixDisplay)
|
|
220
|
+
]);
|
|
221
|
+
screen.render();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
list.refresh();
|
|
225
|
+
list.focus();
|
|
226
|
+
return list;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function showModelSelector(screen, config, openPopup, closePopup, role = 'generation', onClose) {
|
|
230
|
+
const cfg = loadConfig();
|
|
231
|
+
const current = (cfg.models?.[role] || (role === 'generation' ? cfg.model : null)) || '';
|
|
232
|
+
const envMap = loadEnv();
|
|
233
|
+
const { items, values } = buildModelList(envMap);
|
|
234
|
+
|
|
235
|
+
// Mark current selection
|
|
236
|
+
const markedItems = items.map((label, i) => {
|
|
237
|
+
const val = values[i];
|
|
238
|
+
if (!val || val === '__custom__') return ` ${label}`;
|
|
239
|
+
return val === current
|
|
240
|
+
? ` {green-fg}● ${val}{/green-fg}`
|
|
241
|
+
: ` ${val}`;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const noteHeight = 2;
|
|
245
|
+
const listHeight = Math.min(items.length + noteHeight + 4, 24);
|
|
246
|
+
|
|
247
|
+
const roleLabel = role.charAt(0).toUpperCase() + role.slice(1);
|
|
248
|
+
|
|
249
|
+
const popup = blessed.box({
|
|
250
|
+
parent: screen,
|
|
251
|
+
border: 'line',
|
|
252
|
+
height: listHeight,
|
|
253
|
+
width: 54,
|
|
254
|
+
top: 'center',
|
|
255
|
+
left: 'center',
|
|
256
|
+
label: ` Select ${roleLabel} Model `,
|
|
257
|
+
tags: true,
|
|
258
|
+
style: { border: { fg: 'blue' } }
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Note about what this field controls
|
|
262
|
+
blessed.box({
|
|
263
|
+
parent: popup,
|
|
264
|
+
top: 0,
|
|
265
|
+
left: 1,
|
|
266
|
+
width: '100%-2',
|
|
267
|
+
height: 1,
|
|
268
|
+
tags: true,
|
|
269
|
+
content: `{#888888-fg}Controls which model is used for ${role} tasks{/#888888-fg}`
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const list = blessed.list({
|
|
273
|
+
parent: popup,
|
|
274
|
+
top: noteHeight,
|
|
275
|
+
left: 0,
|
|
276
|
+
width: '100%',
|
|
277
|
+
height: listHeight - noteHeight - 2,
|
|
278
|
+
tags: true,
|
|
279
|
+
keys: true,
|
|
280
|
+
vi: true,
|
|
281
|
+
style: { selected: { bg: '#1a3a5c', fg: 'white' } },
|
|
282
|
+
items: markedItems
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
function applySelection(idx) {
|
|
286
|
+
const val = values[idx];
|
|
287
|
+
if (val === null) return; // divider row — skip
|
|
288
|
+
if (val === '__custom__') {
|
|
289
|
+
closePopup?.(); // close the model list popup
|
|
290
|
+
popup.destroy();
|
|
291
|
+
screen.render();
|
|
292
|
+
openPopup?.(); // open for the new text prompt
|
|
293
|
+
const prompt = blessed.prompt({
|
|
294
|
+
parent: screen,
|
|
295
|
+
border: 'line',
|
|
296
|
+
height: 'shrink',
|
|
297
|
+
width: 'half',
|
|
298
|
+
top: 'center',
|
|
299
|
+
left: 'center',
|
|
300
|
+
label: ' Custom Model Name ',
|
|
301
|
+
tags: true,
|
|
302
|
+
keys: true
|
|
303
|
+
});
|
|
304
|
+
prompt.input('Enter model ID (e.g. gpt-4o, gemini-2.0-flash):', current, (err, val) => {
|
|
305
|
+
closePopup?.(); // close the text prompt
|
|
306
|
+
if (!err && val && val.trim()) {
|
|
307
|
+
cfg.models = cfg.models || {};
|
|
308
|
+
cfg.models[role] = val.trim();
|
|
309
|
+
try { saveConfig(cfg); } catch (_) { onClose?.(); return; }
|
|
310
|
+
if (!config.models) config.models = {};
|
|
311
|
+
config.models[role] = val.trim();
|
|
312
|
+
if (role === 'generation') config.model = val.trim();
|
|
313
|
+
}
|
|
314
|
+
onClose?.();
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
cfg.models = cfg.models || {};
|
|
319
|
+
cfg.models[role] = val;
|
|
320
|
+
try { saveConfig(cfg); } catch (_) { onClose?.(); return; }
|
|
321
|
+
if (!config.models) config.models = {};
|
|
322
|
+
config.models[role] = val;
|
|
323
|
+
// Backwards compat: also update config.model for generation role
|
|
324
|
+
if (role === 'generation') config.model = val;
|
|
325
|
+
closePopup?.();
|
|
326
|
+
popup.destroy();
|
|
327
|
+
onClose?.();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
openPopup?.();
|
|
331
|
+
list.on('select', (item, idx) => applySelection(idx));
|
|
332
|
+
list.key(['escape', 'b'], () => { closePopup?.(); popup.destroy(); onClose?.(); });
|
|
333
|
+
list.focus();
|
|
334
|
+
screen.render();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function showPromptEditor(screen, config, type, openPopup, closePopup, onClose) {
|
|
338
|
+
const cfg = loadConfig();
|
|
339
|
+
let filePath, label;
|
|
340
|
+
|
|
341
|
+
if (type === 'agent') {
|
|
342
|
+
filePath = cfg.systemPromptPath ? resolve(PROJECT_ROOT, cfg.systemPromptPath) : null;
|
|
343
|
+
label = ' Agent System Prompt ';
|
|
344
|
+
} else {
|
|
345
|
+
filePath = SKILL_FILE;
|
|
346
|
+
label = ' Forge Skill Prompt ';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let content = '';
|
|
350
|
+
if (filePath && existsSync(filePath)) {
|
|
351
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch (_) { content = '(could not read file)'; }
|
|
352
|
+
} else if (type === 'agent' && !cfg.systemPromptPath) {
|
|
353
|
+
content = '(systemPromptPath not set in forge.config.json)';
|
|
354
|
+
} else {
|
|
355
|
+
content = '(file not found: ' + filePath + ')';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const box = blessed.scrollablebox({
|
|
359
|
+
parent: screen,
|
|
360
|
+
border: 'line',
|
|
361
|
+
top: 1,
|
|
362
|
+
left: 2,
|
|
363
|
+
right: 2,
|
|
364
|
+
bottom: 3,
|
|
365
|
+
label,
|
|
366
|
+
tags: false,
|
|
367
|
+
scrollable: true,
|
|
368
|
+
alwaysScroll: true,
|
|
369
|
+
keys: true,
|
|
370
|
+
content,
|
|
371
|
+
scrollbar: { ch: '│', style: { fg: 'white' } }
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const helpBar = blessed.box({
|
|
375
|
+
parent: screen,
|
|
376
|
+
bottom: 1,
|
|
377
|
+
left: 0,
|
|
378
|
+
width: '100%',
|
|
379
|
+
height: 1,
|
|
380
|
+
tags: true,
|
|
381
|
+
content: ' {bold}e{/bold} edit in $EDITOR {bold}p{/bold} change path {bold}Escape{/bold} close',
|
|
382
|
+
style: { bg: 'black', fg: 'white' }
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
box.key('e', async () => {
|
|
386
|
+
if (!filePath) return;
|
|
387
|
+
const editor = process.env.EDITOR || 'vi';
|
|
388
|
+
const { spawn } = await import('child_process');
|
|
389
|
+
screen.program.disableMouse();
|
|
390
|
+
screen.program.normalBuffer();
|
|
391
|
+
const child = spawn(editor, [filePath], { stdio: 'inherit' });
|
|
392
|
+
child.on('exit', () => {
|
|
393
|
+
screen.program.alternateBuffer();
|
|
394
|
+
screen.program.enableMouse();
|
|
395
|
+
screen.render();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
openPopup?.();
|
|
400
|
+
|
|
401
|
+
box.key('p', () => {
|
|
402
|
+
if (type !== 'agent') return; // Only agent path is changeable
|
|
403
|
+
const prompt = blessed.prompt({
|
|
404
|
+
parent: screen,
|
|
405
|
+
border: 'line',
|
|
406
|
+
height: 'shrink',
|
|
407
|
+
width: 'half',
|
|
408
|
+
top: 'center',
|
|
409
|
+
left: 'center',
|
|
410
|
+
label: ' Set System Prompt Path ',
|
|
411
|
+
tags: true,
|
|
412
|
+
keys: true
|
|
413
|
+
});
|
|
414
|
+
prompt.input('Enter path to system prompt file:', cfg.systemPromptPath || '', (err, val) => {
|
|
415
|
+
if (!err && val) {
|
|
416
|
+
cfg.systemPromptPath = val;
|
|
417
|
+
try { saveConfig(cfg); } catch (_) { box.focus(); screen.render(); return; }
|
|
418
|
+
config.systemPromptPath = val;
|
|
419
|
+
// Reload the file content in the box
|
|
420
|
+
const newPath = resolve(PROJECT_ROOT, val);
|
|
421
|
+
let newContent = '';
|
|
422
|
+
if (existsSync(newPath)) {
|
|
423
|
+
try { newContent = readFileSync(newPath, 'utf-8'); } catch (_) { newContent = '(could not read file)'; }
|
|
424
|
+
} else {
|
|
425
|
+
newContent = '(file not found: ' + val + ')';
|
|
426
|
+
}
|
|
427
|
+
filePath = newPath;
|
|
428
|
+
box.setContent(newContent);
|
|
429
|
+
}
|
|
430
|
+
box.focus();
|
|
431
|
+
screen.render();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
box.key(['escape', 'b', 'q'], () => {
|
|
436
|
+
closePopup?.();
|
|
437
|
+
box.destroy();
|
|
438
|
+
helpBar.destroy();
|
|
439
|
+
onClose?.();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
box.focus();
|
|
443
|
+
screen.render();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function showApiSourceEditor(screen, config, openPopup, closePopup, onClose) {
|
|
447
|
+
const cfg = loadConfig();
|
|
448
|
+
const api = cfg.api || {};
|
|
449
|
+
|
|
450
|
+
// Determine current values
|
|
451
|
+
const currentManifest = api.manifestPath || '';
|
|
452
|
+
const currentDiscoveryUrl = api.discovery?.url || '';
|
|
453
|
+
const currentDiscoveryFile = api.discovery?.file || '';
|
|
454
|
+
|
|
455
|
+
const box = blessed.form({
|
|
456
|
+
parent: screen,
|
|
457
|
+
border: 'line',
|
|
458
|
+
top: 2,
|
|
459
|
+
left: 4,
|
|
460
|
+
right: 4,
|
|
461
|
+
height: 14,
|
|
462
|
+
label: ' Configure API Source ',
|
|
463
|
+
tags: true,
|
|
464
|
+
keys: true
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
blessed.text({ parent: box, top: 0, left: 1, tags: true,
|
|
468
|
+
content: '{#888888-fg}Set one or more sources. Manifest overrides OpenAPI for the same endpoint.{/#888888-fg}' });
|
|
469
|
+
|
|
470
|
+
blessed.text({ parent: box, top: 2, left: 1, content: 'Manifest path: ' });
|
|
471
|
+
const manifestInput = blessed.textbox({
|
|
472
|
+
parent: box, top: 2, left: 18, right: 2, height: 1,
|
|
473
|
+
inputOnFocus: true, style: { fg: 'white', bg: '#1a3a5c' },
|
|
474
|
+
value: currentManifest
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
blessed.text({ parent: box, top: 4, left: 1, content: 'OpenAPI URL: ' });
|
|
478
|
+
const urlInput = blessed.textbox({
|
|
479
|
+
parent: box, top: 4, left: 18, right: 2, height: 1,
|
|
480
|
+
inputOnFocus: true, style: { fg: 'white', bg: '#1a3a5c' },
|
|
481
|
+
value: currentDiscoveryUrl
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
blessed.text({ parent: box, top: 6, left: 1, content: 'OpenAPI file: ' });
|
|
485
|
+
const fileInput = blessed.textbox({
|
|
486
|
+
parent: box, top: 6, left: 18, right: 2, height: 1,
|
|
487
|
+
inputOnFocus: true, style: { fg: 'white', bg: '#1a3a5c' },
|
|
488
|
+
value: currentDiscoveryFile
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const statusBar = blessed.box({ parent: box, bottom: 2, left: 1, right: 1, height: 1, tags: true });
|
|
492
|
+
|
|
493
|
+
const saveBtn = blessed.button({
|
|
494
|
+
parent: box, bottom: 0, left: 1, width: 10, height: 1, content: ' Save ',
|
|
495
|
+
style: { bg: 'green', fg: 'white', focus: { bg: 'blue' } }, keys: true, mouse: true
|
|
496
|
+
});
|
|
497
|
+
const cancelBtn = blessed.button({
|
|
498
|
+
parent: box, bottom: 0, left: 13, width: 10, height: 1, content: ' Cancel ',
|
|
499
|
+
style: { bg: 'red', fg: 'white', focus: { bg: 'blue' } }, keys: true, mouse: true
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
function doClose() {
|
|
503
|
+
closePopup?.();
|
|
504
|
+
box.destroy();
|
|
505
|
+
onClose?.();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
saveBtn.on('press', () => {
|
|
509
|
+
const manifest = manifestInput.getValue().trim();
|
|
510
|
+
const url = urlInput.getValue().trim();
|
|
511
|
+
const file = fileInput.getValue().trim();
|
|
512
|
+
|
|
513
|
+
cfg.api = cfg.api || {};
|
|
514
|
+
if (manifest) {
|
|
515
|
+
cfg.api.manifestPath = manifest;
|
|
516
|
+
} else {
|
|
517
|
+
delete cfg.api.manifestPath;
|
|
518
|
+
}
|
|
519
|
+
if (url || file) {
|
|
520
|
+
cfg.api.discovery = { type: 'openapi' };
|
|
521
|
+
if (url) cfg.api.discovery.url = url;
|
|
522
|
+
if (file) cfg.api.discovery.file = file;
|
|
523
|
+
} else {
|
|
524
|
+
delete cfg.api.discovery;
|
|
525
|
+
}
|
|
526
|
+
// Sync runtime config
|
|
527
|
+
config.api = cfg.api;
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
saveConfig(cfg);
|
|
531
|
+
statusBar.setContent('{green-fg}Saved!{/green-fg}');
|
|
532
|
+
screen.render();
|
|
533
|
+
setTimeout(doClose, 600);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
statusBar.setContent(`{red-fg}Save failed: ${err.message}{/red-fg}`);
|
|
536
|
+
screen.render();
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
cancelBtn.on('press', doClose);
|
|
541
|
+
box.key(['escape', 'b'], doClose);
|
|
542
|
+
|
|
543
|
+
openPopup?.();
|
|
544
|
+
manifestInput.focus();
|
|
545
|
+
screen.render();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function showModelMatrixEditor(screen, config, openPopup, closePopup, onClose) {
|
|
549
|
+
const cfg = loadConfig();
|
|
550
|
+
let matrix = [...(cfg.modelMatrix || [])];
|
|
551
|
+
|
|
552
|
+
const container = blessed.box({
|
|
553
|
+
parent: screen,
|
|
554
|
+
border: 'line',
|
|
555
|
+
top: 2,
|
|
556
|
+
left: 4,
|
|
557
|
+
right: 4,
|
|
558
|
+
height: 18,
|
|
559
|
+
label: ' Model Comparison Matrix ',
|
|
560
|
+
tags: true
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
blessed.text({
|
|
564
|
+
parent: container, top: 0, left: 1, tags: true,
|
|
565
|
+
content: '{#888888-fg}Models run in parallel when "Compare models" is triggered from Tools & Evals.{/#888888-fg}'
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const modelList = blessed.list({
|
|
569
|
+
parent: container,
|
|
570
|
+
top: 2, left: 1, right: 1, height: 10,
|
|
571
|
+
tags: true, keys: true, vi: true,
|
|
572
|
+
style: {
|
|
573
|
+
selected: { bg: '#1a3a5c', fg: 'white' },
|
|
574
|
+
item: { fg: 'white' }
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const statusBar = blessed.box({
|
|
579
|
+
parent: container, bottom: 2, left: 1, right: 1, height: 1, tags: true
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const helpBar = blessed.box({
|
|
583
|
+
parent: container, bottom: 0, left: 1, right: 1, height: 1, tags: true,
|
|
584
|
+
content: ' {bold}n{/bold} add {bold}d{/bold} delete {bold}s{/bold} save {bold}Escape{/bold} cancel'
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
function renderList() {
|
|
588
|
+
if (matrix.length === 0) {
|
|
589
|
+
modelList.setItems(['{#888888-fg}(no models — press n to add){/#888888-fg}']);
|
|
590
|
+
} else {
|
|
591
|
+
modelList.setItems(matrix.map((m) => ` ${m}`));
|
|
592
|
+
}
|
|
593
|
+
screen.render();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
renderList();
|
|
597
|
+
|
|
598
|
+
modelList.key('n', () => {
|
|
599
|
+
const envMap = loadEnv();
|
|
600
|
+
const { items, values } = buildModelList(envMap);
|
|
601
|
+
const popup = blessed.list({
|
|
602
|
+
parent: screen,
|
|
603
|
+
border: 'line',
|
|
604
|
+
height: Math.min(items.length + 4, 20),
|
|
605
|
+
width: 54,
|
|
606
|
+
top: 'center', left: 'center',
|
|
607
|
+
label: ' Add Model to Matrix ',
|
|
608
|
+
tags: true, keys: true, vi: true,
|
|
609
|
+
style: { border: { fg: 'blue' }, selected: { bg: '#1a3a5c', fg: 'white' } },
|
|
610
|
+
items: items.map((it, i) => values[i] === null ? ` ${it}` : ` ${it}`)
|
|
611
|
+
});
|
|
612
|
+
popup.on('select', (item, idx) => {
|
|
613
|
+
const val = values[idx];
|
|
614
|
+
if (!val || val === null) return;
|
|
615
|
+
let modelName = val;
|
|
616
|
+
if (val === '__custom__') {
|
|
617
|
+
popup.destroy();
|
|
618
|
+
const prompt = blessed.prompt({
|
|
619
|
+
parent: screen, border: 'line', height: 'shrink', width: 'half',
|
|
620
|
+
top: 'center', left: 'center', label: ' Custom Model ', tags: true, keys: true
|
|
621
|
+
});
|
|
622
|
+
prompt.input('Model ID:', '', (err, v) => {
|
|
623
|
+
if (!err && v?.trim() && !matrix.includes(v.trim())) {
|
|
624
|
+
matrix.push(v.trim());
|
|
625
|
+
renderList();
|
|
626
|
+
}
|
|
627
|
+
screen.render();
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
popup.destroy();
|
|
632
|
+
if (!matrix.includes(modelName)) {
|
|
633
|
+
matrix.push(modelName);
|
|
634
|
+
renderList();
|
|
635
|
+
}
|
|
636
|
+
modelList.focus();
|
|
637
|
+
screen.render();
|
|
638
|
+
});
|
|
639
|
+
popup.key(['escape', 'q'], () => { popup.destroy(); modelList.focus(); screen.render(); });
|
|
640
|
+
popup.focus();
|
|
641
|
+
screen.render();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
modelList.key('d', () => {
|
|
645
|
+
const idx = modelList.selected;
|
|
646
|
+
if (matrix.length === 0 || idx >= matrix.length) return;
|
|
647
|
+
matrix.splice(idx, 1);
|
|
648
|
+
renderList();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
modelList.key('s', () => {
|
|
652
|
+
cfg.modelMatrix = matrix;
|
|
653
|
+
saveConfig(cfg);
|
|
654
|
+
config.modelMatrix = matrix;
|
|
655
|
+
statusBar.setContent('{green-fg}Saved!{/green-fg}');
|
|
656
|
+
screen.render();
|
|
657
|
+
setTimeout(() => {
|
|
658
|
+
closePopup?.();
|
|
659
|
+
container.destroy();
|
|
660
|
+
onClose?.();
|
|
661
|
+
}, 600);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
openPopup?.();
|
|
665
|
+
container.key(['escape', 'b'], () => { closePopup?.(); container.destroy(); onClose?.(); });
|
|
666
|
+
modelList.focus();
|
|
667
|
+
screen.render();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function showEnvManager(screen, config, openPopup, closePopup, onClose) {
|
|
671
|
+
let envMap = loadEnv();
|
|
672
|
+
let unsaved = false;
|
|
673
|
+
|
|
674
|
+
const container = blessed.box({
|
|
675
|
+
parent: screen,
|
|
676
|
+
border: 'line',
|
|
677
|
+
top: 1,
|
|
678
|
+
left: 2,
|
|
679
|
+
right: 2,
|
|
680
|
+
bottom: 3,
|
|
681
|
+
label: ' API Keys / Secrets ',
|
|
682
|
+
tags: true
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
const table = blessed.listtable({
|
|
686
|
+
parent: container,
|
|
687
|
+
top: 0,
|
|
688
|
+
left: 0,
|
|
689
|
+
width: '100%',
|
|
690
|
+
height: '100%-2',
|
|
691
|
+
tags: true,
|
|
692
|
+
keys: true,
|
|
693
|
+
vi: true,
|
|
694
|
+
style: {
|
|
695
|
+
header: { bold: true, fg: 'cyan' },
|
|
696
|
+
cell: { selected: { bg: 'blue', fg: 'white' } }
|
|
697
|
+
},
|
|
698
|
+
pad: 1
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
const statusBar = blessed.box({
|
|
702
|
+
parent: container,
|
|
703
|
+
bottom: 0,
|
|
704
|
+
left: 0,
|
|
705
|
+
width: '100%',
|
|
706
|
+
height: 1,
|
|
707
|
+
tags: true
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const helpBar = blessed.box({
|
|
711
|
+
parent: screen,
|
|
712
|
+
bottom: 1,
|
|
713
|
+
left: 0,
|
|
714
|
+
width: '100%',
|
|
715
|
+
height: 1,
|
|
716
|
+
tags: true,
|
|
717
|
+
content: ' {bold}Enter{/bold} edit {bold}n{/bold} new key {bold}d{/bold} delete {bold}s{/bold} save {bold}Escape{/bold} close',
|
|
718
|
+
style: { bg: 'black', fg: 'white' }
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
function renderTable() {
|
|
722
|
+
const entries = Object.entries(envMap);
|
|
723
|
+
if (entries.length === 0) {
|
|
724
|
+
table.setData([['Key', 'Value'], ['(no keys)', '']]);
|
|
725
|
+
} else {
|
|
726
|
+
table.setData([['Key', 'Value'], ...entries.map(([k, v]) => [k, maskValue(v)])]);
|
|
727
|
+
}
|
|
728
|
+
statusBar.setContent(unsaved ? ' {yellow-fg}⚠ Unsaved changes — press s to save{/yellow-fg}' : ' {green-fg}Saved{/green-fg}');
|
|
729
|
+
screen.render();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
renderTable();
|
|
733
|
+
|
|
734
|
+
table.key('enter', () => {
|
|
735
|
+
const idx = table.selected;
|
|
736
|
+
if (idx < 1) return;
|
|
737
|
+
const entries = Object.entries(envMap);
|
|
738
|
+
if (!entries[idx - 1]) return;
|
|
739
|
+
const [key, currentVal] = entries[idx - 1];
|
|
740
|
+
|
|
741
|
+
const prompt = blessed.prompt({
|
|
742
|
+
parent: screen,
|
|
743
|
+
border: 'line',
|
|
744
|
+
height: 'shrink',
|
|
745
|
+
width: 'half',
|
|
746
|
+
top: 'center',
|
|
747
|
+
left: 'center',
|
|
748
|
+
label: ` Edit ${key} `,
|
|
749
|
+
tags: true,
|
|
750
|
+
keys: true
|
|
751
|
+
});
|
|
752
|
+
prompt.input(`New value for ${key}:`, currentVal, (err, val) => {
|
|
753
|
+
if (!err && val !== null && val !== undefined) {
|
|
754
|
+
envMap[key] = val;
|
|
755
|
+
unsaved = true;
|
|
756
|
+
renderTable();
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
table.key('n', () => {
|
|
762
|
+
const prompt = blessed.prompt({
|
|
763
|
+
parent: screen,
|
|
764
|
+
border: 'line',
|
|
765
|
+
height: 'shrink',
|
|
766
|
+
width: 'half',
|
|
767
|
+
top: 'center',
|
|
768
|
+
left: 'center',
|
|
769
|
+
label: ' New Key ',
|
|
770
|
+
tags: true,
|
|
771
|
+
keys: true
|
|
772
|
+
});
|
|
773
|
+
prompt.input('Key name:', '', (err, key) => {
|
|
774
|
+
if (!err && key) {
|
|
775
|
+
const prompt2 = blessed.prompt({
|
|
776
|
+
parent: screen,
|
|
777
|
+
border: 'line',
|
|
778
|
+
height: 'shrink',
|
|
779
|
+
width: 'half',
|
|
780
|
+
top: 'center',
|
|
781
|
+
left: 'center',
|
|
782
|
+
label: ` Value for ${key} `,
|
|
783
|
+
tags: true,
|
|
784
|
+
keys: true
|
|
785
|
+
});
|
|
786
|
+
prompt2.input('Value:', '', (err2, val) => {
|
|
787
|
+
if (!err2) {
|
|
788
|
+
envMap[key] = val || '';
|
|
789
|
+
unsaved = true;
|
|
790
|
+
renderTable();
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
table.key('d', () => {
|
|
798
|
+
const idx = table.selected;
|
|
799
|
+
if (idx < 1) return;
|
|
800
|
+
const entries = Object.entries(envMap);
|
|
801
|
+
if (!entries[idx - 1]) return;
|
|
802
|
+
const [key] = entries[idx - 1];
|
|
803
|
+
delete envMap[key];
|
|
804
|
+
unsaved = true;
|
|
805
|
+
renderTable();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
table.key('s', () => {
|
|
809
|
+
saveEnv(envMap);
|
|
810
|
+
unsaved = false;
|
|
811
|
+
renderTable();
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
openPopup?.();
|
|
815
|
+
|
|
816
|
+
function closeEnvManager() {
|
|
817
|
+
closePopup?.();
|
|
818
|
+
container.destroy();
|
|
819
|
+
helpBar.destroy();
|
|
820
|
+
onClose?.();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
container.key(['escape', 'b'], closeEnvManager);
|
|
824
|
+
table.key(['escape', 'b'], closeEnvManager);
|
|
825
|
+
|
|
826
|
+
table.focus();
|
|
827
|
+
screen.render();
|
|
828
|
+
}
|
|
829
|
+
|