@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.
Files changed (46) hide show
  1. package/README.md +2 -1
  2. package/docs/CLI.md +65 -1
  3. package/docs/DEMO.md +450 -0
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/README.md +1 -0
  6. package/package.json +7 -6
  7. package/src/agent/{local.mjs → fixture.mjs} +1 -1
  8. package/src/cli/execute.mjs +6 -4
  9. package/src/cli/interactive-core.mjs +5 -279
  10. package/src/cli/interactive-io.mjs +101 -0
  11. package/src/cli/interactive-slash.mjs +184 -0
  12. package/src/cli/repl.mjs +1 -2
  13. package/src/cli/tui-actions.mjs +72 -0
  14. package/src/cli/tui-blessed.mjs +198 -0
  15. package/src/cli/tui-keys.mjs +80 -0
  16. package/src/cli/tui-lane.mjs +73 -0
  17. package/src/cli/tui-render.mjs +169 -0
  18. package/src/cli/tui-submit.mjs +82 -0
  19. package/src/cli/tui.mjs +30 -613
  20. package/src/commands/permissions-eval.mjs +122 -0
  21. package/src/commands/permissions-rules.mjs +53 -0
  22. package/src/commands/permissions-text.mjs +112 -0
  23. package/src/commands/permissions.mjs +15 -281
  24. package/src/commands/usage.mjs +1 -1
  25. package/src/constants.mjs +7 -1
  26. package/src/mcp/local.mjs +55 -0
  27. package/src/mcp/parsers.mjs +46 -0
  28. package/src/mcp/probe.mjs +12 -351
  29. package/src/mcp/remote-oauth.mjs +55 -0
  30. package/src/mcp/remote-session.mjs +54 -0
  31. package/src/mcp/remote-sse.mjs +82 -0
  32. package/src/mcp/remote.mjs +74 -0
  33. package/src/plugins/api.mjs +187 -0
  34. package/src/plugins/configuration.mjs +124 -0
  35. package/src/plugins/discover.mjs +8 -804
  36. package/src/plugins/helpers.mjs +187 -0
  37. package/src/plugins/subsystems.mjs +198 -0
  38. package/src/plugins/validators.mjs +142 -0
  39. package/src/sdk-execute.mjs +82 -0
  40. package/src/sdk-settings.mjs +88 -0
  41. package/src/sdk.mjs +13 -164
  42. package/src/tools/builtin/oracle.mjs +2 -2
  43. package/src/tools/builtin/runtime-content.mjs +31 -0
  44. package/src/tools/builtin/runtime-decisions.mjs +115 -0
  45. package/src/tools/builtin/runtime.mjs +18 -148
  46. 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 { CLI_NAME, VERSION } from '../constants.mjs';
4
- import { createInteractiveSession, handleInteractiveInput } from './interactive-core.mjs';
5
- import { splitShellWords } from '../util/shell.mjs';
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
- const TABS = ['chat', 'lane', 'tools', 'threads', 'config', 'help'];
27
- const PALETTE_ACTIONS = [
28
- ['New thread', '/new'],
29
- ['Continue latest thread', '/continue'],
30
- ['Refresh lane', '/lane refresh'],
31
- ['Cycle harness', '/lane harness next'],
32
- ['Run verification', '/lane verify'],
33
- ['Open help', '/help'],
34
- ['List tools', '/tools list'],
35
- ['List skills', '/skill: list'],
36
- ['List plugins', '/plugins: list'],
37
- ['Open editor', '/editor'],
38
- ['Edit previous prompt', '/edit'],
39
- ['Archive and quit', '/thread: archive and quit'],
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 (isPrintableKey(key)) {
133
- insertComposerText(model, key.sequence);
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 && key.name === 'enter') {
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
- }