deepclause-sdk 0.0.59 → 0.0.61
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 +326 -57
- package/dist/cli/compile.d.ts +9 -0
- package/dist/cli/compile.d.ts.map +1 -1
- package/dist/cli/compile.js +106 -138
- package/dist/cli/compile.js.map +1 -1
- package/dist/cli/config.d.ts +74 -23
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +162 -58
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/default-skills.d.ts +9 -0
- package/dist/cli/default-skills.d.ts.map +1 -0
- package/dist/cli/default-skills.js +175 -0
- package/dist/cli/default-skills.js.map +1 -0
- package/dist/cli/index.js +79 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts +2 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +20 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/prompt.d.ts +1 -19
- package/dist/cli/prompt.d.ts.map +1 -1
- package/dist/cli/prompt.js +1 -869
- package/dist/cli/prompt.js.map +1 -1
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +79 -337
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/search.d.ts +1 -0
- package/dist/cli/search.d.ts.map +1 -1
- package/dist/cli/search.js +10 -4
- package/dist/cli/search.js.map +1 -1
- package/dist/cli/tools.d.ts +2 -2
- package/dist/cli/tools.d.ts.map +1 -1
- package/dist/cli/tools.js +30 -5
- package/dist/cli/tools.js.map +1 -1
- package/dist/cli/tui.d.ts +83 -0
- package/dist/cli/tui.d.ts.map +1 -0
- package/dist/cli/tui.js +3552 -0
- package/dist/cli/tui.js.map +1 -0
- package/dist/compiler_prompt.d.ts +1 -1
- package/dist/compiler_prompt.d.ts.map +1 -1
- package/dist/compiler_prompt.js +8 -10
- package/dist/compiler_prompt.js.map +1 -1
- package/dist/prolog/bridge.js +1 -1
- package/dist/prolog/bridge.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +5 -0
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/CONDUCTOR_PROMPT.md +46 -0
- package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +664 -0
- package/dist/system/assets/index.d.ts +9 -0
- package/dist/system/assets/index.d.ts.map +1 -0
- package/dist/system/assets/index.js +47 -0
- package/dist/system/assets/index.js.map +1 -0
- package/dist/system/assets/skills/conductor.dml +70 -0
- package/dist/system/assets/skills/skill-creator.dml +186 -0
- package/dist/system/config/model-slots.d.ts +34 -0
- package/dist/system/config/model-slots.d.ts.map +1 -0
- package/dist/system/config/model-slots.js +121 -0
- package/dist/system/config/model-slots.js.map +1 -0
- package/dist/system/runtime/agentvm-manager.d.ts +18 -0
- package/dist/system/runtime/agentvm-manager.d.ts.map +1 -0
- package/dist/system/runtime/agentvm-manager.js +98 -0
- package/dist/system/runtime/agentvm-manager.js.map +1 -0
- package/dist/system/runtime/catalog-skills.d.ts +45 -0
- package/dist/system/runtime/catalog-skills.d.ts.map +1 -0
- package/dist/system/runtime/catalog-skills.js +110 -0
- package/dist/system/runtime/catalog-skills.js.map +1 -0
- package/dist/system/runtime/conductor.d.ts +68 -0
- package/dist/system/runtime/conductor.d.ts.map +1 -0
- package/dist/system/runtime/conductor.js +407 -0
- package/dist/system/runtime/conductor.js.map +1 -0
- package/dist/system/runtime/console-capture.d.ts +9 -0
- package/dist/system/runtime/console-capture.d.ts.map +1 -0
- package/dist/system/runtime/console-capture.js +67 -0
- package/dist/system/runtime/console-capture.js.map +1 -0
- package/dist/system/runtime/dml-executor.d.ts +49 -0
- package/dist/system/runtime/dml-executor.d.ts.map +1 -0
- package/dist/system/runtime/dml-executor.js +185 -0
- package/dist/system/runtime/dml-executor.js.map +1 -0
- package/dist/system/runtime/dml-params.d.ts +11 -0
- package/dist/system/runtime/dml-params.d.ts.map +1 -0
- package/dist/system/runtime/dml-params.js +45 -0
- package/dist/system/runtime/dml-params.js.map +1 -0
- package/dist/system/runtime/runtime-tools.d.ts +20 -0
- package/dist/system/runtime/runtime-tools.d.ts.map +1 -0
- package/dist/system/runtime/runtime-tools.js +170 -0
- package/dist/system/runtime/runtime-tools.js.map +1 -0
- package/dist/system/runtime/shell-manager.d.ts +20 -0
- package/dist/system/runtime/shell-manager.d.ts.map +1 -0
- package/dist/system/runtime/shell-manager.js +133 -0
- package/dist/system/runtime/shell-manager.js.map +1 -0
- package/dist/system/runtime/skill-creator.d.ts +33 -0
- package/dist/system/runtime/skill-creator.d.ts.map +1 -0
- package/dist/system/runtime/skill-creator.js +529 -0
- package/dist/system/runtime/skill-creator.js.map +1 -0
- package/dist/system/runtime/token-usage.d.ts +16 -0
- package/dist/system/runtime/token-usage.d.ts.map +1 -0
- package/dist/system/runtime/token-usage.js +45 -0
- package/dist/system/runtime/token-usage.js.map +1 -0
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/package.json +4 -3
package/dist/cli/tui.js
ADDED
|
@@ -0,0 +1,3552 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { emitKeypressEvents } from 'readline';
|
|
4
|
+
import { createInterface } from 'readline/promises';
|
|
5
|
+
import { stdin as input, stdout as output } from 'process';
|
|
6
|
+
import { listCommands } from './commands.js';
|
|
7
|
+
import { getConfigPath, loadConfig, resolveModelSlot } from './config.js';
|
|
8
|
+
import { run } from './run.js';
|
|
9
|
+
import { createLocalSkill, createConductorSession, getConductorSessionDetail, listConductorSessions, runConductorTurn, } from '../system/runtime/conductor.js';
|
|
10
|
+
const IGNORED_LIVE_LOG_TOOLS = new Set(['set_result', 'update_memory']);
|
|
11
|
+
const BUILTIN_SLASH_COMMANDS = ['new', 'sessions', 'help', 'compile', 'skill-creator', 'cancel', 'exit', 'quit'];
|
|
12
|
+
const CHILD_EVENT_INDENT = '\t';
|
|
13
|
+
const CHILD_EVENT_TAB_WIDTH = 4;
|
|
14
|
+
const MAX_ACTIVITY_LINES = 400;
|
|
15
|
+
const SPINNER_FRAMES = ['|', '/', '-', '\\'];
|
|
16
|
+
const IDLE_RENDER_INTERVAL_MS = 16;
|
|
17
|
+
const BUSY_RENDER_INTERVAL_MS = 40;
|
|
18
|
+
const TYPING_RENDER_INTERVAL_MS = 90;
|
|
19
|
+
const COMBINING_MARK_RE = /\p{Mark}/u;
|
|
20
|
+
const EXTENDED_PICTOGRAPHIC_RE = /\p{Extended_Pictographic}/u;
|
|
21
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
22
|
+
'.dml', '.md', '.txt', '.json', '.yaml', '.yml', '.toml', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
23
|
+
'.css', '.scss', '.html', '.sql', '.py', '.sh', '.bash', '.zsh', '.pro', '.pl', '.csv', '.env', '.example',
|
|
24
|
+
]);
|
|
25
|
+
const TEXT_FILE_BASENAMES = new Set(['README', 'README.md', 'AGENTS.md', 'SPEC.md', 'ARCHITECTURE.md', 'CMD_LINE.md']);
|
|
26
|
+
const IGNORED_WORKSPACE_DIRS = new Set(['.git', 'node_modules', 'dist', '.next', 'coverage', 'agentvm-cache']);
|
|
27
|
+
const graphemeSegmenter = (() => {
|
|
28
|
+
const Segmenter = Intl.Segmenter;
|
|
29
|
+
return Segmenter ? new Segmenter(undefined, { granularity: 'grapheme' }) : null;
|
|
30
|
+
})();
|
|
31
|
+
const ANSI = {
|
|
32
|
+
reset: '\u001b[0m',
|
|
33
|
+
bold: '\u001b[1m',
|
|
34
|
+
dim: '\u001b[2m',
|
|
35
|
+
black: '\u001b[30m',
|
|
36
|
+
red: '\u001b[31m',
|
|
37
|
+
blue: '\u001b[34m',
|
|
38
|
+
white: '\u001b[37m',
|
|
39
|
+
green: '\u001b[32m',
|
|
40
|
+
yellow: '\u001b[33m',
|
|
41
|
+
cyan: '\u001b[36m',
|
|
42
|
+
brightYellow: '\u001b[93m',
|
|
43
|
+
brightCyan: '\u001b[96m',
|
|
44
|
+
brightWhite: '\u001b[97m',
|
|
45
|
+
bgBlue: '\u001b[48;2;0;0;95m',
|
|
46
|
+
bgCyan: '\u001b[48;2;0;96;128m',
|
|
47
|
+
bgWhite: '\u001b[47m',
|
|
48
|
+
};
|
|
49
|
+
export class LiveExecutionPrinter {
|
|
50
|
+
write;
|
|
51
|
+
writeLine;
|
|
52
|
+
activeStreamKey = null;
|
|
53
|
+
streamOpen = false;
|
|
54
|
+
constructor(write = (text) => process.stdout.write(text), writeLine = (text) => console.log(text)) {
|
|
55
|
+
this.write = write;
|
|
56
|
+
this.writeLine = writeLine;
|
|
57
|
+
}
|
|
58
|
+
handle(logEvent) {
|
|
59
|
+
const { event } = logEvent;
|
|
60
|
+
if (event.type === 'usage' || event.type === 'finished') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (event.type === 'stream') {
|
|
64
|
+
this.handleStream(logEvent);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.flushStream();
|
|
68
|
+
switch (event.type) {
|
|
69
|
+
case 'tool_call':
|
|
70
|
+
if (!event.toolName || IGNORED_LIVE_LOG_TOOLS.has(event.toolName)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.writeLine(`${formatEventPrefix(logEvent)}tool ${event.toolName}(${formatToolArgs(event.toolArgs)})`);
|
|
74
|
+
break;
|
|
75
|
+
case 'output':
|
|
76
|
+
if (event.content) {
|
|
77
|
+
this.writeLine(`${formatEventPrefix(logEvent)}output ${event.content}`);
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
case 'answer':
|
|
81
|
+
if (event.content) {
|
|
82
|
+
this.writeLine(`${formatEventPrefix(logEvent)}answer ${event.content}`);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
case 'error':
|
|
86
|
+
if (event.content) {
|
|
87
|
+
this.writeLine(`${formatEventPrefix(logEvent)}error ${event.content}`);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case 'log':
|
|
91
|
+
if (event.content) {
|
|
92
|
+
this.writeLine(`${formatEventPrefix(logEvent)}log ${event.content}`);
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case 'input_required':
|
|
96
|
+
if (event.prompt) {
|
|
97
|
+
this.writeLine(`${formatEventPrefix(logEvent)}clarify ${event.prompt}`);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finish() {
|
|
103
|
+
this.flushStream();
|
|
104
|
+
}
|
|
105
|
+
handleStream(logEvent) {
|
|
106
|
+
const streamKey = logEvent.scope === 'child' ? `child:${logEvent.childSlug ?? '?'}` : 'main';
|
|
107
|
+
if (this.activeStreamKey !== streamKey) {
|
|
108
|
+
this.flushStream();
|
|
109
|
+
this.write(`${streamLabel(logEvent)} `);
|
|
110
|
+
this.activeStreamKey = streamKey;
|
|
111
|
+
this.streamOpen = true;
|
|
112
|
+
}
|
|
113
|
+
if (logEvent.event.content) {
|
|
114
|
+
this.write(logEvent.event.content);
|
|
115
|
+
}
|
|
116
|
+
if (logEvent.event.done) {
|
|
117
|
+
this.flushStream();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
flushStream() {
|
|
121
|
+
if (!this.streamOpen) {
|
|
122
|
+
this.activeStreamKey = null;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.write('\n');
|
|
126
|
+
this.streamOpen = false;
|
|
127
|
+
this.activeStreamKey = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
class ActivityBuffer {
|
|
131
|
+
lines = [];
|
|
132
|
+
activeStreamKey = null;
|
|
133
|
+
activeStreamLine = '';
|
|
134
|
+
handle(logEvent) {
|
|
135
|
+
const { event } = logEvent;
|
|
136
|
+
if (event.type === 'usage' || event.type === 'finished') {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (event.type === 'stream') {
|
|
140
|
+
this.handleStream(logEvent);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.flushStream();
|
|
144
|
+
switch (event.type) {
|
|
145
|
+
case 'tool_call':
|
|
146
|
+
if (!event.toolName || IGNORED_LIVE_LOG_TOOLS.has(event.toolName)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.pushLine(`${formatEventPrefix(logEvent)}tool ${event.toolName}(${formatToolArgs(event.toolArgs)})`);
|
|
150
|
+
break;
|
|
151
|
+
case 'output':
|
|
152
|
+
if (event.content) {
|
|
153
|
+
this.pushLine(`${formatEventPrefix(logEvent)}output ${event.content}`);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case 'answer':
|
|
157
|
+
if (event.content) {
|
|
158
|
+
this.pushLine(`${formatEventPrefix(logEvent)}answer ${event.content}`);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
case 'error':
|
|
162
|
+
if (event.content) {
|
|
163
|
+
this.pushLine(`${formatEventPrefix(logEvent)}error ${event.content}`);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
case 'log':
|
|
167
|
+
if (event.content) {
|
|
168
|
+
this.pushLine(`${formatEventPrefix(logEvent)}log ${event.content}`);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
case 'input_required':
|
|
172
|
+
if (event.prompt) {
|
|
173
|
+
this.pushLine(`${formatEventPrefix(logEvent)}clarify ${event.prompt}`);
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
pushLine(line) {
|
|
179
|
+
this.flushStream();
|
|
180
|
+
if (!line) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.lines.push(line);
|
|
184
|
+
this.trim();
|
|
185
|
+
}
|
|
186
|
+
finish() {
|
|
187
|
+
this.flushStream();
|
|
188
|
+
}
|
|
189
|
+
clear() {
|
|
190
|
+
this.lines.length = 0;
|
|
191
|
+
this.activeStreamKey = null;
|
|
192
|
+
this.activeStreamLine = '';
|
|
193
|
+
}
|
|
194
|
+
snapshot() {
|
|
195
|
+
return this.activeStreamLine
|
|
196
|
+
? [...this.lines, this.activeStreamLine]
|
|
197
|
+
: [...this.lines];
|
|
198
|
+
}
|
|
199
|
+
handleStream(logEvent) {
|
|
200
|
+
const streamKey = logEvent.scope === 'child' ? `child:${logEvent.childSlug ?? '?'}` : 'main';
|
|
201
|
+
if (this.activeStreamKey !== streamKey) {
|
|
202
|
+
this.flushStream();
|
|
203
|
+
this.activeStreamKey = streamKey;
|
|
204
|
+
this.activeStreamLine = `${streamLabel(logEvent)} `;
|
|
205
|
+
}
|
|
206
|
+
if (logEvent.event.content) {
|
|
207
|
+
this.activeStreamLine += logEvent.event.content;
|
|
208
|
+
}
|
|
209
|
+
if (logEvent.event.done) {
|
|
210
|
+
this.flushStream();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
flushStream() {
|
|
214
|
+
if (!this.activeStreamLine) {
|
|
215
|
+
this.activeStreamKey = null;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.lines.push(this.activeStreamLine);
|
|
219
|
+
this.activeStreamLine = '';
|
|
220
|
+
this.activeStreamKey = null;
|
|
221
|
+
this.trim();
|
|
222
|
+
}
|
|
223
|
+
trim() {
|
|
224
|
+
if (this.lines.length > MAX_ACTIVITY_LINES) {
|
|
225
|
+
this.lines.splice(0, this.lines.length - MAX_ACTIVITY_LINES);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
class FullscreenTui {
|
|
230
|
+
workspaceRoot;
|
|
231
|
+
options;
|
|
232
|
+
sessions = [];
|
|
233
|
+
selectedSessionId = '';
|
|
234
|
+
sessionDetail = null;
|
|
235
|
+
commands = [];
|
|
236
|
+
activityBySessionId = new Map();
|
|
237
|
+
ephemeralMessagesBySessionId = new Map();
|
|
238
|
+
focusedPane = 'messages';
|
|
239
|
+
uiMode = 'command';
|
|
240
|
+
menuReturnMode = 'command';
|
|
241
|
+
menuState = {
|
|
242
|
+
activeIndex: 0,
|
|
243
|
+
selectedIndex: 0,
|
|
244
|
+
typeahead: '',
|
|
245
|
+
typeaheadAt: 0,
|
|
246
|
+
};
|
|
247
|
+
paneScroll = {
|
|
248
|
+
sessions: 0,
|
|
249
|
+
messages: 0,
|
|
250
|
+
process: 0,
|
|
251
|
+
context: 0,
|
|
252
|
+
};
|
|
253
|
+
currentPreview = null;
|
|
254
|
+
currentAbortController = null;
|
|
255
|
+
inputValue = '';
|
|
256
|
+
cursor = 0;
|
|
257
|
+
busy = false;
|
|
258
|
+
exitRequested = false;
|
|
259
|
+
statusLine = 'Loading...';
|
|
260
|
+
pendingPrompt = null;
|
|
261
|
+
pickerState = null;
|
|
262
|
+
viewerState = null;
|
|
263
|
+
editorState = null;
|
|
264
|
+
recentFiles = [];
|
|
265
|
+
lastSubmittedInput = null;
|
|
266
|
+
renderTimer = null;
|
|
267
|
+
animationTimer = null;
|
|
268
|
+
closeResolver = null;
|
|
269
|
+
lastRenderAt = 0;
|
|
270
|
+
previousFrameLines = null;
|
|
271
|
+
previousFrameSize = null;
|
|
272
|
+
constructor(workspaceRoot, options = {}) {
|
|
273
|
+
this.workspaceRoot = workspaceRoot;
|
|
274
|
+
this.options = options;
|
|
275
|
+
}
|
|
276
|
+
async start() {
|
|
277
|
+
try {
|
|
278
|
+
await this.refreshCommands();
|
|
279
|
+
await this.refreshSessions({ createIfMissing: true });
|
|
280
|
+
this.statusLine = 'Ready. F10 or Ctrl+G opens menus. Left/right changes pane when the menu is closed.';
|
|
281
|
+
emitKeypressEvents(input);
|
|
282
|
+
if (input.isTTY) {
|
|
283
|
+
input.setRawMode(true);
|
|
284
|
+
}
|
|
285
|
+
input.on('keypress', this.onKeypress);
|
|
286
|
+
output.write('\u001b[?1049h\u001b[2J\u001b[H');
|
|
287
|
+
this.requestRender();
|
|
288
|
+
await new Promise((resolve) => {
|
|
289
|
+
this.closeResolver = resolve;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
if (this.renderTimer) {
|
|
294
|
+
clearTimeout(this.renderTimer);
|
|
295
|
+
this.renderTimer = null;
|
|
296
|
+
}
|
|
297
|
+
if (this.animationTimer) {
|
|
298
|
+
clearInterval(this.animationTimer);
|
|
299
|
+
this.animationTimer = null;
|
|
300
|
+
}
|
|
301
|
+
input.off('keypress', this.onKeypress);
|
|
302
|
+
if (input.isTTY) {
|
|
303
|
+
input.setRawMode(false);
|
|
304
|
+
}
|
|
305
|
+
this.previousFrameLines = null;
|
|
306
|
+
this.previousFrameSize = null;
|
|
307
|
+
output.write('\u001b[?25h\u001b[?1049l');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
onKeypress = (text, key) => {
|
|
311
|
+
void this.handleKeypress(text, key);
|
|
312
|
+
};
|
|
313
|
+
async handleKeypress(text, key) {
|
|
314
|
+
if ((key.ctrl && key.name === 'g') || key.name === 'f10') {
|
|
315
|
+
this.toggleMenuMode();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (key.ctrl && key.name === 'p' && !this.pendingPrompt && !this.busy) {
|
|
319
|
+
this.openActionPalette();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (key.ctrl && key.name === 'w') {
|
|
323
|
+
this.cyclePaneFocus(1);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (key.ctrl && key.name === 'c') {
|
|
327
|
+
if (this.pendingPrompt) {
|
|
328
|
+
const pending = this.pendingPrompt;
|
|
329
|
+
this.pendingPrompt = null;
|
|
330
|
+
pending.resolve('');
|
|
331
|
+
if (!this.busy) {
|
|
332
|
+
this.statusLine = 'Input cancelled.';
|
|
333
|
+
this.requestRender();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (this.busy) {
|
|
338
|
+
this.exitRequested = true;
|
|
339
|
+
this.cancelCurrentExecution('Cancelling current execution, then quitting...');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.close();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (this.uiMode === 'menu') {
|
|
346
|
+
await this.handleMenuKeypress(text, key);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
350
|
+
await this.handlePickerKeypress(text, key);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (this.uiMode === 'viewer') {
|
|
354
|
+
await this.handleViewerKeypress(key);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (this.uiMode === 'editor') {
|
|
358
|
+
await this.handleEditorKeypress(text, key);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
switch (key.name) {
|
|
362
|
+
case 'return':
|
|
363
|
+
await this.submitInput();
|
|
364
|
+
return;
|
|
365
|
+
case 'backspace':
|
|
366
|
+
this.deleteBackward();
|
|
367
|
+
break;
|
|
368
|
+
case 'delete':
|
|
369
|
+
this.deleteForward();
|
|
370
|
+
break;
|
|
371
|
+
case 'left':
|
|
372
|
+
if (!this.inputValue) {
|
|
373
|
+
this.cyclePaneFocus(-1);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (this.cursor > 0) {
|
|
377
|
+
this.cursor -= 1;
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
case 'right':
|
|
381
|
+
if (!this.inputValue) {
|
|
382
|
+
this.cyclePaneFocus(1);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (this.cursor < this.inputValue.length) {
|
|
386
|
+
this.cursor += 1;
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
case 'home':
|
|
390
|
+
if (!this.inputValue) {
|
|
391
|
+
this.jumpFocusedPane('top');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this.cursor = 0;
|
|
395
|
+
break;
|
|
396
|
+
case 'end':
|
|
397
|
+
if (!this.inputValue) {
|
|
398
|
+
this.jumpFocusedPane('bottom');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.cursor = this.inputValue.length;
|
|
402
|
+
break;
|
|
403
|
+
case 'up':
|
|
404
|
+
if (!this.inputValue) {
|
|
405
|
+
await this.handleUpDown(-1);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
case 'down':
|
|
410
|
+
if (!this.inputValue) {
|
|
411
|
+
await this.handleUpDown(1);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
case 'pageup':
|
|
416
|
+
if (!this.inputValue) {
|
|
417
|
+
this.scrollFocusedPane('up', this.pageScrollAmount());
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
case 'pagedown':
|
|
422
|
+
if (!this.inputValue) {
|
|
423
|
+
this.scrollFocusedPane('down', this.pageScrollAmount());
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
case 'tab':
|
|
428
|
+
await this.applyCompletion();
|
|
429
|
+
return;
|
|
430
|
+
case 'escape':
|
|
431
|
+
this.inputValue = '';
|
|
432
|
+
this.cursor = 0;
|
|
433
|
+
break;
|
|
434
|
+
default:
|
|
435
|
+
if (text && !key.ctrl && !key.meta) {
|
|
436
|
+
this.insertText(text);
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
this.requestRender();
|
|
441
|
+
}
|
|
442
|
+
async handleMenuKeypress(text, key) {
|
|
443
|
+
const menus = this.getMenuDefinitions();
|
|
444
|
+
const activeMenu = menus[this.menuState.activeIndex];
|
|
445
|
+
const activeItems = activeMenu?.items ?? [];
|
|
446
|
+
if (key.name === 'escape') {
|
|
447
|
+
this.closeMenuMode();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (key.name && /^[1-6]$/.test(key.name)) {
|
|
451
|
+
this.selectMenu(Number.parseInt(key.name, 10) - 1);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
switch (key.name) {
|
|
455
|
+
case 'left':
|
|
456
|
+
this.moveMenu(-1);
|
|
457
|
+
return;
|
|
458
|
+
case 'right':
|
|
459
|
+
this.moveMenu(1);
|
|
460
|
+
return;
|
|
461
|
+
case 'up':
|
|
462
|
+
this.moveMenuItem(-1, activeItems.length);
|
|
463
|
+
return;
|
|
464
|
+
case 'down':
|
|
465
|
+
this.moveMenuItem(1, activeItems.length);
|
|
466
|
+
return;
|
|
467
|
+
case 'home':
|
|
468
|
+
this.menuState.selectedIndex = 0;
|
|
469
|
+
this.statusLine = `${activeMenu?.label ?? 'Menu'} menu.`;
|
|
470
|
+
this.requestRender();
|
|
471
|
+
return;
|
|
472
|
+
case 'end':
|
|
473
|
+
this.menuState.selectedIndex = Math.max(0, activeItems.length - 1);
|
|
474
|
+
this.statusLine = `${activeMenu?.label ?? 'Menu'} menu.`;
|
|
475
|
+
this.requestRender();
|
|
476
|
+
return;
|
|
477
|
+
case 'return':
|
|
478
|
+
case 'space': {
|
|
479
|
+
const selected = activeItems[this.menuState.selectedIndex];
|
|
480
|
+
if (selected) {
|
|
481
|
+
if (selected.enabled === false) {
|
|
482
|
+
this.statusLine = `${selected.label} is not available right now.`;
|
|
483
|
+
this.requestRender();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await this.runMenuAction(selected.id);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
case 'backspace':
|
|
491
|
+
if (this.menuState.typeahead) {
|
|
492
|
+
this.menuState.typeahead = this.menuState.typeahead.slice(0, -1);
|
|
493
|
+
this.menuState.typeaheadAt = Date.now();
|
|
494
|
+
this.requestRender();
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
default:
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
if (text && !key.ctrl && !key.meta && /\S/.test(text)) {
|
|
501
|
+
const now = Date.now();
|
|
502
|
+
this.menuState.typeahead = now - this.menuState.typeaheadAt > 900
|
|
503
|
+
? text.toLowerCase()
|
|
504
|
+
: `${this.menuState.typeahead}${text.toLowerCase()}`;
|
|
505
|
+
this.menuState.typeaheadAt = now;
|
|
506
|
+
this.menuState.selectedIndex = selectMenuItemByTypeahead(activeItems, this.menuState.typeahead, this.menuState.selectedIndex);
|
|
507
|
+
this.requestRender();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async handlePickerKeypress(text, key) {
|
|
511
|
+
const picker = this.pickerState;
|
|
512
|
+
if (!picker) {
|
|
513
|
+
this.closeWorkspaceMode();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
switch (key.name) {
|
|
517
|
+
case 'escape':
|
|
518
|
+
this.closeWorkspaceMode();
|
|
519
|
+
return;
|
|
520
|
+
case 'return':
|
|
521
|
+
await this.activatePickerSelection();
|
|
522
|
+
return;
|
|
523
|
+
case 'up':
|
|
524
|
+
this.movePickerSelection(-1);
|
|
525
|
+
return;
|
|
526
|
+
case 'down':
|
|
527
|
+
this.movePickerSelection(1);
|
|
528
|
+
return;
|
|
529
|
+
case 'pageup':
|
|
530
|
+
this.movePickerSelection(-Math.max(3, Math.floor(this.workspaceViewportLines() / 2)));
|
|
531
|
+
return;
|
|
532
|
+
case 'pagedown':
|
|
533
|
+
this.movePickerSelection(Math.max(3, Math.floor(this.workspaceViewportLines() / 2)));
|
|
534
|
+
return;
|
|
535
|
+
case 'home':
|
|
536
|
+
picker.selectedIndex = 0;
|
|
537
|
+
this.ensurePickerSelectionVisible();
|
|
538
|
+
this.requestRender();
|
|
539
|
+
return;
|
|
540
|
+
case 'end':
|
|
541
|
+
picker.selectedIndex = Math.max(0, picker.filteredItems.length - 1);
|
|
542
|
+
this.ensurePickerSelectionVisible();
|
|
543
|
+
this.requestRender();
|
|
544
|
+
return;
|
|
545
|
+
case 'backspace':
|
|
546
|
+
this.deleteBackward();
|
|
547
|
+
this.refreshPickerFilter();
|
|
548
|
+
return;
|
|
549
|
+
case 'delete':
|
|
550
|
+
this.deleteForward();
|
|
551
|
+
this.refreshPickerFilter();
|
|
552
|
+
return;
|
|
553
|
+
case 'left':
|
|
554
|
+
if (this.cursor > 0) {
|
|
555
|
+
this.cursor -= 1;
|
|
556
|
+
this.requestRender();
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
case 'right':
|
|
560
|
+
if (this.cursor < this.inputValue.length) {
|
|
561
|
+
this.cursor += 1;
|
|
562
|
+
this.requestRender();
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
default:
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
if (key.ctrl && key.name === 'e') {
|
|
569
|
+
await this.activatePickerSelection('edit');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (key.ctrl && key.name === 'r') {
|
|
573
|
+
await this.activatePickerSelection('run');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (key.ctrl && key.name === 'm') {
|
|
577
|
+
await this.activatePickerSelection('meta');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (key.ctrl && key.name === 's') {
|
|
581
|
+
await this.activatePickerSelection('source');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (text && !key.ctrl && !key.meta) {
|
|
585
|
+
this.insertText(text);
|
|
586
|
+
this.refreshPickerFilter();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async handleViewerKeypress(key) {
|
|
590
|
+
if (!this.viewerState) {
|
|
591
|
+
this.closeWorkspaceMode();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (key.ctrl && key.name === 'e' && this.viewerState.canEdit && this.viewerState.path) {
|
|
595
|
+
await this.openEditorForFile(this.viewerState.path, { title: this.viewerState.title, preferredExtension: this.viewerState.preferredExtension });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (key.ctrl && key.name === 'r' && this.viewerState.skillName) {
|
|
599
|
+
this.closeWorkspaceMode();
|
|
600
|
+
await this.executeSkill(this.viewerState.skillName, []);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
switch (key.name) {
|
|
604
|
+
case 'escape':
|
|
605
|
+
this.closeWorkspaceMode();
|
|
606
|
+
return;
|
|
607
|
+
case 'up':
|
|
608
|
+
this.viewerState.scrollTop = Math.max(0, this.viewerState.scrollTop - 1);
|
|
609
|
+
this.requestRender();
|
|
610
|
+
return;
|
|
611
|
+
case 'down':
|
|
612
|
+
this.viewerState.scrollTop += 1;
|
|
613
|
+
this.requestRender();
|
|
614
|
+
return;
|
|
615
|
+
case 'pageup':
|
|
616
|
+
this.viewerState.scrollTop = Math.max(0, this.viewerState.scrollTop - this.workspaceViewportLines());
|
|
617
|
+
this.requestRender();
|
|
618
|
+
return;
|
|
619
|
+
case 'pagedown':
|
|
620
|
+
this.viewerState.scrollTop += this.workspaceViewportLines();
|
|
621
|
+
this.requestRender();
|
|
622
|
+
return;
|
|
623
|
+
case 'home':
|
|
624
|
+
this.viewerState.scrollTop = 0;
|
|
625
|
+
this.requestRender();
|
|
626
|
+
return;
|
|
627
|
+
default:
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async handleEditorKeypress(text, key) {
|
|
632
|
+
if (!this.editorState) {
|
|
633
|
+
this.closeWorkspaceMode();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (key.ctrl && key.name === 's') {
|
|
637
|
+
await this.saveCurrentEditor(false);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
switch (key.name) {
|
|
641
|
+
case 'escape':
|
|
642
|
+
if (this.editorState.dirty && !this.editorState.discardPending) {
|
|
643
|
+
this.editorState.discardPending = true;
|
|
644
|
+
this.statusLine = 'Unsaved changes. Press Esc again to discard or Ctrl+S to save.';
|
|
645
|
+
this.requestRender();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.closeWorkspaceMode(true);
|
|
649
|
+
return;
|
|
650
|
+
case 'return':
|
|
651
|
+
this.splitEditorLine();
|
|
652
|
+
return;
|
|
653
|
+
case 'backspace':
|
|
654
|
+
this.deleteEditorBackward();
|
|
655
|
+
return;
|
|
656
|
+
case 'delete':
|
|
657
|
+
this.deleteEditorForward();
|
|
658
|
+
return;
|
|
659
|
+
case 'left':
|
|
660
|
+
this.moveEditorCursor('left');
|
|
661
|
+
return;
|
|
662
|
+
case 'right':
|
|
663
|
+
this.moveEditorCursor('right');
|
|
664
|
+
return;
|
|
665
|
+
case 'up':
|
|
666
|
+
this.moveEditorCursor('up');
|
|
667
|
+
return;
|
|
668
|
+
case 'down':
|
|
669
|
+
this.moveEditorCursor('down');
|
|
670
|
+
return;
|
|
671
|
+
case 'pageup':
|
|
672
|
+
this.pageEditorCursor(-1);
|
|
673
|
+
return;
|
|
674
|
+
case 'pagedown':
|
|
675
|
+
this.pageEditorCursor(1);
|
|
676
|
+
return;
|
|
677
|
+
case 'home':
|
|
678
|
+
this.editorState.cursorColumn = 0;
|
|
679
|
+
this.ensureEditorCursorVisible();
|
|
680
|
+
this.requestRender();
|
|
681
|
+
return;
|
|
682
|
+
case 'end':
|
|
683
|
+
this.editorState.cursorColumn = this.currentEditorLine().length;
|
|
684
|
+
this.ensureEditorCursorVisible();
|
|
685
|
+
this.requestRender();
|
|
686
|
+
return;
|
|
687
|
+
case 'tab':
|
|
688
|
+
this.insertEditorText(' ');
|
|
689
|
+
return;
|
|
690
|
+
default:
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
if (text && !key.ctrl && !key.meta) {
|
|
694
|
+
this.insertEditorText(text);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async submitInput() {
|
|
698
|
+
const rawInput = this.inputValue;
|
|
699
|
+
const trimmedInput = rawInput.trim();
|
|
700
|
+
this.inputValue = '';
|
|
701
|
+
this.cursor = 0;
|
|
702
|
+
if (this.pendingPrompt) {
|
|
703
|
+
const pending = this.pendingPrompt;
|
|
704
|
+
this.pendingPrompt = null;
|
|
705
|
+
pending.resolve(trimmedInput);
|
|
706
|
+
this.statusLine = pending.kind === 'clarify' ? 'Continuing...' : 'Working...';
|
|
707
|
+
this.requestRender();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (!trimmedInput) {
|
|
711
|
+
this.requestRender();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const parsed = parseSlashInput(trimmedInput);
|
|
715
|
+
if (this.busy && !this.pendingPrompt && !canSubmitParsedInputWhileBusy(parsed)) {
|
|
716
|
+
this.statusLine = 'Execution is still running. Use /cancel or Ctrl+C to stop it.';
|
|
717
|
+
this.requestRender();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (parsed.kind === 'builtin') {
|
|
721
|
+
await this.executeBuiltin(parsed);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (parsed.kind === 'skill') {
|
|
725
|
+
await this.executeSkill(parsed.name, parsed.args);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
await this.executeTask(parsed.prompt);
|
|
729
|
+
}
|
|
730
|
+
async executeBuiltin(parsed) {
|
|
731
|
+
switch (parsed.name) {
|
|
732
|
+
case 'cancel':
|
|
733
|
+
this.cancelCurrentExecution('Cancellation requested.');
|
|
734
|
+
return;
|
|
735
|
+
case 'exit':
|
|
736
|
+
case 'quit':
|
|
737
|
+
this.close();
|
|
738
|
+
return;
|
|
739
|
+
case 'help': {
|
|
740
|
+
const activity = this.currentProcessActivity();
|
|
741
|
+
for (const line of buildHelpLines()) {
|
|
742
|
+
activity.pushLine(line);
|
|
743
|
+
}
|
|
744
|
+
this.statusLine = 'Help added to the process pane.';
|
|
745
|
+
this.requestRender();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
case 'compile':
|
|
749
|
+
case 'skill-creator':
|
|
750
|
+
await this.executeSkillCreator(parsed.name, parsed.rawArgs);
|
|
751
|
+
return;
|
|
752
|
+
case 'sessions':
|
|
753
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: this.selectedSessionId });
|
|
754
|
+
await this.refreshCommands();
|
|
755
|
+
this.statusLine = `Loaded ${this.sessions.length} session${this.sessions.length === 1 ? '' : 's'}.`;
|
|
756
|
+
this.requestRender();
|
|
757
|
+
return;
|
|
758
|
+
case 'new': {
|
|
759
|
+
const created = await createConductorSession(this.workspaceRoot, parsed.rawArgs || undefined);
|
|
760
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: created.id });
|
|
761
|
+
this.statusLine = `Created session ${created.title}.`;
|
|
762
|
+
this.requestRender();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
async executeTask(promptText) {
|
|
768
|
+
const sessionId = this.selectedSessionId;
|
|
769
|
+
const activity = this.activityFor(sessionId);
|
|
770
|
+
this.lastSubmittedInput = { kind: 'text', prompt: promptText };
|
|
771
|
+
this.focusedPane = 'messages';
|
|
772
|
+
this.paneScroll.messages = 0;
|
|
773
|
+
this.paneScroll.process = 0;
|
|
774
|
+
this.beginExecutionPreview(sessionId, 'task', { role: 'user', content: promptText });
|
|
775
|
+
activity.pushLine(`task ${promptText}`);
|
|
776
|
+
this.busy = true;
|
|
777
|
+
this.currentAbortController = new AbortController();
|
|
778
|
+
this.startAnimation();
|
|
779
|
+
this.statusLine = 'Running conductor...';
|
|
780
|
+
this.requestRender();
|
|
781
|
+
try {
|
|
782
|
+
const result = await runConductorTurn(promptText, {
|
|
783
|
+
workspaceRoot: this.workspaceRoot,
|
|
784
|
+
sessionId,
|
|
785
|
+
stream: true,
|
|
786
|
+
headless: true,
|
|
787
|
+
sandbox: this.options.sandbox,
|
|
788
|
+
signal: this.currentAbortController.signal,
|
|
789
|
+
onUserInput: (question) => this.requestClarification(question),
|
|
790
|
+
onEvent: (event) => {
|
|
791
|
+
activity.handle(event);
|
|
792
|
+
this.updatePreviewFromEvent(event);
|
|
793
|
+
this.requestRender();
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
activity.finish();
|
|
797
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: result.sessionId });
|
|
798
|
+
await this.refreshCommands();
|
|
799
|
+
this.finishExecutionPreview({ persist: false });
|
|
800
|
+
this.statusLine = result.error ? 'Task failed.' : 'Task finished.';
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
activity.finish();
|
|
804
|
+
const message = error.message;
|
|
805
|
+
activity.pushLine(`error ${message}`);
|
|
806
|
+
this.finishExecutionPreview({ persist: true, error: message });
|
|
807
|
+
this.statusLine = 'Task failed.';
|
|
808
|
+
}
|
|
809
|
+
finally {
|
|
810
|
+
this.busy = false;
|
|
811
|
+
this.currentAbortController = null;
|
|
812
|
+
this.stopAnimation();
|
|
813
|
+
if (this.exitRequested) {
|
|
814
|
+
this.close();
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
this.requestRender();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async executeSkill(skillName, args) {
|
|
822
|
+
const sessionId = this.selectedSessionId || 'global';
|
|
823
|
+
const activity = this.activityFor(sessionId);
|
|
824
|
+
const commandText = `/${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`;
|
|
825
|
+
this.lastSubmittedInput = { kind: 'skill', name: skillName, rawArgs: args.join(' '), args: [...args] };
|
|
826
|
+
this.focusedPane = 'messages';
|
|
827
|
+
this.paneScroll.messages = 0;
|
|
828
|
+
this.paneScroll.process = 0;
|
|
829
|
+
this.beginExecutionPreview(sessionId, 'skill', { role: 'system', content: commandText }, skillName);
|
|
830
|
+
activity.pushLine(`skill /${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`);
|
|
831
|
+
this.busy = true;
|
|
832
|
+
this.currentAbortController = new AbortController();
|
|
833
|
+
this.startAnimation();
|
|
834
|
+
this.statusLine = `Running /${skillName}...`;
|
|
835
|
+
this.requestRender();
|
|
836
|
+
try {
|
|
837
|
+
const result = await runSkillCommand(this.workspaceRoot, skillName, args, {
|
|
838
|
+
sandbox: this.options.sandbox,
|
|
839
|
+
signal: this.currentAbortController.signal,
|
|
840
|
+
onUserInput: (question) => this.requestClarification(question),
|
|
841
|
+
onEvent: (event) => {
|
|
842
|
+
activity.handle(event);
|
|
843
|
+
this.updatePreviewFromEvent(event);
|
|
844
|
+
this.requestRender();
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
activity.finish();
|
|
848
|
+
await this.refreshCommands();
|
|
849
|
+
this.finishExecutionPreview({
|
|
850
|
+
persist: true,
|
|
851
|
+
finalText: result.answer || summarizeRunResult(result),
|
|
852
|
+
error: result.error,
|
|
853
|
+
});
|
|
854
|
+
this.statusLine = result.error ? `/${skillName} failed.` : `/${skillName} finished.`;
|
|
855
|
+
}
|
|
856
|
+
catch (error) {
|
|
857
|
+
activity.finish();
|
|
858
|
+
const message = error.message;
|
|
859
|
+
activity.pushLine(`[${skillName}] error ${message}`);
|
|
860
|
+
this.finishExecutionPreview({ persist: true, error: message });
|
|
861
|
+
this.statusLine = `/${skillName} failed.`;
|
|
862
|
+
}
|
|
863
|
+
finally {
|
|
864
|
+
this.busy = false;
|
|
865
|
+
this.currentAbortController = null;
|
|
866
|
+
this.stopAnimation();
|
|
867
|
+
if (this.exitRequested) {
|
|
868
|
+
this.close();
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
this.requestRender();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async executeSkillCreator(commandName, specText) {
|
|
876
|
+
const trimmedSpec = specText.trim();
|
|
877
|
+
if (!trimmedSpec) {
|
|
878
|
+
this.statusLine = `Provide a skill specification after /${commandName}.`;
|
|
879
|
+
this.requestRender();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const sessionId = this.selectedSessionId;
|
|
883
|
+
const activity = this.activityFor(sessionId);
|
|
884
|
+
const commandText = `/${commandName} ${trimmedSpec}`;
|
|
885
|
+
this.lastSubmittedInput = { kind: 'builtin', name: commandName, rawArgs: trimmedSpec, args: [trimmedSpec] };
|
|
886
|
+
this.focusedPane = 'messages';
|
|
887
|
+
this.paneScroll.messages = 0;
|
|
888
|
+
this.paneScroll.process = 0;
|
|
889
|
+
this.beginExecutionPreview(sessionId, 'skill', { role: 'system', content: commandText }, 'skill-creator');
|
|
890
|
+
activity.pushLine(`skill ${commandText}`);
|
|
891
|
+
this.busy = true;
|
|
892
|
+
this.currentAbortController = new AbortController();
|
|
893
|
+
this.startAnimation();
|
|
894
|
+
this.statusLine = `Running /${commandName}...`;
|
|
895
|
+
this.requestRender();
|
|
896
|
+
try {
|
|
897
|
+
const result = await runSkillCreatorCommand(this.workspaceRoot, sessionId, trimmedSpec, {
|
|
898
|
+
sandbox: this.options.sandbox,
|
|
899
|
+
signal: this.currentAbortController.signal,
|
|
900
|
+
onUserInput: (question) => this.requestClarification(question),
|
|
901
|
+
onEvent: (event) => {
|
|
902
|
+
activity.handle(event);
|
|
903
|
+
this.updatePreviewFromEvent(event);
|
|
904
|
+
this.requestRender();
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
activity.finish();
|
|
908
|
+
await this.refreshCommands();
|
|
909
|
+
this.finishExecutionPreview({
|
|
910
|
+
persist: true,
|
|
911
|
+
finalText: formatSkillCreatorSummary(this.workspaceRoot, result),
|
|
912
|
+
});
|
|
913
|
+
this.statusLine = `/${commandName} finished.`;
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
activity.finish();
|
|
917
|
+
const message = error.message;
|
|
918
|
+
activity.pushLine(`[skill-creator] error ${message}`);
|
|
919
|
+
this.finishExecutionPreview({ persist: true, error: message });
|
|
920
|
+
this.statusLine = `/${commandName} failed.`;
|
|
921
|
+
}
|
|
922
|
+
finally {
|
|
923
|
+
this.busy = false;
|
|
924
|
+
this.currentAbortController = null;
|
|
925
|
+
this.stopAnimation();
|
|
926
|
+
if (this.exitRequested) {
|
|
927
|
+
this.close();
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
this.requestRender();
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async requestClarification(promptText) {
|
|
935
|
+
this.ensureClarificationVisible(promptText);
|
|
936
|
+
return this.promptForValue(promptText, 'clarify');
|
|
937
|
+
}
|
|
938
|
+
async promptForValue(promptText, kind = 'input') {
|
|
939
|
+
this.inputValue = '';
|
|
940
|
+
this.cursor = 0;
|
|
941
|
+
this.statusLine = kind === 'clarify' ? 'Awaiting clarification...' : promptText;
|
|
942
|
+
this.requestRender();
|
|
943
|
+
return new Promise((resolve) => {
|
|
944
|
+
this.pendingPrompt = { kind, prompt: promptText, resolve };
|
|
945
|
+
this.requestRender();
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
ensureClarificationVisible(promptText) {
|
|
949
|
+
if (!this.currentPreview) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const entry = previewQuestionMessage(promptText);
|
|
953
|
+
const entries = this.currentPreview.entries;
|
|
954
|
+
const pendingAssistantIndex = entries.length > 0 && entries[entries.length - 1].role === 'assistant'
|
|
955
|
+
? entries.length - 1
|
|
956
|
+
: entries.length;
|
|
957
|
+
const previous = pendingAssistantIndex > 0 ? entries[pendingAssistantIndex - 1] : undefined;
|
|
958
|
+
if (sameDisplayMessage(previous, entry)) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
this.insertPreviewMessage(entry);
|
|
962
|
+
}
|
|
963
|
+
async applyCompletion() {
|
|
964
|
+
if (!this.inputValue.startsWith('/')) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
await this.refreshCommands();
|
|
968
|
+
const completion = completeSlashCommand(this.inputValue, [
|
|
969
|
+
...BUILTIN_SLASH_COMMANDS,
|
|
970
|
+
...this.commands.map((command) => command.name),
|
|
971
|
+
]);
|
|
972
|
+
if (completion.applied) {
|
|
973
|
+
this.inputValue = completion.value;
|
|
974
|
+
this.cursor = this.inputValue.length;
|
|
975
|
+
}
|
|
976
|
+
if (completion.matches.length === 0) {
|
|
977
|
+
this.statusLine = 'No matching commands or skills.';
|
|
978
|
+
}
|
|
979
|
+
else if (completion.matches.length === 1) {
|
|
980
|
+
this.statusLine = `Completed /${completion.matches[0]}`;
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
const preview = completion.matches.slice(0, 6).join(', ');
|
|
984
|
+
this.statusLine = `Matches: ${preview}${completion.matches.length > 6 ? ', ...' : ''}`;
|
|
985
|
+
}
|
|
986
|
+
this.requestRender();
|
|
987
|
+
}
|
|
988
|
+
async handleUpDown(offset) {
|
|
989
|
+
if (this.focusedPane === 'sessions') {
|
|
990
|
+
await this.moveSession(offset);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
this.scrollFocusedPane(offset < 0 ? 'up' : 'down', 1);
|
|
994
|
+
}
|
|
995
|
+
cyclePaneFocus(direction) {
|
|
996
|
+
const order = ['sessions', 'messages', 'process', 'context'];
|
|
997
|
+
const currentIndex = order.indexOf(this.focusedPane);
|
|
998
|
+
const nextIndex = (currentIndex + direction + order.length) % order.length;
|
|
999
|
+
this.focusedPane = order[nextIndex];
|
|
1000
|
+
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}. Left/right switches panes; PgUp/PgDn scrolls.`;
|
|
1001
|
+
this.requestRender();
|
|
1002
|
+
}
|
|
1003
|
+
toggleMenuMode() {
|
|
1004
|
+
if (this.pendingPrompt) {
|
|
1005
|
+
this.statusLine = this.pendingPrompt.kind === 'clarify'
|
|
1006
|
+
? 'Finish the clarification prompt before opening menus.'
|
|
1007
|
+
: 'Finish the current input prompt before opening menus.';
|
|
1008
|
+
this.requestRender();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (this.uiMode === 'menu') {
|
|
1012
|
+
this.closeMenuMode();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
this.menuReturnMode = this.uiMode;
|
|
1016
|
+
this.uiMode = 'menu';
|
|
1017
|
+
this.menuState.typeahead = '';
|
|
1018
|
+
this.menuState.typeaheadAt = 0;
|
|
1019
|
+
this.menuState.selectedIndex = 0;
|
|
1020
|
+
this.statusLine = `${this.getMenuDefinitions()[this.menuState.activeIndex]?.label ?? 'Menu'} menu.`;
|
|
1021
|
+
this.requestRender();
|
|
1022
|
+
}
|
|
1023
|
+
closeMenuMode() {
|
|
1024
|
+
if (this.uiMode !== 'menu') {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
this.uiMode = this.menuReturnMode;
|
|
1028
|
+
this.menuState.typeahead = '';
|
|
1029
|
+
this.menuState.typeaheadAt = 0;
|
|
1030
|
+
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}. F10 or Ctrl+G reopens the menu bar.`;
|
|
1031
|
+
this.requestRender();
|
|
1032
|
+
}
|
|
1033
|
+
moveMenu(direction) {
|
|
1034
|
+
const menus = this.getMenuDefinitions();
|
|
1035
|
+
this.menuState.activeIndex = nextWrappedIndex(this.menuState.activeIndex, direction, menus.length);
|
|
1036
|
+
this.menuState.selectedIndex = 0;
|
|
1037
|
+
this.menuState.typeahead = '';
|
|
1038
|
+
this.menuState.typeaheadAt = 0;
|
|
1039
|
+
this.statusLine = `${menus[this.menuState.activeIndex]?.label ?? 'Menu'} menu.`;
|
|
1040
|
+
this.requestRender();
|
|
1041
|
+
}
|
|
1042
|
+
selectMenu(index) {
|
|
1043
|
+
const menus = this.getMenuDefinitions();
|
|
1044
|
+
if (index < 0 || index >= menus.length) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
this.menuState.activeIndex = index;
|
|
1048
|
+
this.menuState.selectedIndex = 0;
|
|
1049
|
+
this.menuState.typeahead = '';
|
|
1050
|
+
this.menuState.typeaheadAt = 0;
|
|
1051
|
+
this.statusLine = `${menus[index]?.label ?? 'Menu'} menu.`;
|
|
1052
|
+
this.requestRender();
|
|
1053
|
+
}
|
|
1054
|
+
moveMenuItem(direction, itemCount) {
|
|
1055
|
+
this.menuState.selectedIndex = nextWrappedIndex(this.menuState.selectedIndex, direction, itemCount);
|
|
1056
|
+
this.menuState.typeahead = '';
|
|
1057
|
+
this.menuState.typeaheadAt = 0;
|
|
1058
|
+
this.requestRender();
|
|
1059
|
+
}
|
|
1060
|
+
getMenuDefinitions() {
|
|
1061
|
+
const busy = this.busy || !!this.pendingPrompt;
|
|
1062
|
+
const menus = [
|
|
1063
|
+
{
|
|
1064
|
+
id: 'session',
|
|
1065
|
+
label: 'Session',
|
|
1066
|
+
items: [
|
|
1067
|
+
{ id: 'session.new', label: 'New Session', description: 'Create and switch to a fresh conductor session.', shortcut: 'Enter' },
|
|
1068
|
+
{ id: 'session.refresh', label: 'Refresh Sessions', description: 'Reload the session list and command catalog.' },
|
|
1069
|
+
{ id: 'session.next', label: 'Next Session', description: 'Move to the next session in the left pane.', shortcut: 'Down' },
|
|
1070
|
+
{ id: 'session.previous', label: 'Previous Session', description: 'Move to the previous session in the left pane.', shortcut: 'Up' },
|
|
1071
|
+
{ id: 'session.quit', label: 'Quit', description: 'Leave the fullscreen TUI.', shortcut: 'Ctrl+C' },
|
|
1072
|
+
],
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
id: 'skills',
|
|
1076
|
+
label: 'Skills',
|
|
1077
|
+
items: [
|
|
1078
|
+
{ id: 'skills.browse', label: 'Browse All Skills', description: 'Open a searchable catalog of compiled skills.', shortcut: 'Enter' },
|
|
1079
|
+
{ id: 'skills.run', label: 'Run Skill…', description: 'Pick a skill and execute it from the menu bar.' },
|
|
1080
|
+
{ id: 'skills.inspect', label: 'Inspect Skill Code', description: 'View the compiled DML for a skill.' },
|
|
1081
|
+
{ id: 'skills.edit', label: 'Edit Skill Code', description: 'Open a skill in the inline editor.' },
|
|
1082
|
+
{ id: 'skills.meta', label: 'Inspect Skill Metadata', description: 'View the metadata JSON for a compiled skill.' },
|
|
1083
|
+
{ id: 'skills.source', label: 'Open Skill Source Spec', description: 'Jump from a compiled skill back to its source Markdown.' },
|
|
1084
|
+
],
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
id: 'files',
|
|
1088
|
+
label: 'Files',
|
|
1089
|
+
items: [
|
|
1090
|
+
{ id: 'files.open', label: 'Open Text File…', description: 'Browse workspace text files in a searchable picker.', shortcut: 'Enter' },
|
|
1091
|
+
{ id: 'files.recent', label: 'Recent Files', description: 'Reopen a recently inspected or edited file.' },
|
|
1092
|
+
{ id: 'files.newMarkdown', label: 'New Markdown Note', description: 'Create a new Markdown buffer inside the workspace.' },
|
|
1093
|
+
{ id: 'files.save', label: 'Save', description: 'Save the current editor buffer.', shortcut: 'Ctrl+S', enabled: !!this.editorState },
|
|
1094
|
+
{ id: 'files.saveAs', label: 'Save As…', description: 'Save the current editor buffer to a new path.', enabled: !!this.editorState },
|
|
1095
|
+
{ id: 'files.readme', label: 'Open Workspace README', description: 'Inspect the top-level README.md.' },
|
|
1096
|
+
{ id: 'files.config', label: 'Open .deepclause Config', description: 'Inspect the local DeepClause config file.' },
|
|
1097
|
+
],
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
id: 'run',
|
|
1101
|
+
label: 'Run',
|
|
1102
|
+
items: [
|
|
1103
|
+
{ id: 'run.prompt', label: 'Send Prompt…', description: 'Return to the command bar and enter a freeform prompt.', shortcut: 'Enter' },
|
|
1104
|
+
{ id: 'run.repeat', label: 'Repeat Last Command', description: 'Re-run the last prompt or skill invocation.', enabled: !!this.lastSubmittedInput },
|
|
1105
|
+
{ id: 'run.cancel', label: 'Cancel Current Execution', description: 'Abort the running task or skill.', shortcut: '/cancel', enabled: this.busy },
|
|
1106
|
+
{ id: 'run.clear', label: 'Clear Execution Pane', description: 'Discard the current session execution log.' },
|
|
1107
|
+
{ id: 'run.refreshSkills', label: 'Refresh Skill Catalog', description: 'Re-scan the workspace for compiled skills.' },
|
|
1108
|
+
],
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
id: 'view',
|
|
1112
|
+
label: 'View',
|
|
1113
|
+
items: [
|
|
1114
|
+
{ id: 'view.focus.sessions', label: 'Focus Sessions Pane', description: 'Focus the session list pane.' },
|
|
1115
|
+
{ id: 'view.focus.messages', label: 'Focus Messages Pane', description: 'Focus the conversation history pane.' },
|
|
1116
|
+
{ id: 'view.focus.process', label: 'Focus Execution Pane', description: 'Focus the live execution log pane.' },
|
|
1117
|
+
{ id: 'view.focus.context', label: 'Focus Context Pane', description: 'Focus the session context and token usage pane.' },
|
|
1118
|
+
{ id: 'view.follow', label: 'Follow Focused Pane', description: 'Jump back to the newest visible content in the active pane.' },
|
|
1119
|
+
{ id: 'view.palette', label: 'Open Action Palette', description: 'Search all menu actions from one picker.', shortcut: 'Ctrl+P' },
|
|
1120
|
+
],
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
id: 'help',
|
|
1124
|
+
label: 'Help',
|
|
1125
|
+
items: [
|
|
1126
|
+
{ id: 'help.shortcuts', label: 'Keyboard Shortcuts', description: 'Show the fullscreen keyboard map.' },
|
|
1127
|
+
{ id: 'help.slash', label: 'Slash Commands', description: 'Show available slash commands and direct skill invocation.' },
|
|
1128
|
+
{ id: 'help.about', label: 'About This TUI', description: 'Show a short explanation of the fullscreen workspace.' },
|
|
1129
|
+
],
|
|
1130
|
+
},
|
|
1131
|
+
];
|
|
1132
|
+
return menus.map((menu) => ({
|
|
1133
|
+
...menu,
|
|
1134
|
+
items: menu.items.map((item) => ({
|
|
1135
|
+
...item,
|
|
1136
|
+
enabled: item.enabled ?? (!busy || item.id === 'run.cancel'),
|
|
1137
|
+
})),
|
|
1138
|
+
}));
|
|
1139
|
+
}
|
|
1140
|
+
async runMenuAction(actionId) {
|
|
1141
|
+
switch (actionId) {
|
|
1142
|
+
case 'session.new': {
|
|
1143
|
+
const created = await createConductorSession(this.workspaceRoot);
|
|
1144
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: created.id });
|
|
1145
|
+
this.closeMenuMode();
|
|
1146
|
+
this.statusLine = `Created session ${created.title}.`;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
case 'session.refresh':
|
|
1150
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: this.selectedSessionId });
|
|
1151
|
+
await this.refreshCommands();
|
|
1152
|
+
this.closeMenuMode();
|
|
1153
|
+
this.statusLine = `Loaded ${this.sessions.length} session${this.sessions.length === 1 ? '' : 's'}.`;
|
|
1154
|
+
return;
|
|
1155
|
+
case 'session.next':
|
|
1156
|
+
await this.moveSession(1);
|
|
1157
|
+
return;
|
|
1158
|
+
case 'session.previous':
|
|
1159
|
+
await this.moveSession(-1);
|
|
1160
|
+
return;
|
|
1161
|
+
case 'session.quit':
|
|
1162
|
+
this.close();
|
|
1163
|
+
return;
|
|
1164
|
+
case 'skills.browse':
|
|
1165
|
+
await this.openSkillPicker('skills.browse');
|
|
1166
|
+
return;
|
|
1167
|
+
case 'skills.run':
|
|
1168
|
+
await this.openSkillPicker('skills.run');
|
|
1169
|
+
return;
|
|
1170
|
+
case 'skills.inspect':
|
|
1171
|
+
await this.openSkillPicker('skills.inspect');
|
|
1172
|
+
return;
|
|
1173
|
+
case 'skills.edit':
|
|
1174
|
+
await this.openSkillPicker('skills.edit');
|
|
1175
|
+
return;
|
|
1176
|
+
case 'skills.meta':
|
|
1177
|
+
await this.openSkillPicker('skills.meta');
|
|
1178
|
+
return;
|
|
1179
|
+
case 'skills.source':
|
|
1180
|
+
await this.openSkillPicker('skills.source');
|
|
1181
|
+
return;
|
|
1182
|
+
case 'files.open':
|
|
1183
|
+
await this.openFilePicker('files.inspect');
|
|
1184
|
+
return;
|
|
1185
|
+
case 'files.recent':
|
|
1186
|
+
await this.openFilePicker('files.recent');
|
|
1187
|
+
return;
|
|
1188
|
+
case 'files.newMarkdown':
|
|
1189
|
+
this.openEditorBuffer('', { title: 'New Markdown Note', preferredExtension: '.md' });
|
|
1190
|
+
return;
|
|
1191
|
+
case 'files.save':
|
|
1192
|
+
await this.saveCurrentEditor(false);
|
|
1193
|
+
return;
|
|
1194
|
+
case 'files.saveAs':
|
|
1195
|
+
await this.saveCurrentEditor(true);
|
|
1196
|
+
return;
|
|
1197
|
+
case 'files.readme':
|
|
1198
|
+
await this.openFileViewer(path.join(this.workspaceRoot, 'README.md'), { title: 'README.md' });
|
|
1199
|
+
return;
|
|
1200
|
+
case 'files.config':
|
|
1201
|
+
await this.openFileViewer(getConfigPath(this.workspaceRoot), { title: '.deepclause/config.json' });
|
|
1202
|
+
return;
|
|
1203
|
+
case 'run.prompt':
|
|
1204
|
+
this.closeMenuMode();
|
|
1205
|
+
this.statusLine = 'Command bar ready.';
|
|
1206
|
+
return;
|
|
1207
|
+
case 'run.repeat':
|
|
1208
|
+
await this.repeatLastCommand();
|
|
1209
|
+
return;
|
|
1210
|
+
case 'run.cancel':
|
|
1211
|
+
this.closeMenuMode();
|
|
1212
|
+
this.cancelCurrentExecution('Cancellation requested.');
|
|
1213
|
+
return;
|
|
1214
|
+
case 'run.clear':
|
|
1215
|
+
this.currentProcessActivity().clear();
|
|
1216
|
+
this.closeMenuMode();
|
|
1217
|
+
this.statusLine = 'Execution pane cleared.';
|
|
1218
|
+
return;
|
|
1219
|
+
case 'run.refreshSkills':
|
|
1220
|
+
await this.refreshCommands();
|
|
1221
|
+
this.closeMenuMode();
|
|
1222
|
+
this.statusLine = `Loaded ${this.commands.length} skill${this.commands.length === 1 ? '' : 's'}.`;
|
|
1223
|
+
return;
|
|
1224
|
+
case 'view.focus.sessions':
|
|
1225
|
+
case 'view.focus.messages':
|
|
1226
|
+
case 'view.focus.process':
|
|
1227
|
+
case 'view.focus.context': {
|
|
1228
|
+
const pane = actionId.slice('view.focus.'.length);
|
|
1229
|
+
this.focusedPane = pane;
|
|
1230
|
+
this.closeMenuMode();
|
|
1231
|
+
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}.`;
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
case 'view.follow':
|
|
1235
|
+
if (this.focusedPane !== 'sessions') {
|
|
1236
|
+
this.paneScroll[this.focusedPane] = 0;
|
|
1237
|
+
}
|
|
1238
|
+
this.closeMenuMode();
|
|
1239
|
+
this.statusLine = `${paneDisplayName(this.focusedPane)} following newest content.`;
|
|
1240
|
+
return;
|
|
1241
|
+
case 'view.palette':
|
|
1242
|
+
this.openActionPalette();
|
|
1243
|
+
return;
|
|
1244
|
+
case 'help.shortcuts':
|
|
1245
|
+
this.openTextViewer('Keyboard Shortcuts', getShortcutHelpLines().join('\n'), { preferredExtension: '.txt' });
|
|
1246
|
+
return;
|
|
1247
|
+
case 'help.slash':
|
|
1248
|
+
this.openTextViewer('Slash Commands', getSlashHelpLines().join('\n'), { preferredExtension: '.txt' });
|
|
1249
|
+
return;
|
|
1250
|
+
case 'help.about':
|
|
1251
|
+
this.openTextViewer('About This TUI', [
|
|
1252
|
+
'DeepClause fullscreen TUI',
|
|
1253
|
+
'',
|
|
1254
|
+
'Use F10 or Ctrl+G for the menu bar, Ctrl+W to cycle panes, and /cancel to stop a running execution.',
|
|
1255
|
+
'The Skills and Files menus open searchable pickers, and Ctrl+P opens the action palette directly.',
|
|
1256
|
+
].join('\n'), { preferredExtension: '.txt' });
|
|
1257
|
+
return;
|
|
1258
|
+
default:
|
|
1259
|
+
this.closeMenuMode();
|
|
1260
|
+
this.statusLine = 'This menu action is being wired up next.';
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
closeWorkspaceMode(force = false) {
|
|
1265
|
+
if (!force && !this.canReplaceWorkspaceSurface()) {
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
this.uiMode = 'command';
|
|
1269
|
+
this.pickerState = null;
|
|
1270
|
+
this.viewerState = null;
|
|
1271
|
+
this.editorState = null;
|
|
1272
|
+
this.inputValue = '';
|
|
1273
|
+
this.cursor = 0;
|
|
1274
|
+
this.requestRender();
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
canReplaceWorkspaceSurface() {
|
|
1278
|
+
if (this.editorState?.dirty) {
|
|
1279
|
+
this.statusLine = 'Save the current editor with Ctrl+S or press Esc again in the editor to discard changes.';
|
|
1280
|
+
this.requestRender();
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
workspaceViewportLines() {
|
|
1286
|
+
return Math.max(1, (output.rows ?? 40) - 5);
|
|
1287
|
+
}
|
|
1288
|
+
openActionPalette() {
|
|
1289
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const items = this.getMenuDefinitions()
|
|
1293
|
+
.flatMap((menu) => menu.items)
|
|
1294
|
+
.filter((item) => item.enabled !== false)
|
|
1295
|
+
.map((item) => ({
|
|
1296
|
+
id: item.id,
|
|
1297
|
+
label: item.label,
|
|
1298
|
+
description: item.description,
|
|
1299
|
+
detail: item.shortcut,
|
|
1300
|
+
actionId: item.id,
|
|
1301
|
+
}));
|
|
1302
|
+
this.openPicker({
|
|
1303
|
+
mode: 'palette',
|
|
1304
|
+
title: 'Action Palette',
|
|
1305
|
+
emptyText: 'No actions match the current query.',
|
|
1306
|
+
items,
|
|
1307
|
+
filteredItems: items,
|
|
1308
|
+
selectedIndex: 0,
|
|
1309
|
+
scrollTop: 0,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
async openSkillPicker(mode) {
|
|
1313
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
await this.refreshCommands();
|
|
1317
|
+
const items = await Promise.all(this.commands.map(async (command) => {
|
|
1318
|
+
const skillPath = this.resolveCommandDmlPath(command);
|
|
1319
|
+
const metaPath = this.resolveCommandMetaPath(command);
|
|
1320
|
+
let sourcePath;
|
|
1321
|
+
try {
|
|
1322
|
+
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
|
|
1323
|
+
if (meta.source) {
|
|
1324
|
+
sourcePath = path.resolve(this.workspaceRoot, meta.source);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
// Metadata is optional while browsing.
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
id: command.name,
|
|
1332
|
+
label: command.name,
|
|
1333
|
+
description: command.description,
|
|
1334
|
+
detail: command.path,
|
|
1335
|
+
path: skillPath,
|
|
1336
|
+
skillName: command.name,
|
|
1337
|
+
metaPath,
|
|
1338
|
+
sourcePath,
|
|
1339
|
+
};
|
|
1340
|
+
}));
|
|
1341
|
+
const titles = {
|
|
1342
|
+
'skills.browse': 'Browse Skills',
|
|
1343
|
+
'skills.run': 'Run Skill',
|
|
1344
|
+
'skills.inspect': 'Inspect Skill Code',
|
|
1345
|
+
'skills.edit': 'Edit Skill Code',
|
|
1346
|
+
'skills.meta': 'Inspect Skill Metadata',
|
|
1347
|
+
'skills.source': 'Open Skill Source Spec',
|
|
1348
|
+
};
|
|
1349
|
+
this.openPicker({
|
|
1350
|
+
mode,
|
|
1351
|
+
title: titles[mode],
|
|
1352
|
+
emptyText: 'No skills match the current query.',
|
|
1353
|
+
items,
|
|
1354
|
+
filteredItems: items,
|
|
1355
|
+
selectedIndex: 0,
|
|
1356
|
+
scrollTop: 0,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
async openFilePicker(mode) {
|
|
1360
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const relativePaths = mode === 'files.recent'
|
|
1364
|
+
? [...this.recentFiles]
|
|
1365
|
+
: await listWorkspaceTextFiles(this.workspaceRoot);
|
|
1366
|
+
const items = relativePaths.map((relativePath) => ({
|
|
1367
|
+
id: relativePath,
|
|
1368
|
+
label: path.basename(relativePath),
|
|
1369
|
+
description: relativePath,
|
|
1370
|
+
detail: path.extname(relativePath) || 'text',
|
|
1371
|
+
path: path.join(this.workspaceRoot, relativePath),
|
|
1372
|
+
}));
|
|
1373
|
+
const titles = {
|
|
1374
|
+
'files.inspect': 'Open Text File',
|
|
1375
|
+
'files.edit': 'Edit Text File',
|
|
1376
|
+
'files.recent': 'Recent Files',
|
|
1377
|
+
};
|
|
1378
|
+
this.openPicker({
|
|
1379
|
+
mode,
|
|
1380
|
+
title: titles[mode],
|
|
1381
|
+
emptyText: mode === 'files.recent'
|
|
1382
|
+
? 'No recent files yet. Open or edit a file first.'
|
|
1383
|
+
: 'No text files match the current query.',
|
|
1384
|
+
items,
|
|
1385
|
+
filteredItems: items,
|
|
1386
|
+
selectedIndex: 0,
|
|
1387
|
+
scrollTop: 0,
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
openPicker(state) {
|
|
1391
|
+
this.closeMenuMode();
|
|
1392
|
+
this.uiMode = state.mode === 'palette' ? 'palette' : 'picker';
|
|
1393
|
+
this.pickerState = state;
|
|
1394
|
+
this.viewerState = null;
|
|
1395
|
+
this.editorState = null;
|
|
1396
|
+
this.inputValue = '';
|
|
1397
|
+
this.cursor = 0;
|
|
1398
|
+
this.ensurePickerSelectionVisible();
|
|
1399
|
+
this.statusLine = `${state.title}.`;
|
|
1400
|
+
this.requestRender();
|
|
1401
|
+
}
|
|
1402
|
+
refreshPickerFilter() {
|
|
1403
|
+
if (!this.pickerState) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
this.pickerState.filteredItems = filterPickerItems(this.pickerState.items, this.inputValue);
|
|
1407
|
+
this.pickerState.selectedIndex = clamp(this.pickerState.selectedIndex, 0, Math.max(0, this.pickerState.filteredItems.length - 1));
|
|
1408
|
+
this.pickerState.scrollTop = 0;
|
|
1409
|
+
this.ensurePickerSelectionVisible();
|
|
1410
|
+
this.requestRender();
|
|
1411
|
+
}
|
|
1412
|
+
movePickerSelection(delta) {
|
|
1413
|
+
if (!this.pickerState || this.pickerState.filteredItems.length === 0) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
this.pickerState.selectedIndex = clamp(this.pickerState.selectedIndex + delta, 0, Math.max(0, this.pickerState.filteredItems.length - 1));
|
|
1417
|
+
this.ensurePickerSelectionVisible();
|
|
1418
|
+
this.requestRender();
|
|
1419
|
+
}
|
|
1420
|
+
ensurePickerSelectionVisible() {
|
|
1421
|
+
if (!this.pickerState) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const targetLine = 3 + (this.pickerState.selectedIndex * 3);
|
|
1425
|
+
const viewport = this.workspaceViewportLines();
|
|
1426
|
+
if (targetLine < this.pickerState.scrollTop) {
|
|
1427
|
+
this.pickerState.scrollTop = targetLine;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (targetLine >= this.pickerState.scrollTop + viewport) {
|
|
1431
|
+
this.pickerState.scrollTop = Math.max(0, targetLine - viewport + 1);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
async activatePickerSelection(override) {
|
|
1435
|
+
const picker = this.pickerState;
|
|
1436
|
+
const item = picker?.filteredItems[picker.selectedIndex];
|
|
1437
|
+
if (!picker || !item) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
if (picker.mode === 'palette') {
|
|
1441
|
+
await this.runMenuAction(item.actionId ?? 'help.about');
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const action = override ?? picker.mode;
|
|
1445
|
+
switch (action) {
|
|
1446
|
+
case 'skills.browse':
|
|
1447
|
+
case 'skills.inspect':
|
|
1448
|
+
await this.openSkillViewer(item, 'dml');
|
|
1449
|
+
return;
|
|
1450
|
+
case 'skills.run': {
|
|
1451
|
+
this.closeWorkspaceMode();
|
|
1452
|
+
const rawArgs = await this.promptForValue(`Arguments for /${item.skillName} (optional):`);
|
|
1453
|
+
await this.executeSkill(item.skillName ?? item.label, parseCommandArgs(rawArgs));
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
case 'run':
|
|
1457
|
+
if (item.skillName) {
|
|
1458
|
+
this.closeWorkspaceMode();
|
|
1459
|
+
const rawArgs = await this.promptForValue(`Arguments for /${item.skillName} (optional):`);
|
|
1460
|
+
await this.executeSkill(item.skillName, parseCommandArgs(rawArgs));
|
|
1461
|
+
}
|
|
1462
|
+
return;
|
|
1463
|
+
case 'edit':
|
|
1464
|
+
case 'skills.edit':
|
|
1465
|
+
case 'files.edit':
|
|
1466
|
+
if (item.path) {
|
|
1467
|
+
await this.openEditorForFile(item.path, { title: item.description || item.label });
|
|
1468
|
+
}
|
|
1469
|
+
return;
|
|
1470
|
+
case 'meta':
|
|
1471
|
+
case 'skills.meta':
|
|
1472
|
+
if (item.metaPath) {
|
|
1473
|
+
await this.openSkillViewer(item, 'meta');
|
|
1474
|
+
}
|
|
1475
|
+
return;
|
|
1476
|
+
case 'source':
|
|
1477
|
+
case 'skills.source':
|
|
1478
|
+
if (item.sourcePath) {
|
|
1479
|
+
await this.openSkillViewer(item, 'source');
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
this.statusLine = `No source spec recorded for ${item.label}.`;
|
|
1483
|
+
this.requestRender();
|
|
1484
|
+
}
|
|
1485
|
+
return;
|
|
1486
|
+
case 'files.inspect':
|
|
1487
|
+
case 'files.recent':
|
|
1488
|
+
if (item.path) {
|
|
1489
|
+
await this.openFileViewer(item.path, { title: item.description || item.label });
|
|
1490
|
+
}
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async openSkillViewer(item, target) {
|
|
1495
|
+
const filePath = target === 'dml' ? item.path : target === 'meta' ? item.metaPath : item.sourcePath;
|
|
1496
|
+
if (!filePath) {
|
|
1497
|
+
this.statusLine = `No ${target} file available for ${item.label}.`;
|
|
1498
|
+
this.requestRender();
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const title = target === 'dml'
|
|
1502
|
+
? `Skill: ${item.label}`
|
|
1503
|
+
: target === 'meta'
|
|
1504
|
+
? `Metadata: ${item.label}`
|
|
1505
|
+
: `Source Spec: ${item.label}`;
|
|
1506
|
+
await this.openFileViewer(filePath, {
|
|
1507
|
+
title,
|
|
1508
|
+
skillName: item.skillName,
|
|
1509
|
+
canEdit: target !== 'meta',
|
|
1510
|
+
preferredExtension: target === 'meta' ? '.json' : path.extname(filePath) || '.txt',
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
openTextViewer(title, content, options = {}) {
|
|
1514
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
this.closeMenuMode();
|
|
1518
|
+
this.uiMode = 'viewer';
|
|
1519
|
+
this.pickerState = null;
|
|
1520
|
+
this.editorState = null;
|
|
1521
|
+
this.viewerState = {
|
|
1522
|
+
title,
|
|
1523
|
+
content,
|
|
1524
|
+
path: options.path,
|
|
1525
|
+
canEdit: options.canEdit ?? false,
|
|
1526
|
+
scrollTop: 0,
|
|
1527
|
+
skillName: options.skillName,
|
|
1528
|
+
metaPath: options.metaPath,
|
|
1529
|
+
sourcePath: options.sourcePath,
|
|
1530
|
+
preferredExtension: options.preferredExtension,
|
|
1531
|
+
};
|
|
1532
|
+
this.inputValue = '';
|
|
1533
|
+
this.cursor = 0;
|
|
1534
|
+
if (options.path) {
|
|
1535
|
+
this.pushRecentFile(options.path);
|
|
1536
|
+
}
|
|
1537
|
+
this.statusLine = `Viewing ${title}.`;
|
|
1538
|
+
this.requestRender();
|
|
1539
|
+
}
|
|
1540
|
+
async openFileViewer(filePath, options = {}) {
|
|
1541
|
+
try {
|
|
1542
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1543
|
+
this.openTextViewer(options.title ?? path.relative(this.workspaceRoot, filePath), content, {
|
|
1544
|
+
path: filePath,
|
|
1545
|
+
canEdit: options.canEdit ?? isEditableTextFile(filePath),
|
|
1546
|
+
skillName: options.skillName,
|
|
1547
|
+
preferredExtension: options.preferredExtension ?? (path.extname(filePath) || '.txt'),
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
this.closeMenuMode();
|
|
1552
|
+
this.statusLine = `Unable to open ${path.relative(this.workspaceRoot, filePath)}: ${error.message}`;
|
|
1553
|
+
this.requestRender();
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async openEditorForFile(filePath, options = {}) {
|
|
1557
|
+
try {
|
|
1558
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1559
|
+
this.openEditorBuffer(content, {
|
|
1560
|
+
title: options.title ?? path.relative(this.workspaceRoot, filePath),
|
|
1561
|
+
path: filePath,
|
|
1562
|
+
preferredExtension: options.preferredExtension ?? (path.extname(filePath) || '.txt'),
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
catch (error) {
|
|
1566
|
+
this.statusLine = `Unable to edit ${path.relative(this.workspaceRoot, filePath)}: ${error.message}`;
|
|
1567
|
+
this.requestRender();
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
openEditorBuffer(content, options) {
|
|
1571
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
this.closeMenuMode();
|
|
1575
|
+
this.uiMode = 'editor';
|
|
1576
|
+
this.pickerState = null;
|
|
1577
|
+
this.viewerState = null;
|
|
1578
|
+
this.editorState = {
|
|
1579
|
+
title: options.title,
|
|
1580
|
+
path: options.path,
|
|
1581
|
+
lines: splitEditorContent(content),
|
|
1582
|
+
cursorLine: 0,
|
|
1583
|
+
cursorColumn: 0,
|
|
1584
|
+
scrollTop: 0,
|
|
1585
|
+
scrollLeft: 0,
|
|
1586
|
+
dirty: false,
|
|
1587
|
+
preferredExtension: options.preferredExtension ?? '.txt',
|
|
1588
|
+
discardPending: false,
|
|
1589
|
+
};
|
|
1590
|
+
this.inputValue = '';
|
|
1591
|
+
this.cursor = 0;
|
|
1592
|
+
if (options.path) {
|
|
1593
|
+
this.pushRecentFile(options.path);
|
|
1594
|
+
}
|
|
1595
|
+
this.ensureEditorCursorVisible();
|
|
1596
|
+
this.statusLine = `Editing ${options.title}.`;
|
|
1597
|
+
this.requestRender();
|
|
1598
|
+
}
|
|
1599
|
+
currentEditorLine() {
|
|
1600
|
+
return this.editorState?.lines[this.editorState.cursorLine] ?? '';
|
|
1601
|
+
}
|
|
1602
|
+
updateCurrentEditorLine(nextLine) {
|
|
1603
|
+
if (!this.editorState) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
this.editorState.lines[this.editorState.cursorLine] = nextLine;
|
|
1607
|
+
this.editorState.dirty = true;
|
|
1608
|
+
this.editorState.discardPending = false;
|
|
1609
|
+
}
|
|
1610
|
+
insertEditorText(text) {
|
|
1611
|
+
if (!this.editorState) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const line = this.currentEditorLine();
|
|
1615
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn)}${text}${line.slice(this.editorState.cursorColumn)}`);
|
|
1616
|
+
this.editorState.cursorColumn += text.length;
|
|
1617
|
+
this.ensureEditorCursorVisible();
|
|
1618
|
+
this.requestRender();
|
|
1619
|
+
}
|
|
1620
|
+
splitEditorLine() {
|
|
1621
|
+
if (!this.editorState) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const line = this.currentEditorLine();
|
|
1625
|
+
const before = line.slice(0, this.editorState.cursorColumn);
|
|
1626
|
+
const after = line.slice(this.editorState.cursorColumn);
|
|
1627
|
+
this.editorState.lines.splice(this.editorState.cursorLine, 1, before, after);
|
|
1628
|
+
this.editorState.cursorLine += 1;
|
|
1629
|
+
this.editorState.cursorColumn = 0;
|
|
1630
|
+
this.editorState.dirty = true;
|
|
1631
|
+
this.editorState.discardPending = false;
|
|
1632
|
+
this.ensureEditorCursorVisible();
|
|
1633
|
+
this.requestRender();
|
|
1634
|
+
}
|
|
1635
|
+
deleteEditorBackward() {
|
|
1636
|
+
if (!this.editorState) {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (this.editorState.cursorColumn > 0) {
|
|
1640
|
+
const line = this.currentEditorLine();
|
|
1641
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn - 1)}${line.slice(this.editorState.cursorColumn)}`);
|
|
1642
|
+
this.editorState.cursorColumn -= 1;
|
|
1643
|
+
}
|
|
1644
|
+
else if (this.editorState.cursorLine > 0) {
|
|
1645
|
+
const previous = this.editorState.lines[this.editorState.cursorLine - 1] ?? '';
|
|
1646
|
+
const current = this.currentEditorLine();
|
|
1647
|
+
this.editorState.lines.splice(this.editorState.cursorLine - 1, 2, `${previous}${current}`);
|
|
1648
|
+
this.editorState.cursorLine -= 1;
|
|
1649
|
+
this.editorState.cursorColumn = previous.length;
|
|
1650
|
+
this.editorState.dirty = true;
|
|
1651
|
+
this.editorState.discardPending = false;
|
|
1652
|
+
}
|
|
1653
|
+
this.ensureEditorCursorVisible();
|
|
1654
|
+
this.requestRender();
|
|
1655
|
+
}
|
|
1656
|
+
deleteEditorForward() {
|
|
1657
|
+
if (!this.editorState) {
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
const line = this.currentEditorLine();
|
|
1661
|
+
if (this.editorState.cursorColumn < line.length) {
|
|
1662
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn)}${line.slice(this.editorState.cursorColumn + 1)}`);
|
|
1663
|
+
}
|
|
1664
|
+
else if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1665
|
+
const next = this.editorState.lines[this.editorState.cursorLine + 1] ?? '';
|
|
1666
|
+
this.editorState.lines.splice(this.editorState.cursorLine, 2, `${line}${next}`);
|
|
1667
|
+
this.editorState.dirty = true;
|
|
1668
|
+
this.editorState.discardPending = false;
|
|
1669
|
+
}
|
|
1670
|
+
this.ensureEditorCursorVisible();
|
|
1671
|
+
this.requestRender();
|
|
1672
|
+
}
|
|
1673
|
+
moveEditorCursor(direction) {
|
|
1674
|
+
if (!this.editorState) {
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
switch (direction) {
|
|
1678
|
+
case 'left':
|
|
1679
|
+
if (this.editorState.cursorColumn > 0) {
|
|
1680
|
+
this.editorState.cursorColumn -= 1;
|
|
1681
|
+
}
|
|
1682
|
+
else if (this.editorState.cursorLine > 0) {
|
|
1683
|
+
this.editorState.cursorLine -= 1;
|
|
1684
|
+
this.editorState.cursorColumn = this.currentEditorLine().length;
|
|
1685
|
+
}
|
|
1686
|
+
break;
|
|
1687
|
+
case 'right':
|
|
1688
|
+
if (this.editorState.cursorColumn < this.currentEditorLine().length) {
|
|
1689
|
+
this.editorState.cursorColumn += 1;
|
|
1690
|
+
}
|
|
1691
|
+
else if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1692
|
+
this.editorState.cursorLine += 1;
|
|
1693
|
+
this.editorState.cursorColumn = 0;
|
|
1694
|
+
}
|
|
1695
|
+
break;
|
|
1696
|
+
case 'up':
|
|
1697
|
+
if (this.editorState.cursorLine > 0) {
|
|
1698
|
+
this.editorState.cursorLine -= 1;
|
|
1699
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1700
|
+
}
|
|
1701
|
+
break;
|
|
1702
|
+
case 'down':
|
|
1703
|
+
if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1704
|
+
this.editorState.cursorLine += 1;
|
|
1705
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1706
|
+
}
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1709
|
+
this.ensureEditorCursorVisible();
|
|
1710
|
+
this.requestRender();
|
|
1711
|
+
}
|
|
1712
|
+
pageEditorCursor(direction) {
|
|
1713
|
+
if (!this.editorState) {
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const nextLine = clamp(this.editorState.cursorLine + (direction * this.workspaceViewportLines()), 0, Math.max(0, this.editorState.lines.length - 1));
|
|
1717
|
+
this.editorState.cursorLine = nextLine;
|
|
1718
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1719
|
+
this.ensureEditorCursorVisible();
|
|
1720
|
+
this.requestRender();
|
|
1721
|
+
}
|
|
1722
|
+
ensureEditorCursorVisible() {
|
|
1723
|
+
if (!this.editorState) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const viewportLines = this.workspaceViewportLines();
|
|
1727
|
+
if (this.editorState.cursorLine < this.editorState.scrollTop) {
|
|
1728
|
+
this.editorState.scrollTop = this.editorState.cursorLine;
|
|
1729
|
+
}
|
|
1730
|
+
else if (this.editorState.cursorLine >= this.editorState.scrollTop + viewportLines) {
|
|
1731
|
+
this.editorState.scrollTop = Math.max(0, this.editorState.cursorLine - viewportLines + 1);
|
|
1732
|
+
}
|
|
1733
|
+
const width = output.columns ?? 120;
|
|
1734
|
+
const innerWidth = Math.max(1, width - 2);
|
|
1735
|
+
const lineNumberWidth = Math.max(2, String(Math.max(1, this.editorState.lines.length)).length);
|
|
1736
|
+
const prefixWidth = 2 + lineNumberWidth + 3;
|
|
1737
|
+
const contentWidth = Math.max(1, innerWidth - prefixWidth);
|
|
1738
|
+
if (this.editorState.cursorColumn < this.editorState.scrollLeft) {
|
|
1739
|
+
this.editorState.scrollLeft = this.editorState.cursorColumn;
|
|
1740
|
+
}
|
|
1741
|
+
else if (this.editorState.cursorColumn >= this.editorState.scrollLeft + contentWidth) {
|
|
1742
|
+
this.editorState.scrollLeft = Math.max(0, this.editorState.cursorColumn - contentWidth + 1);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async saveCurrentEditor(saveAs) {
|
|
1746
|
+
if (!this.editorState) {
|
|
1747
|
+
this.closeMenuMode();
|
|
1748
|
+
this.statusLine = 'No editor buffer is active.';
|
|
1749
|
+
this.requestRender();
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
let targetPath = this.editorState.path;
|
|
1753
|
+
if (saveAs || !targetPath) {
|
|
1754
|
+
const suggested = this.editorState.path
|
|
1755
|
+
? path.relative(this.workspaceRoot, this.editorState.path)
|
|
1756
|
+
: `notes/${Date.now()}.${this.editorState.preferredExtension.replace(/^\./, '')}`;
|
|
1757
|
+
const requestedPath = (await this.promptForValue(`Save file as (relative path) [${suggested}]:`)).trim() || suggested;
|
|
1758
|
+
targetPath = resolveWorkspaceTextPath(this.workspaceRoot, requestedPath);
|
|
1759
|
+
}
|
|
1760
|
+
try {
|
|
1761
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1762
|
+
await fs.writeFile(targetPath, this.editorState.lines.join('\n'), 'utf8');
|
|
1763
|
+
this.editorState.path = targetPath;
|
|
1764
|
+
this.editorState.title = path.relative(this.workspaceRoot, targetPath);
|
|
1765
|
+
this.editorState.dirty = false;
|
|
1766
|
+
this.editorState.discardPending = false;
|
|
1767
|
+
this.pushRecentFile(targetPath);
|
|
1768
|
+
this.statusLine = `Saved ${path.relative(this.workspaceRoot, targetPath)}.`;
|
|
1769
|
+
this.requestRender();
|
|
1770
|
+
}
|
|
1771
|
+
catch (error) {
|
|
1772
|
+
this.statusLine = `Unable to save file: ${error.message}`;
|
|
1773
|
+
this.requestRender();
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
pushRecentFile(filePath) {
|
|
1777
|
+
const relativePath = path.relative(this.workspaceRoot, filePath).replace(/\\/g, '/');
|
|
1778
|
+
const existingIndex = this.recentFiles.indexOf(relativePath);
|
|
1779
|
+
if (existingIndex >= 0) {
|
|
1780
|
+
this.recentFiles.splice(existingIndex, 1);
|
|
1781
|
+
}
|
|
1782
|
+
this.recentFiles.unshift(relativePath);
|
|
1783
|
+
if (this.recentFiles.length > 20) {
|
|
1784
|
+
this.recentFiles.length = 20;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async repeatLastCommand() {
|
|
1788
|
+
if (!this.lastSubmittedInput) {
|
|
1789
|
+
this.closeMenuMode();
|
|
1790
|
+
this.statusLine = 'No previous prompt or skill to repeat yet.';
|
|
1791
|
+
this.requestRender();
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (!this.closeWorkspaceMode()) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (this.lastSubmittedInput.kind === 'text') {
|
|
1798
|
+
await this.executeTask(this.lastSubmittedInput.prompt);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (this.lastSubmittedInput.kind === 'skill') {
|
|
1802
|
+
await this.executeSkill(this.lastSubmittedInput.name, this.lastSubmittedInput.args);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (this.lastSubmittedInput.kind === 'builtin') {
|
|
1806
|
+
await this.executeBuiltin(this.lastSubmittedInput);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
resolveCommandDmlPath(command) {
|
|
1810
|
+
return path.join(this.workspaceRoot, `${command.path}.dml`);
|
|
1811
|
+
}
|
|
1812
|
+
resolveCommandMetaPath(command) {
|
|
1813
|
+
return path.join(this.workspaceRoot, `${command.path}.meta.json`);
|
|
1814
|
+
}
|
|
1815
|
+
beginExecutionPreview(sessionId, kind, lead, rootTag) {
|
|
1816
|
+
this.currentPreview = {
|
|
1817
|
+
sessionId,
|
|
1818
|
+
kind,
|
|
1819
|
+
rootTag,
|
|
1820
|
+
activeChildTag: kind === 'skill' ? rootTag : undefined,
|
|
1821
|
+
entries: [lead, { role: 'assistant', content: '', pending: true }],
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
updatePreviewFromEvent(logEvent) {
|
|
1825
|
+
if (!this.currentPreview) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
this.updatePreviewChildState(logEvent);
|
|
1829
|
+
const previewMessage = previewMessageFromEvent(logEvent);
|
|
1830
|
+
if (previewMessage) {
|
|
1831
|
+
this.insertPreviewMessage(previewMessage);
|
|
1832
|
+
}
|
|
1833
|
+
if (!this.isPrimaryPreviewEvent(logEvent)) {
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
const message = this.ensurePreviewAssistantMessage();
|
|
1837
|
+
switch (logEvent.event.type) {
|
|
1838
|
+
case 'stream':
|
|
1839
|
+
if (logEvent.event.content) {
|
|
1840
|
+
message.content += logEvent.event.content;
|
|
1841
|
+
}
|
|
1842
|
+
break;
|
|
1843
|
+
case 'answer':
|
|
1844
|
+
if (logEvent.event.content) {
|
|
1845
|
+
message.content = logEvent.event.content;
|
|
1846
|
+
}
|
|
1847
|
+
message.pending = false;
|
|
1848
|
+
message.error = false;
|
|
1849
|
+
break;
|
|
1850
|
+
case 'error':
|
|
1851
|
+
message.content = logEvent.event.content ?? (message.content || 'Execution failed');
|
|
1852
|
+
message.pending = false;
|
|
1853
|
+
message.error = true;
|
|
1854
|
+
break;
|
|
1855
|
+
default:
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
isPrimaryPreviewEvent(logEvent) {
|
|
1860
|
+
if (!this.currentPreview) {
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
if (this.currentPreview.kind === 'task') {
|
|
1864
|
+
return logEvent.scope === 'main';
|
|
1865
|
+
}
|
|
1866
|
+
return logEvent.scope === 'child' && logEvent.childSlug === this.currentPreview.rootTag;
|
|
1867
|
+
}
|
|
1868
|
+
insertPreviewMessage(entry) {
|
|
1869
|
+
if (!this.currentPreview) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const entries = this.currentPreview.entries;
|
|
1873
|
+
const last = entries[entries.length - 1];
|
|
1874
|
+
if (last && last.role === 'assistant' && last.pending && !last.content) {
|
|
1875
|
+
entries.splice(entries.length - 1, 0, entry);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
entries.push(entry);
|
|
1879
|
+
}
|
|
1880
|
+
updatePreviewChildState(logEvent) {
|
|
1881
|
+
if (!this.currentPreview || this.currentPreview.kind !== 'task') {
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (logEvent.scope === 'child' && logEvent.childSlug) {
|
|
1885
|
+
this.currentPreview.activeChildTag = logEvent.childSlug;
|
|
1886
|
+
this.ensurePreviewChildSkillMessage(logEvent.childSlug);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (logEvent.scope === 'main' && (logEvent.event.type === 'stream' || logEvent.event.type === 'answer' || logEvent.event.type === 'error')) {
|
|
1890
|
+
this.currentPreview.activeChildTag = undefined;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
ensurePreviewChildSkillMessage(childSlug) {
|
|
1894
|
+
if (!this.currentPreview) {
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const message = previewChildSkillActivityMessage(childSlug);
|
|
1898
|
+
const exists = this.currentPreview.entries.some((entry) => sameDisplayMessage(entry, message));
|
|
1899
|
+
if (!exists) {
|
|
1900
|
+
this.insertPreviewMessage(message);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
ensurePreviewAssistantMessage() {
|
|
1904
|
+
if (!this.currentPreview) {
|
|
1905
|
+
return { role: 'assistant', content: '', pending: false };
|
|
1906
|
+
}
|
|
1907
|
+
const desiredTag = this.currentPreview.rootTag ?? this.currentPreview.activeChildTag;
|
|
1908
|
+
const last = this.currentPreview.entries[this.currentPreview.entries.length - 1];
|
|
1909
|
+
if (last && last.role === 'assistant') {
|
|
1910
|
+
last.tag = desiredTag;
|
|
1911
|
+
return last;
|
|
1912
|
+
}
|
|
1913
|
+
const created = { role: 'assistant', content: '', pending: true, tag: desiredTag };
|
|
1914
|
+
this.currentPreview.entries.push(created);
|
|
1915
|
+
return created;
|
|
1916
|
+
}
|
|
1917
|
+
finishExecutionPreview(options) {
|
|
1918
|
+
if (!this.currentPreview) {
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
this.currentPreview.activeChildTag = undefined;
|
|
1922
|
+
const assistant = this.ensurePreviewAssistantMessage();
|
|
1923
|
+
if (typeof options.finalText === 'string' && options.finalText.trim()) {
|
|
1924
|
+
assistant.content = options.finalText;
|
|
1925
|
+
}
|
|
1926
|
+
if (typeof options.error === 'string' && options.error.trim()) {
|
|
1927
|
+
assistant.content = options.error;
|
|
1928
|
+
assistant.error = true;
|
|
1929
|
+
}
|
|
1930
|
+
assistant.pending = false;
|
|
1931
|
+
if (options.persist) {
|
|
1932
|
+
const existing = this.ephemeralMessagesBySessionId.get(this.currentPreview.sessionId) ?? [];
|
|
1933
|
+
existing.push(...this.currentPreview.entries.map((entry) => ({ ...entry, pending: false })));
|
|
1934
|
+
this.ephemeralMessagesBySessionId.set(this.currentPreview.sessionId, existing);
|
|
1935
|
+
}
|
|
1936
|
+
this.currentPreview = null;
|
|
1937
|
+
}
|
|
1938
|
+
cancelCurrentExecution(message) {
|
|
1939
|
+
if (!this.busy || !this.currentAbortController) {
|
|
1940
|
+
this.statusLine = 'No execution is currently running.';
|
|
1941
|
+
this.requestRender();
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
if (this.currentAbortController.signal.aborted) {
|
|
1945
|
+
this.statusLine = 'Cancellation already requested.';
|
|
1946
|
+
this.requestRender();
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
this.currentAbortController.abort();
|
|
1950
|
+
this.statusLine = message;
|
|
1951
|
+
this.requestRender();
|
|
1952
|
+
}
|
|
1953
|
+
startAnimation() {
|
|
1954
|
+
if (this.animationTimer) {
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
this.animationTimer = setInterval(() => {
|
|
1958
|
+
if ((this.busy || this.currentPreview?.entries.some((entry) => entry.pending)) && !this.isEditingInput()) {
|
|
1959
|
+
this.requestRender();
|
|
1960
|
+
}
|
|
1961
|
+
}, 120);
|
|
1962
|
+
}
|
|
1963
|
+
stopAnimation() {
|
|
1964
|
+
if (!this.animationTimer) {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
clearInterval(this.animationTimer);
|
|
1968
|
+
this.animationTimer = null;
|
|
1969
|
+
}
|
|
1970
|
+
scrollFocusedPane(direction, amount) {
|
|
1971
|
+
const metrics = this.getPaneMetrics(this.focusedPane);
|
|
1972
|
+
if (metrics.maxStart === 0) {
|
|
1973
|
+
this.statusLine = `${paneDisplayName(this.focusedPane)} fits on screen.`;
|
|
1974
|
+
this.requestRender();
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (this.focusedPane === 'sessions') {
|
|
1978
|
+
const delta = direction === 'down' ? amount : -amount;
|
|
1979
|
+
this.paneScroll.sessions = clamp(this.paneScroll.sessions + delta, 0, metrics.maxStart);
|
|
1980
|
+
this.statusLine = `Sessions scroll ${this.paneScroll.sessions + 1}/${metrics.maxStart + 1}.`;
|
|
1981
|
+
this.requestRender();
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const pane = this.focusedPane;
|
|
1985
|
+
const delta = direction === 'up' ? amount : -amount;
|
|
1986
|
+
this.paneScroll[pane] = clamp(this.paneScroll[pane] + delta, 0, metrics.maxStart);
|
|
1987
|
+
this.statusLine = this.paneScroll[pane] === 0
|
|
1988
|
+
? `${paneDisplayName(pane)} following newest content.`
|
|
1989
|
+
: `${paneDisplayName(pane)} scrolled ${this.paneScroll[pane]} line${this.paneScroll[pane] === 1 ? '' : 's'} above latest.`;
|
|
1990
|
+
this.requestRender();
|
|
1991
|
+
}
|
|
1992
|
+
jumpFocusedPane(target) {
|
|
1993
|
+
const metrics = this.getPaneMetrics(this.focusedPane);
|
|
1994
|
+
if (this.focusedPane === 'sessions') {
|
|
1995
|
+
this.paneScroll.sessions = target === 'top' ? 0 : metrics.maxStart;
|
|
1996
|
+
this.statusLine = target === 'top'
|
|
1997
|
+
? 'Sessions at top.'
|
|
1998
|
+
: 'Sessions at bottom.';
|
|
1999
|
+
this.requestRender();
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
const pane = this.focusedPane;
|
|
2003
|
+
this.paneScroll[pane] = target === 'bottom' ? 0 : metrics.maxStart;
|
|
2004
|
+
this.statusLine = target === 'bottom'
|
|
2005
|
+
? `${paneDisplayName(pane)} following newest content.`
|
|
2006
|
+
: `${paneDisplayName(pane)} at oldest visible content.`;
|
|
2007
|
+
this.requestRender();
|
|
2008
|
+
}
|
|
2009
|
+
pageScrollAmount() {
|
|
2010
|
+
const rows = output.rows ?? 40;
|
|
2011
|
+
return Math.max(3, Math.floor(Math.max(1, rows - 5) / 2));
|
|
2012
|
+
}
|
|
2013
|
+
async moveSession(offset) {
|
|
2014
|
+
if (this.sessions.length === 0) {
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const currentIndex = Math.max(0, this.sessions.findIndex((session) => session.id === this.selectedSessionId));
|
|
2018
|
+
const nextIndex = (currentIndex + offset + this.sessions.length) % this.sessions.length;
|
|
2019
|
+
const nextSession = this.sessions[nextIndex];
|
|
2020
|
+
if (!nextSession || nextSession.id === this.selectedSessionId) {
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
this.selectedSessionId = nextSession.id;
|
|
2024
|
+
await this.loadSelectedSessionDetail();
|
|
2025
|
+
this.statusLine = `Selected session ${nextSession.title}.`;
|
|
2026
|
+
this.requestRender();
|
|
2027
|
+
}
|
|
2028
|
+
currentProcessActivity() {
|
|
2029
|
+
return this.activityFor(this.selectedSessionId || 'global');
|
|
2030
|
+
}
|
|
2031
|
+
insertText(text) {
|
|
2032
|
+
this.inputValue = `${this.inputValue.slice(0, this.cursor)}${text}${this.inputValue.slice(this.cursor)}`;
|
|
2033
|
+
this.cursor += text.length;
|
|
2034
|
+
}
|
|
2035
|
+
deleteBackward() {
|
|
2036
|
+
if (this.cursor === 0) {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
this.inputValue = `${this.inputValue.slice(0, this.cursor - 1)}${this.inputValue.slice(this.cursor)}`;
|
|
2040
|
+
this.cursor -= 1;
|
|
2041
|
+
}
|
|
2042
|
+
deleteForward() {
|
|
2043
|
+
if (this.cursor >= this.inputValue.length) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
this.inputValue = `${this.inputValue.slice(0, this.cursor)}${this.inputValue.slice(this.cursor + 1)}`;
|
|
2047
|
+
}
|
|
2048
|
+
async refreshCommands() {
|
|
2049
|
+
this.commands = await listCommands(this.workspaceRoot);
|
|
2050
|
+
}
|
|
2051
|
+
async refreshSessions(options = {}) {
|
|
2052
|
+
let sessions = await listConductorSessions(this.workspaceRoot);
|
|
2053
|
+
if (sessions.length === 0 && options.createIfMissing) {
|
|
2054
|
+
const created = await createConductorSession(this.workspaceRoot);
|
|
2055
|
+
sessions = [created];
|
|
2056
|
+
}
|
|
2057
|
+
this.sessions = sessions;
|
|
2058
|
+
const desiredSessionId = options.selectedSessionId ?? this.selectedSessionId ?? sessions[0]?.id ?? '';
|
|
2059
|
+
this.selectedSessionId = sessions.find((session) => session.id === desiredSessionId)?.id ?? sessions[0]?.id ?? '';
|
|
2060
|
+
this.ensureSessionVisible();
|
|
2061
|
+
await this.loadSelectedSessionDetail();
|
|
2062
|
+
}
|
|
2063
|
+
async loadSelectedSessionDetail() {
|
|
2064
|
+
if (!this.selectedSessionId) {
|
|
2065
|
+
this.sessionDetail = null;
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
this.sessionDetail = await getConductorSessionDetail(this.workspaceRoot, this.selectedSessionId);
|
|
2069
|
+
}
|
|
2070
|
+
activityFor(key) {
|
|
2071
|
+
const existing = this.activityBySessionId.get(key);
|
|
2072
|
+
if (existing) {
|
|
2073
|
+
return existing;
|
|
2074
|
+
}
|
|
2075
|
+
const created = new ActivityBuffer();
|
|
2076
|
+
this.activityBySessionId.set(key, created);
|
|
2077
|
+
return created;
|
|
2078
|
+
}
|
|
2079
|
+
requestRender() {
|
|
2080
|
+
if (this.renderTimer) {
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
const minInterval = this.isEditingInput()
|
|
2084
|
+
? TYPING_RENDER_INTERVAL_MS
|
|
2085
|
+
: this.busy
|
|
2086
|
+
? BUSY_RENDER_INTERVAL_MS
|
|
2087
|
+
: IDLE_RENDER_INTERVAL_MS;
|
|
2088
|
+
const delay = Math.max(0, minInterval - (Date.now() - this.lastRenderAt));
|
|
2089
|
+
this.renderTimer = setTimeout(() => {
|
|
2090
|
+
this.renderTimer = null;
|
|
2091
|
+
this.render();
|
|
2092
|
+
}, delay);
|
|
2093
|
+
}
|
|
2094
|
+
render() {
|
|
2095
|
+
this.lastRenderAt = Date.now();
|
|
2096
|
+
const columns = output.columns ?? 120;
|
|
2097
|
+
const rows = output.rows ?? 40;
|
|
2098
|
+
const contentHeight = Math.max(1, rows - 3);
|
|
2099
|
+
const widths = computePaneWidths(columns);
|
|
2100
|
+
const rightHeights = computeRightPaneHeights(contentHeight);
|
|
2101
|
+
const sessionsPane = this.buildPaneView('sessions', 'Sessions', widths.left, contentHeight);
|
|
2102
|
+
const messagesPane = this.buildPaneView('messages', 'Messages', widths.center, contentHeight);
|
|
2103
|
+
const processPane = this.buildPaneView('process', 'Execution', widths.right, rightHeights.process);
|
|
2104
|
+
const contextPane = this.buildPaneView('context', 'Context', widths.right, rightHeights.context);
|
|
2105
|
+
const workspaceView = this.uiMode === 'command'
|
|
2106
|
+
? null
|
|
2107
|
+
: this.buildWorkspacePaneView(columns, contentHeight);
|
|
2108
|
+
const header = renderMenuBar(this.buildHeaderLine(), columns, this.uiMode === 'menu' ? this.menuState.activeIndex : null);
|
|
2109
|
+
const status = renderStatusBar(this.buildStatusLine(), columns, this.buildHotkeys());
|
|
2110
|
+
const inputPrefix = this.buildInputPrefix();
|
|
2111
|
+
const inputValue = this.buildInputValue();
|
|
2112
|
+
const inputLine = renderCommandBar(inputPrefix, inputValue, columns);
|
|
2113
|
+
const gap = paint(' ', ANSI.bgBlue);
|
|
2114
|
+
const lines = [header];
|
|
2115
|
+
if (workspaceView) {
|
|
2116
|
+
lines.push(...workspaceView.lines);
|
|
2117
|
+
}
|
|
2118
|
+
else {
|
|
2119
|
+
for (let index = 0; index < contentHeight; index += 1) {
|
|
2120
|
+
const rightLine = index < rightHeights.process
|
|
2121
|
+
? processPane[index]
|
|
2122
|
+
: contextPane[index - rightHeights.process];
|
|
2123
|
+
lines.push(`${sessionsPane[index]}${gap}${messagesPane[index]}${gap}${rightLine}`);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
lines.push(status);
|
|
2127
|
+
lines.push(inputLine);
|
|
2128
|
+
this.paintFrame(lines, { columns, rows });
|
|
2129
|
+
if (workspaceView?.cursor && !this.pendingPrompt) {
|
|
2130
|
+
output.write(`\u001b[${workspaceView.cursor.row};${workspaceView.cursor.column}H`);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const cursorColumn = Math.min(columns, inputPrefix.length + this.cursor + 1);
|
|
2134
|
+
output.write(`\u001b[${rows};${cursorColumn}H`);
|
|
2135
|
+
}
|
|
2136
|
+
paintFrame(lines, size) {
|
|
2137
|
+
const patch = computeFramePatch(this.previousFrameLines, lines, this.previousFrameSize, size);
|
|
2138
|
+
let buffer = '\u001b[0m';
|
|
2139
|
+
if (patch.fullRender) {
|
|
2140
|
+
buffer += `\u001b[H\u001b[2J${lines.join('\n')}\u001b[?25h`;
|
|
2141
|
+
}
|
|
2142
|
+
else if (patch.changedRows.length > 0) {
|
|
2143
|
+
for (const change of patch.changedRows) {
|
|
2144
|
+
buffer += `\u001b[${change.row};1H\u001b[2K${change.line}`;
|
|
2145
|
+
}
|
|
2146
|
+
buffer += '\u001b[?25h';
|
|
2147
|
+
}
|
|
2148
|
+
else {
|
|
2149
|
+
buffer += '\u001b[?25h';
|
|
2150
|
+
}
|
|
2151
|
+
output.write(buffer);
|
|
2152
|
+
this.previousFrameLines = [...lines];
|
|
2153
|
+
this.previousFrameSize = size;
|
|
2154
|
+
}
|
|
2155
|
+
buildHeaderLine() {
|
|
2156
|
+
const sessionLabel = this.sessionDetail
|
|
2157
|
+
? `${this.sessionDetail.title} (${this.sessionDetail.id.slice(0, 8)})`
|
|
2158
|
+
: 'no session';
|
|
2159
|
+
const skillLabel = this.commands.length > 0
|
|
2160
|
+
? `${this.commands.length} skill${this.commands.length === 1 ? '' : 's'}`
|
|
2161
|
+
: 'no skills';
|
|
2162
|
+
const mode = this.busy ? '[Running]' : '[Idle]';
|
|
2163
|
+
return `${sessionLabel} ${mode} ${skillLabel}`;
|
|
2164
|
+
}
|
|
2165
|
+
buildStatusLine() {
|
|
2166
|
+
if (this.uiMode === 'menu') {
|
|
2167
|
+
const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
|
|
2168
|
+
return `Menu ${menu?.label ?? ''} | ${menu?.items[this.menuState.selectedIndex]?.description ?? 'Choose an action.'}`;
|
|
2169
|
+
}
|
|
2170
|
+
if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
|
|
2171
|
+
const selected = this.pickerState.filteredItems[this.pickerState.selectedIndex];
|
|
2172
|
+
return `${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
|
|
2173
|
+
}
|
|
2174
|
+
if (this.uiMode === 'viewer' && this.viewerState) {
|
|
2175
|
+
return `${this.viewerState.title} | ${this.viewerState.path ? path.relative(this.workspaceRoot, this.viewerState.path) : 'read-only view'}`;
|
|
2176
|
+
}
|
|
2177
|
+
if (this.uiMode === 'editor' && this.editorState) {
|
|
2178
|
+
const dirty = this.editorState.dirty ? 'modified' : 'saved';
|
|
2179
|
+
return `${this.editorState.title} | ${dirty} | Ln ${this.editorState.cursorLine + 1}, Col ${this.editorState.cursorColumn + 1}`;
|
|
2180
|
+
}
|
|
2181
|
+
const focus = paneDisplayName(this.focusedPane);
|
|
2182
|
+
if (!this.pendingPrompt && this.focusedPane !== 'sessions') {
|
|
2183
|
+
const follow = this.paneScroll[this.focusedPane] === 0 ? 'follow' : `scroll ${this.paneScroll[this.focusedPane]}`;
|
|
2184
|
+
return `${this.statusLine} | Focus ${focus} | ${follow}`;
|
|
2185
|
+
}
|
|
2186
|
+
if (this.pendingPrompt) {
|
|
2187
|
+
return this.pendingPrompt.kind === 'clarify'
|
|
2188
|
+
? `Clarify: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`
|
|
2189
|
+
: `Input: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`;
|
|
2190
|
+
}
|
|
2191
|
+
return `${this.statusLine} | Focus ${focus}`;
|
|
2192
|
+
}
|
|
2193
|
+
buildInputPrefix() {
|
|
2194
|
+
if (this.uiMode === 'menu') {
|
|
2195
|
+
return ' Menu > ';
|
|
2196
|
+
}
|
|
2197
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
2198
|
+
return ' Search > ';
|
|
2199
|
+
}
|
|
2200
|
+
if (this.pendingPrompt?.kind === 'clarify') {
|
|
2201
|
+
return ' Clarify ? ';
|
|
2202
|
+
}
|
|
2203
|
+
if (this.pendingPrompt) {
|
|
2204
|
+
return ' Input > ';
|
|
2205
|
+
}
|
|
2206
|
+
if (this.uiMode === 'editor') {
|
|
2207
|
+
return ' Editor > ';
|
|
2208
|
+
}
|
|
2209
|
+
if (this.uiMode === 'viewer') {
|
|
2210
|
+
return ' Viewer > ';
|
|
2211
|
+
}
|
|
2212
|
+
return ' Command > ';
|
|
2213
|
+
}
|
|
2214
|
+
buildInputValue() {
|
|
2215
|
+
if (this.uiMode === 'editor') {
|
|
2216
|
+
return 'Ctrl+S save Esc close arrows move';
|
|
2217
|
+
}
|
|
2218
|
+
if (this.uiMode === 'viewer') {
|
|
2219
|
+
return this.viewerState?.canEdit ? 'Ctrl+E edit Esc close PgUp/PgDn scroll' : 'Esc close PgUp/PgDn scroll';
|
|
2220
|
+
}
|
|
2221
|
+
return this.inputValue;
|
|
2222
|
+
}
|
|
2223
|
+
buildSessionPaneBody() {
|
|
2224
|
+
if (this.sessions.length === 0) {
|
|
2225
|
+
return ['No sessions yet.'];
|
|
2226
|
+
}
|
|
2227
|
+
const lines = [];
|
|
2228
|
+
for (const session of this.sessions) {
|
|
2229
|
+
const selected = session.id === this.selectedSessionId ? '>' : ' ';
|
|
2230
|
+
lines.push(`${selected} ${session.title}`);
|
|
2231
|
+
lines.push(` ${formatTimestamp(session.updatedAt)}`);
|
|
2232
|
+
}
|
|
2233
|
+
return lines;
|
|
2234
|
+
}
|
|
2235
|
+
buildMessagesPaneBody() {
|
|
2236
|
+
const entries = [];
|
|
2237
|
+
if (this.sessionDetail) {
|
|
2238
|
+
for (const message of this.sessionDetail.messages) {
|
|
2239
|
+
entries.push({ role: message.role, content: message.content });
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
const persistedEphemeral = this.ephemeralMessagesBySessionId.get(this.selectedSessionId) ?? [];
|
|
2243
|
+
entries.push(...persistedEphemeral);
|
|
2244
|
+
if (this.currentPreview && this.currentPreview.sessionId === this.selectedSessionId) {
|
|
2245
|
+
entries.push(...this.currentPreview.entries);
|
|
2246
|
+
}
|
|
2247
|
+
if (entries.length === 0) {
|
|
2248
|
+
return [
|
|
2249
|
+
'[System] No messages yet.',
|
|
2250
|
+
' Enter a prompt to start a conductor turn.',
|
|
2251
|
+
' The center pane shows user and assistant messages.',
|
|
2252
|
+
];
|
|
2253
|
+
}
|
|
2254
|
+
const lines = [];
|
|
2255
|
+
for (const entry of entries) {
|
|
2256
|
+
const header = this.formatMessageHeader(entry);
|
|
2257
|
+
lines.push(header);
|
|
2258
|
+
const bodyLines = entry.content
|
|
2259
|
+
? entry.content.split(/\r?\n/)
|
|
2260
|
+
: (entry.pending ? [''] : ['(empty)']);
|
|
2261
|
+
if (entry.pending && !entry.content) {
|
|
2262
|
+
bodyLines[0] = 'thinking...';
|
|
2263
|
+
}
|
|
2264
|
+
for (const line of bodyLines) {
|
|
2265
|
+
lines.push(` ${line}`);
|
|
2266
|
+
}
|
|
2267
|
+
lines.push('');
|
|
2268
|
+
}
|
|
2269
|
+
return lines;
|
|
2270
|
+
}
|
|
2271
|
+
buildProcessPaneBody() {
|
|
2272
|
+
const activity = this.currentProcessActivity().snapshot();
|
|
2273
|
+
if (activity.length > 0) {
|
|
2274
|
+
return activity;
|
|
2275
|
+
}
|
|
2276
|
+
return [
|
|
2277
|
+
'No execution activity yet.',
|
|
2278
|
+
'Tool calls, logs, outputs, and exceptions render here.',
|
|
2279
|
+
'Use /cancel to abort the current execution.',
|
|
2280
|
+
];
|
|
2281
|
+
}
|
|
2282
|
+
buildContextPaneBody() {
|
|
2283
|
+
if (!this.sessionDetail) {
|
|
2284
|
+
return ['No session selected.'];
|
|
2285
|
+
}
|
|
2286
|
+
const contextSize = summarizeContextSize(this.sessionDetail);
|
|
2287
|
+
const usageEntries = Object.entries(this.sessionDetail.usageByModel ?? {})
|
|
2288
|
+
.sort((left, right) => right[1].totalTokens - left[1].totalTokens || left[0].localeCompare(right[0]));
|
|
2289
|
+
const body = [
|
|
2290
|
+
'Context Size',
|
|
2291
|
+
`Messages ${formatInteger(contextSize.messageCount)}`,
|
|
2292
|
+
`Transcript ${formatByteSize(contextSize.messageBytes)} (~${formatInteger(contextSize.messageTokens)} tok)`,
|
|
2293
|
+
`Task Memory ${formatByteSize(contextSize.taskMemoryBytes)} (~${formatInteger(contextSize.taskMemoryTokens)} tok)`,
|
|
2294
|
+
`Assistant Memory ${formatByteSize(contextSize.assistantMemoryBytes)} (~${formatInteger(contextSize.assistantMemoryTokens)} tok)`,
|
|
2295
|
+
`Total ${formatByteSize(contextSize.totalBytes)} (~${formatInteger(contextSize.totalTokens)} tok)`,
|
|
2296
|
+
'',
|
|
2297
|
+
'Session Token Usage',
|
|
2298
|
+
];
|
|
2299
|
+
if (usageEntries.length === 0) {
|
|
2300
|
+
body.push('(no usage recorded yet)');
|
|
2301
|
+
return body;
|
|
2302
|
+
}
|
|
2303
|
+
for (const [modelId, usage] of usageEntries) {
|
|
2304
|
+
body.push(modelId);
|
|
2305
|
+
body.push(` total ${formatInteger(usage.totalTokens)} in ${formatInteger(usage.inputTokens)} out ${formatInteger(usage.outputTokens)} calls ${formatInteger(usage.calls)}`);
|
|
2306
|
+
const extras = [];
|
|
2307
|
+
if (usage.cacheReadTokens > 0) {
|
|
2308
|
+
extras.push(`cache-read ${formatInteger(usage.cacheReadTokens)}`);
|
|
2309
|
+
}
|
|
2310
|
+
if (usage.cacheWriteTokens > 0) {
|
|
2311
|
+
extras.push(`cache-write ${formatInteger(usage.cacheWriteTokens)}`);
|
|
2312
|
+
}
|
|
2313
|
+
if (usage.reasoningTokens > 0) {
|
|
2314
|
+
extras.push(`reasoning ${formatInteger(usage.reasoningTokens)}`);
|
|
2315
|
+
}
|
|
2316
|
+
if (extras.length > 0) {
|
|
2317
|
+
body.push(` ${extras.join(' ')}`);
|
|
2318
|
+
}
|
|
2319
|
+
body.push('');
|
|
2320
|
+
}
|
|
2321
|
+
return body;
|
|
2322
|
+
}
|
|
2323
|
+
buildPaneView(kind, title, width, height) {
|
|
2324
|
+
const metrics = this.getPaneMetrics(kind, width, height);
|
|
2325
|
+
const paneTitle = this.buildPaneTitle(title, kind, metrics);
|
|
2326
|
+
return buildPane(paneTitle, metrics.lines.slice(metrics.start, metrics.start + metrics.viewportLines), width, height, kind, this.focusedPane === kind);
|
|
2327
|
+
}
|
|
2328
|
+
buildWorkspacePaneView(width, height) {
|
|
2329
|
+
if (this.uiMode === 'editor' && this.editorState) {
|
|
2330
|
+
return this.buildEditorPaneView(width, height, this.editorState);
|
|
2331
|
+
}
|
|
2332
|
+
if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
|
|
2333
|
+
return {
|
|
2334
|
+
lines: buildPane(this.pickerState.title, this.buildPickerPaneBody(this.pickerState), width, height, 'workspace', true),
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
if (this.uiMode === 'viewer' && this.viewerState) {
|
|
2338
|
+
return {
|
|
2339
|
+
lines: buildPane(this.viewerState.title, this.buildViewerPaneBody(this.viewerState), width, height, 'workspace', true),
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
|
|
2343
|
+
return {
|
|
2344
|
+
lines: buildPane(`Menu: ${menu?.label ?? 'Menu'}`, this.buildMenuPaneBody(menu), width, height, 'workspace', true),
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
buildMenuPaneBody(menu) {
|
|
2348
|
+
if (!menu) {
|
|
2349
|
+
return ['No menu available.'];
|
|
2350
|
+
}
|
|
2351
|
+
const lines = [
|
|
2352
|
+
'Use Left/Right or 1-6 to move across the menu bar, Up/Down to choose an item, and Enter to run it.',
|
|
2353
|
+
'',
|
|
2354
|
+
];
|
|
2355
|
+
menu.items.forEach((item, index) => {
|
|
2356
|
+
const prefix = index === this.menuState.selectedIndex ? '>' : ' ';
|
|
2357
|
+
const suffix = item.enabled === false ? ' [disabled]' : item.shortcut ? ` [${item.shortcut}]` : '';
|
|
2358
|
+
lines.push(`${prefix} ${item.label}${suffix}`);
|
|
2359
|
+
lines.push(` ${item.description}`);
|
|
2360
|
+
lines.push('');
|
|
2361
|
+
});
|
|
2362
|
+
return lines;
|
|
2363
|
+
}
|
|
2364
|
+
buildPickerPaneBody(state) {
|
|
2365
|
+
const lines = [
|
|
2366
|
+
`Search: ${this.inputValue || '(type to filter)'}`,
|
|
2367
|
+
`Results: ${state.filteredItems.length}/${state.items.length}`,
|
|
2368
|
+
'',
|
|
2369
|
+
];
|
|
2370
|
+
if (state.filteredItems.length === 0) {
|
|
2371
|
+
lines.push(state.emptyText);
|
|
2372
|
+
return lines;
|
|
2373
|
+
}
|
|
2374
|
+
state.filteredItems.forEach((item, index) => {
|
|
2375
|
+
const prefix = index === state.selectedIndex ? '>' : ' ';
|
|
2376
|
+
lines.push(`${prefix} ${item.label}${item.detail ? ` ${item.detail}` : ''}`);
|
|
2377
|
+
lines.push(` ${item.description}`);
|
|
2378
|
+
lines.push('');
|
|
2379
|
+
});
|
|
2380
|
+
return lines.slice(state.scrollTop);
|
|
2381
|
+
}
|
|
2382
|
+
buildViewerPaneBody(state) {
|
|
2383
|
+
const lines = [
|
|
2384
|
+
state.path ? `Path: ${path.relative(this.workspaceRoot, state.path)}` : 'Read-only view',
|
|
2385
|
+
state.canEdit ? 'Ctrl+E opens the editor for this file.' : 'Read-only content.',
|
|
2386
|
+
'',
|
|
2387
|
+
...state.content.split(/\r?\n/).map((line, index) => `${String(index + 1).padStart(4, ' ')} | ${line}`),
|
|
2388
|
+
];
|
|
2389
|
+
return lines.slice(state.scrollTop);
|
|
2390
|
+
}
|
|
2391
|
+
buildEditorPaneView(width, height, state) {
|
|
2392
|
+
const innerWidth = Math.max(1, width - 2);
|
|
2393
|
+
const viewportLines = Math.max(0, height - 2);
|
|
2394
|
+
const lineNumberWidth = Math.max(2, String(Math.max(1, state.lines.length)).length);
|
|
2395
|
+
const prefixWidth = 2 + lineNumberWidth + 3;
|
|
2396
|
+
const contentWidth = Math.max(1, innerWidth - prefixWidth);
|
|
2397
|
+
const maxStart = Math.max(0, state.lines.length - viewportLines);
|
|
2398
|
+
state.scrollTop = clamp(state.scrollTop, 0, maxStart);
|
|
2399
|
+
const lines = [renderWindowTop(`${state.title}${state.dirty ? ' [modified]' : ''}`, innerWidth, true)];
|
|
2400
|
+
for (let offset = 0; offset < viewportLines; offset += 1) {
|
|
2401
|
+
const lineIndex = state.scrollTop + offset;
|
|
2402
|
+
const lineNumber = String(lineIndex + 1).padStart(lineNumberWidth, ' ');
|
|
2403
|
+
const content = state.lines[lineIndex] ?? '';
|
|
2404
|
+
const visibleContent = content.slice(state.scrollLeft, state.scrollLeft + contentWidth);
|
|
2405
|
+
const prefix = lineIndex === state.cursorLine ? '> ' : ' ';
|
|
2406
|
+
const rowText = `${prefix}${lineNumber} | ${visibleContent}`;
|
|
2407
|
+
lines.push(renderWindowBody(padRight(rowText, innerWidth), innerWidth, 'workspace'));
|
|
2408
|
+
}
|
|
2409
|
+
if (height >= 2) {
|
|
2410
|
+
lines.push(renderWindowBottom(innerWidth, true));
|
|
2411
|
+
}
|
|
2412
|
+
const cursorRow = Math.min(2 + viewportLines, 3 + Math.max(0, state.cursorLine - state.scrollTop));
|
|
2413
|
+
const cursorColumn = Math.min(width, 2 + prefixWidth + Math.max(0, state.cursorColumn - state.scrollLeft));
|
|
2414
|
+
return {
|
|
2415
|
+
lines: lines.slice(0, height),
|
|
2416
|
+
cursor: { row: cursorRow, column: cursorColumn },
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
buildHotkeys() {
|
|
2420
|
+
if (this.uiMode === 'menu') {
|
|
2421
|
+
return [
|
|
2422
|
+
['1-6', 'Menu'],
|
|
2423
|
+
['<- ->', 'Switch'],
|
|
2424
|
+
['Up/Down', 'Item'],
|
|
2425
|
+
['Enter', 'Run'],
|
|
2426
|
+
['Esc', 'Close'],
|
|
2427
|
+
];
|
|
2428
|
+
}
|
|
2429
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
2430
|
+
return [
|
|
2431
|
+
['type', 'Filter'],
|
|
2432
|
+
['Up/Down', 'Pick'],
|
|
2433
|
+
['Enter', 'Open'],
|
|
2434
|
+
['Ctrl+E', 'Edit'],
|
|
2435
|
+
['Esc', 'Close'],
|
|
2436
|
+
];
|
|
2437
|
+
}
|
|
2438
|
+
if (this.uiMode === 'viewer') {
|
|
2439
|
+
return [
|
|
2440
|
+
['PgUp/PgDn', 'Scroll'],
|
|
2441
|
+
['Ctrl+E', 'Edit'],
|
|
2442
|
+
['Ctrl+R', 'Run'],
|
|
2443
|
+
['Esc', 'Close'],
|
|
2444
|
+
];
|
|
2445
|
+
}
|
|
2446
|
+
if (this.uiMode === 'editor') {
|
|
2447
|
+
return [
|
|
2448
|
+
['Arrows', 'Move'],
|
|
2449
|
+
['Enter', 'Split'],
|
|
2450
|
+
['Ctrl+S', 'Save'],
|
|
2451
|
+
['Esc', 'Close'],
|
|
2452
|
+
];
|
|
2453
|
+
}
|
|
2454
|
+
return [
|
|
2455
|
+
['F10', 'Menu'],
|
|
2456
|
+
['Ctrl+P', 'Palette'],
|
|
2457
|
+
['<- ->', 'Pane'],
|
|
2458
|
+
[this.focusedPane === 'sessions' ? 'Up/Down' : 'PgUp/PgDn', this.focusedPane === 'sessions' ? 'Select' : 'Scroll'],
|
|
2459
|
+
['End', this.focusedPane === 'sessions' ? 'Bottom' : 'Follow'],
|
|
2460
|
+
['/cancel', 'Stop'],
|
|
2461
|
+
];
|
|
2462
|
+
}
|
|
2463
|
+
buildPaneTitle(title, kind, metrics) {
|
|
2464
|
+
if (kind === 'sessions') {
|
|
2465
|
+
return this.focusedPane === kind ? `${title} [focus]` : title;
|
|
2466
|
+
}
|
|
2467
|
+
if (metrics.maxStart === 0) {
|
|
2468
|
+
return this.focusedPane === kind ? `${title} [focus]` : title;
|
|
2469
|
+
}
|
|
2470
|
+
return this.paneScroll[kind] === 0
|
|
2471
|
+
? `${title}${this.focusedPane === kind ? ' [focus]' : ''} [follow]`
|
|
2472
|
+
: `${title}${this.focusedPane === kind ? ' [focus]' : ''} [-${this.paneScroll[kind]}]`;
|
|
2473
|
+
}
|
|
2474
|
+
getPaneMetrics(kind, width = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).width, height = paneDimensionsForKind(kind, output.columns ?? 120, output.rows ?? 40).height) {
|
|
2475
|
+
const innerWidth = Math.max(1, width - 2);
|
|
2476
|
+
const rawBody = this.getPaneBody(kind);
|
|
2477
|
+
const lines = wrapBodyLines(rawBody, innerWidth);
|
|
2478
|
+
const viewportLines = Math.max(0, height - 2);
|
|
2479
|
+
const maxStart = Math.max(0, lines.length - viewportLines);
|
|
2480
|
+
if (kind === 'sessions') {
|
|
2481
|
+
this.paneScroll.sessions = clamp(this.paneScroll.sessions, 0, maxStart);
|
|
2482
|
+
return {
|
|
2483
|
+
lines,
|
|
2484
|
+
start: this.paneScroll.sessions,
|
|
2485
|
+
maxStart,
|
|
2486
|
+
viewportLines,
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
this.paneScroll[kind] = clamp(this.paneScroll[kind], 0, maxStart);
|
|
2490
|
+
return {
|
|
2491
|
+
lines,
|
|
2492
|
+
start: Math.max(0, maxStart - this.paneScroll[kind]),
|
|
2493
|
+
maxStart,
|
|
2494
|
+
viewportLines,
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
getPaneBody(kind) {
|
|
2498
|
+
switch (kind) {
|
|
2499
|
+
case 'sessions':
|
|
2500
|
+
return this.buildSessionPaneBody();
|
|
2501
|
+
case 'messages':
|
|
2502
|
+
return this.buildMessagesPaneBody();
|
|
2503
|
+
case 'process':
|
|
2504
|
+
return this.buildProcessPaneBody();
|
|
2505
|
+
case 'context':
|
|
2506
|
+
return this.buildContextPaneBody();
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
ensureSessionVisible() {
|
|
2510
|
+
const selectedIndex = Math.max(0, this.sessions.findIndex((session) => session.id === this.selectedSessionId));
|
|
2511
|
+
const contentRows = Math.max(1, (output.rows ?? 40) - 5);
|
|
2512
|
+
const selectedLine = selectedIndex * 2;
|
|
2513
|
+
if (selectedLine < this.paneScroll.sessions) {
|
|
2514
|
+
this.paneScroll.sessions = selectedLine;
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
if (selectedLine >= this.paneScroll.sessions + contentRows) {
|
|
2518
|
+
this.paneScroll.sessions = Math.max(0, selectedLine - contentRows + 1);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
formatMessageHeader(entry) {
|
|
2522
|
+
const spinner = entry.pending ? ` ${currentSpinnerFrame()}` : '';
|
|
2523
|
+
return formatDisplayMessageHeader(entry, spinner);
|
|
2524
|
+
}
|
|
2525
|
+
close() {
|
|
2526
|
+
if (!this.closeResolver) {
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
const resolve = this.closeResolver;
|
|
2530
|
+
this.closeResolver = null;
|
|
2531
|
+
resolve();
|
|
2532
|
+
}
|
|
2533
|
+
isEditingInput() {
|
|
2534
|
+
return this.uiMode !== 'editor' && this.inputValue.length > 0;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
export async function startTui(workspaceRoot = process.cwd(), options = {}) {
|
|
2538
|
+
if (input.isTTY && output.isTTY) {
|
|
2539
|
+
const app = new FullscreenTui(workspaceRoot, options);
|
|
2540
|
+
await app.start();
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
await startLineTui(workspaceRoot, options);
|
|
2544
|
+
}
|
|
2545
|
+
async function startLineTui(workspaceRoot = process.cwd(), options = {}) {
|
|
2546
|
+
const rl = createInterface({ input, output });
|
|
2547
|
+
try {
|
|
2548
|
+
let session = await chooseSession(rl, workspaceRoot);
|
|
2549
|
+
renderHeader(session);
|
|
2550
|
+
while (true) {
|
|
2551
|
+
const prompt = `${paint(session.title || session.id.slice(0, 8), ANSI.bold)} ${paint('>', ANSI.dim)} `;
|
|
2552
|
+
const line = (await rl.question(prompt)).trim();
|
|
2553
|
+
if (!line) {
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
const parsed = parseSlashInput(line);
|
|
2557
|
+
if (parsed.kind === 'builtin') {
|
|
2558
|
+
switch (parsed.name) {
|
|
2559
|
+
case 'cancel':
|
|
2560
|
+
console.log('No execution is currently running in line mode.');
|
|
2561
|
+
continue;
|
|
2562
|
+
case 'exit':
|
|
2563
|
+
case 'quit':
|
|
2564
|
+
return;
|
|
2565
|
+
case 'new':
|
|
2566
|
+
session = parsed.rawArgs
|
|
2567
|
+
? await createConductorSession(workspaceRoot, parsed.rawArgs)
|
|
2568
|
+
: await createSessionInteractive(rl, workspaceRoot);
|
|
2569
|
+
console.log(`${paint('switched', ANSI.green)} ${session.title}`);
|
|
2570
|
+
renderHeader(session);
|
|
2571
|
+
continue;
|
|
2572
|
+
case 'sessions':
|
|
2573
|
+
session = await chooseSession(rl, workspaceRoot);
|
|
2574
|
+
renderHeader(session);
|
|
2575
|
+
continue;
|
|
2576
|
+
case 'help':
|
|
2577
|
+
for (const helpLine of buildHelpLines()) {
|
|
2578
|
+
console.log(helpLine);
|
|
2579
|
+
}
|
|
2580
|
+
continue;
|
|
2581
|
+
case 'compile':
|
|
2582
|
+
case 'skill-creator':
|
|
2583
|
+
if (!parsed.rawArgs.trim()) {
|
|
2584
|
+
console.log(`Provide a skill specification after /${parsed.name}.`);
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
console.log(divider('-'));
|
|
2588
|
+
console.log(`${paint('skill', ANSI.dim)} /${parsed.name} ${parsed.rawArgs}`);
|
|
2589
|
+
console.log(divider('-'));
|
|
2590
|
+
{
|
|
2591
|
+
const printer = new LiveExecutionPrinter();
|
|
2592
|
+
try {
|
|
2593
|
+
const result = await runSkillCreatorCommand(workspaceRoot, session.id, parsed.rawArgs, {
|
|
2594
|
+
sandbox: options.sandbox,
|
|
2595
|
+
onUserInput: async (question) => {
|
|
2596
|
+
printer.finish();
|
|
2597
|
+
console.log(`${paint('clarify', ANSI.yellow)} ${question}`);
|
|
2598
|
+
return (await rl.question('> ')).trim();
|
|
2599
|
+
},
|
|
2600
|
+
onEvent: (event) => printer.handle(event),
|
|
2601
|
+
});
|
|
2602
|
+
printer.finish();
|
|
2603
|
+
console.log(formatSkillCreatorSummary(workspaceRoot, result));
|
|
2604
|
+
}
|
|
2605
|
+
catch (error) {
|
|
2606
|
+
printer.finish();
|
|
2607
|
+
console.log(`${paint('error', ANSI.red)} ${error.message}`);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
console.log('');
|
|
2611
|
+
console.log(divider('-'));
|
|
2612
|
+
console.log('');
|
|
2613
|
+
continue;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (parsed.kind === 'skill') {
|
|
2617
|
+
console.log(divider('-'));
|
|
2618
|
+
console.log(`${paint('skill', ANSI.dim)} /${parsed.name}${parsed.args.length > 0 ? ` ${parsed.args.join(' ')}` : ''}`);
|
|
2619
|
+
console.log(divider('-'));
|
|
2620
|
+
const printer = new LiveExecutionPrinter();
|
|
2621
|
+
try {
|
|
2622
|
+
await runSkillCommand(workspaceRoot, parsed.name, parsed.args, {
|
|
2623
|
+
sandbox: options.sandbox,
|
|
2624
|
+
onUserInput: async (question) => {
|
|
2625
|
+
printer.finish();
|
|
2626
|
+
console.log(`${paint('clarify', ANSI.yellow)} ${question}`);
|
|
2627
|
+
return (await rl.question('> ')).trim();
|
|
2628
|
+
},
|
|
2629
|
+
onEvent: (event) => printer.handle(event),
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
catch (error) {
|
|
2633
|
+
printer.finish();
|
|
2634
|
+
console.log(`${paint('error', ANSI.red)} ${error.message}`);
|
|
2635
|
+
}
|
|
2636
|
+
printer.finish();
|
|
2637
|
+
console.log('');
|
|
2638
|
+
console.log(divider('-'));
|
|
2639
|
+
console.log('');
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
console.log(divider('-'));
|
|
2643
|
+
console.log(`${paint('task', ANSI.dim)} ${parsed.prompt}`);
|
|
2644
|
+
console.log(divider('-'));
|
|
2645
|
+
const printer = new LiveExecutionPrinter();
|
|
2646
|
+
const result = await runConductorTurn(parsed.prompt, {
|
|
2647
|
+
workspaceRoot,
|
|
2648
|
+
sessionId: session.id,
|
|
2649
|
+
stream: true,
|
|
2650
|
+
headless: true,
|
|
2651
|
+
sandbox: options.sandbox,
|
|
2652
|
+
onUserInput: async (question) => {
|
|
2653
|
+
printer.finish();
|
|
2654
|
+
console.log(`${paint('clarify', ANSI.yellow)} ${question}`);
|
|
2655
|
+
return (await rl.question('> ')).trim();
|
|
2656
|
+
},
|
|
2657
|
+
onEvent: (event) => printer.handle(event),
|
|
2658
|
+
});
|
|
2659
|
+
printer.finish();
|
|
2660
|
+
console.log('');
|
|
2661
|
+
console.log(divider('-'));
|
|
2662
|
+
console.log('');
|
|
2663
|
+
const sessions = await listConductorSessions(workspaceRoot);
|
|
2664
|
+
session = sessions.find((entry) => entry.id === result.sessionId) ?? session;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
finally {
|
|
2668
|
+
rl.close();
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
export async function runPromptHeadless(prompt, workspaceRoot = process.cwd(), options = {}) {
|
|
2672
|
+
const session = await createConductorSession(workspaceRoot);
|
|
2673
|
+
console.log(`Session: ${session.id}`);
|
|
2674
|
+
const result = await runConductorTurn(prompt, {
|
|
2675
|
+
workspaceRoot,
|
|
2676
|
+
sessionId: session.id,
|
|
2677
|
+
stream: false,
|
|
2678
|
+
headless: true,
|
|
2679
|
+
sandbox: options.sandbox,
|
|
2680
|
+
});
|
|
2681
|
+
if (result.output.length > 0 || result.answer) {
|
|
2682
|
+
console.log('');
|
|
2683
|
+
}
|
|
2684
|
+
for (const line of result.output) {
|
|
2685
|
+
console.log(line);
|
|
2686
|
+
}
|
|
2687
|
+
if (result.answer) {
|
|
2688
|
+
if (result.output.length > 0) {
|
|
2689
|
+
console.log('');
|
|
2690
|
+
}
|
|
2691
|
+
console.log(result.answer);
|
|
2692
|
+
}
|
|
2693
|
+
if (result.error) {
|
|
2694
|
+
throw new Error(result.error);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
async function runSkillCommand(workspaceRoot, skillName, args, options = {}) {
|
|
2698
|
+
const commands = await listCommands(workspaceRoot);
|
|
2699
|
+
const command = commands.find((entry) => entry.name === skillName);
|
|
2700
|
+
if (!command) {
|
|
2701
|
+
throw new Error(`Unknown skill: ${skillName}`);
|
|
2702
|
+
}
|
|
2703
|
+
const result = await run(command.path, args, {
|
|
2704
|
+
configRoot: workspaceRoot,
|
|
2705
|
+
headless: true,
|
|
2706
|
+
stream: true,
|
|
2707
|
+
sandbox: options.sandbox,
|
|
2708
|
+
signal: options.signal,
|
|
2709
|
+
onUserInput: options.onUserInput,
|
|
2710
|
+
onEvent: (event) => options.onEvent?.({ scope: 'child', childSlug: skillName, event }),
|
|
2711
|
+
onChildEvent: (childSlug, event) => options.onEvent?.({ scope: 'child', childSlug, event }),
|
|
2712
|
+
});
|
|
2713
|
+
return result;
|
|
2714
|
+
}
|
|
2715
|
+
async function runSkillCreatorCommand(workspaceRoot, sessionId, spec, options = {}) {
|
|
2716
|
+
const trimmedSpec = spec.trim();
|
|
2717
|
+
if (!trimmedSpec) {
|
|
2718
|
+
throw new Error('spec is required');
|
|
2719
|
+
}
|
|
2720
|
+
const config = await loadConfig(workspaceRoot);
|
|
2721
|
+
const compileSelection = resolveModelSlot(config, 'compile');
|
|
2722
|
+
const runSelection = resolveModelSlot(config, 'run');
|
|
2723
|
+
return createLocalSkill({
|
|
2724
|
+
spec: trimmedSpec,
|
|
2725
|
+
workspaceRoot,
|
|
2726
|
+
workspacePath: path.resolve(workspaceRoot, config.workspace || './workspace'),
|
|
2727
|
+
config,
|
|
2728
|
+
compileSelection,
|
|
2729
|
+
runSelection,
|
|
2730
|
+
sessionId,
|
|
2731
|
+
sandbox: options.sandbox ?? false,
|
|
2732
|
+
signal: options.signal,
|
|
2733
|
+
onUserInput: options.onUserInput ?? (async () => ''),
|
|
2734
|
+
onEvent: options.onEvent,
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
function formatSkillCreatorSummary(workspaceRoot, result) {
|
|
2738
|
+
const slug = typeof result.slug === 'string' ? result.slug : 'unknown-skill';
|
|
2739
|
+
const outputPath = typeof result.output_path === 'string' ? result.output_path : '';
|
|
2740
|
+
if (!outputPath) {
|
|
2741
|
+
return `Published /${slug}.`;
|
|
2742
|
+
}
|
|
2743
|
+
const relativeOutputPath = path.relative(workspaceRoot, outputPath);
|
|
2744
|
+
return `Published /${slug} to ${relativeOutputPath || outputPath}.`;
|
|
2745
|
+
}
|
|
2746
|
+
export function previewMessageFromEvent(logEvent) {
|
|
2747
|
+
switch (logEvent.event.type) {
|
|
2748
|
+
case 'output':
|
|
2749
|
+
if (!logEvent.event.content) {
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
role: 'system',
|
|
2754
|
+
kind: 'output',
|
|
2755
|
+
tag: logEvent.scope === 'child' ? logEvent.childSlug : undefined,
|
|
2756
|
+
content: logEvent.event.content,
|
|
2757
|
+
};
|
|
2758
|
+
case 'input_required':
|
|
2759
|
+
if (!logEvent.event.prompt) {
|
|
2760
|
+
return null;
|
|
2761
|
+
}
|
|
2762
|
+
return previewQuestionMessage(logEvent.event.prompt, logEvent.scope === 'child' ? logEvent.childSlug : undefined);
|
|
2763
|
+
default:
|
|
2764
|
+
return null;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
export function previewChildSkillActivityMessage(childSlug) {
|
|
2768
|
+
return {
|
|
2769
|
+
role: 'system',
|
|
2770
|
+
kind: 'output',
|
|
2771
|
+
tag: childSlug,
|
|
2772
|
+
content: 'Running child skill...',
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
export function previewQuestionMessage(promptText, explicitTag) {
|
|
2776
|
+
const parsed = explicitTag ? { tag: explicitTag, content: promptText } : parseTaggedPrompt(promptText);
|
|
2777
|
+
return {
|
|
2778
|
+
role: 'system',
|
|
2779
|
+
kind: 'question',
|
|
2780
|
+
tag: parsed.tag,
|
|
2781
|
+
content: parsed.content,
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
export function parseCommandArgs(rawArgs) {
|
|
2785
|
+
const args = [];
|
|
2786
|
+
let current = '';
|
|
2787
|
+
let quote = null;
|
|
2788
|
+
let escaping = false;
|
|
2789
|
+
for (const char of rawArgs.trim()) {
|
|
2790
|
+
if (escaping) {
|
|
2791
|
+
current += char;
|
|
2792
|
+
escaping = false;
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
if (char === '\\') {
|
|
2796
|
+
escaping = true;
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
if (quote) {
|
|
2800
|
+
if (char === quote) {
|
|
2801
|
+
quote = null;
|
|
2802
|
+
}
|
|
2803
|
+
else {
|
|
2804
|
+
current += char;
|
|
2805
|
+
}
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
if (char === '"' || char === '\'') {
|
|
2809
|
+
quote = char;
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
if (/\s/.test(char)) {
|
|
2813
|
+
if (current) {
|
|
2814
|
+
args.push(current);
|
|
2815
|
+
current = '';
|
|
2816
|
+
}
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
current += char;
|
|
2820
|
+
}
|
|
2821
|
+
if (escaping) {
|
|
2822
|
+
current += '\\';
|
|
2823
|
+
}
|
|
2824
|
+
if (current) {
|
|
2825
|
+
args.push(current);
|
|
2826
|
+
}
|
|
2827
|
+
return args;
|
|
2828
|
+
}
|
|
2829
|
+
export function parseSlashInput(line) {
|
|
2830
|
+
const trimmed = line.trim();
|
|
2831
|
+
if (!trimmed.startsWith('/')) {
|
|
2832
|
+
return { kind: 'text', prompt: trimmed };
|
|
2833
|
+
}
|
|
2834
|
+
if (trimmed === '/') {
|
|
2835
|
+
return { kind: 'builtin', name: 'help', rawArgs: '', args: [] };
|
|
2836
|
+
}
|
|
2837
|
+
const match = trimmed.slice(1).match(/^(\S+)(?:\s+(.*))?$/);
|
|
2838
|
+
if (!match) {
|
|
2839
|
+
return { kind: 'builtin', name: 'help', rawArgs: '', args: [] };
|
|
2840
|
+
}
|
|
2841
|
+
const name = match[1];
|
|
2842
|
+
const rawArgs = (match[2] ?? '').trim();
|
|
2843
|
+
if (BUILTIN_SLASH_COMMANDS.includes(name)) {
|
|
2844
|
+
const passthroughRawArgBuiltins = new Set(['new', 'compile', 'skill-creator']);
|
|
2845
|
+
return {
|
|
2846
|
+
kind: 'builtin',
|
|
2847
|
+
name: name,
|
|
2848
|
+
rawArgs,
|
|
2849
|
+
args: passthroughRawArgBuiltins.has(name) && rawArgs
|
|
2850
|
+
? [rawArgs]
|
|
2851
|
+
: parseCommandArgs(rawArgs),
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
return {
|
|
2855
|
+
kind: 'skill',
|
|
2856
|
+
name,
|
|
2857
|
+
rawArgs,
|
|
2858
|
+
args: parseCommandArgs(rawArgs),
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
export function canSubmitParsedInputWhileBusy(parsed) {
|
|
2862
|
+
return parsed.kind === 'builtin' && (parsed.name === 'cancel' || parsed.name === 'exit' || parsed.name === 'quit');
|
|
2863
|
+
}
|
|
2864
|
+
export function computeFramePatch(previousLines, nextLines, previousSize, nextSize) {
|
|
2865
|
+
if (!previousLines
|
|
2866
|
+
|| !previousSize
|
|
2867
|
+
|| previousSize.columns !== nextSize.columns
|
|
2868
|
+
|| previousSize.rows !== nextSize.rows
|
|
2869
|
+
|| previousLines.length !== nextLines.length) {
|
|
2870
|
+
return { fullRender: true, changedRows: [] };
|
|
2871
|
+
}
|
|
2872
|
+
const changedRows = [];
|
|
2873
|
+
for (let index = 0; index < nextLines.length; index += 1) {
|
|
2874
|
+
if (previousLines[index] !== nextLines[index]) {
|
|
2875
|
+
changedRows.push({ row: index + 1, line: nextLines[index] });
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
return { fullRender: false, changedRows };
|
|
2879
|
+
}
|
|
2880
|
+
export function completeSlashCommand(inputValue, candidates) {
|
|
2881
|
+
if (!inputValue.startsWith('/')) {
|
|
2882
|
+
return { value: inputValue, matches: [], applied: false };
|
|
2883
|
+
}
|
|
2884
|
+
const match = inputValue.match(/^\/([^\s]*)(.*)$/s);
|
|
2885
|
+
if (!match) {
|
|
2886
|
+
return { value: inputValue, matches: [], applied: false };
|
|
2887
|
+
}
|
|
2888
|
+
const prefix = match[1];
|
|
2889
|
+
const suffix = match[2] ?? '';
|
|
2890
|
+
const uniqueCandidates = Array.from(new Set(candidates)).sort();
|
|
2891
|
+
const matches = uniqueCandidates.filter((candidate) => candidate.startsWith(prefix));
|
|
2892
|
+
if (matches.length === 0) {
|
|
2893
|
+
return { value: inputValue, matches: [], applied: false };
|
|
2894
|
+
}
|
|
2895
|
+
if (matches.length === 1) {
|
|
2896
|
+
return {
|
|
2897
|
+
value: `/${matches[0]}${suffix.length > 0 ? suffix : ' '}`,
|
|
2898
|
+
matches,
|
|
2899
|
+
applied: true,
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
const sharedPrefix = longestCommonPrefix(matches);
|
|
2903
|
+
if (sharedPrefix.length > prefix.length) {
|
|
2904
|
+
return {
|
|
2905
|
+
value: `/${sharedPrefix}${suffix}`,
|
|
2906
|
+
matches,
|
|
2907
|
+
applied: true,
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
return { value: inputValue, matches, applied: false };
|
|
2911
|
+
}
|
|
2912
|
+
async function chooseSession(rl, workspaceRoot) {
|
|
2913
|
+
const sessions = await listConductorSessions(workspaceRoot);
|
|
2914
|
+
if (sessions.length === 0) {
|
|
2915
|
+
return createSessionInteractive(rl, workspaceRoot);
|
|
2916
|
+
}
|
|
2917
|
+
console.log(divider('='));
|
|
2918
|
+
console.log(paint('Sessions', ANSI.bold));
|
|
2919
|
+
sessions.slice(0, 9).forEach((session, index) => {
|
|
2920
|
+
console.log(`${paint(String(index + 1).padStart(2, ' '), ANSI.cyan)} ${session.title} ${paint(session.updatedAt.replace('T', ' ').slice(0, 16), ANSI.dim)}`);
|
|
2921
|
+
});
|
|
2922
|
+
console.log(`${paint(' 0', ANSI.cyan)} Start a new session`);
|
|
2923
|
+
console.log(divider('='));
|
|
2924
|
+
while (true) {
|
|
2925
|
+
const answer = (await rl.question(paint('Select a session [0]: ', ANSI.dim))).trim();
|
|
2926
|
+
if (!answer || answer === '0') {
|
|
2927
|
+
return createSessionInteractive(rl, workspaceRoot);
|
|
2928
|
+
}
|
|
2929
|
+
const numeric = Number.parseInt(answer, 10);
|
|
2930
|
+
if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= Math.min(sessions.length, 9)) {
|
|
2931
|
+
return sessions[numeric - 1];
|
|
2932
|
+
}
|
|
2933
|
+
console.log('Enter a listed session number or 0 for a new session.');
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
async function createSessionInteractive(rl, workspaceRoot) {
|
|
2937
|
+
const title = (await rl.question(paint('New session title (optional): ', ANSI.dim))).trim();
|
|
2938
|
+
return createConductorSession(workspaceRoot, title || undefined);
|
|
2939
|
+
}
|
|
2940
|
+
function renderHeader(session) {
|
|
2941
|
+
console.log(divider('='));
|
|
2942
|
+
console.log(`${paint('DeepClause', ANSI.bold, ANSI.cyan)} ${paint('interactive conductor', ANSI.dim)}`);
|
|
2943
|
+
console.log(`${paint('commands', ANSI.dim)} /compile <spec> /new /sessions /help /exit /skill args`);
|
|
2944
|
+
console.log(`${paint('session', ANSI.dim)} ${paint(session.id, ANSI.cyan)}`);
|
|
2945
|
+
console.log(divider('='));
|
|
2946
|
+
console.log('');
|
|
2947
|
+
}
|
|
2948
|
+
function streamLabel(logEvent) {
|
|
2949
|
+
return logEvent.scope === 'child'
|
|
2950
|
+
? `${CHILD_EVENT_INDENT}[${logEvent.childSlug ?? '?'}] llm`
|
|
2951
|
+
: 'llm';
|
|
2952
|
+
}
|
|
2953
|
+
function formatEventPrefix(logEvent) {
|
|
2954
|
+
return logEvent.scope === 'child'
|
|
2955
|
+
? `${CHILD_EVENT_INDENT}[${logEvent.childSlug ?? '?'}] `
|
|
2956
|
+
: '';
|
|
2957
|
+
}
|
|
2958
|
+
function parseTaggedPrompt(promptText) {
|
|
2959
|
+
const match = promptText.match(/^\[([^\]]+)\]\s*(.*)$/);
|
|
2960
|
+
if (!match) {
|
|
2961
|
+
return { content: promptText };
|
|
2962
|
+
}
|
|
2963
|
+
return {
|
|
2964
|
+
tag: match[1],
|
|
2965
|
+
content: match[2] || promptText,
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function sameDisplayMessage(left, right) {
|
|
2969
|
+
return !!left
|
|
2970
|
+
&& left.role === right.role
|
|
2971
|
+
&& left.kind === right.kind
|
|
2972
|
+
&& left.tag === right.tag
|
|
2973
|
+
&& left.content === right.content;
|
|
2974
|
+
}
|
|
2975
|
+
function formatSystemMessageHeader(entry, spinner) {
|
|
2976
|
+
const base = entry.kind === 'output'
|
|
2977
|
+
? 'Output'
|
|
2978
|
+
: entry.kind === 'question'
|
|
2979
|
+
? 'Question'
|
|
2980
|
+
: 'System';
|
|
2981
|
+
return entry.tag
|
|
2982
|
+
? `[${base}: ${entry.tag}${spinner}]`
|
|
2983
|
+
: `[${base}${spinner}]`;
|
|
2984
|
+
}
|
|
2985
|
+
export function formatDisplayMessageHeader(entry, spinner = '') {
|
|
2986
|
+
switch (entry.role) {
|
|
2987
|
+
case 'user':
|
|
2988
|
+
return '[You]';
|
|
2989
|
+
case 'assistant': {
|
|
2990
|
+
const base = entry.error ? 'Assistant Error' : 'Assistant';
|
|
2991
|
+
return entry.tag
|
|
2992
|
+
? `[${base}: ${entry.tag}${spinner}]`
|
|
2993
|
+
: `[${base}${spinner}]`;
|
|
2994
|
+
}
|
|
2995
|
+
case 'system':
|
|
2996
|
+
return formatSystemMessageHeader(entry, spinner);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
function formatToolArgs(args) {
|
|
3000
|
+
if (!args) {
|
|
3001
|
+
return '';
|
|
3002
|
+
}
|
|
3003
|
+
const parts = [];
|
|
3004
|
+
for (const [key, value] of Object.entries(args)) {
|
|
3005
|
+
let rendered = typeof value === 'string' ? value : JSON.stringify(value);
|
|
3006
|
+
if (rendered.length > 80) {
|
|
3007
|
+
rendered = rendered.slice(0, 77) + '...';
|
|
3008
|
+
}
|
|
3009
|
+
parts.push(`${key}=${rendered}`);
|
|
3010
|
+
}
|
|
3011
|
+
return parts.join(', ');
|
|
3012
|
+
}
|
|
3013
|
+
function buildHelpLines() {
|
|
3014
|
+
return [
|
|
3015
|
+
'commands',
|
|
3016
|
+
'/new [title] create a new conductor session',
|
|
3017
|
+
'/sessions refresh or choose sessions',
|
|
3018
|
+
'/compile <spec> compile and publish a new local skill',
|
|
3019
|
+
'/skill-creator <spec> alias for /compile',
|
|
3020
|
+
'/cancel abort the current execution',
|
|
3021
|
+
'/help show this help',
|
|
3022
|
+
'/quit exit the TUI',
|
|
3023
|
+
'/<skill> [args] run a compiled skill directly',
|
|
3024
|
+
'keys',
|
|
3025
|
+
'F10 / Ctrl+G open the menu bar',
|
|
3026
|
+
'Ctrl+P open the action palette',
|
|
3027
|
+
'Left/Right change focused pane',
|
|
3028
|
+
'Up/Down select session or scroll focused pane',
|
|
3029
|
+
'PgUp/PgDn page-scroll the focused pane',
|
|
3030
|
+
'End jump to bottom or re-enable follow mode',
|
|
3031
|
+
'Tab autocomplete /commands and /skills',
|
|
3032
|
+
];
|
|
3033
|
+
}
|
|
3034
|
+
function getSlashHelpLines() {
|
|
3035
|
+
const lines = buildHelpLines();
|
|
3036
|
+
const keysIndex = lines.indexOf('keys');
|
|
3037
|
+
return keysIndex === -1 ? lines : lines.slice(0, keysIndex);
|
|
3038
|
+
}
|
|
3039
|
+
function getShortcutHelpLines() {
|
|
3040
|
+
const lines = buildHelpLines();
|
|
3041
|
+
const keysIndex = lines.indexOf('keys');
|
|
3042
|
+
return keysIndex === -1 ? [] : lines.slice(keysIndex);
|
|
3043
|
+
}
|
|
3044
|
+
function computePaneWidths(columns) {
|
|
3045
|
+
const available = Math.max(36, columns - 2);
|
|
3046
|
+
let left = Math.max(18, Math.floor(available * 0.2));
|
|
3047
|
+
let right = Math.max(28, Math.floor(available * 0.3));
|
|
3048
|
+
let center = available - left - right;
|
|
3049
|
+
if (center < 30) {
|
|
3050
|
+
const needed = 30 - center;
|
|
3051
|
+
const reduceRight = Math.min(needed, Math.max(0, right - 18));
|
|
3052
|
+
right -= reduceRight;
|
|
3053
|
+
center += reduceRight;
|
|
3054
|
+
if (center < 30) {
|
|
3055
|
+
const reduceLeft = Math.min(30 - center, Math.max(0, left - 14));
|
|
3056
|
+
left -= reduceLeft;
|
|
3057
|
+
center += reduceLeft;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
center += available - left - center - right;
|
|
3061
|
+
return { left, center, right };
|
|
3062
|
+
}
|
|
3063
|
+
function computeRightPaneHeights(totalHeight) {
|
|
3064
|
+
if (totalHeight <= 8) {
|
|
3065
|
+
return { process: Math.max(1, totalHeight - 1), context: 1 };
|
|
3066
|
+
}
|
|
3067
|
+
let process = Math.max(8, Math.floor(totalHeight * 0.58));
|
|
3068
|
+
let context = totalHeight - process;
|
|
3069
|
+
if (context < 6) {
|
|
3070
|
+
const deficit = 6 - context;
|
|
3071
|
+
process = Math.max(6, process - deficit);
|
|
3072
|
+
context = totalHeight - process;
|
|
3073
|
+
}
|
|
3074
|
+
return { process, context };
|
|
3075
|
+
}
|
|
3076
|
+
function buildPane(title, body, width, height, kind, active = false) {
|
|
3077
|
+
if (height <= 0) {
|
|
3078
|
+
return [];
|
|
3079
|
+
}
|
|
3080
|
+
const innerWidth = Math.max(1, width - 2);
|
|
3081
|
+
const lines = [renderWindowTop(title, innerWidth, active)];
|
|
3082
|
+
bodyLoop: for (const line of body) {
|
|
3083
|
+
for (const wrapped of wrapPlainText(line, innerWidth)) {
|
|
3084
|
+
if (lines.length >= height - 1) {
|
|
3085
|
+
break bodyLoop;
|
|
3086
|
+
}
|
|
3087
|
+
lines.push(renderWindowBody(wrapped, innerWidth, kind));
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
while (lines.length < height - 1) {
|
|
3091
|
+
lines.push(renderWindowBody('', innerWidth, kind));
|
|
3092
|
+
}
|
|
3093
|
+
if (height >= 2) {
|
|
3094
|
+
lines.push(renderWindowBottom(innerWidth, active));
|
|
3095
|
+
}
|
|
3096
|
+
return lines.slice(0, height);
|
|
3097
|
+
}
|
|
3098
|
+
export function wrapPlainText(text, width) {
|
|
3099
|
+
if (width <= 0) {
|
|
3100
|
+
return [''];
|
|
3101
|
+
}
|
|
3102
|
+
const result = [];
|
|
3103
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
3104
|
+
const line = expandTabs(rawLine || '', CHILD_EVENT_TAB_WIDTH);
|
|
3105
|
+
if (!line) {
|
|
3106
|
+
result.push('');
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
let remaining = line;
|
|
3110
|
+
while (measureDisplayWidth(remaining) > width) {
|
|
3111
|
+
const { head: slice, tail } = splitByDisplayWidth(remaining, width, { forceOne: true });
|
|
3112
|
+
const breakIndex = slice.lastIndexOf(' ');
|
|
3113
|
+
const breakSlice = breakIndex === -1 ? '' : slice.slice(0, breakIndex);
|
|
3114
|
+
if (breakIndex > 0 && measureDisplayWidth(breakSlice) > Math.floor(width * 0.4)) {
|
|
3115
|
+
result.push(breakSlice);
|
|
3116
|
+
remaining = remaining.slice(breakIndex + 1);
|
|
3117
|
+
}
|
|
3118
|
+
else {
|
|
3119
|
+
result.push(slice);
|
|
3120
|
+
remaining = tail;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
result.push(remaining);
|
|
3124
|
+
}
|
|
3125
|
+
return result;
|
|
3126
|
+
}
|
|
3127
|
+
function expandTabs(text, tabWidth) {
|
|
3128
|
+
if (!text.includes('\t')) {
|
|
3129
|
+
return text;
|
|
3130
|
+
}
|
|
3131
|
+
return text.replace(/\t/g, ' '.repeat(tabWidth));
|
|
3132
|
+
}
|
|
3133
|
+
export function measureDisplayWidth(text) {
|
|
3134
|
+
return splitGraphemes(text).reduce((total, grapheme) => total + graphemeDisplayWidth(grapheme), 0);
|
|
3135
|
+
}
|
|
3136
|
+
export function padRight(text, width) {
|
|
3137
|
+
const visibleWidth = measureDisplayWidth(text);
|
|
3138
|
+
return visibleWidth >= width ? text : `${text}${' '.repeat(width - visibleWidth)}`;
|
|
3139
|
+
}
|
|
3140
|
+
export function ellipsize(text, width) {
|
|
3141
|
+
if (measureDisplayWidth(text) <= width) {
|
|
3142
|
+
return text;
|
|
3143
|
+
}
|
|
3144
|
+
if (width <= 3) {
|
|
3145
|
+
return splitByDisplayWidth(text, width).head;
|
|
3146
|
+
}
|
|
3147
|
+
return `${splitByDisplayWidth(text, width - 3).head}...`;
|
|
3148
|
+
}
|
|
3149
|
+
function splitGraphemes(text) {
|
|
3150
|
+
if (!text) {
|
|
3151
|
+
return [];
|
|
3152
|
+
}
|
|
3153
|
+
if (!graphemeSegmenter) {
|
|
3154
|
+
return Array.from(text);
|
|
3155
|
+
}
|
|
3156
|
+
return Array.from(graphemeSegmenter.segment(text), (entry) => entry.segment);
|
|
3157
|
+
}
|
|
3158
|
+
function splitByDisplayWidth(text, width, options = {}) {
|
|
3159
|
+
if (width <= 0) {
|
|
3160
|
+
return { head: '', tail: text };
|
|
3161
|
+
}
|
|
3162
|
+
const graphemes = splitGraphemes(text);
|
|
3163
|
+
let consumedWidth = 0;
|
|
3164
|
+
let index = 0;
|
|
3165
|
+
while (index < graphemes.length) {
|
|
3166
|
+
const nextWidth = graphemeDisplayWidth(graphemes[index]);
|
|
3167
|
+
if (consumedWidth + nextWidth > width) {
|
|
3168
|
+
if (index === 0 && options.forceOne) {
|
|
3169
|
+
index = 1;
|
|
3170
|
+
}
|
|
3171
|
+
break;
|
|
3172
|
+
}
|
|
3173
|
+
consumedWidth += nextWidth;
|
|
3174
|
+
index += 1;
|
|
3175
|
+
}
|
|
3176
|
+
return {
|
|
3177
|
+
head: graphemes.slice(0, index).join(''),
|
|
3178
|
+
tail: graphemes.slice(index).join(''),
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
function graphemeDisplayWidth(grapheme) {
|
|
3182
|
+
if (!grapheme) {
|
|
3183
|
+
return 0;
|
|
3184
|
+
}
|
|
3185
|
+
if (isEmojiGrapheme(grapheme)) {
|
|
3186
|
+
return 2;
|
|
3187
|
+
}
|
|
3188
|
+
let width = 0;
|
|
3189
|
+
for (const symbol of Array.from(grapheme)) {
|
|
3190
|
+
const codePoint = symbol.codePointAt(0);
|
|
3191
|
+
if (codePoint === undefined || isZeroWidthCodePoint(codePoint, symbol)) {
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
width = Math.max(width, isFullWidthCodePoint(codePoint) ? 2 : 1);
|
|
3195
|
+
}
|
|
3196
|
+
return width;
|
|
3197
|
+
}
|
|
3198
|
+
function isEmojiGrapheme(grapheme) {
|
|
3199
|
+
if (EXTENDED_PICTOGRAPHIC_RE.test(grapheme) || grapheme.includes('\u200D') || grapheme.includes('\uFE0F') || grapheme.includes('\u20E3')) {
|
|
3200
|
+
return true;
|
|
3201
|
+
}
|
|
3202
|
+
return Array.from(grapheme).some((symbol) => {
|
|
3203
|
+
const codePoint = symbol.codePointAt(0);
|
|
3204
|
+
return codePoint !== undefined && isRegionalIndicatorCodePoint(codePoint);
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
function isZeroWidthCodePoint(codePoint, symbol) {
|
|
3208
|
+
return COMBINING_MARK_RE.test(symbol)
|
|
3209
|
+
|| codePoint === 0x200D
|
|
3210
|
+
|| (codePoint >= 0xFE00 && codePoint <= 0xFE0F)
|
|
3211
|
+
|| (codePoint >= 0xE0100 && codePoint <= 0xE01EF)
|
|
3212
|
+
|| (codePoint >= 0x1F3FB && codePoint <= 0x1F3FF);
|
|
3213
|
+
}
|
|
3214
|
+
function isRegionalIndicatorCodePoint(codePoint) {
|
|
3215
|
+
return codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF;
|
|
3216
|
+
}
|
|
3217
|
+
function isFullWidthCodePoint(codePoint) {
|
|
3218
|
+
if (codePoint < 0x1100) {
|
|
3219
|
+
return false;
|
|
3220
|
+
}
|
|
3221
|
+
return codePoint <= 0x115F
|
|
3222
|
+
|| codePoint === 0x2329
|
|
3223
|
+
|| codePoint === 0x232A
|
|
3224
|
+
|| (codePoint >= 0x2E80 && codePoint <= 0x3247 && codePoint !== 0x303F)
|
|
3225
|
+
|| (codePoint >= 0x3250 && codePoint <= 0x4DBF)
|
|
3226
|
+
|| (codePoint >= 0x4E00 && codePoint <= 0xA4C6)
|
|
3227
|
+
|| (codePoint >= 0xA960 && codePoint <= 0xA97C)
|
|
3228
|
+
|| (codePoint >= 0xAC00 && codePoint <= 0xD7A3)
|
|
3229
|
+
|| (codePoint >= 0xF900 && codePoint <= 0xFAFF)
|
|
3230
|
+
|| (codePoint >= 0xFE10 && codePoint <= 0xFE19)
|
|
3231
|
+
|| (codePoint >= 0xFE30 && codePoint <= 0xFE6B)
|
|
3232
|
+
|| (codePoint >= 0xFF01 && codePoint <= 0xFF60)
|
|
3233
|
+
|| (codePoint >= 0xFFE0 && codePoint <= 0xFFE6)
|
|
3234
|
+
|| (codePoint >= 0x1F200 && codePoint <= 0x1F251)
|
|
3235
|
+
|| (codePoint >= 0x20000 && codePoint <= 0x3FFFD);
|
|
3236
|
+
}
|
|
3237
|
+
function condenseWhitespace(text) {
|
|
3238
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
3239
|
+
}
|
|
3240
|
+
function formatTimestamp(value) {
|
|
3241
|
+
return value.replace('T', ' ').slice(0, 16);
|
|
3242
|
+
}
|
|
3243
|
+
function summarizeContextSize(detail) {
|
|
3244
|
+
const transcript = detail.messages.map((message) => `${message.role}: ${message.content}`).join('\n');
|
|
3245
|
+
const taskMemory = detail.taskMemory ?? '';
|
|
3246
|
+
const assistantMemory = detail.assistantMemory ?? '';
|
|
3247
|
+
const messageBytes = Buffer.byteLength(transcript, 'utf8');
|
|
3248
|
+
const taskMemoryBytes = Buffer.byteLength(taskMemory, 'utf8');
|
|
3249
|
+
const assistantMemoryBytes = Buffer.byteLength(assistantMemory, 'utf8');
|
|
3250
|
+
const messageTokens = estimateTokenCount(transcript);
|
|
3251
|
+
const taskMemoryTokens = estimateTokenCount(taskMemory);
|
|
3252
|
+
const assistantMemoryTokens = estimateTokenCount(assistantMemory);
|
|
3253
|
+
return {
|
|
3254
|
+
messageCount: detail.messages.length,
|
|
3255
|
+
messageBytes,
|
|
3256
|
+
messageTokens,
|
|
3257
|
+
taskMemoryBytes,
|
|
3258
|
+
taskMemoryTokens,
|
|
3259
|
+
assistantMemoryBytes,
|
|
3260
|
+
assistantMemoryTokens,
|
|
3261
|
+
totalBytes: messageBytes + taskMemoryBytes + assistantMemoryBytes,
|
|
3262
|
+
totalTokens: messageTokens + taskMemoryTokens + assistantMemoryTokens,
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
function estimateTokenCount(text) {
|
|
3266
|
+
const normalized = text.trim();
|
|
3267
|
+
if (!normalized) {
|
|
3268
|
+
return 0;
|
|
3269
|
+
}
|
|
3270
|
+
return Math.max(1, Math.ceil(normalized.length / 4));
|
|
3271
|
+
}
|
|
3272
|
+
function formatByteSize(bytes) {
|
|
3273
|
+
if (bytes < 1024) {
|
|
3274
|
+
return `${bytes} B`;
|
|
3275
|
+
}
|
|
3276
|
+
if (bytes < 1024 * 1024) {
|
|
3277
|
+
const kib = bytes / 1024;
|
|
3278
|
+
return `${kib >= 10 ? kib.toFixed(0) : kib.toFixed(1)} KB`;
|
|
3279
|
+
}
|
|
3280
|
+
const mib = bytes / (1024 * 1024);
|
|
3281
|
+
return `${mib.toFixed(1)} MB`;
|
|
3282
|
+
}
|
|
3283
|
+
function formatInteger(value) {
|
|
3284
|
+
return new Intl.NumberFormat('en-US').format(value);
|
|
3285
|
+
}
|
|
3286
|
+
function longestCommonPrefix(values) {
|
|
3287
|
+
if (values.length === 0) {
|
|
3288
|
+
return '';
|
|
3289
|
+
}
|
|
3290
|
+
let prefix = values[0];
|
|
3291
|
+
for (const value of values.slice(1)) {
|
|
3292
|
+
while (!value.startsWith(prefix) && prefix) {
|
|
3293
|
+
prefix = prefix.slice(0, -1);
|
|
3294
|
+
}
|
|
3295
|
+
if (!prefix) {
|
|
3296
|
+
break;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
return prefix;
|
|
3300
|
+
}
|
|
3301
|
+
function renderMenuBar(text, columns, activeMenuIndex) {
|
|
3302
|
+
const labels = ['Session', 'Skills', 'Files', 'Run', 'View', 'Help'];
|
|
3303
|
+
let rendered = paint(' DeepClause ', ANSI.black, ANSI.bgWhite, ANSI.bold);
|
|
3304
|
+
let usedWidth = measureDisplayWidth(' DeepClause ');
|
|
3305
|
+
labels.forEach((label, index) => {
|
|
3306
|
+
const raw = ` [${index + 1}] ${label} `;
|
|
3307
|
+
usedWidth += measureDisplayWidth(raw);
|
|
3308
|
+
rendered += paint(raw, ANSI.black, activeMenuIndex === index ? ANSI.bgCyan : ANSI.bgWhite, ANSI.bold);
|
|
3309
|
+
});
|
|
3310
|
+
const trailingWidth = Math.max(0, columns - usedWidth);
|
|
3311
|
+
rendered += paint(padRight(ellipsize(` ${text}`, trailingWidth), trailingWidth), ANSI.black, ANSI.bgWhite);
|
|
3312
|
+
return rendered;
|
|
3313
|
+
}
|
|
3314
|
+
function renderStatusBar(status, columns, hotkeys) {
|
|
3315
|
+
let hotkeyWidth = 0;
|
|
3316
|
+
const selected = [];
|
|
3317
|
+
for (const hotkey of hotkeys) {
|
|
3318
|
+
const width = measureDisplayWidth(hotkey[0]) + measureDisplayWidth(hotkey[1]) + 4;
|
|
3319
|
+
if (hotkeyWidth + width > Math.max(0, columns - 12)) {
|
|
3320
|
+
break;
|
|
3321
|
+
}
|
|
3322
|
+
hotkeyWidth += width;
|
|
3323
|
+
selected.push(hotkey);
|
|
3324
|
+
}
|
|
3325
|
+
const leftWidth = Math.max(0, columns - hotkeyWidth);
|
|
3326
|
+
const statusPart = paint(padRight(ellipsize(status, leftWidth), leftWidth), ANSI.black, ANSI.bgCyan);
|
|
3327
|
+
const hotkeyPart = selected.map(([key, label]) => (`${paint(` ${key} `, ANSI.black, ANSI.bgWhite, ANSI.bold)}${paint(` ${label} `, ANSI.black, ANSI.bgCyan)}`)).join('');
|
|
3328
|
+
return `${statusPart}${hotkeyPart}`;
|
|
3329
|
+
}
|
|
3330
|
+
function renderCommandBar(prefix, value, columns) {
|
|
3331
|
+
const prefixPart = paint(prefix, ANSI.black, ANSI.bgWhite, ANSI.bold);
|
|
3332
|
+
const remainingWidth = Math.max(0, columns - measureDisplayWidth(prefix));
|
|
3333
|
+
const valuePart = paint(padRight(ellipsize(value, remainingWidth), remainingWidth), ANSI.brightWhite, ANSI.bgBlue);
|
|
3334
|
+
return `${prefixPart}${valuePart}`;
|
|
3335
|
+
}
|
|
3336
|
+
function renderWindowTop(title, innerWidth, active) {
|
|
3337
|
+
const safeLabel = ` ${ellipsize(title, Math.max(1, innerWidth - 2))} `;
|
|
3338
|
+
const fillWidth = Math.max(0, innerWidth - measureDisplayWidth(safeLabel));
|
|
3339
|
+
const leftFill = Math.floor(fillWidth / 2);
|
|
3340
|
+
const rightFill = fillWidth - leftFill;
|
|
3341
|
+
return [
|
|
3342
|
+
paint(`╔${'═'.repeat(leftFill)}`, ANSI.brightCyan, ANSI.bgBlue),
|
|
3343
|
+
paint(safeLabel, ANSI.black, active ? ANSI.bgCyan : ANSI.bgWhite, ANSI.bold),
|
|
3344
|
+
paint(`${'═'.repeat(rightFill)}╗`, ANSI.brightCyan, ANSI.bgBlue),
|
|
3345
|
+
].join('');
|
|
3346
|
+
}
|
|
3347
|
+
function renderWindowBottom(innerWidth, _active) {
|
|
3348
|
+
return paint(`╚${'═'.repeat(innerWidth)}╝`, ANSI.brightCyan, ANSI.bgBlue);
|
|
3349
|
+
}
|
|
3350
|
+
function renderWindowBody(text, innerWidth, kind) {
|
|
3351
|
+
const normalized = text.trim().toLowerCase();
|
|
3352
|
+
const content = padRight(ellipsize(text, innerWidth), innerWidth);
|
|
3353
|
+
const border = paint('║', ANSI.brightCyan, ANSI.bgBlue);
|
|
3354
|
+
if (kind === 'workspace' && text.startsWith('> ')) {
|
|
3355
|
+
return `${border}${paint(content, ANSI.black, ANSI.bgCyan, ANSI.bold)}${border}`;
|
|
3356
|
+
}
|
|
3357
|
+
if (kind === 'sessions' && text.startsWith('> ')) {
|
|
3358
|
+
return `${border}${paint(content, ANSI.black, ANSI.bgCyan, ANSI.bold)}${border}`;
|
|
3359
|
+
}
|
|
3360
|
+
if (kind === 'messages') {
|
|
3361
|
+
if (text.startsWith('[You]')) {
|
|
3362
|
+
return `${border}${paint(content, ANSI.brightCyan, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3363
|
+
}
|
|
3364
|
+
if (text.startsWith('[Assistant Error]')) {
|
|
3365
|
+
return `${border}${paint(content, ANSI.red, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3366
|
+
}
|
|
3367
|
+
if (text.startsWith('[Assistant')) {
|
|
3368
|
+
return `${border}${paint(content, ANSI.brightYellow, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3369
|
+
}
|
|
3370
|
+
if (text.startsWith('[System')) {
|
|
3371
|
+
return `${border}${paint(content, ANSI.cyan, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
if (kind === 'process') {
|
|
3375
|
+
if (normalized.startsWith('error')) {
|
|
3376
|
+
return `${border}${paint(content, ANSI.red, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3377
|
+
}
|
|
3378
|
+
if (normalized.startsWith('answer')) {
|
|
3379
|
+
return `${border}${paint(content, ANSI.brightYellow, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3380
|
+
}
|
|
3381
|
+
if (normalized.startsWith('tool')) {
|
|
3382
|
+
return `${border}${paint(content, ANSI.brightCyan, ANSI.bgBlue)}${border}`;
|
|
3383
|
+
}
|
|
3384
|
+
if (normalized.startsWith('clarify')) {
|
|
3385
|
+
return `${border}${paint(content, ANSI.yellow, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3386
|
+
}
|
|
3387
|
+
if (normalized.startsWith('task') || normalized.startsWith('skill')) {
|
|
3388
|
+
return `${border}${paint(content, ANSI.brightWhite, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
if (normalized === 'task memory' ||
|
|
3392
|
+
normalized === 'assistant memory' ||
|
|
3393
|
+
normalized === 'recent messages' ||
|
|
3394
|
+
normalized === 'commands' ||
|
|
3395
|
+
normalized === 'keys') {
|
|
3396
|
+
return `${border}${paint(content, ANSI.brightYellow, ANSI.bgBlue, ANSI.bold)}${border}`;
|
|
3397
|
+
}
|
|
3398
|
+
if (kind === 'context' && (normalized.startsWith('created ') || normalized.startsWith('updated ') || normalized.startsWith('session '))) {
|
|
3399
|
+
return `${border}${paint(content, ANSI.cyan, ANSI.bgBlue)}${border}`;
|
|
3400
|
+
}
|
|
3401
|
+
if (kind === 'sessions' && text.startsWith(' ')) {
|
|
3402
|
+
return `${border}${paint(content, ANSI.cyan, ANSI.bgBlue)}${border}`;
|
|
3403
|
+
}
|
|
3404
|
+
return `${border}${paint(content, ANSI.white, ANSI.bgBlue)}${border}`;
|
|
3405
|
+
}
|
|
3406
|
+
function divider(character) {
|
|
3407
|
+
const glyph = character === '=' ? '═' : character === '-' ? '─' : character;
|
|
3408
|
+
return paint(glyph.repeat(72), ANSI.brightCyan, ANSI.bgBlue);
|
|
3409
|
+
}
|
|
3410
|
+
function paneDisplayName(kind) {
|
|
3411
|
+
switch (kind) {
|
|
3412
|
+
case 'sessions':
|
|
3413
|
+
return 'Sessions';
|
|
3414
|
+
case 'messages':
|
|
3415
|
+
return 'Messages';
|
|
3416
|
+
case 'process':
|
|
3417
|
+
return 'Execution';
|
|
3418
|
+
case 'context':
|
|
3419
|
+
return 'Context';
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
function paneDimensionsForKind(kind, columns, rows) {
|
|
3423
|
+
const widths = computePaneWidths(columns);
|
|
3424
|
+
const rightHeights = computeRightPaneHeights(Math.max(1, rows - 3));
|
|
3425
|
+
switch (kind) {
|
|
3426
|
+
case 'sessions':
|
|
3427
|
+
return { width: widths.left, height: Math.max(1, rows - 3) };
|
|
3428
|
+
case 'messages':
|
|
3429
|
+
return { width: widths.center, height: Math.max(1, rows - 3) };
|
|
3430
|
+
case 'process':
|
|
3431
|
+
return { width: widths.right, height: rightHeights.process };
|
|
3432
|
+
case 'context':
|
|
3433
|
+
return { width: widths.right, height: rightHeights.context };
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
export function nextWrappedIndex(currentIndex, delta, length) {
|
|
3437
|
+
if (length <= 0) {
|
|
3438
|
+
return 0;
|
|
3439
|
+
}
|
|
3440
|
+
return (currentIndex + delta + length) % length;
|
|
3441
|
+
}
|
|
3442
|
+
export function selectMenuItemByTypeahead(items, query, currentIndex = -1) {
|
|
3443
|
+
const normalized = query.trim().toLowerCase();
|
|
3444
|
+
if (!normalized || items.length === 0) {
|
|
3445
|
+
return Math.max(0, currentIndex);
|
|
3446
|
+
}
|
|
3447
|
+
const matches = items
|
|
3448
|
+
.map((item, index) => ({ label: item.label.toLowerCase(), index }))
|
|
3449
|
+
.filter((item) => item.label.startsWith(normalized))
|
|
3450
|
+
.map((item) => item.index);
|
|
3451
|
+
if (matches.length === 0) {
|
|
3452
|
+
return Math.max(0, currentIndex);
|
|
3453
|
+
}
|
|
3454
|
+
const next = matches.find((index) => index > currentIndex);
|
|
3455
|
+
return next ?? matches[0];
|
|
3456
|
+
}
|
|
3457
|
+
function wrapBodyLines(body, innerWidth) {
|
|
3458
|
+
const lines = [];
|
|
3459
|
+
for (const line of body) {
|
|
3460
|
+
lines.push(...wrapPlainText(line, innerWidth));
|
|
3461
|
+
}
|
|
3462
|
+
return lines;
|
|
3463
|
+
}
|
|
3464
|
+
async function listWorkspaceTextFiles(workspaceRoot) {
|
|
3465
|
+
const results = [];
|
|
3466
|
+
const visit = async (relativeDir) => {
|
|
3467
|
+
const absoluteDir = relativeDir ? path.join(workspaceRoot, relativeDir) : workspaceRoot;
|
|
3468
|
+
let entries;
|
|
3469
|
+
try {
|
|
3470
|
+
entries = await fs.readdir(absoluteDir, { withFileTypes: true });
|
|
3471
|
+
}
|
|
3472
|
+
catch {
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
for (const entry of entries) {
|
|
3476
|
+
const relativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name;
|
|
3477
|
+
if (entry.isDirectory()) {
|
|
3478
|
+
if (IGNORED_WORKSPACE_DIRS.has(entry.name)) {
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
await visit(relativePath);
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
if (entry.isFile() && isEditableTextFile(relativePath)) {
|
|
3485
|
+
results.push(relativePath.replace(/\\/g, '/'));
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
await visit('');
|
|
3490
|
+
results.sort((left, right) => left.localeCompare(right));
|
|
3491
|
+
return results;
|
|
3492
|
+
}
|
|
3493
|
+
function isEditableTextFile(filePath) {
|
|
3494
|
+
const basename = path.basename(filePath);
|
|
3495
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
3496
|
+
return TEXT_FILE_EXTENSIONS.has(extension) || TEXT_FILE_BASENAMES.has(basename);
|
|
3497
|
+
}
|
|
3498
|
+
function splitEditorContent(content) {
|
|
3499
|
+
const lines = content.split(/\r?\n/);
|
|
3500
|
+
return lines.length > 0 ? lines : [''];
|
|
3501
|
+
}
|
|
3502
|
+
function resolveWorkspaceTextPath(workspaceRoot, requestedPath) {
|
|
3503
|
+
const absolutePath = path.resolve(workspaceRoot, requestedPath);
|
|
3504
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
3505
|
+
if (absolutePath === resolvedRoot || absolutePath.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
3506
|
+
return absolutePath;
|
|
3507
|
+
}
|
|
3508
|
+
return path.join(resolvedRoot, path.basename(requestedPath));
|
|
3509
|
+
}
|
|
3510
|
+
export function filterPickerItems(items, query) {
|
|
3511
|
+
const normalized = query.trim().toLowerCase();
|
|
3512
|
+
if (!normalized) {
|
|
3513
|
+
return [...items];
|
|
3514
|
+
}
|
|
3515
|
+
const tokens = normalized.split(/\s+/).filter(Boolean);
|
|
3516
|
+
return [...items].filter((item) => {
|
|
3517
|
+
const haystack = `${item.label} ${item.description} ${item.detail ?? ''}`.toLowerCase();
|
|
3518
|
+
return tokens.every((token) => haystack.includes(token));
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
function clamp(value, min, max) {
|
|
3522
|
+
if (value < min) {
|
|
3523
|
+
return min;
|
|
3524
|
+
}
|
|
3525
|
+
if (value > max) {
|
|
3526
|
+
return max;
|
|
3527
|
+
}
|
|
3528
|
+
return value;
|
|
3529
|
+
}
|
|
3530
|
+
function currentSpinnerFrame() {
|
|
3531
|
+
return SPINNER_FRAMES[Math.floor(Date.now() / 120) % SPINNER_FRAMES.length];
|
|
3532
|
+
}
|
|
3533
|
+
function summarizeRunResult(result) {
|
|
3534
|
+
if (result.answer?.trim()) {
|
|
3535
|
+
return result.answer;
|
|
3536
|
+
}
|
|
3537
|
+
const lastOutput = [...result.output].reverse().find((line) => line.trim());
|
|
3538
|
+
if (lastOutput) {
|
|
3539
|
+
return lastOutput;
|
|
3540
|
+
}
|
|
3541
|
+
if (result.error?.trim()) {
|
|
3542
|
+
return result.error;
|
|
3543
|
+
}
|
|
3544
|
+
return 'Execution completed without a final answer.';
|
|
3545
|
+
}
|
|
3546
|
+
function paint(text, ...codes) {
|
|
3547
|
+
if (!output.isTTY || codes.length === 0) {
|
|
3548
|
+
return text;
|
|
3549
|
+
}
|
|
3550
|
+
return `${codes.join('')}${text}${ANSI.reset}`;
|
|
3551
|
+
}
|
|
3552
|
+
//# sourceMappingURL=tui.js.map
|