@opencoven/coven-code 0.0.1
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 +145 -0
- package/bin/coven-code-sdk.mjs +12 -0
- package/bin/coven-code.mjs +19 -0
- package/docs/CLI.md +192 -0
- package/docs/CONFIGURATION.md +107 -0
- package/docs/DEVELOPMENT.md +104 -0
- package/docs/DOGFOOD-PROTOCOL.md +263 -0
- package/docs/MCP-SKILLS-PLUGINS.md +127 -0
- package/docs/README.md +38 -0
- package/docs/RELEASE.md +33 -0
- package/docs/SDK.md +107 -0
- package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +904 -0
- package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +670 -0
- package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +235 -0
- package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +63 -0
- package/package.json +36 -0
- package/src/agent/lane.mjs +136 -0
- package/src/agent/local.mjs +95 -0
- package/src/cli/dispatch.mjs +66 -0
- package/src/cli/execute.mjs +588 -0
- package/src/cli/help.mjs +58 -0
- package/src/cli/interactive-core.mjs +302 -0
- package/src/cli/notifications.mjs +13 -0
- package/src/cli/parse.mjs +83 -0
- package/src/cli/reasoning.mjs +45 -0
- package/src/cli/refs.mjs +162 -0
- package/src/cli/repl.mjs +61 -0
- package/src/cli/slash-commands.mjs +357 -0
- package/src/cli/stream-json.mjs +116 -0
- package/src/cli/tui.mjs +757 -0
- package/src/commands/agents.mjs +53 -0
- package/src/commands/config.mjs +27 -0
- package/src/commands/ide.mjs +17 -0
- package/src/commands/login.mjs +84 -0
- package/src/commands/mcp.mjs +176 -0
- package/src/commands/permissions.mjs +328 -0
- package/src/commands/plugins.mjs +86 -0
- package/src/commands/review.mjs +74 -0
- package/src/commands/skill.mjs +23 -0
- package/src/commands/threads.mjs +165 -0
- package/src/commands/tools.mjs +77 -0
- package/src/commands/update.mjs +31 -0
- package/src/commands/usage.mjs +34 -0
- package/src/constants.mjs +46 -0
- package/src/main.mjs +87 -0
- package/src/mcp/discover.mjs +154 -0
- package/src/mcp/permissions.mjs +52 -0
- package/src/mcp/probe.mjs +424 -0
- package/src/mcp/registry.mjs +96 -0
- package/src/plugins/discover.mjs +880 -0
- package/src/sdk-install.mjs +187 -0
- package/src/sdk.mjs +314 -0
- package/src/settings/load.mjs +134 -0
- package/src/settings/paths.mjs +101 -0
- package/src/skills/builtin/building-skills/SKILL.md +20 -0
- package/src/skills/discover.mjs +95 -0
- package/src/threads/store.mjs +176 -0
- package/src/tools/builtin/bash.mjs +110 -0
- package/src/tools/builtin/create-file.mjs +66 -0
- package/src/tools/builtin/edit-file.mjs +76 -0
- package/src/tools/builtin/finder.mjs +73 -0
- package/src/tools/builtin/glob.mjs +74 -0
- package/src/tools/builtin/grep.mjs +82 -0
- package/src/tools/builtin/index.mjs +83 -0
- package/src/tools/builtin/librarian.mjs +97 -0
- package/src/tools/builtin/look-at.mjs +92 -0
- package/src/tools/builtin/mcp.mjs +51 -0
- package/src/tools/builtin/mermaid.mjs +59 -0
- package/src/tools/builtin/oracle.mjs +56 -0
- package/src/tools/builtin/painter.mjs +81 -0
- package/src/tools/builtin/plugin-tool.mjs +53 -0
- package/src/tools/builtin/read-mcp-resource.mjs +63 -0
- package/src/tools/builtin/read-web-page.mjs +72 -0
- package/src/tools/builtin/read.mjs +59 -0
- package/src/tools/builtin/runtime.mjs +215 -0
- package/src/tools/builtin/task.mjs +63 -0
- package/src/tools/builtin/toolbox-tool.mjs +57 -0
- package/src/tools/builtin/undo-edit.mjs +97 -0
- package/src/tools/builtin/web-search.mjs +128 -0
- package/src/tools/toolbox.mjs +273 -0
- package/src/util/fs.mjs +13 -0
- package/src/util/glob.mjs +46 -0
- package/src/util/html.mjs +21 -0
- package/src/util/media.mjs +13 -0
- package/src/util/shell.mjs +24 -0
- package/src/util/table.mjs +11 -0
package/src/cli/tui.mjs
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
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';
|
|
13
|
+
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
|
+
import {
|
|
18
|
+
buildSlashCommandCatalog,
|
|
19
|
+
buildStaticSlashCommandCatalog,
|
|
20
|
+
builtinToolSummaryLines,
|
|
21
|
+
filterSlashCommands,
|
|
22
|
+
formatSlashCommandDetails,
|
|
23
|
+
formatSlashHelpLines,
|
|
24
|
+
} 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
|
+
];
|
|
41
|
+
|
|
42
|
+
export async function runTuiInteractive(parsed, initialInput = '') {
|
|
43
|
+
const session = createInteractiveSession(parsed);
|
|
44
|
+
const slashCatalog = await safeBuildSlashCommandCatalog(parsed);
|
|
45
|
+
const model = createTuiModel({
|
|
46
|
+
mode: parsed.mode,
|
|
47
|
+
reasoningEffort: parsed.reasoningEffort,
|
|
48
|
+
slashCatalog,
|
|
49
|
+
parsed,
|
|
50
|
+
});
|
|
51
|
+
if (process.env.COVEN_CODE_TUI_SCRIPTED === '1') {
|
|
52
|
+
for (const line of initialInput.split(/\r?\n/)) {
|
|
53
|
+
const text = line.trim();
|
|
54
|
+
if (!text) continue;
|
|
55
|
+
await submitTuiText(model, session, text);
|
|
56
|
+
if (model.status === 'done') break;
|
|
57
|
+
}
|
|
58
|
+
console.log(renderTuiFrame(model, { columns: process.stdout.columns ?? 80, rows: process.stdout.rows ?? 24 }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return runInteractive(parsed, initialInput);
|
|
62
|
+
return runLiveTui(model, session);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createTuiModel(options = {}) {
|
|
66
|
+
const slashCatalog = options.slashCatalog ?? buildStaticSlashCommandCatalog();
|
|
67
|
+
const composer = options.composer ?? '';
|
|
68
|
+
const workspaceCwd = options.cwd ?? process.cwd();
|
|
69
|
+
const model = {
|
|
70
|
+
version: options.version ?? VERSION,
|
|
71
|
+
cwd: displayCwd(workspaceCwd),
|
|
72
|
+
workspaceCwd,
|
|
73
|
+
mode: options.mode ?? 'smart',
|
|
74
|
+
reasoningEffort: options.reasoningEffort ?? 'high',
|
|
75
|
+
threadId: options.threadId ?? 'new thread',
|
|
76
|
+
toolCount: options.toolCount ?? 0,
|
|
77
|
+
queueCount: options.queueCount ?? 0,
|
|
78
|
+
activeTab: options.activeTab ?? 'chat',
|
|
79
|
+
paletteOpen: false,
|
|
80
|
+
paletteIndex: 0,
|
|
81
|
+
composer,
|
|
82
|
+
composerCursor: options.composerCursor ?? composer.length,
|
|
83
|
+
multiline: false,
|
|
84
|
+
slashCatalog,
|
|
85
|
+
slashOpen: false,
|
|
86
|
+
slashIndex: 0,
|
|
87
|
+
slashQuery: '',
|
|
88
|
+
slashMatches: filterSlashCommands(slashCatalog, ''),
|
|
89
|
+
transcript: [],
|
|
90
|
+
lane: defaultLaneState({ harness: options.mode ?? 'smart', ...options.lane }),
|
|
91
|
+
status: 'idle',
|
|
92
|
+
panels: options.panels ?? buildPanelSummaries(options.parsed, slashCatalog, workspaceCwd),
|
|
93
|
+
};
|
|
94
|
+
updateSlashState(model);
|
|
95
|
+
return model;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function renderTuiFrame(model, size = {}) {
|
|
99
|
+
const columns = Math.max(50, size.columns ?? process.stdout.columns ?? 80);
|
|
100
|
+
const rows = Math.max(16, size.rows ?? process.stdout.rows ?? 24);
|
|
101
|
+
const divider = '-'.repeat(columns);
|
|
102
|
+
const header = [
|
|
103
|
+
`Coven Code ${model.version}`.slice(0, columns),
|
|
104
|
+
`${model.cwd}`.slice(0, columns),
|
|
105
|
+
`${renderTabLine(model)} mode: ${model.mode} effort: ${model.reasoningEffort}`.slice(0, columns),
|
|
106
|
+
divider,
|
|
107
|
+
];
|
|
108
|
+
const status = renderCompactStatus(model).slice(0, columns);
|
|
109
|
+
const composerLines = renderComposerLines(model, columns);
|
|
110
|
+
const slashLines = model.slashOpen ? renderSlashOverlay(model, columns, Math.min(10, Math.max(4, rows - 10))) : [];
|
|
111
|
+
const footer = [
|
|
112
|
+
divider,
|
|
113
|
+
...composerLines,
|
|
114
|
+
...slashLines,
|
|
115
|
+
status,
|
|
116
|
+
];
|
|
117
|
+
const bodyRows = Math.max(1, rows - header.length - footer.length);
|
|
118
|
+
const body = renderTabContent(model, bodyRows, columns);
|
|
119
|
+
return [
|
|
120
|
+
...header,
|
|
121
|
+
...body,
|
|
122
|
+
...footer,
|
|
123
|
+
].slice(0, rows).join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function handleTuiKey(model, session, key = {}) {
|
|
127
|
+
if (key.ctrl && key.name === 'c') {
|
|
128
|
+
model.status = 'done';
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
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
|
+
}
|
|
184
|
+
|
|
185
|
+
if (key.name === 'tab') {
|
|
186
|
+
const index = TABS.indexOf(model.activeTab);
|
|
187
|
+
model.activeTab = TABS[(index + 1) % TABS.length];
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (key.name === 'escape') {
|
|
192
|
+
model.paletteOpen = false;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (key.ctrl && key.name === 'p') {
|
|
197
|
+
model.paletteOpen = true;
|
|
198
|
+
model.paletteIndex = 0;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
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
|
+
}
|
|
218
|
+
|
|
219
|
+
if (key.ctrl && key.name === 'n') {
|
|
220
|
+
await submitTuiText(model, session, '/new');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (key.ctrl && key.name === 'r') {
|
|
225
|
+
await submitTuiText(model, session, '/reasoning next');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (key.ctrl && key.name === 'm') {
|
|
230
|
+
const next = session.parsed.mode === 'smart' ? 'deep' : session.parsed.mode === 'deep' ? 'rush' : 'smart';
|
|
231
|
+
await submitTuiText(model, session, `/mode ${next}`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if ((key.meta || key.shift) && key.name === 'enter') {
|
|
236
|
+
insertComposerText(model, '\n');
|
|
237
|
+
model.multiline = true;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (key.name === 'enter') {
|
|
242
|
+
const text = model.composer.trim();
|
|
243
|
+
model.composer = '';
|
|
244
|
+
model.composerCursor = 0;
|
|
245
|
+
closeSlashMenu(model);
|
|
246
|
+
await submitTuiText(model, session, text);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
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
|
+
}
|