@opencoven/coven-code 0.0.1 → 0.0.2
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 +2 -1
- package/docs/CLI.md +65 -1
- package/docs/DEMO.md +450 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/README.md +1 -0
- package/package.json +7 -6
- package/src/agent/{local.mjs → fixture.mjs} +1 -1
- package/src/cli/execute.mjs +6 -4
- package/src/cli/interactive-core.mjs +5 -279
- package/src/cli/interactive-io.mjs +101 -0
- package/src/cli/interactive-slash.mjs +184 -0
- package/src/cli/repl.mjs +1 -2
- package/src/cli/tui-actions.mjs +72 -0
- package/src/cli/tui-blessed.mjs +198 -0
- package/src/cli/tui-keys.mjs +80 -0
- package/src/cli/tui-lane.mjs +73 -0
- package/src/cli/tui-render.mjs +169 -0
- package/src/cli/tui-submit.mjs +82 -0
- package/src/cli/tui.mjs +30 -613
- package/src/commands/permissions-eval.mjs +122 -0
- package/src/commands/permissions-rules.mjs +53 -0
- package/src/commands/permissions-text.mjs +112 -0
- package/src/commands/permissions.mjs +15 -281
- package/src/commands/usage.mjs +1 -1
- package/src/constants.mjs +7 -1
- package/src/mcp/local.mjs +55 -0
- package/src/mcp/parsers.mjs +46 -0
- package/src/mcp/probe.mjs +12 -351
- package/src/mcp/remote-oauth.mjs +55 -0
- package/src/mcp/remote-session.mjs +54 -0
- package/src/mcp/remote-sse.mjs +82 -0
- package/src/mcp/remote.mjs +74 -0
- package/src/plugins/api.mjs +187 -0
- package/src/plugins/configuration.mjs +124 -0
- package/src/plugins/discover.mjs +8 -804
- package/src/plugins/helpers.mjs +187 -0
- package/src/plugins/subsystems.mjs +198 -0
- package/src/plugins/validators.mjs +142 -0
- package/src/sdk-execute.mjs +82 -0
- package/src/sdk-settings.mjs +88 -0
- package/src/sdk.mjs +13 -164
- package/src/tools/builtin/oracle.mjs +2 -2
- package/src/tools/builtin/runtime-content.mjs +31 -0
- package/src/tools/builtin/runtime-decisions.mjs +115 -0
- package/src/tools/builtin/runtime.mjs +18 -148
- package/src/tools/builtin/task.mjs +2 -2
package/src/cli/tui.mjs
CHANGED
|
@@ -1,46 +1,37 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import { runInteractive } from './repl.mjs';
|
|
3
|
-
import {
|
|
4
|
-
import { createInteractiveSession
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
defaultLaneState,
|
|
8
|
-
inspectLane,
|
|
9
|
-
nextLaneHarness,
|
|
10
|
-
normalizeLaneHarness,
|
|
11
|
-
runLaneVerification,
|
|
12
|
-
} from '../agent/lane.mjs';
|
|
2
|
+
import { VERSION } from '../constants.mjs';
|
|
3
|
+
import { createInteractiveSession } from './interactive-core.mjs';
|
|
4
|
+
import { defaultLaneState } from '../agent/lane.mjs';
|
|
13
5
|
import { displayCwd } from '../util/fs.mjs';
|
|
14
|
-
import { findWorkspaceSettingsFile, settingsFile } from '../settings/paths.mjs';
|
|
15
|
-
import { readEffectiveSettings } from '../settings/load.mjs';
|
|
16
|
-
import { listThreads } from '../threads/store.mjs';
|
|
17
6
|
import {
|
|
18
|
-
buildSlashCommandCatalog,
|
|
19
7
|
buildStaticSlashCommandCatalog,
|
|
20
|
-
builtinToolSummaryLines,
|
|
21
8
|
filterSlashCommands,
|
|
22
|
-
formatSlashCommandDetails,
|
|
23
|
-
formatSlashHelpLines,
|
|
24
9
|
} from './slash-commands.mjs';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
10
|
+
import {
|
|
11
|
+
TABS,
|
|
12
|
+
buildPanelSummaries,
|
|
13
|
+
renderCompactStatus,
|
|
14
|
+
renderComposerLines,
|
|
15
|
+
renderSlashOverlay,
|
|
16
|
+
renderTabContent,
|
|
17
|
+
renderTabLine,
|
|
18
|
+
} from './tui-render.mjs';
|
|
19
|
+
import { runLiveTui } from './tui-blessed.mjs';
|
|
20
|
+
import {
|
|
21
|
+
closeSlashMenu,
|
|
22
|
+
insertComposerText,
|
|
23
|
+
safeBuildSlashCommandCatalog,
|
|
24
|
+
updateSlashState,
|
|
25
|
+
} from './tui-actions.mjs';
|
|
26
|
+
import { submitTuiText } from './tui-submit.mjs';
|
|
27
|
+
import {
|
|
28
|
+
handleComposerKey,
|
|
29
|
+
handlePaletteKey,
|
|
30
|
+
handleSlashMenuKey,
|
|
31
|
+
} from './tui-keys.mjs';
|
|
41
32
|
|
|
42
33
|
export async function runTuiInteractive(parsed, initialInput = '') {
|
|
43
|
-
const session = createInteractiveSession(parsed);
|
|
34
|
+
const session = createInteractiveSession(parsed, { silent: true });
|
|
44
35
|
const slashCatalog = await safeBuildSlashCommandCatalog(parsed);
|
|
45
36
|
const model = createTuiModel({
|
|
46
37
|
mode: parsed.mode,
|
|
@@ -59,7 +50,7 @@ export async function runTuiInteractive(parsed, initialInput = '') {
|
|
|
59
50
|
return;
|
|
60
51
|
}
|
|
61
52
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return runInteractive(parsed, initialInput);
|
|
62
|
-
return runLiveTui(model, session);
|
|
53
|
+
return runLiveTui(model, session, handleTuiKey);
|
|
63
54
|
}
|
|
64
55
|
|
|
65
56
|
export function createTuiModel(options = {}) {
|
|
@@ -129,58 +120,8 @@ export async function handleTuiKey(model, session, key = {}) {
|
|
|
129
120
|
return;
|
|
130
121
|
}
|
|
131
122
|
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (key.name === 'backspace' || key.name === 'delete') {
|
|
138
|
-
deleteComposerText(model, key.name);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (key.name === 'left') {
|
|
143
|
-
model.composerCursor = Math.max(0, model.composerCursor - 1);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (key.name === 'right') {
|
|
148
|
-
model.composerCursor = Math.min(model.composer.length, model.composerCursor + 1);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (key.name === 'home') {
|
|
153
|
-
model.composerCursor = 0;
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (key.name === 'end') {
|
|
158
|
-
model.composerCursor = model.composer.length;
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (model.slashOpen) {
|
|
163
|
-
if (key.name === 'escape') {
|
|
164
|
-
closeSlashMenu(model);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (key.name === 'down') {
|
|
168
|
-
model.slashIndex = (model.slashIndex + 1) % Math.max(1, model.slashMatches.length);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (key.name === 'up') {
|
|
172
|
-
model.slashIndex = (model.slashIndex + Math.max(1, model.slashMatches.length) - 1) % Math.max(1, model.slashMatches.length);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (key.name === 'tab') {
|
|
176
|
-
completeSlashSelection(model);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (key.name === 'enter') {
|
|
180
|
-
await acceptSlashSelection(model, session);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
123
|
+
if (handleComposerKey(model, key)) return;
|
|
124
|
+
if (model.slashOpen && await handleSlashMenuKey(model, session, key)) return;
|
|
184
125
|
|
|
185
126
|
if (key.name === 'tab') {
|
|
186
127
|
const index = TABS.indexOf(model.activeTab);
|
|
@@ -199,22 +140,7 @@ export async function handleTuiKey(model, session, key = {}) {
|
|
|
199
140
|
return;
|
|
200
141
|
}
|
|
201
142
|
|
|
202
|
-
if (model.paletteOpen &&
|
|
203
|
-
const [, command] = PALETTE_ACTIONS[model.paletteIndex ?? 0] ?? PALETTE_ACTIONS[0];
|
|
204
|
-
model.paletteOpen = false;
|
|
205
|
-
await submitTuiText(model, session, command);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (model.paletteOpen && key.name === 'down') {
|
|
210
|
-
model.paletteIndex = (model.paletteIndex + 1) % PALETTE_ACTIONS.length;
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (model.paletteOpen && key.name === 'up') {
|
|
215
|
-
model.paletteIndex = (model.paletteIndex + PALETTE_ACTIONS.length - 1) % PALETTE_ACTIONS.length;
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
143
|
+
if (model.paletteOpen && await handlePaletteKey(model, session, key)) return;
|
|
218
144
|
|
|
219
145
|
if (key.ctrl && key.name === 'n') {
|
|
220
146
|
await submitTuiText(model, session, '/new');
|
|
@@ -246,512 +172,3 @@ export async function handleTuiKey(model, session, key = {}) {
|
|
|
246
172
|
await submitTuiText(model, session, text);
|
|
247
173
|
}
|
|
248
174
|
}
|
|
249
|
-
|
|
250
|
-
function renderTabContent(model, limit, width) {
|
|
251
|
-
if (model.activeTab === 'help') return clipLines(formatSlashHelpLines(model.slashCatalog), limit, width);
|
|
252
|
-
if (model.activeTab === 'lane') return renderLaneLines(model, limit, width);
|
|
253
|
-
if (model.activeTab === 'tools') return clipLines(model.panels.tools, limit, width);
|
|
254
|
-
if (model.activeTab === 'threads') return clipLines(model.panels.threads, limit, width);
|
|
255
|
-
if (model.activeTab === 'config') return clipLines(model.panels.config, limit, width);
|
|
256
|
-
return renderTranscript(model, limit, width);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function renderTranscript(model, limit, width) {
|
|
260
|
-
const entries = model.transcript.slice(-Math.max(1, limit)).flatMap((entry) => [
|
|
261
|
-
`${entry.role}:`,
|
|
262
|
-
...String(entry.text).split(/\r?\n/),
|
|
263
|
-
]);
|
|
264
|
-
return clipLines(entries.length > 0 ? entries.slice(-limit) : ['Ready. Type a prompt or /help.'], limit, width);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function renderComposerLines(model, width) {
|
|
268
|
-
const prompt = model.composer || '';
|
|
269
|
-
const lines = String(prompt).split('\n');
|
|
270
|
-
return lines.map((line, index) => `${index === 0 ? '> ' : ' '}${line}`.slice(0, width));
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function renderSlashOverlay(model, width, limit) {
|
|
274
|
-
const listWidth = Math.min(34, Math.floor(width * 0.38));
|
|
275
|
-
const detailWidth = width - listWidth - 3;
|
|
276
|
-
const matches = currentSlashMatches(model);
|
|
277
|
-
const selected = matches[model.slashIndex] ?? matches[0];
|
|
278
|
-
const listLines = ['Slash commands', ...matches.map((entry, index) => {
|
|
279
|
-
const marker = index === model.slashIndex ? '>' : ' ';
|
|
280
|
-
const status = entry.availability?.type === 'disabled' ? ' disabled' : '';
|
|
281
|
-
return `${marker} ${entry.command}${status}`;
|
|
282
|
-
})];
|
|
283
|
-
const detailLines = ['Details', ...formatSlashCommandDetails(selected)];
|
|
284
|
-
const count = Math.min(limit, Math.max(listLines.length, detailLines.length));
|
|
285
|
-
const rows = [];
|
|
286
|
-
for (let index = 0; index < count; index += 1) {
|
|
287
|
-
const left = (listLines[index] ?? '').padEnd(listWidth).slice(0, listWidth);
|
|
288
|
-
const right = (detailLines[index] ?? '').slice(0, detailWidth);
|
|
289
|
-
rows.push(`${left} | ${right}`);
|
|
290
|
-
}
|
|
291
|
-
return rows;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function renderCompactStatus(model) {
|
|
295
|
-
return [
|
|
296
|
-
`thread: ${shortThreadId(model.threadId)}`,
|
|
297
|
-
`lane: ${model.lane.branch}`,
|
|
298
|
-
`harness: ${model.lane.harness}`,
|
|
299
|
-
`queued: ${model.queueCount}`,
|
|
300
|
-
`tools: ${model.toolCount}`,
|
|
301
|
-
`tab: ${model.activeTab}`,
|
|
302
|
-
`status: ${model.status}`,
|
|
303
|
-
].join(' | ');
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function buildPanelSummaries(parsed = {}, slashCatalog = buildStaticSlashCommandCatalog(), cwd = process.cwd()) {
|
|
307
|
-
const threads = listThreads().slice(0, 8);
|
|
308
|
-
const latest = threads
|
|
309
|
-
.filter((thread) => !thread.archived)
|
|
310
|
-
.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0];
|
|
311
|
-
const settingsPath = settingsFile(parsed);
|
|
312
|
-
const workspacePath = findWorkspaceSettingsFile(cwd);
|
|
313
|
-
const settings = readEffectiveSettings(parsed, { cwd });
|
|
314
|
-
return {
|
|
315
|
-
tools: [
|
|
316
|
-
...builtinToolSummaryLines(),
|
|
317
|
-
'',
|
|
318
|
-
'Slash command sources:',
|
|
319
|
-
`commands: ${slashCatalog.length}`,
|
|
320
|
-
],
|
|
321
|
-
threads: [
|
|
322
|
-
`Recent threads: ${threads.length}`,
|
|
323
|
-
`Latest: ${latest?.id ?? 'none'}`,
|
|
324
|
-
...threads.map((thread) => `${thread.id} ${thread.archived ? 'archived' : 'active'} ${thread.title}`),
|
|
325
|
-
],
|
|
326
|
-
config: [
|
|
327
|
-
'Settings',
|
|
328
|
-
`user: ${settingsPath}${existsSync(settingsPath) ? '' : ' (not created)'}`,
|
|
329
|
-
`workspace: ${workspacePath ?? 'none'}`,
|
|
330
|
-
`visibility: ${settings['covenCode.defaultVisibility'] ?? 'private'}`,
|
|
331
|
-
`updates: ${settings['covenCode.updates.mode'] ?? 'default'}`,
|
|
332
|
-
],
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function submitTuiText(model, session, text) {
|
|
337
|
-
if (!text) return;
|
|
338
|
-
model.transcript.push({ role: 'you', text });
|
|
339
|
-
model.status = 'running';
|
|
340
|
-
const { result, stdout, stderr } = isLaneCommand(text)
|
|
341
|
-
? await handleTuiLaneCommand(model, session, text)
|
|
342
|
-
: await captureTerminalOutput(() => handleInteractiveInput(session, text));
|
|
343
|
-
model.mode = session.parsed.mode;
|
|
344
|
-
model.reasoningEffort = session.parsed.reasoningEffort ?? model.reasoningEffort;
|
|
345
|
-
model.threadId = session.thread?.id ?? 'new thread';
|
|
346
|
-
model.queueCount = session.queuedMessages.length;
|
|
347
|
-
rememberLaneTerminal(model, stdout, stderr, result.lines);
|
|
348
|
-
model.panels = buildPanelSummaries(session.parsed, model.slashCatalog, model.workspaceCwd);
|
|
349
|
-
if (stderr.trim()) {
|
|
350
|
-
model.transcript.push({ role: 'error', text: stderr.trim() });
|
|
351
|
-
}
|
|
352
|
-
if (stdout.trim()) {
|
|
353
|
-
model.transcript.push({ role: 'coven', text: stdout.trim() });
|
|
354
|
-
}
|
|
355
|
-
if (result.lines.length > 0) {
|
|
356
|
-
model.transcript.push({
|
|
357
|
-
role: result.kind === 'error' ? 'error' : 'coven',
|
|
358
|
-
text: result.lines.join('\n'),
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
model.status = result.kind === 'exit' ? 'done' : 'idle';
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function runLiveTui(model, session) {
|
|
365
|
-
try {
|
|
366
|
-
const blessedModule = await import('neo-blessed');
|
|
367
|
-
return runBlessedTui(blessedModule.default ?? blessedModule, model, session);
|
|
368
|
-
} catch (error) {
|
|
369
|
-
console.error(`${CLI_NAME}: unable to start panel TUI, falling back to classic REPL: ${error?.message ?? error}`);
|
|
370
|
-
return runInteractive(session.parsed, '');
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function runBlessedTui(blessed, model, session) {
|
|
375
|
-
return new Promise((resolve) => {
|
|
376
|
-
const screen = blessed.screen({
|
|
377
|
-
smartCSR: true,
|
|
378
|
-
fullUnicode: true,
|
|
379
|
-
title: `${CLI_NAME} ${model.version}`,
|
|
380
|
-
});
|
|
381
|
-
const header = blessed.box({
|
|
382
|
-
top: 0,
|
|
383
|
-
left: 0,
|
|
384
|
-
width: '100%',
|
|
385
|
-
height: 4,
|
|
386
|
-
padding: { left: 1, right: 1 },
|
|
387
|
-
style: { fg: 'white', bg: 'black' },
|
|
388
|
-
});
|
|
389
|
-
const transcript = blessed.box({
|
|
390
|
-
top: 4,
|
|
391
|
-
left: 0,
|
|
392
|
-
width: '100%',
|
|
393
|
-
bottom: 4,
|
|
394
|
-
scrollable: true,
|
|
395
|
-
alwaysScroll: true,
|
|
396
|
-
keys: true,
|
|
397
|
-
vi: true,
|
|
398
|
-
padding: { left: 1, right: 1 },
|
|
399
|
-
scrollbar: { ch: ' ', style: { bg: 'white' } },
|
|
400
|
-
});
|
|
401
|
-
const composer = blessed.box({
|
|
402
|
-
bottom: 1,
|
|
403
|
-
left: 0,
|
|
404
|
-
width: '100%',
|
|
405
|
-
height: 3,
|
|
406
|
-
padding: { left: 1, right: 1 },
|
|
407
|
-
style: {
|
|
408
|
-
border: { fg: 'green' },
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
const status = blessed.box({
|
|
412
|
-
bottom: 0,
|
|
413
|
-
left: 0,
|
|
414
|
-
width: '100%',
|
|
415
|
-
height: 1,
|
|
416
|
-
style: { fg: 'white', bg: 'black' },
|
|
417
|
-
});
|
|
418
|
-
const palette = blessed.list({
|
|
419
|
-
top: 'center',
|
|
420
|
-
left: 'center',
|
|
421
|
-
width: '60%',
|
|
422
|
-
height: Math.min(13, PALETTE_ACTIONS.length + 2),
|
|
423
|
-
label: ' Command Palette ',
|
|
424
|
-
border: 'line',
|
|
425
|
-
hidden: true,
|
|
426
|
-
keys: true,
|
|
427
|
-
mouse: true,
|
|
428
|
-
items: PALETTE_ACTIONS.map(([label, command]) => `${label} ${command}`),
|
|
429
|
-
style: {
|
|
430
|
-
border: { fg: 'yellow' },
|
|
431
|
-
selected: { bg: 'blue', fg: 'white' },
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
const slashList = blessed.list({
|
|
435
|
-
bottom: 4,
|
|
436
|
-
left: 0,
|
|
437
|
-
width: '36%',
|
|
438
|
-
height: '50%',
|
|
439
|
-
label: ' Slash commands ',
|
|
440
|
-
border: 'line',
|
|
441
|
-
hidden: true,
|
|
442
|
-
keys: true,
|
|
443
|
-
mouse: true,
|
|
444
|
-
style: {
|
|
445
|
-
border: { fg: 'cyan' },
|
|
446
|
-
selected: { bg: 'blue', fg: 'white' },
|
|
447
|
-
},
|
|
448
|
-
});
|
|
449
|
-
const slashDetails = blessed.box({
|
|
450
|
-
bottom: 4,
|
|
451
|
-
right: 0,
|
|
452
|
-
width: '64%',
|
|
453
|
-
height: '50%',
|
|
454
|
-
label: ' Details ',
|
|
455
|
-
border: 'line',
|
|
456
|
-
hidden: true,
|
|
457
|
-
padding: { left: 1, right: 1 },
|
|
458
|
-
style: {
|
|
459
|
-
border: { fg: 'cyan' },
|
|
460
|
-
},
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
screen.append(header);
|
|
464
|
-
screen.append(transcript);
|
|
465
|
-
screen.append(composer);
|
|
466
|
-
screen.append(status);
|
|
467
|
-
screen.append(palette);
|
|
468
|
-
screen.append(slashList);
|
|
469
|
-
screen.append(slashDetails);
|
|
470
|
-
screen.program.hideCursor();
|
|
471
|
-
|
|
472
|
-
let settled = false;
|
|
473
|
-
const cleanup = () => {
|
|
474
|
-
if (settled) return;
|
|
475
|
-
settled = true;
|
|
476
|
-
screen.program.showCursor();
|
|
477
|
-
screen.destroy();
|
|
478
|
-
resolve();
|
|
479
|
-
};
|
|
480
|
-
const sync = () => {
|
|
481
|
-
const width = Number(screen.width) || 80;
|
|
482
|
-
header.setContent(`Coven Code ${model.version}\n${model.cwd}\n${renderTabLine(model)} mode: ${model.mode} effort: ${model.reasoningEffort}`);
|
|
483
|
-
transcript.setContent(renderTabContent(model, Math.max(1, Number(transcript.height) - 2), Math.max(20, width - 4)).join('\n'));
|
|
484
|
-
composer.setContent(renderComposerLines(model, width - 4).join('\n'));
|
|
485
|
-
status.setContent(renderCompactStatus(model));
|
|
486
|
-
if (model.paletteOpen) {
|
|
487
|
-
palette.show();
|
|
488
|
-
palette.select(model.paletteIndex);
|
|
489
|
-
palette.focus();
|
|
490
|
-
} else {
|
|
491
|
-
palette.hide();
|
|
492
|
-
}
|
|
493
|
-
if (model.slashOpen) {
|
|
494
|
-
const matches = currentSlashMatches(model);
|
|
495
|
-
const selected = matches[model.slashIndex] ?? matches[0];
|
|
496
|
-
slashList.setItems(matches.map((entry) => `${entry.command} ${entry.title}`));
|
|
497
|
-
slashList.show();
|
|
498
|
-
slashList.select(model.slashIndex);
|
|
499
|
-
slashDetails.setContent(formatSlashCommandDetails(selected).join('\n'));
|
|
500
|
-
slashDetails.show();
|
|
501
|
-
} else {
|
|
502
|
-
slashList.hide();
|
|
503
|
-
slashDetails.hide();
|
|
504
|
-
}
|
|
505
|
-
screen.render();
|
|
506
|
-
};
|
|
507
|
-
const dispatchKey = async (key) => {
|
|
508
|
-
await handleTuiKey(model, session, normalizeBlessedKey(key));
|
|
509
|
-
sync();
|
|
510
|
-
if (model.status === 'done') cleanup();
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
screen.on('keypress', async (chunk, key = {}) => {
|
|
514
|
-
if (settled) return;
|
|
515
|
-
const normalized = normalizeBlessedKey({
|
|
516
|
-
...key,
|
|
517
|
-
sequence: isPrintableChunk(chunk, key) ? chunk : key.sequence,
|
|
518
|
-
});
|
|
519
|
-
await dispatchKey(normalized);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
screen.on('resize', sync);
|
|
523
|
-
sync();
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function renderTabLine(model) {
|
|
528
|
-
return TABS.map((tab) => tab === model.activeTab ? `[${tab}]` : ` ${tab} `).join(' ');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function insertComposerText(model, text) {
|
|
532
|
-
model.composer = `${model.composer.slice(0, model.composerCursor)}${text}${model.composer.slice(model.composerCursor)}`;
|
|
533
|
-
model.composerCursor += text.length;
|
|
534
|
-
updateSlashState(model);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function deleteComposerText(model, kind) {
|
|
538
|
-
if (kind === 'delete') {
|
|
539
|
-
if (model.composerCursor >= model.composer.length) return;
|
|
540
|
-
model.composer = `${model.composer.slice(0, model.composerCursor)}${model.composer.slice(model.composerCursor + 1)}`;
|
|
541
|
-
} else {
|
|
542
|
-
if (model.composerCursor <= 0) return;
|
|
543
|
-
model.composer = `${model.composer.slice(0, model.composerCursor - 1)}${model.composer.slice(model.composerCursor)}`;
|
|
544
|
-
model.composerCursor -= 1;
|
|
545
|
-
}
|
|
546
|
-
updateSlashState(model);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function updateSlashState(model) {
|
|
550
|
-
const beforeCursor = model.composer.slice(0, model.composerCursor);
|
|
551
|
-
const active = beforeCursor.startsWith('/') && !/\s/.test(beforeCursor.slice(1));
|
|
552
|
-
if (!active) {
|
|
553
|
-
closeSlashMenu(model);
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
model.slashOpen = true;
|
|
557
|
-
model.slashQuery = beforeCursor.replace(/^\/+/, '');
|
|
558
|
-
model.slashMatches = filterSlashCommands(model.slashCatalog, beforeCursor);
|
|
559
|
-
if (model.slashMatches.length === 0) model.slashIndex = 0;
|
|
560
|
-
else model.slashIndex = Math.min(model.slashIndex, model.slashMatches.length - 1);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function closeSlashMenu(model) {
|
|
564
|
-
model.slashOpen = false;
|
|
565
|
-
model.slashQuery = '';
|
|
566
|
-
model.slashMatches = filterSlashCommands(model.slashCatalog, '');
|
|
567
|
-
model.slashIndex = 0;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function completeSlashSelection(model) {
|
|
571
|
-
const selected = currentSlashMatches(model)[model.slashIndex];
|
|
572
|
-
if (!selected) return;
|
|
573
|
-
model.composer = `${selected.command} `;
|
|
574
|
-
model.composerCursor = model.composer.length;
|
|
575
|
-
closeSlashMenu(model);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async function acceptSlashSelection(model, session) {
|
|
579
|
-
const selected = currentSlashMatches(model)[model.slashIndex];
|
|
580
|
-
if (!selected) return;
|
|
581
|
-
const command = selected.command;
|
|
582
|
-
model.composer = '';
|
|
583
|
-
model.composerCursor = 0;
|
|
584
|
-
closeSlashMenu(model);
|
|
585
|
-
await submitTuiText(model, session, command);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function currentSlashMatches(model) {
|
|
589
|
-
return model.slashMatches.length > 0 ? model.slashMatches : filterSlashCommands(model.slashCatalog, model.slashQuery);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function clipLines(lines, limit, width) {
|
|
593
|
-
return lines
|
|
594
|
-
.slice(0, Math.max(0, limit))
|
|
595
|
-
.map((line) => String(line).slice(0, width));
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
function shortThreadId(threadId) {
|
|
599
|
-
if (!threadId || threadId === 'new thread') return 'new';
|
|
600
|
-
return String(threadId).slice(0, 14);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function renderLaneLines(model, limit, width) {
|
|
604
|
-
const lane = model.lane;
|
|
605
|
-
const changedFiles = lane.changedFiles.length > 0 ? lane.changedFiles : ['none'];
|
|
606
|
-
const lines = [
|
|
607
|
-
`worktree: ${lane.worktree}`,
|
|
608
|
-
`branch: ${lane.branch}`,
|
|
609
|
-
`base: ${lane.baseBranch}`,
|
|
610
|
-
`harness: ${lane.harness}`,
|
|
611
|
-
`status: ${lane.status}`,
|
|
612
|
-
`verify: ${lane.verification.status} (${lane.verification.command})`,
|
|
613
|
-
`PR: ${lane.pullRequest}`,
|
|
614
|
-
`merge: ${lane.merge}`,
|
|
615
|
-
`cleanup: ${lane.cleanup}`,
|
|
616
|
-
'',
|
|
617
|
-
'Changed files',
|
|
618
|
-
...changedFiles.map((file) => ` ${file}`),
|
|
619
|
-
'',
|
|
620
|
-
'Diff',
|
|
621
|
-
lane.diffSummary || ' no diff summary',
|
|
622
|
-
'',
|
|
623
|
-
'Terminal',
|
|
624
|
-
...(lane.terminalLines.length > 0 ? lane.terminalLines : [' no lane terminal output yet']),
|
|
625
|
-
];
|
|
626
|
-
return lines.slice(0, limit).map((line) => line.slice(0, width));
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function isLaneCommand(text) {
|
|
630
|
-
return /^\/lane(?:\s|$)/.test(text);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
async function handleTuiLaneCommand(model, session, text) {
|
|
634
|
-
try {
|
|
635
|
-
const [, subcommand = 'status', ...rest] = splitShellWords(text.slice(1));
|
|
636
|
-
if (subcommand === 'refresh') {
|
|
637
|
-
const inspector = session.laneInspector ?? inspectLane;
|
|
638
|
-
model.lane = await inspector({
|
|
639
|
-
cwd: process.cwd(),
|
|
640
|
-
harness: model.lane.harness,
|
|
641
|
-
verification: model.lane.verification,
|
|
642
|
-
});
|
|
643
|
-
model.activeTab = 'lane';
|
|
644
|
-
return laneCommandResult(`lane refreshed: ${model.lane.branch}`);
|
|
645
|
-
}
|
|
646
|
-
if (subcommand === 'harness') {
|
|
647
|
-
const requested = rest[0] === 'next' ? nextLaneHarness(model.lane.harness) : rest[0];
|
|
648
|
-
const harness = normalizeLaneHarness(requested);
|
|
649
|
-
model.lane = { ...model.lane, harness };
|
|
650
|
-
model.activeTab = 'lane';
|
|
651
|
-
return laneCommandResult(`harness: ${harness}`);
|
|
652
|
-
}
|
|
653
|
-
if (subcommand === 'verify') {
|
|
654
|
-
const verifier = session.laneVerifier ?? runLaneVerification;
|
|
655
|
-
model.lane = await verifier(model.lane);
|
|
656
|
-
model.activeTab = 'lane';
|
|
657
|
-
return laneCommandResult(`verification: ${model.lane.verification.status}`);
|
|
658
|
-
}
|
|
659
|
-
if (subcommand === 'diff') {
|
|
660
|
-
model.activeTab = 'lane';
|
|
661
|
-
return laneCommandResult(model.lane.diffSummary || 'no diff summary');
|
|
662
|
-
}
|
|
663
|
-
if (subcommand === 'status') {
|
|
664
|
-
model.activeTab = 'lane';
|
|
665
|
-
return laneCommandResult(renderLaneLines(model, 40, 120).join('\n'));
|
|
666
|
-
}
|
|
667
|
-
return laneCommandResult(`${CLI_NAME}: Unknown lane command: ${subcommand}`, 'error');
|
|
668
|
-
} catch (error) {
|
|
669
|
-
return laneCommandResult(`${CLI_NAME}: ${error?.message ?? error}`, 'error');
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function laneCommandResult(text, kind = 'command') {
|
|
674
|
-
return {
|
|
675
|
-
result: { kind, lines: [text] },
|
|
676
|
-
stdout: '',
|
|
677
|
-
stderr: '',
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function rememberLaneTerminal(model, stdout, stderr, resultLines = []) {
|
|
682
|
-
const lines = [stdout, stderr, resultLines.join('\n')]
|
|
683
|
-
.flatMap((text) => String(text ?? '').split(/\r?\n/))
|
|
684
|
-
.map((line) => line.trimEnd())
|
|
685
|
-
.filter(Boolean);
|
|
686
|
-
if (lines.length === 0) return;
|
|
687
|
-
model.lane = {
|
|
688
|
-
...model.lane,
|
|
689
|
-
terminalLines: [...(model.lane.terminalLines ?? []), ...lines].slice(-40),
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function normalizeBlessedKey(key = {}) {
|
|
694
|
-
if (key.name === 'return') return { ...key, name: 'enter' };
|
|
695
|
-
return key;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function isPrintableKey(key = {}) {
|
|
699
|
-
return isPrintableChunk(key.sequence, key);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function isPrintableChunk(chunk, key = {}) {
|
|
703
|
-
return typeof chunk === 'string'
|
|
704
|
-
&& chunk.length > 0
|
|
705
|
-
&& !key.ctrl
|
|
706
|
-
&& !key.meta
|
|
707
|
-
&& key.name !== 'return'
|
|
708
|
-
&& key.name !== 'enter'
|
|
709
|
-
&& key.name !== 'tab'
|
|
710
|
-
&& key.name !== 'escape'
|
|
711
|
-
&& key.name !== 'up'
|
|
712
|
-
&& key.name !== 'down'
|
|
713
|
-
&& key.name !== 'left'
|
|
714
|
-
&& key.name !== 'right';
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
async function captureTerminalOutput(fn) {
|
|
718
|
-
let stdout = '';
|
|
719
|
-
let stderr = '';
|
|
720
|
-
const originalStdoutWrite = process.stdout.write;
|
|
721
|
-
const originalStderrWrite = process.stderr.write;
|
|
722
|
-
process.stdout.write = function tuiStdoutWrite(chunk, encoding, callback) {
|
|
723
|
-
stdout += normalizeWriteChunk(chunk, encoding);
|
|
724
|
-
callWriteCallback(encoding, callback);
|
|
725
|
-
return true;
|
|
726
|
-
};
|
|
727
|
-
process.stderr.write = function tuiStderrWrite(chunk, encoding, callback) {
|
|
728
|
-
stderr += normalizeWriteChunk(chunk, encoding);
|
|
729
|
-
callWriteCallback(encoding, callback);
|
|
730
|
-
return true;
|
|
731
|
-
};
|
|
732
|
-
try {
|
|
733
|
-
const result = await fn();
|
|
734
|
-
return { result, stdout, stderr };
|
|
735
|
-
} finally {
|
|
736
|
-
process.stdout.write = originalStdoutWrite;
|
|
737
|
-
process.stderr.write = originalStderrWrite;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function normalizeWriteChunk(chunk, encoding) {
|
|
742
|
-
if (Buffer.isBuffer(chunk)) return chunk.toString(typeof encoding === 'string' ? encoding : 'utf8');
|
|
743
|
-
return String(chunk);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function callWriteCallback(encoding, callback) {
|
|
747
|
-
if (typeof encoding === 'function') encoding();
|
|
748
|
-
else if (typeof callback === 'function') callback();
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
async function safeBuildSlashCommandCatalog(parsed) {
|
|
752
|
-
try {
|
|
753
|
-
return await buildSlashCommandCatalog({ parsed, cwd: process.cwd() });
|
|
754
|
-
} catch {
|
|
755
|
-
return buildStaticSlashCommandCatalog();
|
|
756
|
-
}
|
|
757
|
-
}
|