@opencoven/coven-code 0.0.1 → 0.0.3
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 +453 -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/slash-commands.mjs +20 -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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSlashCommandCatalog,
|
|
3
|
+
buildStaticSlashCommandCatalog,
|
|
4
|
+
filterSlashCommands,
|
|
5
|
+
} from './slash-commands.mjs';
|
|
6
|
+
import { currentSlashMatches } from './tui-render.mjs';
|
|
7
|
+
import { submitTuiText } from './tui-submit.mjs';
|
|
8
|
+
|
|
9
|
+
export function insertComposerText(model, text) {
|
|
10
|
+
model.composer = `${model.composer.slice(0, model.composerCursor)}${text}${model.composer.slice(model.composerCursor)}`;
|
|
11
|
+
model.composerCursor += text.length;
|
|
12
|
+
updateSlashState(model);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function deleteComposerText(model, kind) {
|
|
16
|
+
if (kind === 'delete') {
|
|
17
|
+
if (model.composerCursor >= model.composer.length) return;
|
|
18
|
+
model.composer = `${model.composer.slice(0, model.composerCursor)}${model.composer.slice(model.composerCursor + 1)}`;
|
|
19
|
+
} else {
|
|
20
|
+
if (model.composerCursor <= 0) return;
|
|
21
|
+
model.composer = `${model.composer.slice(0, model.composerCursor - 1)}${model.composer.slice(model.composerCursor)}`;
|
|
22
|
+
model.composerCursor -= 1;
|
|
23
|
+
}
|
|
24
|
+
updateSlashState(model);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function updateSlashState(model) {
|
|
28
|
+
const beforeCursor = model.composer.slice(0, model.composerCursor);
|
|
29
|
+
const active = beforeCursor.startsWith('/') && !/\s/.test(beforeCursor.slice(1));
|
|
30
|
+
if (!active) {
|
|
31
|
+
closeSlashMenu(model);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
model.slashOpen = true;
|
|
35
|
+
model.slashQuery = beforeCursor.replace(/^\/+/, '');
|
|
36
|
+
model.slashMatches = filterSlashCommands(model.slashCatalog, beforeCursor);
|
|
37
|
+
if (model.slashMatches.length === 0) model.slashIndex = 0;
|
|
38
|
+
else model.slashIndex = Math.min(model.slashIndex, model.slashMatches.length - 1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function closeSlashMenu(model) {
|
|
42
|
+
model.slashOpen = false;
|
|
43
|
+
model.slashQuery = '';
|
|
44
|
+
model.slashMatches = filterSlashCommands(model.slashCatalog, '');
|
|
45
|
+
model.slashIndex = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function completeSlashSelection(model) {
|
|
49
|
+
const selected = currentSlashMatches(model)[model.slashIndex];
|
|
50
|
+
if (!selected) return;
|
|
51
|
+
model.composer = `${selected.command} `;
|
|
52
|
+
model.composerCursor = model.composer.length;
|
|
53
|
+
closeSlashMenu(model);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function acceptSlashSelection(model, session) {
|
|
57
|
+
const selected = currentSlashMatches(model)[model.slashIndex];
|
|
58
|
+
if (!selected) return;
|
|
59
|
+
const command = selected.command;
|
|
60
|
+
model.composer = '';
|
|
61
|
+
model.composerCursor = 0;
|
|
62
|
+
closeSlashMenu(model);
|
|
63
|
+
await submitTuiText(model, session, command);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function safeBuildSlashCommandCatalog(parsed) {
|
|
67
|
+
try {
|
|
68
|
+
return await buildSlashCommandCatalog({ parsed, cwd: process.cwd() });
|
|
69
|
+
} catch {
|
|
70
|
+
return buildStaticSlashCommandCatalog();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { CLI_NAME } from '../constants.mjs';
|
|
2
|
+
import { runInteractive } from './repl.mjs';
|
|
3
|
+
import { formatSlashCommandDetails } from './slash-commands.mjs';
|
|
4
|
+
import {
|
|
5
|
+
PALETTE_ACTIONS,
|
|
6
|
+
currentSlashMatches,
|
|
7
|
+
renderCompactStatus,
|
|
8
|
+
renderComposerLines,
|
|
9
|
+
renderTabContent,
|
|
10
|
+
renderTabLine,
|
|
11
|
+
} from './tui-render.mjs';
|
|
12
|
+
|
|
13
|
+
export async function runLiveTui(model, session, handleKey) {
|
|
14
|
+
try {
|
|
15
|
+
const blessedModule = await import('neo-blessed');
|
|
16
|
+
return runBlessedTui(blessedModule.default ?? blessedModule, model, session, handleKey);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(`${CLI_NAME}: unable to start panel TUI, falling back to classic REPL: ${error?.message ?? error}`);
|
|
19
|
+
return runInteractive(session.parsed, '');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runBlessedTui(blessed, model, session, handleKey) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const screen = blessed.screen({
|
|
26
|
+
smartCSR: true,
|
|
27
|
+
fullUnicode: true,
|
|
28
|
+
title: `${CLI_NAME} ${model.version}`,
|
|
29
|
+
});
|
|
30
|
+
const header = blessed.box({
|
|
31
|
+
top: 0,
|
|
32
|
+
left: 0,
|
|
33
|
+
width: '100%',
|
|
34
|
+
height: 4,
|
|
35
|
+
padding: { left: 1, right: 1 },
|
|
36
|
+
style: { fg: 'white', bg: 'black' },
|
|
37
|
+
});
|
|
38
|
+
const transcript = blessed.box({
|
|
39
|
+
top: 4,
|
|
40
|
+
left: 0,
|
|
41
|
+
width: '100%',
|
|
42
|
+
bottom: 4,
|
|
43
|
+
scrollable: true,
|
|
44
|
+
alwaysScroll: true,
|
|
45
|
+
keys: true,
|
|
46
|
+
vi: true,
|
|
47
|
+
padding: { left: 1, right: 1 },
|
|
48
|
+
scrollbar: { ch: ' ', style: { bg: 'white' } },
|
|
49
|
+
});
|
|
50
|
+
const composer = blessed.box({
|
|
51
|
+
bottom: 1,
|
|
52
|
+
left: 0,
|
|
53
|
+
width: '100%',
|
|
54
|
+
height: 3,
|
|
55
|
+
label: ' message ',
|
|
56
|
+
border: 'line',
|
|
57
|
+
padding: { left: 1, right: 1 },
|
|
58
|
+
style: {
|
|
59
|
+
border: { fg: 'green' },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const status = blessed.box({
|
|
63
|
+
bottom: 0,
|
|
64
|
+
left: 0,
|
|
65
|
+
width: '100%',
|
|
66
|
+
height: 1,
|
|
67
|
+
style: { fg: 'white', bg: 'black' },
|
|
68
|
+
});
|
|
69
|
+
const palette = blessed.list({
|
|
70
|
+
top: 'center',
|
|
71
|
+
left: 'center',
|
|
72
|
+
width: '60%',
|
|
73
|
+
height: Math.min(13, PALETTE_ACTIONS.length + 2),
|
|
74
|
+
label: ' Command Palette ',
|
|
75
|
+
border: 'line',
|
|
76
|
+
hidden: true,
|
|
77
|
+
keys: true,
|
|
78
|
+
mouse: true,
|
|
79
|
+
items: PALETTE_ACTIONS.map(([label, command]) => `${label} ${command}`),
|
|
80
|
+
style: {
|
|
81
|
+
border: { fg: 'yellow' },
|
|
82
|
+
selected: { bg: 'blue', fg: 'white' },
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const slashList = blessed.list({
|
|
86
|
+
bottom: 4,
|
|
87
|
+
left: 0,
|
|
88
|
+
width: '36%',
|
|
89
|
+
height: '50%',
|
|
90
|
+
label: ' Slash commands ',
|
|
91
|
+
border: 'line',
|
|
92
|
+
hidden: true,
|
|
93
|
+
keys: true,
|
|
94
|
+
mouse: true,
|
|
95
|
+
style: {
|
|
96
|
+
border: { fg: 'cyan' },
|
|
97
|
+
selected: { bg: 'blue', fg: 'white' },
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const slashDetails = blessed.box({
|
|
101
|
+
bottom: 4,
|
|
102
|
+
right: 0,
|
|
103
|
+
width: '64%',
|
|
104
|
+
height: '50%',
|
|
105
|
+
label: ' Details ',
|
|
106
|
+
border: 'line',
|
|
107
|
+
hidden: true,
|
|
108
|
+
padding: { left: 1, right: 1 },
|
|
109
|
+
style: {
|
|
110
|
+
border: { fg: 'cyan' },
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
screen.append(header);
|
|
115
|
+
screen.append(transcript);
|
|
116
|
+
screen.append(composer);
|
|
117
|
+
screen.append(status);
|
|
118
|
+
screen.append(palette);
|
|
119
|
+
screen.append(slashList);
|
|
120
|
+
screen.append(slashDetails);
|
|
121
|
+
|
|
122
|
+
let settled = false;
|
|
123
|
+
const cleanup = () => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
settled = true;
|
|
126
|
+
screen.destroy();
|
|
127
|
+
resolve();
|
|
128
|
+
};
|
|
129
|
+
const sync = () => {
|
|
130
|
+
const width = Number(screen.width) || 80;
|
|
131
|
+
header.setContent(`Coven Code ${model.version}\n${model.cwd}\n${renderTabLine(model)} mode: ${model.mode} effort: ${model.reasoningEffort}`);
|
|
132
|
+
transcript.setContent(renderTabContent(model, Math.max(1, Number(transcript.height) - 2), Math.max(20, width - 4)).join('\n'));
|
|
133
|
+
composer.setContent(renderComposerLines(model, width - 4, { cursor: true }).join('\n'));
|
|
134
|
+
status.setContent(renderCompactStatus(model));
|
|
135
|
+
if (model.paletteOpen) {
|
|
136
|
+
palette.show();
|
|
137
|
+
palette.select(model.paletteIndex);
|
|
138
|
+
palette.focus();
|
|
139
|
+
} else {
|
|
140
|
+
palette.hide();
|
|
141
|
+
}
|
|
142
|
+
if (model.slashOpen) {
|
|
143
|
+
const matches = currentSlashMatches(model);
|
|
144
|
+
const selected = matches[model.slashIndex] ?? matches[0];
|
|
145
|
+
slashList.setItems(matches.map((entry) => `${entry.command} ${entry.title}`));
|
|
146
|
+
slashList.show();
|
|
147
|
+
slashList.select(model.slashIndex);
|
|
148
|
+
slashDetails.setContent(formatSlashCommandDetails(selected).join('\n'));
|
|
149
|
+
slashDetails.show();
|
|
150
|
+
} else {
|
|
151
|
+
slashList.hide();
|
|
152
|
+
slashDetails.hide();
|
|
153
|
+
}
|
|
154
|
+
screen.render();
|
|
155
|
+
};
|
|
156
|
+
const dispatchKey = async (key) => {
|
|
157
|
+
await handleKey(model, session, normalizeBlessedKey(key));
|
|
158
|
+
sync();
|
|
159
|
+
if (model.status === 'done') cleanup();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
screen.on('keypress', async (chunk, key = {}) => {
|
|
163
|
+
if (settled) return;
|
|
164
|
+
const normalized = normalizeBlessedKey({
|
|
165
|
+
...key,
|
|
166
|
+
sequence: isPrintableChunk(chunk, key) ? chunk : key.sequence,
|
|
167
|
+
});
|
|
168
|
+
await dispatchKey(normalized);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
screen.on('resize', sync);
|
|
172
|
+
sync();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeBlessedKey(key = {}) {
|
|
177
|
+
if (key.name === 'return') return { ...key, name: 'enter' };
|
|
178
|
+
return key;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function isPrintableKey(key = {}) {
|
|
182
|
+
return isPrintableChunk(key.sequence, key);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isPrintableChunk(chunk, key = {}) {
|
|
186
|
+
return typeof chunk === 'string'
|
|
187
|
+
&& chunk.length > 0
|
|
188
|
+
&& !key.ctrl
|
|
189
|
+
&& !key.meta
|
|
190
|
+
&& key.name !== 'return'
|
|
191
|
+
&& key.name !== 'enter'
|
|
192
|
+
&& key.name !== 'tab'
|
|
193
|
+
&& key.name !== 'escape'
|
|
194
|
+
&& key.name !== 'up'
|
|
195
|
+
&& key.name !== 'down'
|
|
196
|
+
&& key.name !== 'left'
|
|
197
|
+
&& key.name !== 'right';
|
|
198
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { isPrintableKey } from './tui-blessed.mjs';
|
|
2
|
+
import {
|
|
3
|
+
acceptSlashSelection,
|
|
4
|
+
closeSlashMenu,
|
|
5
|
+
completeSlashSelection,
|
|
6
|
+
deleteComposerText,
|
|
7
|
+
insertComposerText,
|
|
8
|
+
} from './tui-actions.mjs';
|
|
9
|
+
import { PALETTE_ACTIONS } from './tui-render.mjs';
|
|
10
|
+
import { submitTuiText } from './tui-submit.mjs';
|
|
11
|
+
|
|
12
|
+
export function handleComposerKey(model, key) {
|
|
13
|
+
if (isPrintableKey(key)) {
|
|
14
|
+
insertComposerText(model, key.sequence);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (key.name === 'backspace' || key.name === 'delete') {
|
|
18
|
+
deleteComposerText(model, key.name);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (key.name === 'left') {
|
|
22
|
+
model.composerCursor = Math.max(0, model.composerCursor - 1);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (key.name === 'right') {
|
|
26
|
+
model.composerCursor = Math.min(model.composer.length, model.composerCursor + 1);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (key.name === 'home') {
|
|
30
|
+
model.composerCursor = 0;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (key.name === 'end') {
|
|
34
|
+
model.composerCursor = model.composer.length;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function handleSlashMenuKey(model, session, key) {
|
|
41
|
+
if (key.name === 'escape') {
|
|
42
|
+
closeSlashMenu(model);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (key.name === 'down') {
|
|
46
|
+
model.slashIndex = (model.slashIndex + 1) % Math.max(1, model.slashMatches.length);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (key.name === 'up') {
|
|
50
|
+
model.slashIndex = (model.slashIndex + Math.max(1, model.slashMatches.length) - 1) % Math.max(1, model.slashMatches.length);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (key.name === 'tab') {
|
|
54
|
+
completeSlashSelection(model);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (key.name === 'enter') {
|
|
58
|
+
await acceptSlashSelection(model, session);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function handlePaletteKey(model, session, key) {
|
|
65
|
+
if (key.name === 'enter') {
|
|
66
|
+
const [, command] = PALETTE_ACTIONS[model.paletteIndex ?? 0] ?? PALETTE_ACTIONS[0];
|
|
67
|
+
model.paletteOpen = false;
|
|
68
|
+
await submitTuiText(model, session, command);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (key.name === 'down') {
|
|
72
|
+
model.paletteIndex = (model.paletteIndex + 1) % PALETTE_ACTIONS.length;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (key.name === 'up') {
|
|
76
|
+
model.paletteIndex = (model.paletteIndex + PALETTE_ACTIONS.length - 1) % PALETTE_ACTIONS.length;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CLI_NAME } from '../constants.mjs';
|
|
2
|
+
import {
|
|
3
|
+
inspectLane,
|
|
4
|
+
nextLaneHarness,
|
|
5
|
+
normalizeLaneHarness,
|
|
6
|
+
runLaneVerification,
|
|
7
|
+
} from '../agent/lane.mjs';
|
|
8
|
+
import { splitShellWords } from '../util/shell.mjs';
|
|
9
|
+
import { renderLaneLines } from './tui-render.mjs';
|
|
10
|
+
|
|
11
|
+
export function isLaneCommand(text) {
|
|
12
|
+
return /^\/lane(?:\s|$)/.test(text);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleTuiLaneCommand(model, session, text) {
|
|
16
|
+
try {
|
|
17
|
+
const [, subcommand = 'status', ...rest] = splitShellWords(text.slice(1));
|
|
18
|
+
if (subcommand === 'refresh') {
|
|
19
|
+
const inspector = session.laneInspector ?? inspectLane;
|
|
20
|
+
model.lane = await inspector({
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
harness: model.lane.harness,
|
|
23
|
+
verification: model.lane.verification,
|
|
24
|
+
});
|
|
25
|
+
model.activeTab = 'lane';
|
|
26
|
+
return laneCommandResult(`lane refreshed: ${model.lane.branch}`);
|
|
27
|
+
}
|
|
28
|
+
if (subcommand === 'harness') {
|
|
29
|
+
const requested = rest[0] === 'next' ? nextLaneHarness(model.lane.harness) : rest[0];
|
|
30
|
+
const harness = normalizeLaneHarness(requested);
|
|
31
|
+
model.lane = { ...model.lane, harness };
|
|
32
|
+
model.activeTab = 'lane';
|
|
33
|
+
return laneCommandResult(`harness: ${harness}`);
|
|
34
|
+
}
|
|
35
|
+
if (subcommand === 'verify') {
|
|
36
|
+
const verifier = session.laneVerifier ?? runLaneVerification;
|
|
37
|
+
model.lane = await verifier(model.lane);
|
|
38
|
+
model.activeTab = 'lane';
|
|
39
|
+
return laneCommandResult(`verification: ${model.lane.verification.status}`);
|
|
40
|
+
}
|
|
41
|
+
if (subcommand === 'diff') {
|
|
42
|
+
model.activeTab = 'lane';
|
|
43
|
+
return laneCommandResult(model.lane.diffSummary || 'no diff summary');
|
|
44
|
+
}
|
|
45
|
+
if (subcommand === 'status') {
|
|
46
|
+
model.activeTab = 'lane';
|
|
47
|
+
return laneCommandResult(renderLaneLines(model, 40, 120).join('\n'));
|
|
48
|
+
}
|
|
49
|
+
return laneCommandResult(`${CLI_NAME}: Unknown lane command: ${subcommand}`, 'error');
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return laneCommandResult(`${CLI_NAME}: ${error?.message ?? error}`, 'error');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function laneCommandResult(text, kind = 'command') {
|
|
56
|
+
return {
|
|
57
|
+
result: { kind, lines: [text] },
|
|
58
|
+
stdout: '',
|
|
59
|
+
stderr: '',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function rememberLaneTerminal(model, stdout, stderr, resultLines = []) {
|
|
64
|
+
const lines = [stdout, stderr, resultLines.join('\n')]
|
|
65
|
+
.flatMap((text) => String(text ?? '').split(/\r?\n/))
|
|
66
|
+
.map((line) => line.trimEnd())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
if (lines.length === 0) return;
|
|
69
|
+
model.lane = {
|
|
70
|
+
...model.lane,
|
|
71
|
+
terminalLines: [...(model.lane.terminalLines ?? []), ...lines].slice(-40),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readEffectiveSettings } from '../settings/load.mjs';
|
|
3
|
+
import { findWorkspaceSettingsFile, settingsFile } from '../settings/paths.mjs';
|
|
4
|
+
import { listThreads } from '../threads/store.mjs';
|
|
5
|
+
import {
|
|
6
|
+
buildStaticSlashCommandCatalog,
|
|
7
|
+
builtinToolSummaryLines,
|
|
8
|
+
filterSlashCommands,
|
|
9
|
+
formatSlashCommandDetails,
|
|
10
|
+
formatSlashHelpLines,
|
|
11
|
+
} from './slash-commands.mjs';
|
|
12
|
+
|
|
13
|
+
export const TABS = ['chat', 'lane', 'tools', 'threads', 'config', 'help'];
|
|
14
|
+
|
|
15
|
+
export const PALETTE_ACTIONS = [
|
|
16
|
+
['New thread', '/new'],
|
|
17
|
+
['Continue latest thread', '/continue'],
|
|
18
|
+
['Refresh lane', '/lane refresh'],
|
|
19
|
+
['Cycle harness', '/lane harness next'],
|
|
20
|
+
['Run verification', '/lane verify'],
|
|
21
|
+
['Open help', '/help'],
|
|
22
|
+
['List tools', '/tools list'],
|
|
23
|
+
['List skills', '/skill: list'],
|
|
24
|
+
['List plugins', '/plugins: list'],
|
|
25
|
+
['Open editor', '/editor'],
|
|
26
|
+
['Edit previous prompt', '/edit'],
|
|
27
|
+
['Archive and quit', '/thread: archive and quit'],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function renderTabContent(model, limit, width) {
|
|
31
|
+
if (model.activeTab === 'help') return clipLines(formatSlashHelpLines(model.slashCatalog), limit, width);
|
|
32
|
+
if (model.activeTab === 'lane') return renderLaneLines(model, limit, width);
|
|
33
|
+
if (model.activeTab === 'tools') return clipLines(model.panels.tools, limit, width);
|
|
34
|
+
if (model.activeTab === 'threads') return clipLines(model.panels.threads, limit, width);
|
|
35
|
+
if (model.activeTab === 'config') return clipLines(model.panels.config, limit, width);
|
|
36
|
+
return renderTranscript(model, limit, width);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function renderTranscript(model, limit, width) {
|
|
40
|
+
const entries = model.transcript.slice(-Math.max(1, limit)).flatMap((entry) => [
|
|
41
|
+
`${entry.role}:`,
|
|
42
|
+
...String(entry.text).split(/\r?\n/),
|
|
43
|
+
]);
|
|
44
|
+
return clipLines(entries.length > 0 ? entries.slice(-limit) : ['Ready. Type a prompt or /help.'], limit, width);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderComposerLines(model, width, { cursor = false } = {}) {
|
|
48
|
+
const prompt = model.composer || '';
|
|
49
|
+
const text = cursor ? insertComposerCursor(prompt, model.composerCursor ?? prompt.length) : prompt;
|
|
50
|
+
const lines = String(text).split('\n');
|
|
51
|
+
return lines.map((line, index) => `${index === 0 ? '> ' : ' '}${line}`.slice(0, width));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function insertComposerCursor(prompt, cursor) {
|
|
55
|
+
const position = Math.max(0, Math.min(prompt.length, cursor));
|
|
56
|
+
const before = prompt.slice(0, position);
|
|
57
|
+
const at = prompt.slice(position, position + 1);
|
|
58
|
+
const after = prompt.slice(position + 1);
|
|
59
|
+
if (at === '\n' || at === '') return `${before}█${at}${after}`;
|
|
60
|
+
return `${before}█${after}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function renderSlashOverlay(model, width, limit) {
|
|
64
|
+
const listWidth = Math.min(34, Math.floor(width * 0.38));
|
|
65
|
+
const detailWidth = width - listWidth - 3;
|
|
66
|
+
const matches = currentSlashMatches(model);
|
|
67
|
+
const selected = matches[model.slashIndex] ?? matches[0];
|
|
68
|
+
const listLines = ['Slash commands', ...matches.map((entry, index) => {
|
|
69
|
+
const marker = index === model.slashIndex ? '>' : ' ';
|
|
70
|
+
const status = entry.availability?.type === 'disabled' ? ' disabled' : '';
|
|
71
|
+
return `${marker} ${entry.command}${status}`;
|
|
72
|
+
})];
|
|
73
|
+
const detailLines = ['Details', ...formatSlashCommandDetails(selected)];
|
|
74
|
+
const count = Math.min(limit, Math.max(listLines.length, detailLines.length));
|
|
75
|
+
const rows = [];
|
|
76
|
+
for (let index = 0; index < count; index += 1) {
|
|
77
|
+
const left = (listLines[index] ?? '').padEnd(listWidth).slice(0, listWidth);
|
|
78
|
+
const right = (detailLines[index] ?? '').slice(0, detailWidth);
|
|
79
|
+
rows.push(`${left} | ${right}`);
|
|
80
|
+
}
|
|
81
|
+
return rows;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function renderCompactStatus(model) {
|
|
85
|
+
return [
|
|
86
|
+
`thread: ${shortThreadId(model.threadId)}`,
|
|
87
|
+
`lane: ${model.lane.branch}`,
|
|
88
|
+
`harness: ${model.lane.harness}`,
|
|
89
|
+
`queued: ${model.queueCount}`,
|
|
90
|
+
`tools: ${model.toolCount}`,
|
|
91
|
+
`tab: ${model.activeTab}`,
|
|
92
|
+
`status: ${model.status}`,
|
|
93
|
+
].join(' | ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildPanelSummaries(parsed = {}, slashCatalog = buildStaticSlashCommandCatalog(), cwd = process.cwd()) {
|
|
97
|
+
const threads = listThreads().slice(0, 8);
|
|
98
|
+
const latest = threads
|
|
99
|
+
.filter((thread) => !thread.archived)
|
|
100
|
+
.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0];
|
|
101
|
+
const settingsPath = settingsFile(parsed);
|
|
102
|
+
const workspacePath = findWorkspaceSettingsFile(cwd);
|
|
103
|
+
const settings = readEffectiveSettings(parsed, { cwd });
|
|
104
|
+
return {
|
|
105
|
+
tools: [
|
|
106
|
+
...builtinToolSummaryLines(),
|
|
107
|
+
'',
|
|
108
|
+
'Slash command sources:',
|
|
109
|
+
`commands: ${slashCatalog.length}`,
|
|
110
|
+
],
|
|
111
|
+
threads: [
|
|
112
|
+
`Recent threads: ${threads.length}`,
|
|
113
|
+
`Latest: ${latest?.id ?? 'none'}`,
|
|
114
|
+
...threads.map((thread) => `${thread.id} ${thread.archived ? 'archived' : 'active'} ${thread.title}`),
|
|
115
|
+
],
|
|
116
|
+
config: [
|
|
117
|
+
'Settings',
|
|
118
|
+
`user: ${settingsPath}${existsSync(settingsPath) ? '' : ' (not created)'}`,
|
|
119
|
+
`workspace: ${workspacePath ?? 'none'}`,
|
|
120
|
+
`visibility: ${settings['covenCode.defaultVisibility'] ?? 'private'}`,
|
|
121
|
+
`updates: ${settings['covenCode.updates.mode'] ?? 'default'}`,
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function renderTabLine(model) {
|
|
127
|
+
return TABS.map((tab) => tab === model.activeTab ? `[${tab}]` : ` ${tab} `).join(' ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function clipLines(lines, limit, width) {
|
|
131
|
+
return lines
|
|
132
|
+
.slice(0, Math.max(0, limit))
|
|
133
|
+
.map((line) => String(line).slice(0, width));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function shortThreadId(threadId) {
|
|
137
|
+
if (!threadId || threadId === 'new thread') return 'new';
|
|
138
|
+
return String(threadId).slice(0, 14);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function renderLaneLines(model, limit, width) {
|
|
142
|
+
const lane = model.lane;
|
|
143
|
+
const changedFiles = lane.changedFiles.length > 0 ? lane.changedFiles : ['none'];
|
|
144
|
+
const lines = [
|
|
145
|
+
`worktree: ${lane.worktree}`,
|
|
146
|
+
`branch: ${lane.branch}`,
|
|
147
|
+
`base: ${lane.baseBranch}`,
|
|
148
|
+
`harness: ${lane.harness}`,
|
|
149
|
+
`status: ${lane.status}`,
|
|
150
|
+
`verify: ${lane.verification.status} (${lane.verification.command})`,
|
|
151
|
+
`PR: ${lane.pullRequest}`,
|
|
152
|
+
`merge: ${lane.merge}`,
|
|
153
|
+
`cleanup: ${lane.cleanup}`,
|
|
154
|
+
'',
|
|
155
|
+
'Changed files',
|
|
156
|
+
...changedFiles.map((file) => ` ${file}`),
|
|
157
|
+
'',
|
|
158
|
+
'Diff',
|
|
159
|
+
lane.diffSummary || ' no diff summary',
|
|
160
|
+
'',
|
|
161
|
+
'Terminal',
|
|
162
|
+
...(lane.terminalLines.length > 0 ? lane.terminalLines : [' no lane terminal output yet']),
|
|
163
|
+
];
|
|
164
|
+
return lines.slice(0, limit).map((line) => line.slice(0, width));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function currentSlashMatches(model) {
|
|
168
|
+
return model.slashMatches.length > 0 ? model.slashMatches : filterSlashCommands(model.slashCatalog, model.slashQuery);
|
|
169
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { handleInteractiveInput } from './interactive-core.mjs';
|
|
2
|
+
import { buildPanelSummaries } from './tui-render.mjs';
|
|
3
|
+
import { handleTuiLaneCommand, isLaneCommand, rememberLaneTerminal } from './tui-lane.mjs';
|
|
4
|
+
|
|
5
|
+
export async function submitTuiText(model, session, text) {
|
|
6
|
+
if (!text) return;
|
|
7
|
+
model.transcript.push({ role: 'you', text });
|
|
8
|
+
model.status = 'running';
|
|
9
|
+
let result;
|
|
10
|
+
let stdout = '';
|
|
11
|
+
let stderr = '';
|
|
12
|
+
if (isLaneCommand(text)) {
|
|
13
|
+
({ result, stdout, stderr } = await handleTuiLaneCommand(model, session, text));
|
|
14
|
+
} else {
|
|
15
|
+
const priorThreadId = session.thread?.id;
|
|
16
|
+
const priorMessageCount = session.thread?.messages?.length ?? 0;
|
|
17
|
+
({ result, stderr } = await captureTerminalOutput(() => handleInteractiveInput(session, text)));
|
|
18
|
+
stdout = newAssistantText(session.thread, priorThreadId, priorMessageCount);
|
|
19
|
+
}
|
|
20
|
+
model.mode = session.parsed.mode;
|
|
21
|
+
model.reasoningEffort = session.parsed.reasoningEffort ?? model.reasoningEffort;
|
|
22
|
+
model.threadId = session.thread?.id ?? 'new thread';
|
|
23
|
+
model.queueCount = session.queuedMessages.length;
|
|
24
|
+
rememberLaneTerminal(model, stdout, stderr, result.lines);
|
|
25
|
+
model.panels = buildPanelSummaries(session.parsed, model.slashCatalog, model.workspaceCwd);
|
|
26
|
+
if (stderr.trim()) {
|
|
27
|
+
model.transcript.push({ role: 'error', text: stderr.trim() });
|
|
28
|
+
}
|
|
29
|
+
if (stdout.trim()) {
|
|
30
|
+
model.transcript.push({ role: 'coven', text: stdout.trim() });
|
|
31
|
+
}
|
|
32
|
+
if (result.lines.length > 0) {
|
|
33
|
+
model.transcript.push({
|
|
34
|
+
role: result.kind === 'error' ? 'error' : 'coven',
|
|
35
|
+
text: result.lines.join('\n'),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
model.status = result.kind === 'exit' ? 'done' : 'idle';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function newAssistantText(thread, priorThreadId, priorMessageCount) {
|
|
42
|
+
if (!thread?.messages?.length) return '';
|
|
43
|
+
const startIndex = thread.id === priorThreadId ? priorMessageCount : 0;
|
|
44
|
+
return thread.messages
|
|
45
|
+
.slice(startIndex)
|
|
46
|
+
.filter((message) => message.role === 'assistant' && typeof message.content === 'string')
|
|
47
|
+
.map((message) => message.content.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function captureTerminalOutput(fn) {
|
|
53
|
+
let stderr = '';
|
|
54
|
+
const originalStdoutWrite = process.stdout.write;
|
|
55
|
+
const originalStderrWrite = process.stderr.write;
|
|
56
|
+
process.stdout.write = function tuiStdoutWrite(chunk, encoding, callback) {
|
|
57
|
+
callWriteCallback(encoding, callback);
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
process.stderr.write = function tuiStderrWrite(chunk, encoding, callback) {
|
|
61
|
+
stderr += normalizeWriteChunk(chunk, encoding);
|
|
62
|
+
callWriteCallback(encoding, callback);
|
|
63
|
+
return true;
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
const result = await fn();
|
|
67
|
+
return { result, stderr };
|
|
68
|
+
} finally {
|
|
69
|
+
process.stdout.write = originalStdoutWrite;
|
|
70
|
+
process.stderr.write = originalStderrWrite;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeWriteChunk(chunk, encoding) {
|
|
75
|
+
if (Buffer.isBuffer(chunk)) return chunk.toString(typeof encoding === 'string' ? encoding : 'utf8');
|
|
76
|
+
return String(chunk);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function callWriteCallback(encoding, callback) {
|
|
80
|
+
if (typeof encoding === 'function') encoding();
|
|
81
|
+
else if (typeof callback === 'function') callback();
|
|
82
|
+
}
|