@opencoven/coven-code 0.0.3 → 0.0.6
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 +29 -130
- package/bin/coven-code +26 -0
- package/install.js +117 -0
- package/package.json +26 -23
- package/bin/coven-code-sdk.mjs +0 -12
- package/bin/coven-code.mjs +0 -19
- package/docs/CLI.md +0 -256
- package/docs/CONFIGURATION.md +0 -107
- package/docs/DEMO.md +0 -453
- package/docs/DEVELOPMENT.md +0 -104
- package/docs/DOGFOOD-PROTOCOL.md +0 -263
- package/docs/MCP-SKILLS-PLUGINS.md +0 -127
- package/docs/README.md +0 -39
- package/docs/RELEASE.md +0 -33
- package/docs/SDK.md +0 -107
- package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +0 -904
- package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +0 -670
- package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +0 -235
- package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +0 -63
- package/src/agent/fixture.mjs +0 -95
- package/src/agent/lane.mjs +0 -136
- package/src/cli/dispatch.mjs +0 -66
- package/src/cli/execute.mjs +0 -590
- package/src/cli/help.mjs +0 -58
- package/src/cli/interactive-core.mjs +0 -28
- package/src/cli/interactive-io.mjs +0 -101
- package/src/cli/interactive-slash.mjs +0 -184
- package/src/cli/notifications.mjs +0 -13
- package/src/cli/parse.mjs +0 -83
- package/src/cli/reasoning.mjs +0 -45
- package/src/cli/refs.mjs +0 -162
- package/src/cli/repl.mjs +0 -60
- package/src/cli/slash-commands.mjs +0 -375
- package/src/cli/stream-json.mjs +0 -116
- package/src/cli/tui-actions.mjs +0 -72
- package/src/cli/tui-blessed.mjs +0 -198
- package/src/cli/tui-keys.mjs +0 -80
- package/src/cli/tui-lane.mjs +0 -73
- package/src/cli/tui-render.mjs +0 -169
- package/src/cli/tui-submit.mjs +0 -82
- package/src/cli/tui.mjs +0 -174
- package/src/commands/agents.mjs +0 -53
- package/src/commands/config.mjs +0 -27
- package/src/commands/ide.mjs +0 -17
- package/src/commands/login.mjs +0 -84
- package/src/commands/mcp.mjs +0 -176
- package/src/commands/permissions-eval.mjs +0 -122
- package/src/commands/permissions-rules.mjs +0 -53
- package/src/commands/permissions-text.mjs +0 -112
- package/src/commands/permissions.mjs +0 -62
- package/src/commands/plugins.mjs +0 -86
- package/src/commands/review.mjs +0 -74
- package/src/commands/skill.mjs +0 -23
- package/src/commands/threads.mjs +0 -165
- package/src/commands/tools.mjs +0 -77
- package/src/commands/update.mjs +0 -31
- package/src/commands/usage.mjs +0 -34
- package/src/constants.mjs +0 -52
- package/src/main.mjs +0 -87
- package/src/mcp/discover.mjs +0 -154
- package/src/mcp/local.mjs +0 -55
- package/src/mcp/parsers.mjs +0 -46
- package/src/mcp/permissions.mjs +0 -52
- package/src/mcp/probe.mjs +0 -85
- package/src/mcp/registry.mjs +0 -96
- package/src/mcp/remote-oauth.mjs +0 -55
- package/src/mcp/remote-session.mjs +0 -54
- package/src/mcp/remote-sse.mjs +0 -82
- package/src/mcp/remote.mjs +0 -74
- package/src/plugins/api.mjs +0 -187
- package/src/plugins/configuration.mjs +0 -124
- package/src/plugins/discover.mjs +0 -84
- package/src/plugins/helpers.mjs +0 -187
- package/src/plugins/subsystems.mjs +0 -198
- package/src/plugins/validators.mjs +0 -142
- package/src/sdk-execute.mjs +0 -82
- package/src/sdk-install.mjs +0 -187
- package/src/sdk-settings.mjs +0 -88
- package/src/sdk.mjs +0 -163
- package/src/settings/load.mjs +0 -134
- package/src/settings/paths.mjs +0 -101
- package/src/skills/builtin/building-skills/SKILL.md +0 -20
- package/src/skills/discover.mjs +0 -95
- package/src/threads/store.mjs +0 -176
- package/src/tools/builtin/bash.mjs +0 -110
- package/src/tools/builtin/create-file.mjs +0 -66
- package/src/tools/builtin/edit-file.mjs +0 -76
- package/src/tools/builtin/finder.mjs +0 -73
- package/src/tools/builtin/glob.mjs +0 -74
- package/src/tools/builtin/grep.mjs +0 -82
- package/src/tools/builtin/index.mjs +0 -83
- package/src/tools/builtin/librarian.mjs +0 -97
- package/src/tools/builtin/look-at.mjs +0 -92
- package/src/tools/builtin/mcp.mjs +0 -51
- package/src/tools/builtin/mermaid.mjs +0 -59
- package/src/tools/builtin/oracle.mjs +0 -56
- package/src/tools/builtin/painter.mjs +0 -81
- package/src/tools/builtin/plugin-tool.mjs +0 -53
- package/src/tools/builtin/read-mcp-resource.mjs +0 -63
- package/src/tools/builtin/read-web-page.mjs +0 -72
- package/src/tools/builtin/read.mjs +0 -59
- package/src/tools/builtin/runtime-content.mjs +0 -31
- package/src/tools/builtin/runtime-decisions.mjs +0 -115
- package/src/tools/builtin/runtime.mjs +0 -85
- package/src/tools/builtin/task.mjs +0 -63
- package/src/tools/builtin/toolbox-tool.mjs +0 -57
- package/src/tools/builtin/undo-edit.mjs +0 -97
- package/src/tools/builtin/web-search.mjs +0 -128
- package/src/tools/toolbox.mjs +0 -273
- package/src/util/fs.mjs +0 -13
- package/src/util/glob.mjs +0 -46
- package/src/util/html.mjs +0 -21
- package/src/util/media.mjs +0 -13
- package/src/util/shell.mjs +0 -24
- package/src/util/table.mjs +0 -11
|
@@ -1,904 +0,0 @@
|
|
|
1
|
-
# Coven Code Panel TUI Implementation Plan
|
|
2
|
-
|
|
3
|
-
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
-
|
|
5
|
-
**Goal:** Make bare `coven-code` open a keyboard-first panel TUI while preserving execute mode, stream-json mode, command subcommands, and the classic readline REPL escape hatch.
|
|
6
|
-
|
|
7
|
-
**Architecture:** Extract the existing interactive command behavior from `src/cli/repl.mjs` into `src/cli/interactive-core.mjs`, then have both the classic REPL and new TUI call that shared core. Add `src/cli/tui.mjs` as a full-screen terminal UI controller with a transcript, status rail, tabs, composer, command palette, and deterministic test hooks.
|
|
8
|
-
|
|
9
|
-
**Tech Stack:** Node.js ESM, Node built-in test runner, existing `runExecute` command engine, ANSI terminal control via Node stdlib for the first TUI pass.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## File Structure
|
|
14
|
-
|
|
15
|
-
- Modify: `src/cli/repl.mjs`
|
|
16
|
-
- Keep classic readline mode.
|
|
17
|
-
- Delegate slash commands, prompt turns, history, editor, and thread helpers to shared interactive-core helpers.
|
|
18
|
-
- Create: `src/cli/interactive-core.mjs`
|
|
19
|
-
- Own reusable interactive state and command handling.
|
|
20
|
-
- Export `createInteractiveSession`, `handleInteractiveInput`, `printSlashHelp`, `readEditorPrompt`, `interactiveContinuationThread`, and `runInteractiveTurn`.
|
|
21
|
-
- Create: `src/cli/tui.mjs`
|
|
22
|
-
- Own TUI rendering, keyboard handling, command palette state, status rail, tab state, and fallback-safe startup.
|
|
23
|
-
- Export `runTuiInteractive`, `createTuiModel`, `renderTuiFrame`, and `handleTuiKey`.
|
|
24
|
-
- Modify: `src/main.mjs`
|
|
25
|
-
- Route bare TTY sessions to `runTuiInteractive`.
|
|
26
|
-
- Route `COVEN_CODE_REPL=1 coven-code` to `runInteractive`.
|
|
27
|
-
- Preserve execute and command dispatch behavior.
|
|
28
|
-
- Modify: `test/cli.test.mjs`
|
|
29
|
-
- Add tests for interactive routing, classic REPL escape hatch, core slash command behavior, TUI model behavior, and command compatibility.
|
|
30
|
-
- Modify: `README.md`
|
|
31
|
-
- Document the TUI default and classic REPL escape hatch.
|
|
32
|
-
- Modify: `docs/CLI.md`
|
|
33
|
-
- Document interactive TUI behavior and unchanged noninteractive behavior.
|
|
34
|
-
- Modify: `docs/DEVELOPMENT.md`
|
|
35
|
-
- Document TUI test entry points and manual smoke command.
|
|
36
|
-
|
|
37
|
-
## Task 1: Extract Shared Interactive Core
|
|
38
|
-
|
|
39
|
-
**Files:**
|
|
40
|
-
- Create: `src/cli/interactive-core.mjs`
|
|
41
|
-
- Modify: `src/cli/repl.mjs`
|
|
42
|
-
- Test: `test/cli.test.mjs`
|
|
43
|
-
|
|
44
|
-
- [ ] **Step 1: Write failing tests for shared interactive state**
|
|
45
|
-
|
|
46
|
-
Append these tests to `test/cli.test.mjs`:
|
|
47
|
-
|
|
48
|
-
```js
|
|
49
|
-
test('interactive core handles mode, reasoning, queue, and new thread slash commands', async () => {
|
|
50
|
-
const { createInteractiveSession, handleInteractiveInput } = await import(pathToFileURL(path.join(repoRoot, 'src', 'cli', 'interactive-core.mjs')));
|
|
51
|
-
const parsed = { mode: 'smart', reasoningEffort: undefined };
|
|
52
|
-
const session = createInteractiveSession(parsed);
|
|
53
|
-
|
|
54
|
-
const mode = await handleInteractiveInput(session, '/mode deep');
|
|
55
|
-
assert.equal(mode.kind, 'command');
|
|
56
|
-
assert.deepEqual(mode.lines, ['mode: deep', 'reasoning effort: medium']);
|
|
57
|
-
assert.equal(parsed.mode, 'deep');
|
|
58
|
-
assert.equal(parsed.reasoningEffort, 'medium');
|
|
59
|
-
|
|
60
|
-
const reasoning = await handleInteractiveInput(session, '/reasoning next');
|
|
61
|
-
assert.equal(reasoning.kind, 'command');
|
|
62
|
-
assert.deepEqual(reasoning.lines, ['reasoning effort: high']);
|
|
63
|
-
assert.equal(parsed.reasoningEffort, 'high');
|
|
64
|
-
|
|
65
|
-
const queued = await handleInteractiveInput(session, '/queue follow up');
|
|
66
|
-
assert.equal(queued.kind, 'command');
|
|
67
|
-
assert.deepEqual(queued.lines, ['queued: follow up']);
|
|
68
|
-
assert.deepEqual(session.queuedMessages, ['follow up']);
|
|
69
|
-
|
|
70
|
-
session.thread = { id: 'T-test', messages: [{ role: 'user', content: 'hi' }] };
|
|
71
|
-
const fresh = await handleInteractiveInput(session, '/new');
|
|
72
|
-
assert.equal(fresh.kind, 'command');
|
|
73
|
-
assert.deepEqual(fresh.lines, ['new thread']);
|
|
74
|
-
assert.equal(session.thread, undefined);
|
|
75
|
-
});
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
79
|
-
|
|
80
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "interactive core handles mode"`
|
|
81
|
-
|
|
82
|
-
Expected: FAIL with a module-not-found error for `src/cli/interactive-core.mjs`.
|
|
83
|
-
|
|
84
|
-
- [ ] **Step 3: Create `src/cli/interactive-core.mjs`**
|
|
85
|
-
|
|
86
|
-
Move the reusable helpers from `src/cli/repl.mjs` into the new module and include this public API:
|
|
87
|
-
|
|
88
|
-
```js
|
|
89
|
-
import { readFileSync } from 'node:fs';
|
|
90
|
-
import { spawnSync } from 'node:child_process';
|
|
91
|
-
import { appendFile, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
92
|
-
import { tmpdir } from 'node:os';
|
|
93
|
-
import path from 'node:path';
|
|
94
|
-
import { AGENT_MODES, CLI_NAME, CONFIG_SUBDIR, REPL_HISTORY_LIMIT } from '../constants.mjs';
|
|
95
|
-
import { configDir } from '../settings/paths.mjs';
|
|
96
|
-
import { shellQuote, splitShellWords } from '../util/shell.mjs';
|
|
97
|
-
import { latestActiveThread, requireThread } from '../threads/store.mjs';
|
|
98
|
-
import { runCommand } from './dispatch.mjs';
|
|
99
|
-
import { runExecute } from './execute.mjs';
|
|
100
|
-
import {
|
|
101
|
-
coerceReasoningEffortForMode,
|
|
102
|
-
nextReasoningEffortForMode,
|
|
103
|
-
reasoningEffortForMode,
|
|
104
|
-
} from './reasoning.mjs';
|
|
105
|
-
|
|
106
|
-
export function createInteractiveSession(parsed, options = {}) {
|
|
107
|
-
return {
|
|
108
|
-
parsed,
|
|
109
|
-
thread: options.thread,
|
|
110
|
-
queuedMessages: [],
|
|
111
|
-
commandRunner: options.commandRunner ?? runCommand,
|
|
112
|
-
executeRunner: options.executeRunner ?? runExecute,
|
|
113
|
-
editorReader: options.editorReader ?? readEditorPrompt,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function handleInteractiveInput(session, text) {
|
|
118
|
-
if (!text) return { kind: 'empty', lines: [] };
|
|
119
|
-
if (text === '/exit' || text === '/quit') return { kind: 'exit', lines: [] };
|
|
120
|
-
if (text === '/help') return { kind: 'help', lines: slashHelpLines() };
|
|
121
|
-
if (!text.startsWith('/')) {
|
|
122
|
-
session.thread = await runInteractiveTurn(session.parsed, text, session.thread, session.executeRunner);
|
|
123
|
-
while (session.queuedMessages.length > 0) {
|
|
124
|
-
session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
|
|
125
|
-
}
|
|
126
|
-
return { kind: 'turn', lines: [] };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const tokens = splitShellWords(text.slice(1));
|
|
130
|
-
const [cmd, ...rest] = tokens;
|
|
131
|
-
if (!cmd) return { kind: 'empty', lines: [] };
|
|
132
|
-
|
|
133
|
-
if (cmd === 'mode') {
|
|
134
|
-
const nextMode = rest[0];
|
|
135
|
-
if (!nextMode) return { kind: 'command', lines: [`mode: ${session.parsed.mode}`] };
|
|
136
|
-
if (!AGENT_MODES.includes(nextMode)) return { kind: 'error', lines: [`${CLI_NAME}: Unknown mode: ${nextMode}`] };
|
|
137
|
-
session.parsed.mode = nextMode;
|
|
138
|
-
session.parsed.reasoningEffort = coerceReasoningEffortForMode(session.parsed.mode, session.parsed.reasoningEffort);
|
|
139
|
-
return { kind: 'command', lines: [`mode: ${session.parsed.mode}`, `reasoning effort: ${session.parsed.reasoningEffort}`] };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (cmd === 'reasoning') {
|
|
143
|
-
try {
|
|
144
|
-
const nextEffort = rest[0] === 'next'
|
|
145
|
-
? nextReasoningEffortForMode(session.parsed.mode, session.parsed.reasoningEffort)
|
|
146
|
-
: rest[0];
|
|
147
|
-
session.parsed.reasoningEffort = reasoningEffortForMode(session.parsed.mode, nextEffort ?? session.parsed.reasoningEffort);
|
|
148
|
-
return { kind: 'command', lines: [`reasoning effort: ${session.parsed.reasoningEffort}`] };
|
|
149
|
-
} catch (error) {
|
|
150
|
-
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (cmd === 'queue') {
|
|
155
|
-
const queued = rest.join(' ').trim();
|
|
156
|
-
if (!queued) return { kind: 'error', lines: [`${CLI_NAME}: /queue requires a prompt`] };
|
|
157
|
-
session.queuedMessages.push(queued);
|
|
158
|
-
return { kind: 'command', lines: [`queued: ${queued}`] };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (cmd === 'new') {
|
|
162
|
-
session.thread = undefined;
|
|
163
|
-
return { kind: 'command', lines: ['new thread'] };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (cmd === 'continue') {
|
|
167
|
-
try {
|
|
168
|
-
session.thread = interactiveContinuationThread(rest[0]);
|
|
169
|
-
return { kind: 'command', lines: [`continued: ${session.thread.id}`] };
|
|
170
|
-
} catch (error) {
|
|
171
|
-
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (cmd === `${CLI_NAME}:` && rest.join(' ') === 'help') return { kind: 'help', lines: slashHelpLines() };
|
|
176
|
-
|
|
177
|
-
if (cmd === 'editor') {
|
|
178
|
-
const edited = await session.editorReader();
|
|
179
|
-
if (edited) {
|
|
180
|
-
session.thread = await runInteractiveTurn(session.parsed, edited, session.thread, session.executeRunner);
|
|
181
|
-
while (session.queuedMessages.length > 0) {
|
|
182
|
-
session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return { kind: 'command', lines: [] };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (cmd === 'edit') {
|
|
189
|
-
try {
|
|
190
|
-
if (!session.thread) throw new Error('No current thread to edit');
|
|
191
|
-
const editTarget = editablePreviousPrompt(session.thread);
|
|
192
|
-
const edited = await session.editorReader(editTarget.prompt);
|
|
193
|
-
if (edited) {
|
|
194
|
-
session.thread.messages = session.thread.messages.slice(0, editTarget.index);
|
|
195
|
-
if (session.thread.messages.length === 0) {
|
|
196
|
-
session.thread.title = edited.split(/\r?\n/).find(Boolean)?.slice(0, 120) || '(empty prompt)';
|
|
197
|
-
}
|
|
198
|
-
session.thread = await runInteractiveTurn(session.parsed, edited, session.thread, session.executeRunner);
|
|
199
|
-
while (session.queuedMessages.length > 0) {
|
|
200
|
-
session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return { kind: 'command', lines: [] };
|
|
204
|
-
} catch (error) {
|
|
205
|
-
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (cmd === 'thread:' && rest.join(' ') === 'archive and quit') {
|
|
210
|
-
try {
|
|
211
|
-
if (!session.thread) throw new Error('No current thread to archive');
|
|
212
|
-
await session.commandRunner('threads', ['archive', session.thread.id], session.parsed, '');
|
|
213
|
-
return { kind: 'exit', lines: [] };
|
|
214
|
-
} catch (error) {
|
|
215
|
-
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (cmd === 'thread:' && rest[0] === 'set' && rest[1] === 'visibility') {
|
|
220
|
-
try {
|
|
221
|
-
if (!session.thread) throw new Error('No current thread to update');
|
|
222
|
-
await session.commandRunner('threads', ['visibility', session.thread.id, ...rest.slice(2)], session.parsed, '');
|
|
223
|
-
return { kind: 'command', lines: [] };
|
|
224
|
-
} catch (error) {
|
|
225
|
-
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (cmd === 'feedback:' && rest.join(' ') === 'send report with diagnostics') {
|
|
230
|
-
try {
|
|
231
|
-
if (!session.thread) throw new Error('No current thread to report');
|
|
232
|
-
await session.commandRunner('threads', ['report', session.thread.id], session.parsed, '');
|
|
233
|
-
return { kind: 'command', lines: [] };
|
|
234
|
-
} catch (error) {
|
|
235
|
-
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
if (cmd === 'skill:') await session.commandRunner('skill', rest, session.parsed, '');
|
|
241
|
-
else if (cmd === 'plugins:') await session.commandRunner('plugins', rest, session.parsed, '');
|
|
242
|
-
else await session.commandRunner(cmd, rest, session.parsed, '');
|
|
243
|
-
return { kind: 'command', lines: [] };
|
|
244
|
-
} catch (error) {
|
|
245
|
-
if (String(error?.message ?? '').startsWith('Unknown command:')) {
|
|
246
|
-
try {
|
|
247
|
-
await session.commandRunner('plugins', ['run', cmd], session.parsed, '');
|
|
248
|
-
return { kind: 'command', lines: [] };
|
|
249
|
-
} catch (pluginError) {
|
|
250
|
-
if (!String(pluginError?.message ?? '').startsWith('Unknown plugin command:')) throw pluginError;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function slashHelpLines() {
|
|
258
|
-
return [
|
|
259
|
-
'Slash commands:',
|
|
260
|
-
' /exit, /quit leave the REPL',
|
|
261
|
-
' /help show this message',
|
|
262
|
-
` /${CLI_NAME}: help`,
|
|
263
|
-
' show command-palette help',
|
|
264
|
-
' /mode [name] show or set mode: smart, deep, rush, large',
|
|
265
|
-
' /reasoning [level|next]',
|
|
266
|
-
' show, set, or cycle reasoning effort',
|
|
267
|
-
' /new start a fresh thread',
|
|
268
|
-
' /continue [thread-id] continue the latest active thread or a specific thread',
|
|
269
|
-
' /queue <prompt> send a follow-up prompt after the next turn',
|
|
270
|
-
' /editor compose the next prompt in $EDITOR',
|
|
271
|
-
' /edit edit the previous prompt in $EDITOR',
|
|
272
|
-
' /ide connect connect or inspect local IDE integration',
|
|
273
|
-
' /skill: list list installed skills',
|
|
274
|
-
' /plugins: reload reload project and user plugins',
|
|
275
|
-
' /thread: archive and quit',
|
|
276
|
-
' archive the current thread and leave the REPL',
|
|
277
|
-
' /thread: set visibility <level>',
|
|
278
|
-
' set current thread visibility',
|
|
279
|
-
' /feedback: send report with diagnostics',
|
|
280
|
-
' create a diagnostic report for the current thread',
|
|
281
|
-
` /<subcommand> [args] run any top-level ${CLI_NAME} subcommand (e.g. /tools list)`,
|
|
282
|
-
'End a line with `\\` to continue the prompt onto the next line.',
|
|
283
|
-
'Anything else is sent as a one-turn prompt.',
|
|
284
|
-
'',
|
|
285
|
-
'Keybindings:',
|
|
286
|
-
' Ctrl+P open the command palette',
|
|
287
|
-
' Ctrl+E open the current prompt in $EDITOR',
|
|
288
|
-
' Ctrl+M switch agent modes',
|
|
289
|
-
' Ctrl+R cycle reasoning effort',
|
|
290
|
-
' Tab cycle TUI tabs',
|
|
291
|
-
' Esc close overlays',
|
|
292
|
-
' @ mention files',
|
|
293
|
-
];
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function printSlashHelp() {
|
|
297
|
-
for (const line of slashHelpLines()) console.log(line);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export async function readEditorPrompt(initialText = '') {
|
|
301
|
-
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
302
|
-
if (!editor) {
|
|
303
|
-
console.error(`${CLI_NAME}: /editor requires $EDITOR or $VISUAL`);
|
|
304
|
-
return '';
|
|
305
|
-
}
|
|
306
|
-
const file = path.join(tmpdir(), `${CONFIG_SUBDIR}-prompt-${process.pid}-${Date.now()}.md`);
|
|
307
|
-
try {
|
|
308
|
-
await writeFile(file, initialText);
|
|
309
|
-
const result = spawnSync(`${editor} ${shellQuote(file)}`, {
|
|
310
|
-
stdio: 'inherit',
|
|
311
|
-
shell: true,
|
|
312
|
-
});
|
|
313
|
-
if (result.error) {
|
|
314
|
-
console.error(`${CLI_NAME}: Unable to run editor: ${result.error.message}`);
|
|
315
|
-
return '';
|
|
316
|
-
}
|
|
317
|
-
if ((result.status ?? 0) !== 0) {
|
|
318
|
-
console.error(`${CLI_NAME}: Editor exited with status ${result.status}`);
|
|
319
|
-
return '';
|
|
320
|
-
}
|
|
321
|
-
return (await readFile(file, 'utf8')).trim();
|
|
322
|
-
} finally {
|
|
323
|
-
await unlink(file).catch(() => {});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function editablePreviousPrompt(thread) {
|
|
328
|
-
const index = (thread.messages ?? []).findLastIndex((message) => (
|
|
329
|
-
message.role === 'user' && typeof message.content === 'string'
|
|
330
|
-
));
|
|
331
|
-
if (index === -1) throw new Error('No previous user prompt to edit');
|
|
332
|
-
return { index, prompt: thread.messages[index].content };
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export function interactiveContinuationThread(threadId) {
|
|
336
|
-
if (threadId) return requireThread(threadId);
|
|
337
|
-
const thread = latestActiveThread();
|
|
338
|
-
if (!thread) throw new Error('No active thread to continue');
|
|
339
|
-
return thread;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export async function runInteractiveTurn(parsed, text, thread, executeRunner = runExecute) {
|
|
343
|
-
try {
|
|
344
|
-
return await executeRunner(
|
|
345
|
-
{ ...parsed, execute: true, prompt: text, streamJson: false, streamJsonThinking: false, streamJsonInput: false },
|
|
346
|
-
'',
|
|
347
|
-
{ thread },
|
|
348
|
-
);
|
|
349
|
-
} catch (error) {
|
|
350
|
-
console.error(`${CLI_NAME}: ${error?.message ?? error}`);
|
|
351
|
-
return thread;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export function replHistoryFile() {
|
|
356
|
-
return process.env.COVEN_CODE_REPL_HISTORY_FILE
|
|
357
|
-
|| path.join(configDir(), CONFIG_SUBDIR, 'repl_history');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export function loadReplHistory() {
|
|
361
|
-
if (process.env.COVEN_CODE_REPL_HISTORY === '0') return [];
|
|
362
|
-
try {
|
|
363
|
-
return readFileSync(replHistoryFile(), 'utf8')
|
|
364
|
-
.split(/\r?\n/)
|
|
365
|
-
.filter(Boolean)
|
|
366
|
-
.slice(-REPL_HISTORY_LIMIT)
|
|
367
|
-
.reverse();
|
|
368
|
-
} catch {
|
|
369
|
-
return [];
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export async function appendReplHistory(line) {
|
|
374
|
-
if (process.env.COVEN_CODE_REPL_HISTORY === '0') return;
|
|
375
|
-
try {
|
|
376
|
-
const file = replHistoryFile();
|
|
377
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
378
|
-
await appendFile(file, `${line}\n`);
|
|
379
|
-
} catch {
|
|
380
|
-
// history must never break the REPL
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
- [ ] **Step 4: Update `src/cli/repl.mjs` to delegate**
|
|
386
|
-
|
|
387
|
-
Replace embedded command handling with `createInteractiveSession` and `handleInteractiveInput`. Keep readline buffering and prompts in `repl.mjs`.
|
|
388
|
-
|
|
389
|
-
- [ ] **Step 5: Run test to verify it passes**
|
|
390
|
-
|
|
391
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "interactive core handles mode"`
|
|
392
|
-
|
|
393
|
-
Expected: PASS.
|
|
394
|
-
|
|
395
|
-
- [ ] **Step 6: Commit**
|
|
396
|
-
|
|
397
|
-
```bash
|
|
398
|
-
git add src/cli/repl.mjs src/cli/interactive-core.mjs test/cli.test.mjs
|
|
399
|
-
git commit -m "refactor: extract coven-code interactive core"
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
## Task 2: Add Interactive Routing
|
|
403
|
-
|
|
404
|
-
**Files:**
|
|
405
|
-
- Modify: `src/main.mjs`
|
|
406
|
-
- Create: `src/cli/tui.mjs`
|
|
407
|
-
- Test: `test/cli.test.mjs`
|
|
408
|
-
|
|
409
|
-
- [ ] **Step 1: Write failing routing tests**
|
|
410
|
-
|
|
411
|
-
Append tests that import `selectInteractiveRunner` from `src/main.mjs`:
|
|
412
|
-
|
|
413
|
-
```js
|
|
414
|
-
test('interactive routing chooses tui by default for tty sessions and repl when requested', async () => {
|
|
415
|
-
const { selectInteractiveRunner } = await import(pathToFileURL(path.join(repoRoot, 'src', 'main.mjs')));
|
|
416
|
-
|
|
417
|
-
assert.equal(selectInteractiveRunner({
|
|
418
|
-
stdinIsTTY: true,
|
|
419
|
-
stdoutIsTTY: true,
|
|
420
|
-
env: {},
|
|
421
|
-
}), 'tui');
|
|
422
|
-
|
|
423
|
-
assert.equal(selectInteractiveRunner({
|
|
424
|
-
stdinIsTTY: true,
|
|
425
|
-
stdoutIsTTY: true,
|
|
426
|
-
env: { COVEN_CODE_REPL: '1' },
|
|
427
|
-
}), 'repl');
|
|
428
|
-
|
|
429
|
-
assert.equal(selectInteractiveRunner({
|
|
430
|
-
stdinIsTTY: false,
|
|
431
|
-
stdoutIsTTY: true,
|
|
432
|
-
env: {},
|
|
433
|
-
}), 'repl');
|
|
434
|
-
});
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
438
|
-
|
|
439
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "interactive routing chooses tui"`
|
|
440
|
-
|
|
441
|
-
Expected: FAIL because `selectInteractiveRunner` is not exported.
|
|
442
|
-
|
|
443
|
-
- [ ] **Step 3: Add minimal `src/cli/tui.mjs`**
|
|
444
|
-
|
|
445
|
-
Create a first stub that delegates to the classic REPL until the real TUI task:
|
|
446
|
-
|
|
447
|
-
```js
|
|
448
|
-
import { runInteractive } from './repl.mjs';
|
|
449
|
-
|
|
450
|
-
export async function runTuiInteractive(parsed, initialInput = '') {
|
|
451
|
-
return runInteractive(parsed, initialInput);
|
|
452
|
-
}
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
- [ ] **Step 4: Export `selectInteractiveRunner` and wire routing in `src/main.mjs`**
|
|
456
|
-
|
|
457
|
-
Add:
|
|
458
|
-
|
|
459
|
-
```js
|
|
460
|
-
import { runTuiInteractive } from './cli/tui.mjs';
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
Add:
|
|
464
|
-
|
|
465
|
-
```js
|
|
466
|
-
export function selectInteractiveRunner({ stdinIsTTY, stdoutIsTTY, env }) {
|
|
467
|
-
if (env.COVEN_CODE_REPL === '1') return 'repl';
|
|
468
|
-
if (stdinIsTTY && stdoutIsTTY) return 'tui';
|
|
469
|
-
return 'repl';
|
|
470
|
-
}
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
Replace the interactive branch with:
|
|
474
|
-
|
|
475
|
-
```js
|
|
476
|
-
if (process.stdout.isTTY && (process.stdin.isTTY || stdin.length > 0)) {
|
|
477
|
-
const runner = selectInteractiveRunner({
|
|
478
|
-
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
479
|
-
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
480
|
-
env: process.env,
|
|
481
|
-
});
|
|
482
|
-
if (runner === 'tui') await runTuiInteractive(parsed, stdin);
|
|
483
|
-
else await runInteractive(parsed, stdin);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
- [ ] **Step 5: Run test to verify it passes**
|
|
489
|
-
|
|
490
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "interactive routing chooses tui"`
|
|
491
|
-
|
|
492
|
-
Expected: PASS.
|
|
493
|
-
|
|
494
|
-
- [ ] **Step 6: Verify execute mode still works**
|
|
495
|
-
|
|
496
|
-
Run: `node ./bin/coven-code.mjs -x "what is 2+2?"`
|
|
497
|
-
|
|
498
|
-
Expected: `4`.
|
|
499
|
-
|
|
500
|
-
- [ ] **Step 7: Commit**
|
|
501
|
-
|
|
502
|
-
```bash
|
|
503
|
-
git add src/main.mjs src/cli/tui.mjs test/cli.test.mjs
|
|
504
|
-
git commit -m "feat: route coven-code tty sessions to tui"
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
## Task 3: Build the TUI Model and Renderer
|
|
508
|
-
|
|
509
|
-
**Files:**
|
|
510
|
-
- Modify: `src/cli/tui.mjs`
|
|
511
|
-
- Test: `test/cli.test.mjs`
|
|
512
|
-
|
|
513
|
-
- [ ] **Step 1: Write failing model/render tests**
|
|
514
|
-
|
|
515
|
-
Append:
|
|
516
|
-
|
|
517
|
-
```js
|
|
518
|
-
test('tui model renders panel layout with transcript tabs status rail and composer', async () => {
|
|
519
|
-
const { createTuiModel, renderTuiFrame } = await import(pathToFileURL(path.join(repoRoot, 'src', 'cli', 'tui.mjs')));
|
|
520
|
-
|
|
521
|
-
const model = createTuiModel({
|
|
522
|
-
version: '0.0.0-test',
|
|
523
|
-
mode: 'smart',
|
|
524
|
-
reasoningEffort: 'medium',
|
|
525
|
-
threadId: 'T-test',
|
|
526
|
-
toolCount: 18,
|
|
527
|
-
});
|
|
528
|
-
model.transcript.push({ role: 'user', text: 'hello' });
|
|
529
|
-
model.composer = 'what is 2+2?';
|
|
530
|
-
|
|
531
|
-
const frame = renderTuiFrame(model, { columns: 82, rows: 24, color: false });
|
|
532
|
-
|
|
533
|
-
assert.match(frame, /Coven Code 0\.0\.0-test/);
|
|
534
|
-
assert.match(frame, /chat\s+tools\s+threads\s+config\s+help/);
|
|
535
|
-
assert.match(frame, /you/);
|
|
536
|
-
assert.match(frame, /hello/);
|
|
537
|
-
assert.match(frame, /thread: T-test/);
|
|
538
|
-
assert.match(frame, /mode: smart/);
|
|
539
|
-
assert.match(frame, /> what is 2\+2\?/);
|
|
540
|
-
});
|
|
541
|
-
```
|
|
542
|
-
|
|
543
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
544
|
-
|
|
545
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui model renders"`
|
|
546
|
-
|
|
547
|
-
Expected: FAIL because `createTuiModel` and `renderTuiFrame` are not exported.
|
|
548
|
-
|
|
549
|
-
- [ ] **Step 3: Implement model and renderer**
|
|
550
|
-
|
|
551
|
-
Implement:
|
|
552
|
-
|
|
553
|
-
```js
|
|
554
|
-
import { CLI_NAME, VERSION } from '../constants.mjs';
|
|
555
|
-
import { createInteractiveSession, handleInteractiveInput, slashHelpLines } from './interactive-core.mjs';
|
|
556
|
-
|
|
557
|
-
const TABS = ['chat', 'tools', 'threads', 'config', 'help'];
|
|
558
|
-
|
|
559
|
-
export function createTuiModel(options = {}) {
|
|
560
|
-
return {
|
|
561
|
-
version: options.version ?? VERSION,
|
|
562
|
-
mode: options.mode ?? 'smart',
|
|
563
|
-
reasoningEffort: options.reasoningEffort ?? 'medium',
|
|
564
|
-
threadId: options.threadId ?? 'new thread',
|
|
565
|
-
toolCount: options.toolCount ?? 0,
|
|
566
|
-
queueCount: options.queueCount ?? 0,
|
|
567
|
-
activeTab: options.activeTab ?? 'chat',
|
|
568
|
-
paletteOpen: false,
|
|
569
|
-
composer: '',
|
|
570
|
-
transcript: [],
|
|
571
|
-
status: 'idle',
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
export function renderTuiFrame(model, size = {}) {
|
|
576
|
-
const columns = Math.max(50, size.columns ?? process.stdout.columns ?? 80);
|
|
577
|
-
const rows = Math.max(16, size.rows ?? process.stdout.rows ?? 24);
|
|
578
|
-
const width = columns;
|
|
579
|
-
const bodyRows = rows - 7;
|
|
580
|
-
const divider = '-'.repeat(width);
|
|
581
|
-
const tabs = TABS.map((tab) => tab === model.activeTab ? `[${tab}]` : tab).join(' ');
|
|
582
|
-
const transcript = renderTranscript(model, Math.max(3, bodyRows - 4), width - 24);
|
|
583
|
-
const status = [
|
|
584
|
-
`thread: ${model.threadId}`,
|
|
585
|
-
`mode: ${model.mode}`,
|
|
586
|
-
`reasoning: ${model.reasoningEffort}`,
|
|
587
|
-
`queued: ${model.queueCount}`,
|
|
588
|
-
`tools: ${model.toolCount}`,
|
|
589
|
-
`status: ${model.status}`,
|
|
590
|
-
];
|
|
591
|
-
const body = mergeColumns(transcript, status, width);
|
|
592
|
-
const lines = [
|
|
593
|
-
`Coven Code ${model.version}`.slice(0, width),
|
|
594
|
-
tabs.slice(0, width),
|
|
595
|
-
divider,
|
|
596
|
-
...body,
|
|
597
|
-
divider,
|
|
598
|
-
`> ${model.composer}`.slice(0, width),
|
|
599
|
-
];
|
|
600
|
-
return lines.slice(0, rows).join('\n');
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function renderTranscript(model, limit, width) {
|
|
604
|
-
if (model.activeTab === 'help') return slashHelpLines().slice(0, limit).map((line) => line.slice(0, width));
|
|
605
|
-
if (model.activeTab !== 'chat') return [`${model.activeTab} panel`, 'Use slash commands or Ctrl-P palette actions.'];
|
|
606
|
-
const entries = model.transcript.slice(-limit).flatMap((entry) => [
|
|
607
|
-
`${entry.role}:`.slice(0, width),
|
|
608
|
-
...String(entry.text).split(/\r?\n/).map((line) => line.slice(0, width)),
|
|
609
|
-
]);
|
|
610
|
-
return entries.length > 0 ? entries.slice(-limit) : ['Ready. Type a prompt or /help.'];
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function mergeColumns(leftLines, rightLines, width) {
|
|
614
|
-
const rightWidth = Math.min(22, Math.floor(width * 0.32));
|
|
615
|
-
const leftWidth = width - rightWidth - 3;
|
|
616
|
-
const count = Math.max(leftLines.length, rightLines.length);
|
|
617
|
-
const rows = [];
|
|
618
|
-
for (let index = 0; index < count; index += 1) {
|
|
619
|
-
const left = (leftLines[index] ?? '').padEnd(leftWidth).slice(0, leftWidth);
|
|
620
|
-
const right = (rightLines[index] ?? '').slice(0, rightWidth);
|
|
621
|
-
rows.push(`${left} | ${right}`);
|
|
622
|
-
}
|
|
623
|
-
return rows;
|
|
624
|
-
}
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
628
|
-
|
|
629
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui model renders"`
|
|
630
|
-
|
|
631
|
-
Expected: PASS.
|
|
632
|
-
|
|
633
|
-
- [ ] **Step 5: Commit**
|
|
634
|
-
|
|
635
|
-
```bash
|
|
636
|
-
git add src/cli/tui.mjs test/cli.test.mjs
|
|
637
|
-
git commit -m "feat: add coven-code tui frame renderer"
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
## Task 4: Add Keyboard Handling and Command Palette Actions
|
|
641
|
-
|
|
642
|
-
**Files:**
|
|
643
|
-
- Modify: `src/cli/tui.mjs`
|
|
644
|
-
- Test: `test/cli.test.mjs`
|
|
645
|
-
|
|
646
|
-
- [ ] **Step 1: Write failing keyboard tests**
|
|
647
|
-
|
|
648
|
-
Append:
|
|
649
|
-
|
|
650
|
-
```js
|
|
651
|
-
test('tui key handling cycles tabs and command palette actions reuse interactive commands', async () => {
|
|
652
|
-
const { createTuiModel, handleTuiKey } = await import(pathToFileURL(path.join(repoRoot, 'src', 'cli', 'tui.mjs')));
|
|
653
|
-
const model = createTuiModel({ mode: 'smart', reasoningEffort: 'medium' });
|
|
654
|
-
const session = {
|
|
655
|
-
parsed: { mode: 'smart', reasoningEffort: 'medium' },
|
|
656
|
-
queuedMessages: [],
|
|
657
|
-
commandRunner: async () => {},
|
|
658
|
-
executeRunner: async () => undefined,
|
|
659
|
-
editorReader: async () => '',
|
|
660
|
-
};
|
|
661
|
-
|
|
662
|
-
await handleTuiKey(model, session, { name: 'tab' });
|
|
663
|
-
assert.equal(model.activeTab, 'tools');
|
|
664
|
-
|
|
665
|
-
await handleTuiKey(model, session, { ctrl: true, name: 'p' });
|
|
666
|
-
assert.equal(model.paletteOpen, true);
|
|
667
|
-
|
|
668
|
-
model.paletteIndex = 0;
|
|
669
|
-
await handleTuiKey(model, session, { name: 'enter' });
|
|
670
|
-
assert.equal(model.paletteOpen, false);
|
|
671
|
-
assert.equal(model.transcript.at(-1).text, 'new thread');
|
|
672
|
-
|
|
673
|
-
model.composer = '/mode deep';
|
|
674
|
-
await handleTuiKey(model, session, { name: 'enter' });
|
|
675
|
-
assert.equal(session.parsed.mode, 'deep');
|
|
676
|
-
assert.match(model.transcript.at(-1).text, /mode: deep/);
|
|
677
|
-
});
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
681
|
-
|
|
682
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui key handling"`
|
|
683
|
-
|
|
684
|
-
Expected: FAIL because `handleTuiKey` is not exported.
|
|
685
|
-
|
|
686
|
-
- [ ] **Step 3: Implement `handleTuiKey` and palette actions**
|
|
687
|
-
|
|
688
|
-
Add:
|
|
689
|
-
|
|
690
|
-
```js
|
|
691
|
-
const PALETTE_ACTIONS = [
|
|
692
|
-
['New thread', '/new'],
|
|
693
|
-
['Continue latest thread', '/continue'],
|
|
694
|
-
['Open help', '/help'],
|
|
695
|
-
['List tools', '/tools list'],
|
|
696
|
-
['List skills', '/skill: list'],
|
|
697
|
-
['List plugins', '/plugins: list'],
|
|
698
|
-
];
|
|
699
|
-
|
|
700
|
-
export async function handleTuiKey(model, session, key) {
|
|
701
|
-
if (key?.name === 'tab') {
|
|
702
|
-
const index = TABS.indexOf(model.activeTab);
|
|
703
|
-
model.activeTab = TABS[(index + 1) % TABS.length];
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (key?.ctrl && key.name === 'p') {
|
|
707
|
-
model.paletteOpen = true;
|
|
708
|
-
model.paletteIndex = 0;
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
if (model.paletteOpen && key?.name === 'enter') {
|
|
712
|
-
const [, command] = PALETTE_ACTIONS[model.paletteIndex ?? 0];
|
|
713
|
-
model.paletteOpen = false;
|
|
714
|
-
await submitTuiText(model, session, command);
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
if (key?.ctrl && key.name === 'n') {
|
|
718
|
-
await submitTuiText(model, session, '/new');
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
if (key?.ctrl && key.name === 'r') {
|
|
722
|
-
await submitTuiText(model, session, '/reasoning next');
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
if (key?.ctrl && key.name === 'm') {
|
|
726
|
-
const next = session.parsed.mode === 'smart' ? 'deep' : session.parsed.mode === 'deep' ? 'rush' : 'smart';
|
|
727
|
-
await submitTuiText(model, session, `/mode ${next}`);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
if (key?.name === 'enter') {
|
|
731
|
-
const text = model.composer.trim();
|
|
732
|
-
model.composer = '';
|
|
733
|
-
await submitTuiText(model, session, text);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
async function submitTuiText(model, session, text) {
|
|
738
|
-
if (!text) return;
|
|
739
|
-
model.transcript.push({ role: 'you', text });
|
|
740
|
-
model.status = 'running';
|
|
741
|
-
const result = await handleInteractiveInput(session, text);
|
|
742
|
-
model.mode = session.parsed.mode;
|
|
743
|
-
model.reasoningEffort = session.parsed.reasoningEffort ?? model.reasoningEffort;
|
|
744
|
-
model.threadId = session.thread?.id ?? 'new thread';
|
|
745
|
-
model.queueCount = session.queuedMessages.length;
|
|
746
|
-
if (result.lines.length > 0) model.transcript.push({ role: result.kind === 'error' ? 'error' : 'coven', text: result.lines.join('\n') });
|
|
747
|
-
model.status = result.kind === 'exit' ? 'done' : 'idle';
|
|
748
|
-
}
|
|
749
|
-
```
|
|
750
|
-
|
|
751
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
752
|
-
|
|
753
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui key handling"`
|
|
754
|
-
|
|
755
|
-
Expected: PASS.
|
|
756
|
-
|
|
757
|
-
- [ ] **Step 5: Commit**
|
|
758
|
-
|
|
759
|
-
```bash
|
|
760
|
-
git add src/cli/tui.mjs test/cli.test.mjs
|
|
761
|
-
git commit -m "feat: add coven-code tui keyboard actions"
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
## Task 5: Run the Full-Screen TUI Loop
|
|
765
|
-
|
|
766
|
-
**Files:**
|
|
767
|
-
- Modify: `src/cli/tui.mjs`
|
|
768
|
-
- Test: `test/cli.test.mjs`
|
|
769
|
-
|
|
770
|
-
- [ ] **Step 1: Write failing smoke test for scripted TUI input**
|
|
771
|
-
|
|
772
|
-
Append:
|
|
773
|
-
|
|
774
|
-
```js
|
|
775
|
-
test('tui scripted smoke processes input and exits without changing execute mode', async () => {
|
|
776
|
-
const result = runCovenCode([], {
|
|
777
|
-
input: '/mode deep\n/exit\n',
|
|
778
|
-
env: {
|
|
779
|
-
COVEN_CODE_TUI_SCRIPTED: '1',
|
|
780
|
-
COVEN_CODE_REPL_HISTORY: '0',
|
|
781
|
-
},
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
assert.equal(result.status, 0, result.stderr);
|
|
785
|
-
assert.match(result.stdout, /Coven Code/);
|
|
786
|
-
assert.match(result.stdout, /mode: deep/);
|
|
787
|
-
|
|
788
|
-
const execute = runCovenCode(['-x', 'what is 2+2?']);
|
|
789
|
-
assert.equal(execute.status, 0, execute.stderr);
|
|
790
|
-
assert.equal(execute.stdout.trim(), '4');
|
|
791
|
-
});
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
- [ ] **Step 2: Run test to verify it fails**
|
|
795
|
-
|
|
796
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui scripted smoke"`
|
|
797
|
-
|
|
798
|
-
Expected: FAIL because scripted TUI mode is not implemented.
|
|
799
|
-
|
|
800
|
-
- [ ] **Step 3: Implement scripted and interactive TUI loops**
|
|
801
|
-
|
|
802
|
-
In `runTuiInteractive`:
|
|
803
|
-
|
|
804
|
-
- Create `session = createInteractiveSession(parsed)`.
|
|
805
|
-
- Create `model = createTuiModel(...)`.
|
|
806
|
-
- If `COVEN_CODE_TUI_SCRIPTED=1`, read newline-separated commands from `initialInput`, call `submitTuiText`, print `renderTuiFrame`, and return.
|
|
807
|
-
- For real TTY mode, use `readline.emitKeypressEvents(process.stdin)`, raw mode when available, clear/redraw the frame with ANSI `\x1b[2J\x1b[H`, and dispatch keys to `handleTuiKey`.
|
|
808
|
-
- On `Ctrl-C`, restore raw mode and exit.
|
|
809
|
-
|
|
810
|
-
- [ ] **Step 4: Run test to verify it passes**
|
|
811
|
-
|
|
812
|
-
Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui scripted smoke"`
|
|
813
|
-
|
|
814
|
-
Expected: PASS.
|
|
815
|
-
|
|
816
|
-
- [ ] **Step 5: Manual smoke in Terminal**
|
|
817
|
-
|
|
818
|
-
Run in Terminal:
|
|
819
|
-
|
|
820
|
-
```bash
|
|
821
|
-
cd /Users/buns/Documents/GitHub/OpenCoven/coven-code
|
|
822
|
-
COVEN_CODE_REPL_HISTORY=0 COVEN_CODE_SKIP_UPDATE_CHECK=1 npm run coven-code
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
Expected: full-screen TUI opens, shows header/tabs/transcript/status/composer, `/exit` leaves cleanly.
|
|
826
|
-
|
|
827
|
-
- [ ] **Step 6: Commit**
|
|
828
|
-
|
|
829
|
-
```bash
|
|
830
|
-
git add src/cli/tui.mjs test/cli.test.mjs
|
|
831
|
-
git commit -m "feat: run coven-code panel tui"
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
## Task 6: Update Docs and Final Verification
|
|
835
|
-
|
|
836
|
-
**Files:**
|
|
837
|
-
- Modify: `README.md`
|
|
838
|
-
- Modify: `docs/CLI.md`
|
|
839
|
-
- Modify: `docs/DEVELOPMENT.md`
|
|
840
|
-
|
|
841
|
-
- [ ] **Step 1: Write docs updates**
|
|
842
|
-
|
|
843
|
-
Document:
|
|
844
|
-
|
|
845
|
-
- Bare `coven-code` opens the panel TUI.
|
|
846
|
-
- `COVEN_CODE_REPL=1 coven-code` opens the classic readline REPL.
|
|
847
|
-
- `COVEN_CODE_TUI_SCRIPTED=1` is only for tests/automation.
|
|
848
|
-
- `-x`, `--stream-json`, stdin piping, and subcommands are unchanged.
|
|
849
|
-
|
|
850
|
-
- [ ] **Step 2: Verify docs diff**
|
|
851
|
-
|
|
852
|
-
Run: `git diff -- README.md docs/CLI.md docs/DEVELOPMENT.md`
|
|
853
|
-
|
|
854
|
-
Expected: only TUI-related documentation changes.
|
|
855
|
-
|
|
856
|
-
- [ ] **Step 3: Run targeted checks**
|
|
857
|
-
|
|
858
|
-
Run:
|
|
859
|
-
|
|
860
|
-
```bash
|
|
861
|
-
git diff --check
|
|
862
|
-
node ./bin/coven-code.mjs --help
|
|
863
|
-
node ./bin/coven-code.mjs -x "what is 2+2?"
|
|
864
|
-
npm test
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
Expected:
|
|
868
|
-
|
|
869
|
-
- `git diff --check` exits 0
|
|
870
|
-
- help renders
|
|
871
|
-
- execute mode prints `4`
|
|
872
|
-
- tests pass with 0 failures
|
|
873
|
-
|
|
874
|
-
- [ ] **Step 4: Commit**
|
|
875
|
-
|
|
876
|
-
```bash
|
|
877
|
-
git add README.md docs/CLI.md docs/DEVELOPMENT.md
|
|
878
|
-
git commit -m "docs: document coven-code panel tui"
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
## Final Verification
|
|
882
|
-
|
|
883
|
-
Run:
|
|
884
|
-
|
|
885
|
-
```bash
|
|
886
|
-
git diff --check
|
|
887
|
-
node ./bin/coven-code.mjs --help
|
|
888
|
-
node ./bin/coven-code.mjs -x "what is 2+2?"
|
|
889
|
-
COVEN_CODE_TUI_SCRIPTED=1 COVEN_CODE_REPL_HISTORY=0 node ./bin/coven-code.mjs <<'EOF'
|
|
890
|
-
/mode deep
|
|
891
|
-
/exit
|
|
892
|
-
EOF
|
|
893
|
-
npm test
|
|
894
|
-
git status --short --branch
|
|
895
|
-
```
|
|
896
|
-
|
|
897
|
-
Expected:
|
|
898
|
-
|
|
899
|
-
- whitespace check passes
|
|
900
|
-
- help renders
|
|
901
|
-
- execute mode prints `4`
|
|
902
|
-
- scripted TUI output includes `Coven Code` and `mode: deep`
|
|
903
|
-
- tests pass with 0 failures
|
|
904
|
-
- worktree is clean except being ahead by local commits
|