deepclause-sdk 0.0.60 → 0.0.62
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 +346 -87
- package/dist/cli/compile.d.ts +10 -0
- package/dist/cli/compile.d.ts.map +1 -1
- package/dist/cli/compile.js +62 -11
- package/dist/cli/compile.js.map +1 -1
- package/dist/cli/config.d.ts +6 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +72 -0
- 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 +134 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +19 -62
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tui.d.ts +52 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +2326 -109
- package/dist/cli/tui.js.map +1 -1
- package/dist/compiler.d.ts +10 -0
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +27 -6
- package/dist/compiler.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 +26 -14
- package/dist/runner.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +44 -0
- package/dist/sdk.js.map +1 -1
- package/dist/system/assets/docs/CONDUCTOR_PROMPT.md +30 -5
- package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +16 -6
- package/dist/system/assets/docs/TUI.md +139 -0
- package/dist/system/assets/index.d.ts +11 -1
- package/dist/system/assets/index.d.ts.map +1 -1
- package/dist/system/assets/index.js +62 -7
- package/dist/system/assets/index.js.map +1 -1
- package/dist/system/assets/skills/skill-creator.dml +6 -2
- package/dist/system/runtime/agentvm-manager.d.ts +28 -1
- package/dist/system/runtime/agentvm-manager.d.ts.map +1 -1
- package/dist/system/runtime/agentvm-manager.js +18 -2
- package/dist/system/runtime/agentvm-manager.js.map +1 -1
- 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 +15 -1
- package/dist/system/runtime/conductor.d.ts.map +1 -1
- package/dist/system/runtime/conductor.js +19 -65
- package/dist/system/runtime/conductor.js.map +1 -1
- package/dist/system/runtime/dml-executor.d.ts +8 -0
- package/dist/system/runtime/dml-executor.d.ts.map +1 -1
- package/dist/system/runtime/dml-executor.js +106 -54
- package/dist/system/runtime/dml-executor.js.map +1 -1
- 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 +6 -1
- package/dist/system/runtime/runtime-tools.d.ts.map +1 -1
- package/dist/system/runtime/runtime-tools.js +41 -2
- package/dist/system/runtime/runtime-tools.js.map +1 -1
- package/dist/system/runtime/shell-manager.d.ts +3 -3
- package/dist/system/runtime/shell-manager.d.ts.map +1 -1
- package/dist/system/runtime/shell-manager.js +31 -3
- package/dist/system/runtime/shell-manager.js.map +1 -1
- package/dist/system/runtime/shell-tool-events.d.ts +11 -0
- package/dist/system/runtime/shell-tool-events.d.ts.map +1 -0
- package/dist/system/runtime/shell-tool-events.js +139 -0
- package/dist/system/runtime/shell-tool-events.js.map +1 -0
- package/dist/system/runtime/skill-creator.d.ts +3 -0
- package/dist/system/runtime/skill-creator.d.ts.map +1 -1
- package/dist/system/runtime/skill-creator.js +90 -128
- package/dist/system/runtime/skill-creator.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/tui.js
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
1
3
|
import { emitKeypressEvents } from 'readline';
|
|
2
4
|
import { createInterface } from 'readline/promises';
|
|
3
5
|
import { stdin as input, stdout as output } from 'process';
|
|
4
6
|
import { listCommands } from './commands.js';
|
|
7
|
+
import { getConfigPath, loadConfig, resolveModelSlot, setModel } from './config.js';
|
|
5
8
|
import { run } from './run.js';
|
|
6
|
-
import { createConductorSession, getConductorSessionDetail, listConductorSessions, runConductorTurn, } from '../system/runtime/conductor.js';
|
|
9
|
+
import { createLocalSkill, createConductorSession, getConductorSessionDetail, listConductorSessions, runConductorTurn, } from '../system/runtime/conductor.js';
|
|
10
|
+
import { getSystemAssetSourcePaths } from '../system/assets/index.js';
|
|
11
|
+
import { createShellManager } from '../system/runtime/shell-manager.js';
|
|
12
|
+
import { buildToolCompletionEvent, buildToolFailureEvent, buildToolStartEvent, createShellToolEventBridge, } from '../system/runtime/shell-tool-events.js';
|
|
7
13
|
const IGNORED_LIVE_LOG_TOOLS = new Set(['set_result', 'update_memory']);
|
|
8
|
-
const BUILTIN_SLASH_COMMANDS = ['new', 'sessions', 'help', 'cancel', 'exit', 'quit'];
|
|
14
|
+
const BUILTIN_SLASH_COMMANDS = ['new', 'sessions', 'help', 'compile', 'skill-creator', 'set-model', 'cancel', 'exit', 'quit'];
|
|
15
|
+
const MODEL_SLOTS = ['gateway', 'run', 'compile'];
|
|
16
|
+
const CHILD_EVENT_INDENT = '\t';
|
|
17
|
+
const CHILD_EVENT_TAB_WIDTH = 4;
|
|
9
18
|
const MAX_ACTIVITY_LINES = 400;
|
|
10
19
|
const SPINNER_FRAMES = ['|', '/', '-', '\\'];
|
|
11
20
|
const IDLE_RENDER_INTERVAL_MS = 16;
|
|
12
21
|
const BUSY_RENDER_INTERVAL_MS = 40;
|
|
13
22
|
const TYPING_RENDER_INTERVAL_MS = 90;
|
|
23
|
+
const COMBINING_MARK_RE = /\p{Mark}/u;
|
|
24
|
+
const EXTENDED_PICTOGRAPHIC_RE = /\p{Extended_Pictographic}/u;
|
|
25
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
26
|
+
'.dml', '.md', '.txt', '.json', '.yaml', '.yml', '.toml', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
27
|
+
'.css', '.scss', '.html', '.sql', '.py', '.sh', '.bash', '.zsh', '.pro', '.pl', '.csv', '.env', '.example',
|
|
28
|
+
]);
|
|
29
|
+
const TEXT_FILE_BASENAMES = new Set(['README', 'README.md', 'AGENTS.md', 'SPEC.md', 'ARCHITECTURE.md', 'CMD_LINE.md']);
|
|
30
|
+
const IGNORED_WORKSPACE_DIRS = new Set(['.git', 'node_modules', 'dist', '.next', 'coverage', 'agentvm-cache']);
|
|
31
|
+
const graphemeSegmenter = (() => {
|
|
32
|
+
const Segmenter = Intl.Segmenter;
|
|
33
|
+
return Segmenter ? new Segmenter(undefined, { granularity: 'grapheme' }) : null;
|
|
34
|
+
})();
|
|
14
35
|
const ANSI = {
|
|
15
36
|
reset: '\u001b[0m',
|
|
16
37
|
bold: '\u001b[1m',
|
|
@@ -40,7 +61,7 @@ export class LiveExecutionPrinter {
|
|
|
40
61
|
}
|
|
41
62
|
handle(logEvent) {
|
|
42
63
|
const { event } = logEvent;
|
|
43
|
-
if (event.type === 'usage' || event.type === 'finished'
|
|
64
|
+
if (event.type === 'usage' || event.type === 'finished') {
|
|
44
65
|
return;
|
|
45
66
|
}
|
|
46
67
|
if (event.type === 'stream') {
|
|
@@ -50,10 +71,7 @@ export class LiveExecutionPrinter {
|
|
|
50
71
|
this.flushStream();
|
|
51
72
|
switch (event.type) {
|
|
52
73
|
case 'tool_call':
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
this.writeLine(`${formatEventPrefix(logEvent)}tool ${event.toolName}(${formatToolArgs(event.toolArgs)})`);
|
|
74
|
+
this.handleToolCall(logEvent);
|
|
57
75
|
break;
|
|
58
76
|
case 'output':
|
|
59
77
|
if (event.content) {
|
|
@@ -75,6 +93,11 @@ export class LiveExecutionPrinter {
|
|
|
75
93
|
this.writeLine(`${formatEventPrefix(logEvent)}log ${event.content}`);
|
|
76
94
|
}
|
|
77
95
|
break;
|
|
96
|
+
case 'input_required':
|
|
97
|
+
if (event.prompt) {
|
|
98
|
+
this.writeLine(`${formatEventPrefix(logEvent)}clarify ${event.prompt}`);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
78
101
|
}
|
|
79
102
|
}
|
|
80
103
|
finish() {
|
|
@@ -104,14 +127,22 @@ export class LiveExecutionPrinter {
|
|
|
104
127
|
this.streamOpen = false;
|
|
105
128
|
this.activeStreamKey = null;
|
|
106
129
|
}
|
|
130
|
+
handleToolCall(logEvent) {
|
|
131
|
+
const line = formatToolEventLine(logEvent);
|
|
132
|
+
if (!line) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.writeLine(line);
|
|
136
|
+
}
|
|
107
137
|
}
|
|
108
|
-
class ActivityBuffer {
|
|
138
|
+
export class ActivityBuffer {
|
|
109
139
|
lines = [];
|
|
140
|
+
activeTools = new Map();
|
|
110
141
|
activeStreamKey = null;
|
|
111
142
|
activeStreamLine = '';
|
|
112
143
|
handle(logEvent) {
|
|
113
144
|
const { event } = logEvent;
|
|
114
|
-
if (event.type === 'usage' || event.type === 'finished'
|
|
145
|
+
if (event.type === 'usage' || event.type === 'finished') {
|
|
115
146
|
return;
|
|
116
147
|
}
|
|
117
148
|
if (event.type === 'stream') {
|
|
@@ -121,10 +152,7 @@ class ActivityBuffer {
|
|
|
121
152
|
this.flushStream();
|
|
122
153
|
switch (event.type) {
|
|
123
154
|
case 'tool_call':
|
|
124
|
-
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
this.pushLine(`${formatEventPrefix(logEvent)}tool ${event.toolName}(${formatToolArgs(event.toolArgs)})`);
|
|
155
|
+
this.handleToolCall(logEvent);
|
|
128
156
|
break;
|
|
129
157
|
case 'output':
|
|
130
158
|
if (event.content) {
|
|
@@ -146,6 +174,11 @@ class ActivityBuffer {
|
|
|
146
174
|
this.pushLine(`${formatEventPrefix(logEvent)}log ${event.content}`);
|
|
147
175
|
}
|
|
148
176
|
break;
|
|
177
|
+
case 'input_required':
|
|
178
|
+
if (event.prompt) {
|
|
179
|
+
this.pushLine(`${formatEventPrefix(logEvent)}clarify ${event.prompt}`);
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
149
182
|
}
|
|
150
183
|
}
|
|
151
184
|
pushLine(line) {
|
|
@@ -158,11 +191,25 @@ class ActivityBuffer {
|
|
|
158
191
|
}
|
|
159
192
|
finish() {
|
|
160
193
|
this.flushStream();
|
|
194
|
+
this.activeTools.clear();
|
|
195
|
+
}
|
|
196
|
+
clear() {
|
|
197
|
+
this.lines.length = 0;
|
|
198
|
+
this.activeTools.clear();
|
|
199
|
+
this.activeStreamKey = null;
|
|
200
|
+
this.activeStreamLine = '';
|
|
161
201
|
}
|
|
162
202
|
snapshot() {
|
|
163
|
-
|
|
203
|
+
const body = this.activeStreamLine
|
|
164
204
|
? [...this.lines, this.activeStreamLine]
|
|
165
205
|
: [...this.lines];
|
|
206
|
+
const activeLines = this.buildActiveToolLines();
|
|
207
|
+
if (activeLines.length === 0) {
|
|
208
|
+
return body;
|
|
209
|
+
}
|
|
210
|
+
return body.length > 0
|
|
211
|
+
? [...activeLines, '', ...body]
|
|
212
|
+
: activeLines;
|
|
166
213
|
}
|
|
167
214
|
handleStream(logEvent) {
|
|
168
215
|
const streamKey = logEvent.scope === 'child' ? `child:${logEvent.childSlug ?? '?'}` : 'main';
|
|
@@ -193,6 +240,43 @@ class ActivityBuffer {
|
|
|
193
240
|
this.lines.splice(0, this.lines.length - MAX_ACTIVITY_LINES);
|
|
194
241
|
}
|
|
195
242
|
}
|
|
243
|
+
handleToolCall(logEvent) {
|
|
244
|
+
const { event } = logEvent;
|
|
245
|
+
if (!event.toolName || IGNORED_LIVE_LOG_TOOLS.has(event.toolName)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (event.toolState) {
|
|
249
|
+
const scopeKey = toolScopeKey(logEvent);
|
|
250
|
+
if (event.toolState === 'starting' || event.toolState === 'running') {
|
|
251
|
+
this.activeTools.set(scopeKey, {
|
|
252
|
+
scopeKey,
|
|
253
|
+
scopeLabel: toolScopeLabel(logEvent),
|
|
254
|
+
toolName: event.toolName,
|
|
255
|
+
toolState: event.toolState,
|
|
256
|
+
toolPid: event.toolPid,
|
|
257
|
+
toolBackend: event.toolBackend,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
this.activeTools.delete(scopeKey);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const line = formatToolEventLine(logEvent);
|
|
265
|
+
if (line) {
|
|
266
|
+
this.pushLine(line);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
buildActiveToolLines() {
|
|
270
|
+
if (this.activeTools.size === 0) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const lines = ['Active Tool Status'];
|
|
274
|
+
const entries = [...this.activeTools.values()].sort((left, right) => left.scopeKey.localeCompare(right.scopeKey));
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
lines.push(formatActiveToolStatus(entry));
|
|
277
|
+
}
|
|
278
|
+
return lines;
|
|
279
|
+
}
|
|
196
280
|
}
|
|
197
281
|
class FullscreenTui {
|
|
198
282
|
workspaceRoot;
|
|
@@ -204,6 +288,14 @@ class FullscreenTui {
|
|
|
204
288
|
activityBySessionId = new Map();
|
|
205
289
|
ephemeralMessagesBySessionId = new Map();
|
|
206
290
|
focusedPane = 'messages';
|
|
291
|
+
uiMode = 'command';
|
|
292
|
+
menuReturnMode = 'command';
|
|
293
|
+
menuState = {
|
|
294
|
+
activeIndex: 0,
|
|
295
|
+
selectedIndex: 0,
|
|
296
|
+
typeahead: '',
|
|
297
|
+
typeaheadAt: 0,
|
|
298
|
+
};
|
|
207
299
|
paneScroll = {
|
|
208
300
|
sessions: 0,
|
|
209
301
|
messages: 0,
|
|
@@ -217,7 +309,13 @@ class FullscreenTui {
|
|
|
217
309
|
busy = false;
|
|
218
310
|
exitRequested = false;
|
|
219
311
|
statusLine = 'Loading...';
|
|
220
|
-
|
|
312
|
+
pendingPrompt = null;
|
|
313
|
+
pickerState = null;
|
|
314
|
+
viewerState = null;
|
|
315
|
+
editorState = null;
|
|
316
|
+
recentFiles = [];
|
|
317
|
+
lastSubmittedInput = null;
|
|
318
|
+
shellWorkspacePath;
|
|
221
319
|
renderTimer = null;
|
|
222
320
|
animationTimer = null;
|
|
223
321
|
closeResolver = null;
|
|
@@ -227,12 +325,13 @@ class FullscreenTui {
|
|
|
227
325
|
constructor(workspaceRoot, options = {}) {
|
|
228
326
|
this.workspaceRoot = workspaceRoot;
|
|
229
327
|
this.options = options;
|
|
328
|
+
this.shellWorkspacePath = path.resolve(workspaceRoot, 'workspace');
|
|
230
329
|
}
|
|
231
330
|
async start() {
|
|
232
331
|
try {
|
|
233
332
|
await this.refreshCommands();
|
|
234
333
|
await this.refreshSessions({ createIfMissing: true });
|
|
235
|
-
this.statusLine = 'Ready.
|
|
334
|
+
this.statusLine = 'Ready. F10 or Ctrl+G opens menus. Left/right changes pane when the menu is closed.';
|
|
236
335
|
emitKeypressEvents(input);
|
|
237
336
|
if (input.isTTY) {
|
|
238
337
|
input.setRawMode(true);
|
|
@@ -266,15 +365,28 @@ class FullscreenTui {
|
|
|
266
365
|
void this.handleKeypress(text, key);
|
|
267
366
|
};
|
|
268
367
|
async handleKeypress(text, key) {
|
|
368
|
+
if ((key.ctrl && key.name === 'g') || key.name === 'f10') {
|
|
369
|
+
this.toggleMenuMode();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (key.ctrl && key.name === 'p' && !this.pendingPrompt && !this.busy) {
|
|
373
|
+
this.openActionPalette();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
269
376
|
if (key.ctrl && key.name === 'w') {
|
|
270
377
|
this.cyclePaneFocus(1);
|
|
271
378
|
return;
|
|
272
379
|
}
|
|
273
380
|
if (key.ctrl && key.name === 'c') {
|
|
274
|
-
if (this.
|
|
275
|
-
const pending = this.
|
|
276
|
-
this.
|
|
381
|
+
if (this.pendingPrompt) {
|
|
382
|
+
const pending = this.pendingPrompt;
|
|
383
|
+
this.pendingPrompt = null;
|
|
277
384
|
pending.resolve('');
|
|
385
|
+
if (!this.busy) {
|
|
386
|
+
this.statusLine = 'Input cancelled.';
|
|
387
|
+
this.requestRender();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
278
390
|
}
|
|
279
391
|
if (this.busy) {
|
|
280
392
|
this.exitRequested = true;
|
|
@@ -284,6 +396,22 @@ class FullscreenTui {
|
|
|
284
396
|
this.close();
|
|
285
397
|
return;
|
|
286
398
|
}
|
|
399
|
+
if (this.uiMode === 'menu') {
|
|
400
|
+
await this.handleMenuKeypress(text, key);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
404
|
+
await this.handlePickerKeypress(text, key);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (this.uiMode === 'viewer') {
|
|
408
|
+
await this.handleViewerKeypress(key);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (this.uiMode === 'editor') {
|
|
412
|
+
await this.handleEditorKeypress(text, key);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
287
415
|
switch (key.name) {
|
|
288
416
|
case 'return':
|
|
289
417
|
await this.submitInput();
|
|
@@ -354,8 +482,12 @@ class FullscreenTui {
|
|
|
354
482
|
await this.applyCompletion();
|
|
355
483
|
return;
|
|
356
484
|
case 'escape':
|
|
357
|
-
|
|
358
|
-
|
|
485
|
+
{
|
|
486
|
+
const previousInput = this.inputValue;
|
|
487
|
+
this.inputValue = '';
|
|
488
|
+
this.cursor = 0;
|
|
489
|
+
this.refreshCommandBarStatus(previousInput);
|
|
490
|
+
}
|
|
359
491
|
break;
|
|
360
492
|
default:
|
|
361
493
|
if (text && !key.ctrl && !key.meta) {
|
|
@@ -365,16 +497,271 @@ class FullscreenTui {
|
|
|
365
497
|
}
|
|
366
498
|
this.requestRender();
|
|
367
499
|
}
|
|
500
|
+
async handleMenuKeypress(text, key) {
|
|
501
|
+
const menus = this.getMenuDefinitions();
|
|
502
|
+
const activeMenu = menus[this.menuState.activeIndex];
|
|
503
|
+
const activeItems = activeMenu?.items ?? [];
|
|
504
|
+
if (key.name === 'escape') {
|
|
505
|
+
this.closeMenuMode();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (key.name && /^[1-6]$/.test(key.name)) {
|
|
509
|
+
this.selectMenu(Number.parseInt(key.name, 10) - 1);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
switch (key.name) {
|
|
513
|
+
case 'left':
|
|
514
|
+
this.moveMenu(-1);
|
|
515
|
+
return;
|
|
516
|
+
case 'right':
|
|
517
|
+
this.moveMenu(1);
|
|
518
|
+
return;
|
|
519
|
+
case 'up':
|
|
520
|
+
this.moveMenuItem(-1, activeItems.length);
|
|
521
|
+
return;
|
|
522
|
+
case 'down':
|
|
523
|
+
this.moveMenuItem(1, activeItems.length);
|
|
524
|
+
return;
|
|
525
|
+
case 'home':
|
|
526
|
+
this.menuState.selectedIndex = 0;
|
|
527
|
+
this.statusLine = `${activeMenu?.label ?? 'Menu'} menu.`;
|
|
528
|
+
this.requestRender();
|
|
529
|
+
return;
|
|
530
|
+
case 'end':
|
|
531
|
+
this.menuState.selectedIndex = Math.max(0, activeItems.length - 1);
|
|
532
|
+
this.statusLine = `${activeMenu?.label ?? 'Menu'} menu.`;
|
|
533
|
+
this.requestRender();
|
|
534
|
+
return;
|
|
535
|
+
case 'return':
|
|
536
|
+
case 'space': {
|
|
537
|
+
const selected = activeItems[this.menuState.selectedIndex];
|
|
538
|
+
if (selected) {
|
|
539
|
+
if (selected.enabled === false) {
|
|
540
|
+
this.statusLine = `${selected.label} is not available right now.`;
|
|
541
|
+
this.requestRender();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
await this.runMenuAction(selected.id);
|
|
545
|
+
}
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
case 'backspace':
|
|
549
|
+
if (this.menuState.typeahead) {
|
|
550
|
+
this.menuState.typeahead = this.menuState.typeahead.slice(0, -1);
|
|
551
|
+
this.menuState.typeaheadAt = Date.now();
|
|
552
|
+
this.requestRender();
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
default:
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
if (text && !key.ctrl && !key.meta && /\S/.test(text)) {
|
|
559
|
+
const now = Date.now();
|
|
560
|
+
this.menuState.typeahead = now - this.menuState.typeaheadAt > 900
|
|
561
|
+
? text.toLowerCase()
|
|
562
|
+
: `${this.menuState.typeahead}${text.toLowerCase()}`;
|
|
563
|
+
this.menuState.typeaheadAt = now;
|
|
564
|
+
this.menuState.selectedIndex = selectMenuItemByTypeahead(activeItems, this.menuState.typeahead, this.menuState.selectedIndex);
|
|
565
|
+
this.requestRender();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async handlePickerKeypress(text, key) {
|
|
569
|
+
const picker = this.pickerState;
|
|
570
|
+
if (!picker) {
|
|
571
|
+
this.closeWorkspaceMode();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
switch (key.name) {
|
|
575
|
+
case 'escape':
|
|
576
|
+
this.closeWorkspaceMode();
|
|
577
|
+
return;
|
|
578
|
+
case 'return':
|
|
579
|
+
await this.activatePickerSelection();
|
|
580
|
+
return;
|
|
581
|
+
case 'up':
|
|
582
|
+
this.movePickerSelection(-1);
|
|
583
|
+
return;
|
|
584
|
+
case 'down':
|
|
585
|
+
this.movePickerSelection(1);
|
|
586
|
+
return;
|
|
587
|
+
case 'pageup':
|
|
588
|
+
this.movePickerSelection(-Math.max(3, Math.floor(this.workspaceViewportLines() / 2)));
|
|
589
|
+
return;
|
|
590
|
+
case 'pagedown':
|
|
591
|
+
this.movePickerSelection(Math.max(3, Math.floor(this.workspaceViewportLines() / 2)));
|
|
592
|
+
return;
|
|
593
|
+
case 'home':
|
|
594
|
+
picker.selectedIndex = 0;
|
|
595
|
+
this.ensurePickerSelectionVisible();
|
|
596
|
+
this.requestRender();
|
|
597
|
+
return;
|
|
598
|
+
case 'end':
|
|
599
|
+
picker.selectedIndex = Math.max(0, picker.filteredItems.length - 1);
|
|
600
|
+
this.ensurePickerSelectionVisible();
|
|
601
|
+
this.requestRender();
|
|
602
|
+
return;
|
|
603
|
+
case 'backspace':
|
|
604
|
+
this.deleteBackward();
|
|
605
|
+
this.refreshPickerFilter();
|
|
606
|
+
return;
|
|
607
|
+
case 'delete':
|
|
608
|
+
this.deleteForward();
|
|
609
|
+
this.refreshPickerFilter();
|
|
610
|
+
return;
|
|
611
|
+
case 'left':
|
|
612
|
+
if (this.cursor > 0) {
|
|
613
|
+
this.cursor -= 1;
|
|
614
|
+
this.requestRender();
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
case 'right':
|
|
618
|
+
if (this.cursor < this.inputValue.length) {
|
|
619
|
+
this.cursor += 1;
|
|
620
|
+
this.requestRender();
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
default:
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
if (key.ctrl && key.name === 'e') {
|
|
627
|
+
await this.activatePickerSelection('edit');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (key.ctrl && key.name === 'r') {
|
|
631
|
+
await this.activatePickerSelection('run');
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (key.ctrl && key.name === 'm') {
|
|
635
|
+
await this.activatePickerSelection('meta');
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (key.ctrl && key.name === 's') {
|
|
639
|
+
await this.activatePickerSelection('source');
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (text && !key.ctrl && !key.meta) {
|
|
643
|
+
this.insertText(text);
|
|
644
|
+
this.refreshPickerFilter();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async handleViewerKeypress(key) {
|
|
648
|
+
if (!this.viewerState) {
|
|
649
|
+
this.closeWorkspaceMode();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (key.ctrl && key.name === 'e' && this.viewerState.canEdit && this.viewerState.path) {
|
|
653
|
+
await this.openEditorForFile(this.viewerState.path, { title: this.viewerState.title, preferredExtension: this.viewerState.preferredExtension });
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (key.ctrl && key.name === 'r' && this.viewerState.skillName) {
|
|
657
|
+
this.closeWorkspaceMode();
|
|
658
|
+
await this.executeSkill(this.viewerState.skillName, []);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
switch (key.name) {
|
|
662
|
+
case 'escape':
|
|
663
|
+
this.closeWorkspaceMode();
|
|
664
|
+
return;
|
|
665
|
+
case 'up':
|
|
666
|
+
this.viewerState.scrollTop = Math.max(0, this.viewerState.scrollTop - 1);
|
|
667
|
+
this.requestRender();
|
|
668
|
+
return;
|
|
669
|
+
case 'down':
|
|
670
|
+
this.viewerState.scrollTop += 1;
|
|
671
|
+
this.requestRender();
|
|
672
|
+
return;
|
|
673
|
+
case 'pageup':
|
|
674
|
+
this.viewerState.scrollTop = Math.max(0, this.viewerState.scrollTop - this.workspaceViewportLines());
|
|
675
|
+
this.requestRender();
|
|
676
|
+
return;
|
|
677
|
+
case 'pagedown':
|
|
678
|
+
this.viewerState.scrollTop += this.workspaceViewportLines();
|
|
679
|
+
this.requestRender();
|
|
680
|
+
return;
|
|
681
|
+
case 'home':
|
|
682
|
+
this.viewerState.scrollTop = 0;
|
|
683
|
+
this.requestRender();
|
|
684
|
+
return;
|
|
685
|
+
default:
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async handleEditorKeypress(text, key) {
|
|
690
|
+
if (!this.editorState) {
|
|
691
|
+
this.closeWorkspaceMode();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (key.ctrl && key.name === 's') {
|
|
695
|
+
await this.saveCurrentEditor(false);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
switch (key.name) {
|
|
699
|
+
case 'escape':
|
|
700
|
+
if (this.editorState.dirty && !this.editorState.discardPending) {
|
|
701
|
+
this.editorState.discardPending = true;
|
|
702
|
+
this.statusLine = 'Unsaved changes. Press Esc again to discard or Ctrl+S to save.';
|
|
703
|
+
this.requestRender();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
this.closeWorkspaceMode(true);
|
|
707
|
+
return;
|
|
708
|
+
case 'return':
|
|
709
|
+
this.splitEditorLine();
|
|
710
|
+
return;
|
|
711
|
+
case 'backspace':
|
|
712
|
+
this.deleteEditorBackward();
|
|
713
|
+
return;
|
|
714
|
+
case 'delete':
|
|
715
|
+
this.deleteEditorForward();
|
|
716
|
+
return;
|
|
717
|
+
case 'left':
|
|
718
|
+
this.moveEditorCursor('left');
|
|
719
|
+
return;
|
|
720
|
+
case 'right':
|
|
721
|
+
this.moveEditorCursor('right');
|
|
722
|
+
return;
|
|
723
|
+
case 'up':
|
|
724
|
+
this.moveEditorCursor('up');
|
|
725
|
+
return;
|
|
726
|
+
case 'down':
|
|
727
|
+
this.moveEditorCursor('down');
|
|
728
|
+
return;
|
|
729
|
+
case 'pageup':
|
|
730
|
+
this.pageEditorCursor(-1);
|
|
731
|
+
return;
|
|
732
|
+
case 'pagedown':
|
|
733
|
+
this.pageEditorCursor(1);
|
|
734
|
+
return;
|
|
735
|
+
case 'home':
|
|
736
|
+
this.editorState.cursorColumn = 0;
|
|
737
|
+
this.ensureEditorCursorVisible();
|
|
738
|
+
this.requestRender();
|
|
739
|
+
return;
|
|
740
|
+
case 'end':
|
|
741
|
+
this.editorState.cursorColumn = this.currentEditorLine().length;
|
|
742
|
+
this.ensureEditorCursorVisible();
|
|
743
|
+
this.requestRender();
|
|
744
|
+
return;
|
|
745
|
+
case 'tab':
|
|
746
|
+
this.insertEditorText(' ');
|
|
747
|
+
return;
|
|
748
|
+
default:
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
if (text && !key.ctrl && !key.meta) {
|
|
752
|
+
this.insertEditorText(text);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
368
755
|
async submitInput() {
|
|
369
756
|
const rawInput = this.inputValue;
|
|
370
757
|
const trimmedInput = rawInput.trim();
|
|
371
758
|
this.inputValue = '';
|
|
372
759
|
this.cursor = 0;
|
|
373
|
-
if (this.
|
|
374
|
-
const pending = this.
|
|
375
|
-
this.
|
|
760
|
+
if (this.pendingPrompt) {
|
|
761
|
+
const pending = this.pendingPrompt;
|
|
762
|
+
this.pendingPrompt = null;
|
|
376
763
|
pending.resolve(trimmedInput);
|
|
377
|
-
this.statusLine = 'Continuing...';
|
|
764
|
+
this.statusLine = pending.kind === 'clarify' ? 'Continuing...' : 'Working...';
|
|
378
765
|
this.requestRender();
|
|
379
766
|
return;
|
|
380
767
|
}
|
|
@@ -382,8 +769,8 @@ class FullscreenTui {
|
|
|
382
769
|
this.requestRender();
|
|
383
770
|
return;
|
|
384
771
|
}
|
|
385
|
-
const parsed =
|
|
386
|
-
if (this.busy && !this.
|
|
772
|
+
const parsed = parseCommandBarInput(rawInput);
|
|
773
|
+
if (this.busy && !this.pendingPrompt && !canSubmitParsedInputWhileBusy(parsed)) {
|
|
387
774
|
this.statusLine = 'Execution is still running. Use /cancel or Ctrl+C to stop it.';
|
|
388
775
|
this.requestRender();
|
|
389
776
|
return;
|
|
@@ -396,6 +783,10 @@ class FullscreenTui {
|
|
|
396
783
|
await this.executeSkill(parsed.name, parsed.args);
|
|
397
784
|
return;
|
|
398
785
|
}
|
|
786
|
+
if (parsed.kind === 'shell') {
|
|
787
|
+
await this.executeShellCommand(parsed.command);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
399
790
|
await this.executeTask(parsed.prompt);
|
|
400
791
|
}
|
|
401
792
|
async executeBuiltin(parsed) {
|
|
@@ -416,6 +807,13 @@ class FullscreenTui {
|
|
|
416
807
|
this.requestRender();
|
|
417
808
|
return;
|
|
418
809
|
}
|
|
810
|
+
case 'compile':
|
|
811
|
+
case 'skill-creator':
|
|
812
|
+
await this.executeSkillCreator(parsed.name, parsed.rawArgs);
|
|
813
|
+
return;
|
|
814
|
+
case 'set-model':
|
|
815
|
+
await this.executeSetModel(parsed.rawArgs);
|
|
816
|
+
return;
|
|
419
817
|
case 'sessions':
|
|
420
818
|
await this.refreshSessions({ createIfMissing: true, selectedSessionId: this.selectedSessionId });
|
|
421
819
|
await this.refreshCommands();
|
|
@@ -434,6 +832,7 @@ class FullscreenTui {
|
|
|
434
832
|
async executeTask(promptText) {
|
|
435
833
|
const sessionId = this.selectedSessionId;
|
|
436
834
|
const activity = this.activityFor(sessionId);
|
|
835
|
+
this.lastSubmittedInput = { kind: 'text', prompt: promptText };
|
|
437
836
|
this.focusedPane = 'messages';
|
|
438
837
|
this.paneScroll.messages = 0;
|
|
439
838
|
this.paneScroll.process = 0;
|
|
@@ -488,10 +887,11 @@ class FullscreenTui {
|
|
|
488
887
|
const sessionId = this.selectedSessionId || 'global';
|
|
489
888
|
const activity = this.activityFor(sessionId);
|
|
490
889
|
const commandText = `/${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`;
|
|
890
|
+
this.lastSubmittedInput = { kind: 'skill', name: skillName, rawArgs: args.join(' '), args: [...args] };
|
|
491
891
|
this.focusedPane = 'messages';
|
|
492
892
|
this.paneScroll.messages = 0;
|
|
493
893
|
this.paneScroll.process = 0;
|
|
494
|
-
this.beginExecutionPreview(sessionId, 'skill', { role: 'system', content: commandText });
|
|
894
|
+
this.beginExecutionPreview(sessionId, 'skill', { role: 'system', content: commandText }, skillName);
|
|
495
895
|
activity.pushLine(`skill /${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`);
|
|
496
896
|
this.busy = true;
|
|
497
897
|
this.currentAbortController = new AbortController();
|
|
@@ -502,7 +902,7 @@ class FullscreenTui {
|
|
|
502
902
|
const result = await runSkillCommand(this.workspaceRoot, skillName, args, {
|
|
503
903
|
sandbox: this.options.sandbox,
|
|
504
904
|
signal: this.currentAbortController.signal,
|
|
505
|
-
onUserInput: (question) => this.requestClarification(
|
|
905
|
+
onUserInput: (question) => this.requestClarification(question),
|
|
506
906
|
onEvent: (event) => {
|
|
507
907
|
activity.handle(event);
|
|
508
908
|
this.updatePreviewFromEvent(event);
|
|
@@ -537,32 +937,181 @@ class FullscreenTui {
|
|
|
537
937
|
}
|
|
538
938
|
}
|
|
539
939
|
}
|
|
540
|
-
async
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
this.statusLine = 'Awaiting clarification...';
|
|
545
|
-
this.requestRender();
|
|
546
|
-
return new Promise((resolve) => {
|
|
547
|
-
this.pendingQuestion = { prompt: promptText, resolve };
|
|
940
|
+
async executeSkillCreator(commandName, specText) {
|
|
941
|
+
const trimmedSpec = specText.trim();
|
|
942
|
+
if (!trimmedSpec) {
|
|
943
|
+
this.statusLine = `Provide a skill specification after /${commandName}.`;
|
|
548
944
|
this.requestRender();
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
async applyCompletion() {
|
|
552
|
-
if (!this.inputValue.startsWith('/')) {
|
|
553
945
|
return;
|
|
554
946
|
}
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
947
|
+
const sessionId = this.selectedSessionId;
|
|
948
|
+
const activity = this.activityFor(sessionId);
|
|
949
|
+
const commandText = `/${commandName} ${trimmedSpec}`;
|
|
950
|
+
this.lastSubmittedInput = { kind: 'builtin', name: commandName, rawArgs: trimmedSpec, args: [trimmedSpec] };
|
|
951
|
+
this.focusedPane = 'messages';
|
|
952
|
+
this.paneScroll.messages = 0;
|
|
953
|
+
this.paneScroll.process = 0;
|
|
954
|
+
this.beginExecutionPreview(sessionId, 'skill', { role: 'system', content: commandText }, 'skill-creator');
|
|
955
|
+
activity.pushLine(`skill ${commandText}`);
|
|
956
|
+
this.busy = true;
|
|
957
|
+
this.currentAbortController = new AbortController();
|
|
958
|
+
this.startAnimation();
|
|
959
|
+
this.statusLine = `Running /${commandName}...`;
|
|
960
|
+
this.requestRender();
|
|
961
|
+
try {
|
|
962
|
+
const result = await runSkillCreatorCommand(this.workspaceRoot, sessionId, trimmedSpec, {
|
|
963
|
+
sandbox: this.options.sandbox,
|
|
964
|
+
signal: this.currentAbortController.signal,
|
|
965
|
+
onUserInput: (question) => this.requestClarification(question),
|
|
966
|
+
onEvent: (event) => {
|
|
967
|
+
activity.handle(event);
|
|
968
|
+
this.updatePreviewFromEvent(event);
|
|
969
|
+
this.requestRender();
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
activity.finish();
|
|
973
|
+
await this.refreshCommands();
|
|
974
|
+
this.finishExecutionPreview({
|
|
975
|
+
persist: true,
|
|
976
|
+
finalText: formatSkillCreatorSummary(this.workspaceRoot, result),
|
|
977
|
+
});
|
|
978
|
+
this.statusLine = `/${commandName} finished.`;
|
|
563
979
|
}
|
|
564
|
-
|
|
565
|
-
|
|
980
|
+
catch (error) {
|
|
981
|
+
activity.finish();
|
|
982
|
+
const message = error.message;
|
|
983
|
+
activity.pushLine(`[skill-creator] error ${message}`);
|
|
984
|
+
this.finishExecutionPreview({ persist: true, error: message });
|
|
985
|
+
this.statusLine = `/${commandName} failed.`;
|
|
986
|
+
}
|
|
987
|
+
finally {
|
|
988
|
+
this.busy = false;
|
|
989
|
+
this.currentAbortController = null;
|
|
990
|
+
this.stopAnimation();
|
|
991
|
+
if (this.exitRequested) {
|
|
992
|
+
this.close();
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
this.requestRender();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async executeShellCommand(commandText) {
|
|
1000
|
+
const trimmedCommand = commandText.trim();
|
|
1001
|
+
if (!trimmedCommand) {
|
|
1002
|
+
this.statusLine = 'Enter a shell command after !.';
|
|
1003
|
+
this.requestRender();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const sessionId = this.selectedSessionId || 'global';
|
|
1007
|
+
const activity = this.activityFor(sessionId);
|
|
1008
|
+
this.lastSubmittedInput = { kind: 'shell', command: trimmedCommand };
|
|
1009
|
+
this.focusedPane = 'messages';
|
|
1010
|
+
this.paneScroll.messages = 0;
|
|
1011
|
+
this.paneScroll.process = 0;
|
|
1012
|
+
this.beginExecutionPreview(sessionId, 'shell', {
|
|
1013
|
+
role: 'system',
|
|
1014
|
+
kind: 'output',
|
|
1015
|
+
tag: 'shell',
|
|
1016
|
+
content: `!${trimmedCommand}`,
|
|
1017
|
+
}, 'shell');
|
|
1018
|
+
activity.pushLine(`shell !${trimmedCommand}`);
|
|
1019
|
+
this.busy = true;
|
|
1020
|
+
this.currentAbortController = new AbortController();
|
|
1021
|
+
this.startAnimation();
|
|
1022
|
+
this.statusLine = `Running !${trimmedCommand}...`;
|
|
1023
|
+
this.requestRender();
|
|
1024
|
+
try {
|
|
1025
|
+
const result = await runShellCommand(this.workspaceRoot, trimmedCommand, {
|
|
1026
|
+
sandbox: this.options.sandbox,
|
|
1027
|
+
signal: this.currentAbortController.signal,
|
|
1028
|
+
onEvent: (event) => {
|
|
1029
|
+
activity.handle(event);
|
|
1030
|
+
this.requestRender();
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
1033
|
+
activity.finish();
|
|
1034
|
+
await this.refreshCommands();
|
|
1035
|
+
this.finishExecutionPreview(result.success
|
|
1036
|
+
? { persist: true, finalText: formatShellCommandSummary(result) }
|
|
1037
|
+
: { persist: true, error: formatShellCommandSummary(result) });
|
|
1038
|
+
this.statusLine = result.success ? 'Shell command finished.' : 'Shell command failed.';
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
activity.finish();
|
|
1042
|
+
const message = error.message;
|
|
1043
|
+
this.finishExecutionPreview({ persist: true, error: message });
|
|
1044
|
+
this.statusLine = isAbortError(error) ? 'Shell command cancelled.' : 'Shell command failed.';
|
|
1045
|
+
}
|
|
1046
|
+
finally {
|
|
1047
|
+
this.busy = false;
|
|
1048
|
+
this.currentAbortController = null;
|
|
1049
|
+
this.stopAnimation();
|
|
1050
|
+
if (this.exitRequested) {
|
|
1051
|
+
this.close();
|
|
1052
|
+
}
|
|
1053
|
+
else {
|
|
1054
|
+
this.requestRender();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async executeSetModel(rawArgs) {
|
|
1059
|
+
try {
|
|
1060
|
+
const { model, slot } = parseSetModelCommandArgs(rawArgs);
|
|
1061
|
+
const result = await setModel(this.workspaceRoot, model, slot);
|
|
1062
|
+
const slotsLabel = result.updatedSlots.join(', ');
|
|
1063
|
+
this.currentProcessActivity().pushLine(`config model ${result.modelId} slots=${slotsLabel}`);
|
|
1064
|
+
this.statusLine = `Model set to ${result.modelId} for ${slotsLabel}. Applies on the next turn.`;
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
this.statusLine = error.message;
|
|
1068
|
+
}
|
|
1069
|
+
this.requestRender();
|
|
1070
|
+
}
|
|
1071
|
+
async requestClarification(promptText) {
|
|
1072
|
+
this.ensureClarificationVisible(promptText);
|
|
1073
|
+
return this.promptForValue(promptText, 'clarify');
|
|
1074
|
+
}
|
|
1075
|
+
async promptForValue(promptText, kind = 'input') {
|
|
1076
|
+
this.inputValue = '';
|
|
1077
|
+
this.cursor = 0;
|
|
1078
|
+
this.statusLine = kind === 'clarify' ? 'Awaiting clarification...' : promptText;
|
|
1079
|
+
this.requestRender();
|
|
1080
|
+
return new Promise((resolve) => {
|
|
1081
|
+
this.pendingPrompt = { kind, prompt: promptText, resolve };
|
|
1082
|
+
this.requestRender();
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
ensureClarificationVisible(promptText) {
|
|
1086
|
+
if (!this.currentPreview) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const entry = previewQuestionMessage(promptText);
|
|
1090
|
+
const entries = this.currentPreview.entries;
|
|
1091
|
+
const pendingAssistantIndex = entries.length > 0 && entries[entries.length - 1].role === 'assistant'
|
|
1092
|
+
? entries.length - 1
|
|
1093
|
+
: entries.length;
|
|
1094
|
+
const previous = pendingAssistantIndex > 0 ? entries[pendingAssistantIndex - 1] : undefined;
|
|
1095
|
+
if (sameDisplayMessage(previous, entry)) {
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
this.insertPreviewMessage(entry);
|
|
1099
|
+
}
|
|
1100
|
+
async applyCompletion() {
|
|
1101
|
+
if (!this.inputValue.startsWith('/')) {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
await this.refreshCommands();
|
|
1105
|
+
const completion = completeSlashCommand(this.inputValue, [
|
|
1106
|
+
...BUILTIN_SLASH_COMMANDS,
|
|
1107
|
+
...this.commands.map((command) => command.name),
|
|
1108
|
+
]);
|
|
1109
|
+
if (completion.applied) {
|
|
1110
|
+
this.inputValue = completion.value;
|
|
1111
|
+
this.cursor = this.inputValue.length;
|
|
1112
|
+
}
|
|
1113
|
+
if (completion.matches.length === 0) {
|
|
1114
|
+
this.statusLine = 'No matching commands or skills.';
|
|
566
1115
|
}
|
|
567
1116
|
else if (completion.matches.length === 1) {
|
|
568
1117
|
this.statusLine = `Completed /${completion.matches[0]}`;
|
|
@@ -588,18 +1137,852 @@ class FullscreenTui {
|
|
|
588
1137
|
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}. Left/right switches panes; PgUp/PgDn scrolls.`;
|
|
589
1138
|
this.requestRender();
|
|
590
1139
|
}
|
|
591
|
-
|
|
1140
|
+
toggleMenuMode() {
|
|
1141
|
+
if (this.pendingPrompt) {
|
|
1142
|
+
this.statusLine = this.pendingPrompt.kind === 'clarify'
|
|
1143
|
+
? 'Finish the clarification prompt before opening menus.'
|
|
1144
|
+
: 'Finish the current input prompt before opening menus.';
|
|
1145
|
+
this.requestRender();
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (this.uiMode === 'menu') {
|
|
1149
|
+
this.closeMenuMode();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
this.menuReturnMode = this.uiMode;
|
|
1153
|
+
this.uiMode = 'menu';
|
|
1154
|
+
this.menuState.typeahead = '';
|
|
1155
|
+
this.menuState.typeaheadAt = 0;
|
|
1156
|
+
this.menuState.selectedIndex = 0;
|
|
1157
|
+
this.statusLine = `${this.getMenuDefinitions()[this.menuState.activeIndex]?.label ?? 'Menu'} menu.`;
|
|
1158
|
+
this.requestRender();
|
|
1159
|
+
}
|
|
1160
|
+
closeMenuMode() {
|
|
1161
|
+
if (this.uiMode !== 'menu') {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
this.uiMode = this.menuReturnMode;
|
|
1165
|
+
this.menuState.typeahead = '';
|
|
1166
|
+
this.menuState.typeaheadAt = 0;
|
|
1167
|
+
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}. F10 or Ctrl+G reopens the menu bar.`;
|
|
1168
|
+
this.requestRender();
|
|
1169
|
+
}
|
|
1170
|
+
moveMenu(direction) {
|
|
1171
|
+
const menus = this.getMenuDefinitions();
|
|
1172
|
+
this.menuState.activeIndex = nextWrappedIndex(this.menuState.activeIndex, direction, menus.length);
|
|
1173
|
+
this.menuState.selectedIndex = 0;
|
|
1174
|
+
this.menuState.typeahead = '';
|
|
1175
|
+
this.menuState.typeaheadAt = 0;
|
|
1176
|
+
this.statusLine = `${menus[this.menuState.activeIndex]?.label ?? 'Menu'} menu.`;
|
|
1177
|
+
this.requestRender();
|
|
1178
|
+
}
|
|
1179
|
+
selectMenu(index) {
|
|
1180
|
+
const menus = this.getMenuDefinitions();
|
|
1181
|
+
if (index < 0 || index >= menus.length) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
this.menuState.activeIndex = index;
|
|
1185
|
+
this.menuState.selectedIndex = 0;
|
|
1186
|
+
this.menuState.typeahead = '';
|
|
1187
|
+
this.menuState.typeaheadAt = 0;
|
|
1188
|
+
this.statusLine = `${menus[index]?.label ?? 'Menu'} menu.`;
|
|
1189
|
+
this.requestRender();
|
|
1190
|
+
}
|
|
1191
|
+
moveMenuItem(direction, itemCount) {
|
|
1192
|
+
this.menuState.selectedIndex = nextWrappedIndex(this.menuState.selectedIndex, direction, itemCount);
|
|
1193
|
+
this.menuState.typeahead = '';
|
|
1194
|
+
this.menuState.typeaheadAt = 0;
|
|
1195
|
+
this.requestRender();
|
|
1196
|
+
}
|
|
1197
|
+
getMenuDefinitions() {
|
|
1198
|
+
const busy = this.busy || !!this.pendingPrompt;
|
|
1199
|
+
const menus = [
|
|
1200
|
+
{
|
|
1201
|
+
id: 'session',
|
|
1202
|
+
label: 'Session',
|
|
1203
|
+
items: [
|
|
1204
|
+
{ id: 'session.new', label: 'New Session', description: 'Create and switch to a fresh conductor session.', shortcut: 'Enter' },
|
|
1205
|
+
{ id: 'session.refresh', label: 'Refresh Sessions', description: 'Reload the session list and command catalog.' },
|
|
1206
|
+
{ id: 'session.next', label: 'Next Session', description: 'Move to the next session in the left pane.', shortcut: 'Down' },
|
|
1207
|
+
{ id: 'session.previous', label: 'Previous Session', description: 'Move to the previous session in the left pane.', shortcut: 'Up' },
|
|
1208
|
+
{ id: 'session.quit', label: 'Quit', description: 'Leave the fullscreen TUI.', shortcut: 'Ctrl+C' },
|
|
1209
|
+
],
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
id: 'skills',
|
|
1213
|
+
label: 'Skills',
|
|
1214
|
+
items: [
|
|
1215
|
+
{ id: 'skills.browse', label: 'Browse All Skills', description: 'Open a searchable catalog of compiled skills.', shortcut: 'Enter' },
|
|
1216
|
+
{ id: 'skills.run', label: 'Run Skill…', description: 'Pick a skill and execute it from the menu bar.' },
|
|
1217
|
+
{ id: 'skills.inspect', label: 'Inspect Skill Code', description: 'View the compiled DML for a skill.' },
|
|
1218
|
+
{ id: 'skills.edit', label: 'Edit Skill Code', description: 'Open a skill in the inline editor.' },
|
|
1219
|
+
{ id: 'skills.meta', label: 'Inspect Skill Metadata', description: 'View the metadata JSON for a compiled skill.' },
|
|
1220
|
+
{ id: 'skills.source', label: 'Open Skill Source Spec', description: 'Jump from a compiled skill back to its source Markdown.' },
|
|
1221
|
+
],
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
id: 'files',
|
|
1225
|
+
label: 'Files',
|
|
1226
|
+
items: [
|
|
1227
|
+
{ id: 'files.open', label: 'Open Text File…', description: 'Browse workspace text files in a searchable picker.', shortcut: 'Enter' },
|
|
1228
|
+
{ id: 'files.recent', label: 'Recent Files', description: 'Reopen a recently inspected or edited file.' },
|
|
1229
|
+
{ id: 'files.newMarkdown', label: 'New Markdown Note', description: 'Create a new Markdown buffer inside the workspace.' },
|
|
1230
|
+
{ id: 'files.save', label: 'Save', description: 'Save the current editor buffer.', shortcut: 'Ctrl+S', enabled: !!this.editorState },
|
|
1231
|
+
{ id: 'files.saveAs', label: 'Save As…', description: 'Save the current editor buffer to a new path.', enabled: !!this.editorState },
|
|
1232
|
+
{ id: 'files.readme', label: 'Open Workspace README', description: 'Inspect the top-level README.md.' },
|
|
1233
|
+
{ id: 'files.config', label: 'Open .deepclause Config', description: 'Inspect the local DeepClause config file.' },
|
|
1234
|
+
],
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
id: 'run',
|
|
1238
|
+
label: 'Run',
|
|
1239
|
+
items: [
|
|
1240
|
+
{ id: 'run.prompt', label: 'Send Prompt…', description: 'Return to the command bar and enter a freeform prompt.', shortcut: 'Enter' },
|
|
1241
|
+
{ id: 'run.shell', label: 'Run Shell Command…', description: 'Prefill the command bar for a direct workspace shell command.' },
|
|
1242
|
+
{ id: 'run.repeat', label: 'Repeat Last Command', description: 'Re-run the last prompt, shell command, or skill invocation.', enabled: !!this.lastSubmittedInput },
|
|
1243
|
+
{ id: 'run.cancel', label: 'Cancel Current Execution', description: 'Abort the running task or skill.', shortcut: '/cancel', enabled: this.busy },
|
|
1244
|
+
{ id: 'run.clear', label: 'Clear Execution Pane', description: 'Discard the current session execution log.' },
|
|
1245
|
+
{ id: 'run.refreshSkills', label: 'Refresh Skill Catalog', description: 'Re-scan the workspace for compiled skills.' },
|
|
1246
|
+
],
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
id: 'view',
|
|
1250
|
+
label: 'View',
|
|
1251
|
+
items: [
|
|
1252
|
+
{ id: 'view.focus.sessions', label: 'Focus Sessions Pane', description: 'Focus the session list pane.' },
|
|
1253
|
+
{ id: 'view.focus.messages', label: 'Focus Messages Pane', description: 'Focus the conversation history pane.' },
|
|
1254
|
+
{ id: 'view.focus.process', label: 'Focus Execution Pane', description: 'Focus the live execution log pane.' },
|
|
1255
|
+
{ id: 'view.focus.context', label: 'Focus Context Pane', description: 'Focus the session context and token usage pane.' },
|
|
1256
|
+
{ id: 'view.follow', label: 'Follow Focused Pane', description: 'Jump back to the newest visible content in the active pane.' },
|
|
1257
|
+
{ id: 'view.palette', label: 'Open Action Palette', description: 'Search all menu actions from one picker.', shortcut: 'Ctrl+P' },
|
|
1258
|
+
],
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
id: 'help',
|
|
1262
|
+
label: 'Help',
|
|
1263
|
+
items: [
|
|
1264
|
+
{ id: 'help.shortcuts', label: 'Keyboard Shortcuts', description: 'Show the fullscreen keyboard map.' },
|
|
1265
|
+
{ id: 'help.slash', label: 'Commands', description: 'Show slash commands, shell commands, and direct skill invocation.' },
|
|
1266
|
+
{ id: 'help.about', label: 'About This TUI', description: 'Show a short explanation of the fullscreen workspace.' },
|
|
1267
|
+
],
|
|
1268
|
+
},
|
|
1269
|
+
];
|
|
1270
|
+
return menus.map((menu) => ({
|
|
1271
|
+
...menu,
|
|
1272
|
+
items: menu.items.map((item) => ({
|
|
1273
|
+
...item,
|
|
1274
|
+
enabled: item.enabled ?? (!busy || item.id === 'run.cancel'),
|
|
1275
|
+
})),
|
|
1276
|
+
}));
|
|
1277
|
+
}
|
|
1278
|
+
async runMenuAction(actionId) {
|
|
1279
|
+
switch (actionId) {
|
|
1280
|
+
case 'session.new': {
|
|
1281
|
+
const created = await createConductorSession(this.workspaceRoot);
|
|
1282
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: created.id });
|
|
1283
|
+
this.closeMenuMode();
|
|
1284
|
+
this.statusLine = `Created session ${created.title}.`;
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
case 'session.refresh':
|
|
1288
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: this.selectedSessionId });
|
|
1289
|
+
await this.refreshCommands();
|
|
1290
|
+
this.closeMenuMode();
|
|
1291
|
+
this.statusLine = `Loaded ${this.sessions.length} session${this.sessions.length === 1 ? '' : 's'}.`;
|
|
1292
|
+
return;
|
|
1293
|
+
case 'session.next':
|
|
1294
|
+
await this.moveSession(1);
|
|
1295
|
+
return;
|
|
1296
|
+
case 'session.previous':
|
|
1297
|
+
await this.moveSession(-1);
|
|
1298
|
+
return;
|
|
1299
|
+
case 'session.quit':
|
|
1300
|
+
this.close();
|
|
1301
|
+
return;
|
|
1302
|
+
case 'skills.browse':
|
|
1303
|
+
await this.openSkillPicker('skills.browse');
|
|
1304
|
+
return;
|
|
1305
|
+
case 'skills.run':
|
|
1306
|
+
await this.openSkillPicker('skills.run');
|
|
1307
|
+
return;
|
|
1308
|
+
case 'skills.inspect':
|
|
1309
|
+
await this.openSkillPicker('skills.inspect');
|
|
1310
|
+
return;
|
|
1311
|
+
case 'skills.edit':
|
|
1312
|
+
await this.openSkillPicker('skills.edit');
|
|
1313
|
+
return;
|
|
1314
|
+
case 'skills.meta':
|
|
1315
|
+
await this.openSkillPicker('skills.meta');
|
|
1316
|
+
return;
|
|
1317
|
+
case 'skills.source':
|
|
1318
|
+
await this.openSkillPicker('skills.source');
|
|
1319
|
+
return;
|
|
1320
|
+
case 'files.open':
|
|
1321
|
+
await this.openFilePicker('files.inspect');
|
|
1322
|
+
return;
|
|
1323
|
+
case 'files.recent':
|
|
1324
|
+
await this.openFilePicker('files.recent');
|
|
1325
|
+
return;
|
|
1326
|
+
case 'files.newMarkdown':
|
|
1327
|
+
this.openEditorBuffer('', { title: 'New Markdown Note', preferredExtension: '.md' });
|
|
1328
|
+
return;
|
|
1329
|
+
case 'files.save':
|
|
1330
|
+
await this.saveCurrentEditor(false);
|
|
1331
|
+
return;
|
|
1332
|
+
case 'files.saveAs':
|
|
1333
|
+
await this.saveCurrentEditor(true);
|
|
1334
|
+
return;
|
|
1335
|
+
case 'files.readme':
|
|
1336
|
+
await this.openFileViewer(path.join(this.workspaceRoot, 'README.md'), { title: 'README.md' });
|
|
1337
|
+
return;
|
|
1338
|
+
case 'files.config':
|
|
1339
|
+
await this.openFileViewer(getConfigPath(this.workspaceRoot), { title: '.deepclause/config.json' });
|
|
1340
|
+
return;
|
|
1341
|
+
case 'run.prompt':
|
|
1342
|
+
this.closeMenuMode();
|
|
1343
|
+
this.statusLine = 'Command bar ready.';
|
|
1344
|
+
return;
|
|
1345
|
+
case 'run.shell':
|
|
1346
|
+
this.closeMenuMode();
|
|
1347
|
+
this.inputValue = '!';
|
|
1348
|
+
this.cursor = this.inputValue.length;
|
|
1349
|
+
this.statusLine = formatShellModeStatus(this.workspaceRoot, this.shellWorkspacePath, this.options.sandbox);
|
|
1350
|
+
this.requestRender();
|
|
1351
|
+
return;
|
|
1352
|
+
case 'run.repeat':
|
|
1353
|
+
await this.repeatLastCommand();
|
|
1354
|
+
return;
|
|
1355
|
+
case 'run.cancel':
|
|
1356
|
+
this.closeMenuMode();
|
|
1357
|
+
this.cancelCurrentExecution('Cancellation requested.');
|
|
1358
|
+
return;
|
|
1359
|
+
case 'run.clear':
|
|
1360
|
+
this.currentProcessActivity().clear();
|
|
1361
|
+
this.closeMenuMode();
|
|
1362
|
+
this.statusLine = 'Execution pane cleared.';
|
|
1363
|
+
return;
|
|
1364
|
+
case 'run.refreshSkills':
|
|
1365
|
+
await this.refreshCommands();
|
|
1366
|
+
this.closeMenuMode();
|
|
1367
|
+
this.statusLine = `Loaded ${this.commands.length} skill${this.commands.length === 1 ? '' : 's'}.`;
|
|
1368
|
+
return;
|
|
1369
|
+
case 'view.focus.sessions':
|
|
1370
|
+
case 'view.focus.messages':
|
|
1371
|
+
case 'view.focus.process':
|
|
1372
|
+
case 'view.focus.context': {
|
|
1373
|
+
const pane = actionId.slice('view.focus.'.length);
|
|
1374
|
+
this.focusedPane = pane;
|
|
1375
|
+
this.closeMenuMode();
|
|
1376
|
+
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}.`;
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
case 'view.follow':
|
|
1380
|
+
if (this.focusedPane !== 'sessions') {
|
|
1381
|
+
this.paneScroll[this.focusedPane] = 0;
|
|
1382
|
+
}
|
|
1383
|
+
this.closeMenuMode();
|
|
1384
|
+
this.statusLine = `${paneDisplayName(this.focusedPane)} following newest content.`;
|
|
1385
|
+
return;
|
|
1386
|
+
case 'view.palette':
|
|
1387
|
+
this.openActionPalette();
|
|
1388
|
+
return;
|
|
1389
|
+
case 'help.shortcuts':
|
|
1390
|
+
this.openTextViewer('Keyboard Shortcuts', getShortcutHelpLines().join('\n'), { preferredExtension: '.txt' });
|
|
1391
|
+
return;
|
|
1392
|
+
case 'help.slash':
|
|
1393
|
+
this.openTextViewer('Commands', getSlashHelpLines().join('\n'), { preferredExtension: '.txt' });
|
|
1394
|
+
return;
|
|
1395
|
+
case 'help.about':
|
|
1396
|
+
this.openTextViewer('About This TUI', [
|
|
1397
|
+
'DeepClause fullscreen TUI',
|
|
1398
|
+
'',
|
|
1399
|
+
'Use F10 or Ctrl+G for the menu bar, Ctrl+W to cycle panes, and /cancel to stop a running execution.',
|
|
1400
|
+
'Type !<command> to run a direct shell command in the configured workspace shell.',
|
|
1401
|
+
'The Skills and Files menus open searchable pickers, and Ctrl+P opens the action palette directly.',
|
|
1402
|
+
].join('\n'), { preferredExtension: '.txt' });
|
|
1403
|
+
return;
|
|
1404
|
+
default:
|
|
1405
|
+
this.closeMenuMode();
|
|
1406
|
+
this.statusLine = 'This menu action is being wired up next.';
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
closeWorkspaceMode(force = false) {
|
|
1411
|
+
if (!force && !this.canReplaceWorkspaceSurface()) {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
this.uiMode = 'command';
|
|
1415
|
+
this.pickerState = null;
|
|
1416
|
+
this.viewerState = null;
|
|
1417
|
+
this.editorState = null;
|
|
1418
|
+
this.inputValue = '';
|
|
1419
|
+
this.cursor = 0;
|
|
1420
|
+
this.requestRender();
|
|
1421
|
+
return true;
|
|
1422
|
+
}
|
|
1423
|
+
canReplaceWorkspaceSurface() {
|
|
1424
|
+
if (this.editorState?.dirty) {
|
|
1425
|
+
this.statusLine = 'Save the current editor with Ctrl+S or press Esc again in the editor to discard changes.';
|
|
1426
|
+
this.requestRender();
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
return true;
|
|
1430
|
+
}
|
|
1431
|
+
workspaceViewportLines() {
|
|
1432
|
+
return Math.max(1, (output.rows ?? 40) - 5);
|
|
1433
|
+
}
|
|
1434
|
+
openActionPalette() {
|
|
1435
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
const items = this.getMenuDefinitions()
|
|
1439
|
+
.flatMap((menu) => menu.items)
|
|
1440
|
+
.filter((item) => item.enabled !== false)
|
|
1441
|
+
.map((item) => ({
|
|
1442
|
+
id: item.id,
|
|
1443
|
+
label: item.label,
|
|
1444
|
+
description: item.description,
|
|
1445
|
+
detail: item.shortcut,
|
|
1446
|
+
actionId: item.id,
|
|
1447
|
+
}));
|
|
1448
|
+
this.openPicker({
|
|
1449
|
+
mode: 'palette',
|
|
1450
|
+
title: 'Action Palette',
|
|
1451
|
+
emptyText: 'No actions match the current query.',
|
|
1452
|
+
items,
|
|
1453
|
+
filteredItems: items,
|
|
1454
|
+
selectedIndex: 0,
|
|
1455
|
+
scrollTop: 0,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
async openSkillPicker(mode) {
|
|
1459
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
await this.refreshCommands();
|
|
1463
|
+
const items = await Promise.all(this.commands.map(async (command) => {
|
|
1464
|
+
const skillPath = this.resolveCommandDmlPath(command);
|
|
1465
|
+
const metaPath = this.resolveCommandMetaPath(command);
|
|
1466
|
+
let sourcePath;
|
|
1467
|
+
try {
|
|
1468
|
+
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
|
|
1469
|
+
if (meta.source) {
|
|
1470
|
+
sourcePath = path.resolve(this.workspaceRoot, meta.source);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
catch {
|
|
1474
|
+
// Metadata is optional while browsing.
|
|
1475
|
+
}
|
|
1476
|
+
return {
|
|
1477
|
+
id: command.name,
|
|
1478
|
+
label: command.name,
|
|
1479
|
+
description: command.description,
|
|
1480
|
+
detail: command.path,
|
|
1481
|
+
path: skillPath,
|
|
1482
|
+
skillName: command.name,
|
|
1483
|
+
metaPath,
|
|
1484
|
+
sourcePath,
|
|
1485
|
+
};
|
|
1486
|
+
}));
|
|
1487
|
+
const titles = {
|
|
1488
|
+
'skills.browse': 'Browse Skills',
|
|
1489
|
+
'skills.run': 'Run Skill',
|
|
1490
|
+
'skills.inspect': 'Inspect Skill Code',
|
|
1491
|
+
'skills.edit': 'Edit Skill Code',
|
|
1492
|
+
'skills.meta': 'Inspect Skill Metadata',
|
|
1493
|
+
'skills.source': 'Open Skill Source Spec',
|
|
1494
|
+
};
|
|
1495
|
+
this.openPicker({
|
|
1496
|
+
mode,
|
|
1497
|
+
title: titles[mode],
|
|
1498
|
+
emptyText: 'No skills match the current query.',
|
|
1499
|
+
items,
|
|
1500
|
+
filteredItems: items,
|
|
1501
|
+
selectedIndex: 0,
|
|
1502
|
+
scrollTop: 0,
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
async openFilePicker(mode) {
|
|
1506
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const relativePaths = mode === 'files.recent'
|
|
1510
|
+
? [...this.recentFiles]
|
|
1511
|
+
: await listWorkspaceTextFiles(this.workspaceRoot);
|
|
1512
|
+
const items = relativePaths.map((relativePath) => ({
|
|
1513
|
+
id: relativePath,
|
|
1514
|
+
label: path.basename(relativePath),
|
|
1515
|
+
description: relativePath,
|
|
1516
|
+
detail: path.extname(relativePath) || 'text',
|
|
1517
|
+
path: path.join(this.workspaceRoot, relativePath),
|
|
1518
|
+
}));
|
|
1519
|
+
const titles = {
|
|
1520
|
+
'files.inspect': 'Open Text File',
|
|
1521
|
+
'files.edit': 'Edit Text File',
|
|
1522
|
+
'files.recent': 'Recent Files',
|
|
1523
|
+
};
|
|
1524
|
+
this.openPicker({
|
|
1525
|
+
mode,
|
|
1526
|
+
title: titles[mode],
|
|
1527
|
+
emptyText: mode === 'files.recent'
|
|
1528
|
+
? 'No recent files yet. Open or edit a file first.'
|
|
1529
|
+
: 'No text files match the current query.',
|
|
1530
|
+
items,
|
|
1531
|
+
filteredItems: items,
|
|
1532
|
+
selectedIndex: 0,
|
|
1533
|
+
scrollTop: 0,
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
openPicker(state) {
|
|
1537
|
+
this.closeMenuMode();
|
|
1538
|
+
this.uiMode = state.mode === 'palette' ? 'palette' : 'picker';
|
|
1539
|
+
this.pickerState = state;
|
|
1540
|
+
this.viewerState = null;
|
|
1541
|
+
this.editorState = null;
|
|
1542
|
+
this.inputValue = '';
|
|
1543
|
+
this.cursor = 0;
|
|
1544
|
+
this.ensurePickerSelectionVisible();
|
|
1545
|
+
this.statusLine = `${state.title}.`;
|
|
1546
|
+
this.requestRender();
|
|
1547
|
+
}
|
|
1548
|
+
refreshPickerFilter() {
|
|
1549
|
+
if (!this.pickerState) {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
this.pickerState.filteredItems = filterPickerItems(this.pickerState.items, this.inputValue);
|
|
1553
|
+
this.pickerState.selectedIndex = clamp(this.pickerState.selectedIndex, 0, Math.max(0, this.pickerState.filteredItems.length - 1));
|
|
1554
|
+
this.pickerState.scrollTop = 0;
|
|
1555
|
+
this.ensurePickerSelectionVisible();
|
|
1556
|
+
this.requestRender();
|
|
1557
|
+
}
|
|
1558
|
+
movePickerSelection(delta) {
|
|
1559
|
+
if (!this.pickerState || this.pickerState.filteredItems.length === 0) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
this.pickerState.selectedIndex = clamp(this.pickerState.selectedIndex + delta, 0, Math.max(0, this.pickerState.filteredItems.length - 1));
|
|
1563
|
+
this.ensurePickerSelectionVisible();
|
|
1564
|
+
this.requestRender();
|
|
1565
|
+
}
|
|
1566
|
+
ensurePickerSelectionVisible() {
|
|
1567
|
+
if (!this.pickerState) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const targetLine = 3 + (this.pickerState.selectedIndex * 3);
|
|
1571
|
+
const viewport = this.workspaceViewportLines();
|
|
1572
|
+
if (targetLine < this.pickerState.scrollTop) {
|
|
1573
|
+
this.pickerState.scrollTop = targetLine;
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (targetLine >= this.pickerState.scrollTop + viewport) {
|
|
1577
|
+
this.pickerState.scrollTop = Math.max(0, targetLine - viewport + 1);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async activatePickerSelection(override) {
|
|
1581
|
+
const picker = this.pickerState;
|
|
1582
|
+
const item = picker?.filteredItems[picker.selectedIndex];
|
|
1583
|
+
if (!picker || !item) {
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (picker.mode === 'palette') {
|
|
1587
|
+
await this.runMenuAction(item.actionId ?? 'help.about');
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
const action = override ?? picker.mode;
|
|
1591
|
+
switch (action) {
|
|
1592
|
+
case 'skills.browse':
|
|
1593
|
+
case 'skills.inspect':
|
|
1594
|
+
await this.openSkillViewer(item, 'dml');
|
|
1595
|
+
return;
|
|
1596
|
+
case 'skills.run': {
|
|
1597
|
+
this.closeWorkspaceMode();
|
|
1598
|
+
const rawArgs = await this.promptForValue(`Arguments for /${item.skillName} (optional):`);
|
|
1599
|
+
await this.executeSkill(item.skillName ?? item.label, parseCommandArgs(rawArgs));
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
case 'run':
|
|
1603
|
+
if (item.skillName) {
|
|
1604
|
+
this.closeWorkspaceMode();
|
|
1605
|
+
const rawArgs = await this.promptForValue(`Arguments for /${item.skillName} (optional):`);
|
|
1606
|
+
await this.executeSkill(item.skillName, parseCommandArgs(rawArgs));
|
|
1607
|
+
}
|
|
1608
|
+
return;
|
|
1609
|
+
case 'edit':
|
|
1610
|
+
case 'skills.edit':
|
|
1611
|
+
case 'files.edit':
|
|
1612
|
+
if (item.path) {
|
|
1613
|
+
await this.openEditorForFile(item.path, { title: item.description || item.label });
|
|
1614
|
+
}
|
|
1615
|
+
return;
|
|
1616
|
+
case 'meta':
|
|
1617
|
+
case 'skills.meta':
|
|
1618
|
+
if (item.metaPath) {
|
|
1619
|
+
await this.openSkillViewer(item, 'meta');
|
|
1620
|
+
}
|
|
1621
|
+
return;
|
|
1622
|
+
case 'source':
|
|
1623
|
+
case 'skills.source':
|
|
1624
|
+
if (item.sourcePath) {
|
|
1625
|
+
await this.openSkillViewer(item, 'source');
|
|
1626
|
+
}
|
|
1627
|
+
else {
|
|
1628
|
+
this.statusLine = `No source spec recorded for ${item.label}.`;
|
|
1629
|
+
this.requestRender();
|
|
1630
|
+
}
|
|
1631
|
+
return;
|
|
1632
|
+
case 'files.inspect':
|
|
1633
|
+
case 'files.recent':
|
|
1634
|
+
if (item.path) {
|
|
1635
|
+
await this.openFileViewer(item.path, { title: item.description || item.label });
|
|
1636
|
+
}
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async openSkillViewer(item, target) {
|
|
1641
|
+
const filePath = target === 'dml' ? item.path : target === 'meta' ? item.metaPath : item.sourcePath;
|
|
1642
|
+
if (!filePath) {
|
|
1643
|
+
this.statusLine = `No ${target} file available for ${item.label}.`;
|
|
1644
|
+
this.requestRender();
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const title = target === 'dml'
|
|
1648
|
+
? `Skill: ${item.label}`
|
|
1649
|
+
: target === 'meta'
|
|
1650
|
+
? `Metadata: ${item.label}`
|
|
1651
|
+
: `Source Spec: ${item.label}`;
|
|
1652
|
+
await this.openFileViewer(filePath, {
|
|
1653
|
+
title,
|
|
1654
|
+
skillName: item.skillName,
|
|
1655
|
+
canEdit: target !== 'meta',
|
|
1656
|
+
preferredExtension: target === 'meta' ? '.json' : path.extname(filePath) || '.txt',
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
openTextViewer(title, content, options = {}) {
|
|
1660
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
this.closeMenuMode();
|
|
1664
|
+
this.uiMode = 'viewer';
|
|
1665
|
+
this.pickerState = null;
|
|
1666
|
+
this.editorState = null;
|
|
1667
|
+
this.viewerState = {
|
|
1668
|
+
title,
|
|
1669
|
+
content,
|
|
1670
|
+
path: options.path,
|
|
1671
|
+
canEdit: options.canEdit ?? false,
|
|
1672
|
+
scrollTop: 0,
|
|
1673
|
+
skillName: options.skillName,
|
|
1674
|
+
metaPath: options.metaPath,
|
|
1675
|
+
sourcePath: options.sourcePath,
|
|
1676
|
+
preferredExtension: options.preferredExtension,
|
|
1677
|
+
};
|
|
1678
|
+
this.inputValue = '';
|
|
1679
|
+
this.cursor = 0;
|
|
1680
|
+
if (options.path) {
|
|
1681
|
+
this.pushRecentFile(options.path);
|
|
1682
|
+
}
|
|
1683
|
+
this.statusLine = `Viewing ${title}.`;
|
|
1684
|
+
this.requestRender();
|
|
1685
|
+
}
|
|
1686
|
+
async openFileViewer(filePath, options = {}) {
|
|
1687
|
+
try {
|
|
1688
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1689
|
+
this.openTextViewer(options.title ?? path.relative(this.workspaceRoot, filePath), content, {
|
|
1690
|
+
path: filePath,
|
|
1691
|
+
canEdit: options.canEdit ?? isEditableTextFile(filePath),
|
|
1692
|
+
skillName: options.skillName,
|
|
1693
|
+
preferredExtension: options.preferredExtension ?? (path.extname(filePath) || '.txt'),
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
catch (error) {
|
|
1697
|
+
this.closeMenuMode();
|
|
1698
|
+
this.statusLine = `Unable to open ${path.relative(this.workspaceRoot, filePath)}: ${error.message}`;
|
|
1699
|
+
this.requestRender();
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
async openEditorForFile(filePath, options = {}) {
|
|
1703
|
+
try {
|
|
1704
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1705
|
+
this.openEditorBuffer(content, {
|
|
1706
|
+
title: options.title ?? path.relative(this.workspaceRoot, filePath),
|
|
1707
|
+
path: filePath,
|
|
1708
|
+
preferredExtension: options.preferredExtension ?? (path.extname(filePath) || '.txt'),
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
catch (error) {
|
|
1712
|
+
this.statusLine = `Unable to edit ${path.relative(this.workspaceRoot, filePath)}: ${error.message}`;
|
|
1713
|
+
this.requestRender();
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
openEditorBuffer(content, options) {
|
|
1717
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
this.closeMenuMode();
|
|
1721
|
+
this.uiMode = 'editor';
|
|
1722
|
+
this.pickerState = null;
|
|
1723
|
+
this.viewerState = null;
|
|
1724
|
+
this.editorState = {
|
|
1725
|
+
title: options.title,
|
|
1726
|
+
path: options.path,
|
|
1727
|
+
lines: splitEditorContent(content),
|
|
1728
|
+
cursorLine: 0,
|
|
1729
|
+
cursorColumn: 0,
|
|
1730
|
+
scrollTop: 0,
|
|
1731
|
+
scrollLeft: 0,
|
|
1732
|
+
dirty: false,
|
|
1733
|
+
preferredExtension: options.preferredExtension ?? '.txt',
|
|
1734
|
+
discardPending: false,
|
|
1735
|
+
};
|
|
1736
|
+
this.inputValue = '';
|
|
1737
|
+
this.cursor = 0;
|
|
1738
|
+
if (options.path) {
|
|
1739
|
+
this.pushRecentFile(options.path);
|
|
1740
|
+
}
|
|
1741
|
+
this.ensureEditorCursorVisible();
|
|
1742
|
+
this.statusLine = `Editing ${options.title}.`;
|
|
1743
|
+
this.requestRender();
|
|
1744
|
+
}
|
|
1745
|
+
currentEditorLine() {
|
|
1746
|
+
return this.editorState?.lines[this.editorState.cursorLine] ?? '';
|
|
1747
|
+
}
|
|
1748
|
+
updateCurrentEditorLine(nextLine) {
|
|
1749
|
+
if (!this.editorState) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
this.editorState.lines[this.editorState.cursorLine] = nextLine;
|
|
1753
|
+
this.editorState.dirty = true;
|
|
1754
|
+
this.editorState.discardPending = false;
|
|
1755
|
+
}
|
|
1756
|
+
insertEditorText(text) {
|
|
1757
|
+
if (!this.editorState) {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const line = this.currentEditorLine();
|
|
1761
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn)}${text}${line.slice(this.editorState.cursorColumn)}`);
|
|
1762
|
+
this.editorState.cursorColumn += text.length;
|
|
1763
|
+
this.ensureEditorCursorVisible();
|
|
1764
|
+
this.requestRender();
|
|
1765
|
+
}
|
|
1766
|
+
splitEditorLine() {
|
|
1767
|
+
if (!this.editorState) {
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const line = this.currentEditorLine();
|
|
1771
|
+
const before = line.slice(0, this.editorState.cursorColumn);
|
|
1772
|
+
const after = line.slice(this.editorState.cursorColumn);
|
|
1773
|
+
this.editorState.lines.splice(this.editorState.cursorLine, 1, before, after);
|
|
1774
|
+
this.editorState.cursorLine += 1;
|
|
1775
|
+
this.editorState.cursorColumn = 0;
|
|
1776
|
+
this.editorState.dirty = true;
|
|
1777
|
+
this.editorState.discardPending = false;
|
|
1778
|
+
this.ensureEditorCursorVisible();
|
|
1779
|
+
this.requestRender();
|
|
1780
|
+
}
|
|
1781
|
+
deleteEditorBackward() {
|
|
1782
|
+
if (!this.editorState) {
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
if (this.editorState.cursorColumn > 0) {
|
|
1786
|
+
const line = this.currentEditorLine();
|
|
1787
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn - 1)}${line.slice(this.editorState.cursorColumn)}`);
|
|
1788
|
+
this.editorState.cursorColumn -= 1;
|
|
1789
|
+
}
|
|
1790
|
+
else if (this.editorState.cursorLine > 0) {
|
|
1791
|
+
const previous = this.editorState.lines[this.editorState.cursorLine - 1] ?? '';
|
|
1792
|
+
const current = this.currentEditorLine();
|
|
1793
|
+
this.editorState.lines.splice(this.editorState.cursorLine - 1, 2, `${previous}${current}`);
|
|
1794
|
+
this.editorState.cursorLine -= 1;
|
|
1795
|
+
this.editorState.cursorColumn = previous.length;
|
|
1796
|
+
this.editorState.dirty = true;
|
|
1797
|
+
this.editorState.discardPending = false;
|
|
1798
|
+
}
|
|
1799
|
+
this.ensureEditorCursorVisible();
|
|
1800
|
+
this.requestRender();
|
|
1801
|
+
}
|
|
1802
|
+
deleteEditorForward() {
|
|
1803
|
+
if (!this.editorState) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const line = this.currentEditorLine();
|
|
1807
|
+
if (this.editorState.cursorColumn < line.length) {
|
|
1808
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn)}${line.slice(this.editorState.cursorColumn + 1)}`);
|
|
1809
|
+
}
|
|
1810
|
+
else if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1811
|
+
const next = this.editorState.lines[this.editorState.cursorLine + 1] ?? '';
|
|
1812
|
+
this.editorState.lines.splice(this.editorState.cursorLine, 2, `${line}${next}`);
|
|
1813
|
+
this.editorState.dirty = true;
|
|
1814
|
+
this.editorState.discardPending = false;
|
|
1815
|
+
}
|
|
1816
|
+
this.ensureEditorCursorVisible();
|
|
1817
|
+
this.requestRender();
|
|
1818
|
+
}
|
|
1819
|
+
moveEditorCursor(direction) {
|
|
1820
|
+
if (!this.editorState) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
switch (direction) {
|
|
1824
|
+
case 'left':
|
|
1825
|
+
if (this.editorState.cursorColumn > 0) {
|
|
1826
|
+
this.editorState.cursorColumn -= 1;
|
|
1827
|
+
}
|
|
1828
|
+
else if (this.editorState.cursorLine > 0) {
|
|
1829
|
+
this.editorState.cursorLine -= 1;
|
|
1830
|
+
this.editorState.cursorColumn = this.currentEditorLine().length;
|
|
1831
|
+
}
|
|
1832
|
+
break;
|
|
1833
|
+
case 'right':
|
|
1834
|
+
if (this.editorState.cursorColumn < this.currentEditorLine().length) {
|
|
1835
|
+
this.editorState.cursorColumn += 1;
|
|
1836
|
+
}
|
|
1837
|
+
else if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1838
|
+
this.editorState.cursorLine += 1;
|
|
1839
|
+
this.editorState.cursorColumn = 0;
|
|
1840
|
+
}
|
|
1841
|
+
break;
|
|
1842
|
+
case 'up':
|
|
1843
|
+
if (this.editorState.cursorLine > 0) {
|
|
1844
|
+
this.editorState.cursorLine -= 1;
|
|
1845
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1846
|
+
}
|
|
1847
|
+
break;
|
|
1848
|
+
case 'down':
|
|
1849
|
+
if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1850
|
+
this.editorState.cursorLine += 1;
|
|
1851
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1852
|
+
}
|
|
1853
|
+
break;
|
|
1854
|
+
}
|
|
1855
|
+
this.ensureEditorCursorVisible();
|
|
1856
|
+
this.requestRender();
|
|
1857
|
+
}
|
|
1858
|
+
pageEditorCursor(direction) {
|
|
1859
|
+
if (!this.editorState) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
const nextLine = clamp(this.editorState.cursorLine + (direction * this.workspaceViewportLines()), 0, Math.max(0, this.editorState.lines.length - 1));
|
|
1863
|
+
this.editorState.cursorLine = nextLine;
|
|
1864
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1865
|
+
this.ensureEditorCursorVisible();
|
|
1866
|
+
this.requestRender();
|
|
1867
|
+
}
|
|
1868
|
+
ensureEditorCursorVisible() {
|
|
1869
|
+
if (!this.editorState) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const viewportLines = this.workspaceViewportLines();
|
|
1873
|
+
if (this.editorState.cursorLine < this.editorState.scrollTop) {
|
|
1874
|
+
this.editorState.scrollTop = this.editorState.cursorLine;
|
|
1875
|
+
}
|
|
1876
|
+
else if (this.editorState.cursorLine >= this.editorState.scrollTop + viewportLines) {
|
|
1877
|
+
this.editorState.scrollTop = Math.max(0, this.editorState.cursorLine - viewportLines + 1);
|
|
1878
|
+
}
|
|
1879
|
+
const width = output.columns ?? 120;
|
|
1880
|
+
const innerWidth = Math.max(1, width - 2);
|
|
1881
|
+
const lineNumberWidth = Math.max(2, String(Math.max(1, this.editorState.lines.length)).length);
|
|
1882
|
+
const prefixWidth = 2 + lineNumberWidth + 3;
|
|
1883
|
+
const contentWidth = Math.max(1, innerWidth - prefixWidth);
|
|
1884
|
+
if (this.editorState.cursorColumn < this.editorState.scrollLeft) {
|
|
1885
|
+
this.editorState.scrollLeft = this.editorState.cursorColumn;
|
|
1886
|
+
}
|
|
1887
|
+
else if (this.editorState.cursorColumn >= this.editorState.scrollLeft + contentWidth) {
|
|
1888
|
+
this.editorState.scrollLeft = Math.max(0, this.editorState.cursorColumn - contentWidth + 1);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
async saveCurrentEditor(saveAs) {
|
|
1892
|
+
if (!this.editorState) {
|
|
1893
|
+
this.closeMenuMode();
|
|
1894
|
+
this.statusLine = 'No editor buffer is active.';
|
|
1895
|
+
this.requestRender();
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
let targetPath = this.editorState.path;
|
|
1899
|
+
if (saveAs || !targetPath) {
|
|
1900
|
+
const suggested = this.editorState.path
|
|
1901
|
+
? path.relative(this.workspaceRoot, this.editorState.path)
|
|
1902
|
+
: `notes/${Date.now()}.${this.editorState.preferredExtension.replace(/^\./, '')}`;
|
|
1903
|
+
const requestedPath = (await this.promptForValue(`Save file as (relative path) [${suggested}]:`)).trim() || suggested;
|
|
1904
|
+
targetPath = resolveWorkspaceTextPath(this.workspaceRoot, requestedPath);
|
|
1905
|
+
}
|
|
1906
|
+
try {
|
|
1907
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1908
|
+
await fs.writeFile(targetPath, this.editorState.lines.join('\n'), 'utf8');
|
|
1909
|
+
this.editorState.path = targetPath;
|
|
1910
|
+
this.editorState.title = path.relative(this.workspaceRoot, targetPath);
|
|
1911
|
+
this.editorState.dirty = false;
|
|
1912
|
+
this.editorState.discardPending = false;
|
|
1913
|
+
this.pushRecentFile(targetPath);
|
|
1914
|
+
this.statusLine = `Saved ${path.relative(this.workspaceRoot, targetPath)}.`;
|
|
1915
|
+
this.requestRender();
|
|
1916
|
+
}
|
|
1917
|
+
catch (error) {
|
|
1918
|
+
this.statusLine = `Unable to save file: ${error.message}`;
|
|
1919
|
+
this.requestRender();
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
pushRecentFile(filePath) {
|
|
1923
|
+
const relativePath = path.relative(this.workspaceRoot, filePath).replace(/\\/g, '/');
|
|
1924
|
+
const existingIndex = this.recentFiles.indexOf(relativePath);
|
|
1925
|
+
if (existingIndex >= 0) {
|
|
1926
|
+
this.recentFiles.splice(existingIndex, 1);
|
|
1927
|
+
}
|
|
1928
|
+
this.recentFiles.unshift(relativePath);
|
|
1929
|
+
if (this.recentFiles.length > 20) {
|
|
1930
|
+
this.recentFiles.length = 20;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
async repeatLastCommand() {
|
|
1934
|
+
if (!this.lastSubmittedInput) {
|
|
1935
|
+
this.closeMenuMode();
|
|
1936
|
+
this.statusLine = 'No previous prompt, shell command, or skill to repeat yet.';
|
|
1937
|
+
this.requestRender();
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
if (!this.closeWorkspaceMode()) {
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (this.lastSubmittedInput.kind === 'text') {
|
|
1944
|
+
await this.executeTask(this.lastSubmittedInput.prompt);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (this.lastSubmittedInput.kind === 'skill') {
|
|
1948
|
+
await this.executeSkill(this.lastSubmittedInput.name, this.lastSubmittedInput.args);
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (this.lastSubmittedInput.kind === 'shell') {
|
|
1952
|
+
await this.executeShellCommand(this.lastSubmittedInput.command);
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
if (this.lastSubmittedInput.kind === 'builtin') {
|
|
1956
|
+
await this.executeBuiltin(this.lastSubmittedInput);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
resolveCommandDmlPath(command) {
|
|
1960
|
+
return path.join(this.workspaceRoot, `${command.path}.dml`);
|
|
1961
|
+
}
|
|
1962
|
+
resolveCommandMetaPath(command) {
|
|
1963
|
+
return path.join(this.workspaceRoot, `${command.path}.meta.json`);
|
|
1964
|
+
}
|
|
1965
|
+
beginExecutionPreview(sessionId, kind, lead, rootTag) {
|
|
592
1966
|
this.currentPreview = {
|
|
593
1967
|
sessionId,
|
|
594
1968
|
kind,
|
|
595
|
-
|
|
1969
|
+
rootTag,
|
|
1970
|
+
activeChildTag: kind === 'skill' ? rootTag : undefined,
|
|
1971
|
+
entries: kind === 'shell'
|
|
1972
|
+
? [lead]
|
|
1973
|
+
: [lead, { role: 'assistant', content: '', pending: true }],
|
|
596
1974
|
};
|
|
597
1975
|
}
|
|
598
1976
|
updatePreviewFromEvent(logEvent) {
|
|
599
1977
|
if (!this.currentPreview) {
|
|
600
1978
|
return;
|
|
601
1979
|
}
|
|
602
|
-
|
|
1980
|
+
this.updatePreviewChildState(logEvent);
|
|
1981
|
+
const previewMessage = previewMessageFromEvent(logEvent);
|
|
1982
|
+
if (previewMessage) {
|
|
1983
|
+
this.insertPreviewMessage(previewMessage);
|
|
1984
|
+
}
|
|
1985
|
+
if (!this.isPrimaryPreviewEvent(logEvent)) {
|
|
603
1986
|
return;
|
|
604
1987
|
}
|
|
605
1988
|
const message = this.ensurePreviewAssistantMessage();
|
|
@@ -625,15 +2008,64 @@ class FullscreenTui {
|
|
|
625
2008
|
break;
|
|
626
2009
|
}
|
|
627
2010
|
}
|
|
2011
|
+
isPrimaryPreviewEvent(logEvent) {
|
|
2012
|
+
if (!this.currentPreview) {
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
if (this.currentPreview.kind === 'task') {
|
|
2016
|
+
return logEvent.scope === 'main';
|
|
2017
|
+
}
|
|
2018
|
+
if (this.currentPreview.kind === 'shell') {
|
|
2019
|
+
return false;
|
|
2020
|
+
}
|
|
2021
|
+
return logEvent.scope === 'child' && logEvent.childSlug === this.currentPreview.rootTag;
|
|
2022
|
+
}
|
|
2023
|
+
insertPreviewMessage(entry) {
|
|
2024
|
+
if (!this.currentPreview) {
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const entries = this.currentPreview.entries;
|
|
2028
|
+
const last = entries[entries.length - 1];
|
|
2029
|
+
if (last && last.role === 'assistant' && last.pending && !last.content) {
|
|
2030
|
+
entries.splice(entries.length - 1, 0, entry);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
entries.push(entry);
|
|
2034
|
+
}
|
|
2035
|
+
updatePreviewChildState(logEvent) {
|
|
2036
|
+
if (!this.currentPreview || this.currentPreview.kind !== 'task') {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
if (logEvent.scope === 'child' && logEvent.childSlug) {
|
|
2040
|
+
this.currentPreview.activeChildTag = logEvent.childSlug;
|
|
2041
|
+
this.ensurePreviewChildSkillMessage(logEvent.childSlug);
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
if (logEvent.scope === 'main' && (logEvent.event.type === 'stream' || logEvent.event.type === 'answer' || logEvent.event.type === 'error')) {
|
|
2045
|
+
this.currentPreview.activeChildTag = undefined;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
ensurePreviewChildSkillMessage(childSlug) {
|
|
2049
|
+
if (!this.currentPreview) {
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
const message = previewChildSkillActivityMessage(childSlug);
|
|
2053
|
+
const exists = this.currentPreview.entries.some((entry) => sameDisplayMessage(entry, message));
|
|
2054
|
+
if (!exists) {
|
|
2055
|
+
this.insertPreviewMessage(message);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
628
2058
|
ensurePreviewAssistantMessage() {
|
|
629
2059
|
if (!this.currentPreview) {
|
|
630
2060
|
return { role: 'assistant', content: '', pending: false };
|
|
631
2061
|
}
|
|
2062
|
+
const desiredTag = this.currentPreview.rootTag ?? this.currentPreview.activeChildTag;
|
|
632
2063
|
const last = this.currentPreview.entries[this.currentPreview.entries.length - 1];
|
|
633
2064
|
if (last && last.role === 'assistant') {
|
|
2065
|
+
last.tag = desiredTag;
|
|
634
2066
|
return last;
|
|
635
2067
|
}
|
|
636
|
-
const created = { role: 'assistant', content: '', pending: true };
|
|
2068
|
+
const created = { role: 'assistant', content: '', pending: true, tag: desiredTag };
|
|
637
2069
|
this.currentPreview.entries.push(created);
|
|
638
2070
|
return created;
|
|
639
2071
|
}
|
|
@@ -641,6 +2073,24 @@ class FullscreenTui {
|
|
|
641
2073
|
if (!this.currentPreview) {
|
|
642
2074
|
return;
|
|
643
2075
|
}
|
|
2076
|
+
this.currentPreview.activeChildTag = undefined;
|
|
2077
|
+
if (this.currentPreview.kind === 'shell') {
|
|
2078
|
+
const detail = typeof options.error === 'string' && options.error.trim()
|
|
2079
|
+
? { role: 'system', kind: 'output', tag: 'shell', content: options.error, error: true }
|
|
2080
|
+
: typeof options.finalText === 'string' && options.finalText.trim()
|
|
2081
|
+
? { role: 'system', kind: 'output', tag: 'shell', content: options.finalText }
|
|
2082
|
+
: null;
|
|
2083
|
+
if (detail) {
|
|
2084
|
+
this.currentPreview.entries.push(detail);
|
|
2085
|
+
}
|
|
2086
|
+
if (options.persist) {
|
|
2087
|
+
const existing = this.ephemeralMessagesBySessionId.get(this.currentPreview.sessionId) ?? [];
|
|
2088
|
+
existing.push(...this.currentPreview.entries.map((entry) => ({ ...entry, pending: false })));
|
|
2089
|
+
this.ephemeralMessagesBySessionId.set(this.currentPreview.sessionId, existing);
|
|
2090
|
+
}
|
|
2091
|
+
this.currentPreview = null;
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
644
2094
|
const assistant = this.ensurePreviewAssistantMessage();
|
|
645
2095
|
if (typeof options.finalText === 'string' && options.finalText.trim()) {
|
|
646
2096
|
assistant.content = options.finalText;
|
|
@@ -751,24 +2201,49 @@ class FullscreenTui {
|
|
|
751
2201
|
return this.activityFor(this.selectedSessionId || 'global');
|
|
752
2202
|
}
|
|
753
2203
|
insertText(text) {
|
|
2204
|
+
const previousInput = this.inputValue;
|
|
754
2205
|
this.inputValue = `${this.inputValue.slice(0, this.cursor)}${text}${this.inputValue.slice(this.cursor)}`;
|
|
755
2206
|
this.cursor += text.length;
|
|
2207
|
+
this.refreshCommandBarStatus(previousInput);
|
|
756
2208
|
}
|
|
757
2209
|
deleteBackward() {
|
|
758
2210
|
if (this.cursor === 0) {
|
|
759
2211
|
return;
|
|
760
2212
|
}
|
|
2213
|
+
const previousInput = this.inputValue;
|
|
761
2214
|
this.inputValue = `${this.inputValue.slice(0, this.cursor - 1)}${this.inputValue.slice(this.cursor)}`;
|
|
762
2215
|
this.cursor -= 1;
|
|
2216
|
+
this.refreshCommandBarStatus(previousInput);
|
|
763
2217
|
}
|
|
764
2218
|
deleteForward() {
|
|
765
2219
|
if (this.cursor >= this.inputValue.length) {
|
|
766
2220
|
return;
|
|
767
2221
|
}
|
|
2222
|
+
const previousInput = this.inputValue;
|
|
768
2223
|
this.inputValue = `${this.inputValue.slice(0, this.cursor)}${this.inputValue.slice(this.cursor + 1)}`;
|
|
2224
|
+
this.refreshCommandBarStatus(previousInput);
|
|
769
2225
|
}
|
|
770
2226
|
async refreshCommands() {
|
|
771
2227
|
this.commands = await listCommands(this.workspaceRoot);
|
|
2228
|
+
await this.refreshShellContext();
|
|
2229
|
+
}
|
|
2230
|
+
async refreshShellContext() {
|
|
2231
|
+
const config = await loadConfig(this.workspaceRoot);
|
|
2232
|
+
this.shellWorkspacePath = path.resolve(this.workspaceRoot, config.workspace || './workspace');
|
|
2233
|
+
}
|
|
2234
|
+
refreshCommandBarStatus(previousInput = '') {
|
|
2235
|
+
if (this.busy || this.pendingPrompt || this.uiMode !== 'command') {
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
const wasShell = previousInput.trimStart().startsWith('!');
|
|
2239
|
+
const isShell = this.inputValue.trimStart().startsWith('!');
|
|
2240
|
+
if (isShell) {
|
|
2241
|
+
this.statusLine = formatShellModeStatus(this.workspaceRoot, this.shellWorkspacePath, this.options.sandbox);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
if (wasShell) {
|
|
2245
|
+
this.statusLine = 'Command bar ready.';
|
|
2246
|
+
}
|
|
772
2247
|
}
|
|
773
2248
|
async refreshSessions(options = {}) {
|
|
774
2249
|
let sessions = await listConductorSessions(this.workspaceRoot);
|
|
@@ -824,21 +2299,34 @@ class FullscreenTui {
|
|
|
824
2299
|
const messagesPane = this.buildPaneView('messages', 'Messages', widths.center, contentHeight);
|
|
825
2300
|
const processPane = this.buildPaneView('process', 'Execution', widths.right, rightHeights.process);
|
|
826
2301
|
const contextPane = this.buildPaneView('context', 'Context', widths.right, rightHeights.context);
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
2302
|
+
const workspaceView = this.uiMode === 'command'
|
|
2303
|
+
? null
|
|
2304
|
+
: this.buildWorkspacePaneView(columns, contentHeight);
|
|
2305
|
+
const header = renderMenuBar(this.buildHeaderLine(), columns, this.uiMode === 'menu' ? this.menuState.activeIndex : null);
|
|
2306
|
+
const status = renderStatusBar(this.buildStatusLine(), columns, this.buildHotkeys());
|
|
2307
|
+
const inputPrefix = this.buildInputPrefix();
|
|
2308
|
+
const inputValue = this.buildInputValue();
|
|
2309
|
+
const inputLine = renderCommandBar(inputPrefix, inputValue, columns);
|
|
831
2310
|
const gap = paint(' ', ANSI.bgBlue);
|
|
832
2311
|
const lines = [header];
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
2312
|
+
if (workspaceView) {
|
|
2313
|
+
lines.push(...workspaceView.lines);
|
|
2314
|
+
}
|
|
2315
|
+
else {
|
|
2316
|
+
for (let index = 0; index < contentHeight; index += 1) {
|
|
2317
|
+
const rightLine = index < rightHeights.process
|
|
2318
|
+
? processPane[index]
|
|
2319
|
+
: contextPane[index - rightHeights.process];
|
|
2320
|
+
lines.push(`${sessionsPane[index]}${gap}${messagesPane[index]}${gap}${rightLine}`);
|
|
2321
|
+
}
|
|
838
2322
|
}
|
|
839
2323
|
lines.push(status);
|
|
840
2324
|
lines.push(inputLine);
|
|
841
2325
|
this.paintFrame(lines, { columns, rows });
|
|
2326
|
+
if (workspaceView?.cursor && !this.pendingPrompt) {
|
|
2327
|
+
output.write(`\u001b[${workspaceView.cursor.row};${workspaceView.cursor.column}H`);
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
842
2330
|
const cursorColumn = Math.min(columns, inputPrefix.length + this.cursor + 1);
|
|
843
2331
|
output.write(`\u001b[${rows};${cursorColumn}H`);
|
|
844
2332
|
}
|
|
@@ -869,19 +2357,66 @@ class FullscreenTui {
|
|
|
869
2357
|
? `${this.commands.length} skill${this.commands.length === 1 ? '' : 's'}`
|
|
870
2358
|
: 'no skills';
|
|
871
2359
|
const mode = this.busy ? '[Running]' : '[Idle]';
|
|
872
|
-
return
|
|
2360
|
+
return `${sessionLabel} ${mode} ${skillLabel}`;
|
|
873
2361
|
}
|
|
874
2362
|
buildStatusLine() {
|
|
2363
|
+
if (this.uiMode === 'menu') {
|
|
2364
|
+
const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
|
|
2365
|
+
return `Menu ${menu?.label ?? ''} | ${menu?.items[this.menuState.selectedIndex]?.description ?? 'Choose an action.'}`;
|
|
2366
|
+
}
|
|
2367
|
+
if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
|
|
2368
|
+
const selected = this.pickerState.filteredItems[this.pickerState.selectedIndex];
|
|
2369
|
+
return `${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
|
|
2370
|
+
}
|
|
2371
|
+
if (this.uiMode === 'viewer' && this.viewerState) {
|
|
2372
|
+
return `${this.viewerState.title} | ${this.viewerState.path ? path.relative(this.workspaceRoot, this.viewerState.path) : 'read-only view'}`;
|
|
2373
|
+
}
|
|
2374
|
+
if (this.uiMode === 'editor' && this.editorState) {
|
|
2375
|
+
const dirty = this.editorState.dirty ? 'modified' : 'saved';
|
|
2376
|
+
return `${this.editorState.title} | ${dirty} | Ln ${this.editorState.cursorLine + 1}, Col ${this.editorState.cursorColumn + 1}`;
|
|
2377
|
+
}
|
|
875
2378
|
const focus = paneDisplayName(this.focusedPane);
|
|
876
|
-
if (!this.
|
|
2379
|
+
if (!this.pendingPrompt && this.focusedPane !== 'sessions') {
|
|
877
2380
|
const follow = this.paneScroll[this.focusedPane] === 0 ? 'follow' : `scroll ${this.paneScroll[this.focusedPane]}`;
|
|
878
2381
|
return `${this.statusLine} | Focus ${focus} | ${follow}`;
|
|
879
2382
|
}
|
|
880
|
-
if (this.
|
|
881
|
-
return
|
|
2383
|
+
if (this.pendingPrompt) {
|
|
2384
|
+
return this.pendingPrompt.kind === 'clarify'
|
|
2385
|
+
? `Clarify: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`
|
|
2386
|
+
: `Input: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`;
|
|
882
2387
|
}
|
|
883
2388
|
return `${this.statusLine} | Focus ${focus}`;
|
|
884
2389
|
}
|
|
2390
|
+
buildInputPrefix() {
|
|
2391
|
+
if (this.uiMode === 'menu') {
|
|
2392
|
+
return ' Menu > ';
|
|
2393
|
+
}
|
|
2394
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
2395
|
+
return ' Search > ';
|
|
2396
|
+
}
|
|
2397
|
+
if (this.pendingPrompt?.kind === 'clarify') {
|
|
2398
|
+
return ' Clarify ? ';
|
|
2399
|
+
}
|
|
2400
|
+
if (this.pendingPrompt) {
|
|
2401
|
+
return ' Input > ';
|
|
2402
|
+
}
|
|
2403
|
+
if (this.uiMode === 'editor') {
|
|
2404
|
+
return ' Editor > ';
|
|
2405
|
+
}
|
|
2406
|
+
if (this.uiMode === 'viewer') {
|
|
2407
|
+
return ' Viewer > ';
|
|
2408
|
+
}
|
|
2409
|
+
return ' Command > ';
|
|
2410
|
+
}
|
|
2411
|
+
buildInputValue() {
|
|
2412
|
+
if (this.uiMode === 'editor') {
|
|
2413
|
+
return 'Ctrl+S save Esc close arrows move';
|
|
2414
|
+
}
|
|
2415
|
+
if (this.uiMode === 'viewer') {
|
|
2416
|
+
return this.viewerState?.canEdit ? 'Ctrl+E edit Esc close PgUp/PgDn scroll' : 'Esc close PgUp/PgDn scroll';
|
|
2417
|
+
}
|
|
2418
|
+
return this.inputValue;
|
|
2419
|
+
}
|
|
885
2420
|
buildSessionPaneBody() {
|
|
886
2421
|
if (this.sessions.length === 0) {
|
|
887
2422
|
return ['No sessions yet.'];
|
|
@@ -946,6 +2481,7 @@ class FullscreenTui {
|
|
|
946
2481
|
return ['No session selected.'];
|
|
947
2482
|
}
|
|
948
2483
|
const contextSize = summarizeContextSize(this.sessionDetail);
|
|
2484
|
+
const systemAssetSources = getSystemAssetSourcePaths(this.workspaceRoot);
|
|
949
2485
|
const usageEntries = Object.entries(this.sessionDetail.usageByModel ?? {})
|
|
950
2486
|
.sort((left, right) => right[1].totalTokens - left[1].totalTokens || left[0].localeCompare(right[0]));
|
|
951
2487
|
const body = [
|
|
@@ -956,6 +2492,17 @@ class FullscreenTui {
|
|
|
956
2492
|
`Assistant Memory ${formatByteSize(contextSize.assistantMemoryBytes)} (~${formatInteger(contextSize.assistantMemoryTokens)} tok)`,
|
|
957
2493
|
`Total ${formatByteSize(contextSize.totalBytes)} (~${formatInteger(contextSize.totalTokens)} tok)`,
|
|
958
2494
|
'',
|
|
2495
|
+
'System Asset Sources',
|
|
2496
|
+
`Conductor DML ${formatSystemAssetSourcePath(this.workspaceRoot, systemAssetSources.conductorDml)}`,
|
|
2497
|
+
`Conductor Prompt ${formatSystemAssetSourcePath(this.workspaceRoot, systemAssetSources.conductorPrompt)}`,
|
|
2498
|
+
`Skill Creator DML ${formatSystemAssetSourcePath(this.workspaceRoot, systemAssetSources.skillCreatorDml)}`,
|
|
2499
|
+
`Skill Creator Prompt ${formatSystemAssetSourcePath(this.workspaceRoot, systemAssetSources.skillCreatorPrompt)}`,
|
|
2500
|
+
'Changes apply on the next turn or /compile run. No TUI reload is required.',
|
|
2501
|
+
'',
|
|
2502
|
+
'Shell Execution',
|
|
2503
|
+
`Backend ${this.options.sandbox ? 'sandbox' : 'host'}`,
|
|
2504
|
+
`CWD ${this.shellWorkspacePath}`,
|
|
2505
|
+
'',
|
|
959
2506
|
'Session Token Usage',
|
|
960
2507
|
];
|
|
961
2508
|
if (usageEntries.length === 0) {
|
|
@@ -987,6 +2534,141 @@ class FullscreenTui {
|
|
|
987
2534
|
const paneTitle = this.buildPaneTitle(title, kind, metrics);
|
|
988
2535
|
return buildPane(paneTitle, metrics.lines.slice(metrics.start, metrics.start + metrics.viewportLines), width, height, kind, this.focusedPane === kind);
|
|
989
2536
|
}
|
|
2537
|
+
buildWorkspacePaneView(width, height) {
|
|
2538
|
+
if (this.uiMode === 'editor' && this.editorState) {
|
|
2539
|
+
return this.buildEditorPaneView(width, height, this.editorState);
|
|
2540
|
+
}
|
|
2541
|
+
if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
|
|
2542
|
+
return {
|
|
2543
|
+
lines: buildPane(this.pickerState.title, this.buildPickerPaneBody(this.pickerState), width, height, 'workspace', true),
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
if (this.uiMode === 'viewer' && this.viewerState) {
|
|
2547
|
+
return {
|
|
2548
|
+
lines: buildPane(this.viewerState.title, this.buildViewerPaneBody(this.viewerState), width, height, 'workspace', true),
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
|
|
2552
|
+
return {
|
|
2553
|
+
lines: buildPane(`Menu: ${menu?.label ?? 'Menu'}`, this.buildMenuPaneBody(menu), width, height, 'workspace', true),
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
buildMenuPaneBody(menu) {
|
|
2557
|
+
if (!menu) {
|
|
2558
|
+
return ['No menu available.'];
|
|
2559
|
+
}
|
|
2560
|
+
const lines = [
|
|
2561
|
+
'Use Left/Right or 1-6 to move across the menu bar, Up/Down to choose an item, and Enter to run it.',
|
|
2562
|
+
'',
|
|
2563
|
+
];
|
|
2564
|
+
menu.items.forEach((item, index) => {
|
|
2565
|
+
const prefix = index === this.menuState.selectedIndex ? '>' : ' ';
|
|
2566
|
+
const suffix = item.enabled === false ? ' [disabled]' : item.shortcut ? ` [${item.shortcut}]` : '';
|
|
2567
|
+
lines.push(`${prefix} ${item.label}${suffix}`);
|
|
2568
|
+
lines.push(` ${item.description}`);
|
|
2569
|
+
lines.push('');
|
|
2570
|
+
});
|
|
2571
|
+
return lines;
|
|
2572
|
+
}
|
|
2573
|
+
buildPickerPaneBody(state) {
|
|
2574
|
+
const lines = [
|
|
2575
|
+
`Search: ${this.inputValue || '(type to filter)'}`,
|
|
2576
|
+
`Results: ${state.filteredItems.length}/${state.items.length}`,
|
|
2577
|
+
'',
|
|
2578
|
+
];
|
|
2579
|
+
if (state.filteredItems.length === 0) {
|
|
2580
|
+
lines.push(state.emptyText);
|
|
2581
|
+
return lines;
|
|
2582
|
+
}
|
|
2583
|
+
state.filteredItems.forEach((item, index) => {
|
|
2584
|
+
const prefix = index === state.selectedIndex ? '>' : ' ';
|
|
2585
|
+
lines.push(`${prefix} ${item.label}${item.detail ? ` ${item.detail}` : ''}`);
|
|
2586
|
+
lines.push(` ${item.description}`);
|
|
2587
|
+
lines.push('');
|
|
2588
|
+
});
|
|
2589
|
+
return lines.slice(state.scrollTop);
|
|
2590
|
+
}
|
|
2591
|
+
buildViewerPaneBody(state) {
|
|
2592
|
+
const lines = [
|
|
2593
|
+
state.path ? `Path: ${path.relative(this.workspaceRoot, state.path)}` : 'Read-only view',
|
|
2594
|
+
state.canEdit ? 'Ctrl+E opens the editor for this file.' : 'Read-only content.',
|
|
2595
|
+
'',
|
|
2596
|
+
...state.content.split(/\r?\n/).map((line, index) => `${String(index + 1).padStart(4, ' ')} | ${line}`),
|
|
2597
|
+
];
|
|
2598
|
+
return lines.slice(state.scrollTop);
|
|
2599
|
+
}
|
|
2600
|
+
buildEditorPaneView(width, height, state) {
|
|
2601
|
+
const innerWidth = Math.max(1, width - 2);
|
|
2602
|
+
const viewportLines = Math.max(0, height - 2);
|
|
2603
|
+
const lineNumberWidth = Math.max(2, String(Math.max(1, state.lines.length)).length);
|
|
2604
|
+
const prefixWidth = 2 + lineNumberWidth + 3;
|
|
2605
|
+
const contentWidth = Math.max(1, innerWidth - prefixWidth);
|
|
2606
|
+
const maxStart = Math.max(0, state.lines.length - viewportLines);
|
|
2607
|
+
state.scrollTop = clamp(state.scrollTop, 0, maxStart);
|
|
2608
|
+
const lines = [renderWindowTop(`${state.title}${state.dirty ? ' [modified]' : ''}`, innerWidth, true)];
|
|
2609
|
+
for (let offset = 0; offset < viewportLines; offset += 1) {
|
|
2610
|
+
const lineIndex = state.scrollTop + offset;
|
|
2611
|
+
const lineNumber = String(lineIndex + 1).padStart(lineNumberWidth, ' ');
|
|
2612
|
+
const content = state.lines[lineIndex] ?? '';
|
|
2613
|
+
const visibleContent = content.slice(state.scrollLeft, state.scrollLeft + contentWidth);
|
|
2614
|
+
const prefix = lineIndex === state.cursorLine ? '> ' : ' ';
|
|
2615
|
+
const rowText = `${prefix}${lineNumber} | ${visibleContent}`;
|
|
2616
|
+
lines.push(renderWindowBody(padRight(rowText, innerWidth), innerWidth, 'workspace'));
|
|
2617
|
+
}
|
|
2618
|
+
if (height >= 2) {
|
|
2619
|
+
lines.push(renderWindowBottom(innerWidth, true));
|
|
2620
|
+
}
|
|
2621
|
+
const cursorRow = Math.min(2 + viewportLines, 3 + Math.max(0, state.cursorLine - state.scrollTop));
|
|
2622
|
+
const cursorColumn = Math.min(width, 2 + prefixWidth + Math.max(0, state.cursorColumn - state.scrollLeft));
|
|
2623
|
+
return {
|
|
2624
|
+
lines: lines.slice(0, height),
|
|
2625
|
+
cursor: { row: cursorRow, column: cursorColumn },
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
buildHotkeys() {
|
|
2629
|
+
if (this.uiMode === 'menu') {
|
|
2630
|
+
return [
|
|
2631
|
+
['1-6', 'Menu'],
|
|
2632
|
+
['<- ->', 'Switch'],
|
|
2633
|
+
['Up/Down', 'Item'],
|
|
2634
|
+
['Enter', 'Run'],
|
|
2635
|
+
['Esc', 'Close'],
|
|
2636
|
+
];
|
|
2637
|
+
}
|
|
2638
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
2639
|
+
return [
|
|
2640
|
+
['type', 'Filter'],
|
|
2641
|
+
['Up/Down', 'Pick'],
|
|
2642
|
+
['Enter', 'Open'],
|
|
2643
|
+
['Ctrl+E', 'Edit'],
|
|
2644
|
+
['Esc', 'Close'],
|
|
2645
|
+
];
|
|
2646
|
+
}
|
|
2647
|
+
if (this.uiMode === 'viewer') {
|
|
2648
|
+
return [
|
|
2649
|
+
['PgUp/PgDn', 'Scroll'],
|
|
2650
|
+
['Ctrl+E', 'Edit'],
|
|
2651
|
+
['Ctrl+R', 'Run'],
|
|
2652
|
+
['Esc', 'Close'],
|
|
2653
|
+
];
|
|
2654
|
+
}
|
|
2655
|
+
if (this.uiMode === 'editor') {
|
|
2656
|
+
return [
|
|
2657
|
+
['Arrows', 'Move'],
|
|
2658
|
+
['Enter', 'Split'],
|
|
2659
|
+
['Ctrl+S', 'Save'],
|
|
2660
|
+
['Esc', 'Close'],
|
|
2661
|
+
];
|
|
2662
|
+
}
|
|
2663
|
+
return [
|
|
2664
|
+
['F10', 'Menu'],
|
|
2665
|
+
['Ctrl+P', 'Palette'],
|
|
2666
|
+
['<- ->', 'Pane'],
|
|
2667
|
+
[this.focusedPane === 'sessions' ? 'Up/Down' : 'PgUp/PgDn', this.focusedPane === 'sessions' ? 'Select' : 'Scroll'],
|
|
2668
|
+
['End', this.focusedPane === 'sessions' ? 'Bottom' : 'Follow'],
|
|
2669
|
+
['/cancel', 'Stop'],
|
|
2670
|
+
];
|
|
2671
|
+
}
|
|
990
2672
|
buildPaneTitle(title, kind, metrics) {
|
|
991
2673
|
if (kind === 'sessions') {
|
|
992
2674
|
return this.focusedPane === kind ? `${title} [focus]` : title;
|
|
@@ -1047,14 +2729,7 @@ class FullscreenTui {
|
|
|
1047
2729
|
}
|
|
1048
2730
|
formatMessageHeader(entry) {
|
|
1049
2731
|
const spinner = entry.pending ? ` ${currentSpinnerFrame()}` : '';
|
|
1050
|
-
|
|
1051
|
-
case 'user':
|
|
1052
|
-
return '[You]';
|
|
1053
|
-
case 'assistant':
|
|
1054
|
-
return entry.error ? '[Assistant Error]' : `[Assistant${spinner}]`;
|
|
1055
|
-
case 'system':
|
|
1056
|
-
return `[System${spinner}]`;
|
|
1057
|
-
}
|
|
2732
|
+
return formatDisplayMessageHeader(entry, spinner);
|
|
1058
2733
|
}
|
|
1059
2734
|
close() {
|
|
1060
2735
|
if (!this.closeResolver) {
|
|
@@ -1065,7 +2740,7 @@ class FullscreenTui {
|
|
|
1065
2740
|
resolve();
|
|
1066
2741
|
}
|
|
1067
2742
|
isEditingInput() {
|
|
1068
|
-
return
|
|
2743
|
+
return this.uiMode !== 'editor' && this.inputValue.length > 0;
|
|
1069
2744
|
}
|
|
1070
2745
|
}
|
|
1071
2746
|
export async function startTui(workspaceRoot = process.cwd(), options = {}) {
|
|
@@ -1087,7 +2762,7 @@ async function startLineTui(workspaceRoot = process.cwd(), options = {}) {
|
|
|
1087
2762
|
if (!line) {
|
|
1088
2763
|
continue;
|
|
1089
2764
|
}
|
|
1090
|
-
const parsed =
|
|
2765
|
+
const parsed = parseCommandBarInput(line);
|
|
1091
2766
|
if (parsed.kind === 'builtin') {
|
|
1092
2767
|
switch (parsed.name) {
|
|
1093
2768
|
case 'cancel':
|
|
@@ -1112,7 +2787,73 @@ async function startLineTui(workspaceRoot = process.cwd(), options = {}) {
|
|
|
1112
2787
|
console.log(helpLine);
|
|
1113
2788
|
}
|
|
1114
2789
|
continue;
|
|
2790
|
+
case 'compile':
|
|
2791
|
+
case 'skill-creator':
|
|
2792
|
+
if (!parsed.rawArgs.trim()) {
|
|
2793
|
+
console.log(`Provide a skill specification after /${parsed.name}.`);
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
console.log(divider('-'));
|
|
2797
|
+
console.log(`${paint('skill', ANSI.dim)} /${parsed.name} ${parsed.rawArgs}`);
|
|
2798
|
+
console.log(divider('-'));
|
|
2799
|
+
{
|
|
2800
|
+
const printer = new LiveExecutionPrinter();
|
|
2801
|
+
try {
|
|
2802
|
+
const result = await runSkillCreatorCommand(workspaceRoot, session.id, parsed.rawArgs, {
|
|
2803
|
+
sandbox: options.sandbox,
|
|
2804
|
+
onUserInput: async (question) => {
|
|
2805
|
+
printer.finish();
|
|
2806
|
+
console.log(`${paint('clarify', ANSI.yellow)} ${question}`);
|
|
2807
|
+
return (await rl.question('> ')).trim();
|
|
2808
|
+
},
|
|
2809
|
+
onEvent: (event) => printer.handle(event),
|
|
2810
|
+
});
|
|
2811
|
+
printer.finish();
|
|
2812
|
+
console.log(formatSkillCreatorSummary(workspaceRoot, result));
|
|
2813
|
+
}
|
|
2814
|
+
catch (error) {
|
|
2815
|
+
printer.finish();
|
|
2816
|
+
console.log(`${paint('error', ANSI.red)} ${error.message}`);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
console.log('');
|
|
2820
|
+
console.log(divider('-'));
|
|
2821
|
+
console.log('');
|
|
2822
|
+
continue;
|
|
2823
|
+
case 'set-model': {
|
|
2824
|
+
try {
|
|
2825
|
+
const { model, slot } = parseSetModelCommandArgs(parsed.rawArgs);
|
|
2826
|
+
const result = await setModel(workspaceRoot, model, slot);
|
|
2827
|
+
console.log(`Model set to ${result.modelId} (${result.updatedSlots.join(', ')}).`);
|
|
2828
|
+
}
|
|
2829
|
+
catch (error) {
|
|
2830
|
+
console.log(`${paint('error', ANSI.red)} ${error.message}`);
|
|
2831
|
+
}
|
|
2832
|
+
continue;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
if (parsed.kind === 'shell') {
|
|
2837
|
+
console.log(divider('-'));
|
|
2838
|
+
console.log(`${paint('shell', ANSI.dim)} !${parsed.command}`);
|
|
2839
|
+
console.log(divider('-'));
|
|
2840
|
+
const printer = new LiveExecutionPrinter();
|
|
2841
|
+
try {
|
|
2842
|
+
const result = await runShellCommand(workspaceRoot, parsed.command, {
|
|
2843
|
+
sandbox: options.sandbox,
|
|
2844
|
+
onEvent: (event) => printer.handle(event),
|
|
2845
|
+
});
|
|
2846
|
+
printer.finish();
|
|
2847
|
+
console.log(formatShellCommandSummary(result));
|
|
1115
2848
|
}
|
|
2849
|
+
catch (error) {
|
|
2850
|
+
printer.finish();
|
|
2851
|
+
console.log(`${paint('error', ANSI.red)} ${error.message}`);
|
|
2852
|
+
}
|
|
2853
|
+
console.log('');
|
|
2854
|
+
console.log(divider('-'));
|
|
2855
|
+
console.log('');
|
|
2856
|
+
continue;
|
|
1116
2857
|
}
|
|
1117
2858
|
if (parsed.kind === 'skill') {
|
|
1118
2859
|
console.log(divider('-'));
|
|
@@ -1209,9 +2950,122 @@ async function runSkillCommand(workspaceRoot, skillName, args, options = {}) {
|
|
|
1209
2950
|
signal: options.signal,
|
|
1210
2951
|
onUserInput: options.onUserInput,
|
|
1211
2952
|
onEvent: (event) => options.onEvent?.({ scope: 'child', childSlug: skillName, event }),
|
|
2953
|
+
onChildEvent: (childSlug, event) => options.onEvent?.({ scope: 'child', childSlug, event }),
|
|
1212
2954
|
});
|
|
1213
2955
|
return result;
|
|
1214
2956
|
}
|
|
2957
|
+
async function runSkillCreatorCommand(workspaceRoot, sessionId, spec, options = {}) {
|
|
2958
|
+
const trimmedSpec = spec.trim();
|
|
2959
|
+
if (!trimmedSpec) {
|
|
2960
|
+
throw new Error('spec is required');
|
|
2961
|
+
}
|
|
2962
|
+
const config = await loadConfig(workspaceRoot);
|
|
2963
|
+
const compileSelection = resolveModelSlot(config, 'compile');
|
|
2964
|
+
const runSelection = resolveModelSlot(config, 'run');
|
|
2965
|
+
return createLocalSkill({
|
|
2966
|
+
spec: trimmedSpec,
|
|
2967
|
+
workspaceRoot,
|
|
2968
|
+
workspacePath: path.resolve(workspaceRoot, config.workspace || './workspace'),
|
|
2969
|
+
config,
|
|
2970
|
+
compileSelection,
|
|
2971
|
+
runSelection,
|
|
2972
|
+
sessionId,
|
|
2973
|
+
sandbox: options.sandbox ?? false,
|
|
2974
|
+
signal: options.signal,
|
|
2975
|
+
onUserInput: options.onUserInput ?? (async () => ''),
|
|
2976
|
+
onEvent: options.onEvent,
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
async function runShellCommand(workspaceRoot, command, options = {}) {
|
|
2980
|
+
const trimmedCommand = command.trim();
|
|
2981
|
+
if (!trimmedCommand) {
|
|
2982
|
+
throw new Error('command is required');
|
|
2983
|
+
}
|
|
2984
|
+
const toolName = 'shell';
|
|
2985
|
+
const toolArgs = { command: trimmedCommand };
|
|
2986
|
+
const emit = (event) => options.onEvent?.({ scope: 'main', event });
|
|
2987
|
+
emit(buildToolStartEvent(toolName, toolArgs));
|
|
2988
|
+
let shellManager = null;
|
|
2989
|
+
try {
|
|
2990
|
+
const config = await loadConfig(workspaceRoot);
|
|
2991
|
+
const workspacePath = path.resolve(workspaceRoot, config.workspace || './workspace');
|
|
2992
|
+
shellManager = createShellManager({
|
|
2993
|
+
workspacePath,
|
|
2994
|
+
sandbox: options.sandbox,
|
|
2995
|
+
});
|
|
2996
|
+
const observer = createShellToolEventBridge({
|
|
2997
|
+
toolName,
|
|
2998
|
+
toolArgs,
|
|
2999
|
+
emit,
|
|
3000
|
+
});
|
|
3001
|
+
const result = await shellManager.exec(trimmedCommand, options.signal, observer);
|
|
3002
|
+
emit(buildToolCompletionEvent(toolName, toolArgs, result));
|
|
3003
|
+
return result;
|
|
3004
|
+
}
|
|
3005
|
+
catch (error) {
|
|
3006
|
+
emit(buildToolFailureEvent(toolName, toolArgs, error));
|
|
3007
|
+
throw error;
|
|
3008
|
+
}
|
|
3009
|
+
finally {
|
|
3010
|
+
await shellManager?.dispose();
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
function formatSkillCreatorSummary(workspaceRoot, result) {
|
|
3014
|
+
const slug = typeof result.slug === 'string' ? result.slug : 'unknown-skill';
|
|
3015
|
+
const outputPath = typeof result.output_path === 'string' ? result.output_path : '';
|
|
3016
|
+
if (!outputPath) {
|
|
3017
|
+
return `Published /${slug}.`;
|
|
3018
|
+
}
|
|
3019
|
+
const relativeOutputPath = path.relative(workspaceRoot, outputPath);
|
|
3020
|
+
return `Published /${slug} to ${relativeOutputPath || outputPath}.`;
|
|
3021
|
+
}
|
|
3022
|
+
function formatShellCommandSummary(result) {
|
|
3023
|
+
if (result.success) {
|
|
3024
|
+
return result.summary || 'Shell command completed successfully.';
|
|
3025
|
+
}
|
|
3026
|
+
return result.summary || `Shell command failed with exit code ${result.exitCode}.`;
|
|
3027
|
+
}
|
|
3028
|
+
function isAbortError(error) {
|
|
3029
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
3030
|
+
}
|
|
3031
|
+
export function previewMessageFromEvent(logEvent) {
|
|
3032
|
+
switch (logEvent.event.type) {
|
|
3033
|
+
case 'output':
|
|
3034
|
+
if (!logEvent.event.content) {
|
|
3035
|
+
return null;
|
|
3036
|
+
}
|
|
3037
|
+
return {
|
|
3038
|
+
role: 'system',
|
|
3039
|
+
kind: 'output',
|
|
3040
|
+
tag: logEvent.scope === 'child' ? logEvent.childSlug : undefined,
|
|
3041
|
+
content: logEvent.event.content,
|
|
3042
|
+
};
|
|
3043
|
+
case 'input_required':
|
|
3044
|
+
if (!logEvent.event.prompt) {
|
|
3045
|
+
return null;
|
|
3046
|
+
}
|
|
3047
|
+
return previewQuestionMessage(logEvent.event.prompt, logEvent.scope === 'child' ? logEvent.childSlug : undefined);
|
|
3048
|
+
default:
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
export function previewChildSkillActivityMessage(childSlug) {
|
|
3053
|
+
return {
|
|
3054
|
+
role: 'system',
|
|
3055
|
+
kind: 'output',
|
|
3056
|
+
tag: childSlug,
|
|
3057
|
+
content: 'Running child skill...',
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
export function previewQuestionMessage(promptText, explicitTag) {
|
|
3061
|
+
const parsed = explicitTag ? { tag: explicitTag, content: promptText } : parseTaggedPrompt(promptText);
|
|
3062
|
+
return {
|
|
3063
|
+
role: 'system',
|
|
3064
|
+
kind: 'question',
|
|
3065
|
+
tag: parsed.tag,
|
|
3066
|
+
content: parsed.content,
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
1215
3069
|
export function parseCommandArgs(rawArgs) {
|
|
1216
3070
|
const args = [];
|
|
1217
3071
|
let current = '';
|
|
@@ -1257,7 +3111,14 @@ export function parseCommandArgs(rawArgs) {
|
|
|
1257
3111
|
}
|
|
1258
3112
|
return args;
|
|
1259
3113
|
}
|
|
1260
|
-
export function
|
|
3114
|
+
export function parseCommandBarInput(line) {
|
|
3115
|
+
const trimmedStart = line.trimStart();
|
|
3116
|
+
if (trimmedStart.startsWith('\\!')) {
|
|
3117
|
+
return { kind: 'text', prompt: trimmedStart.slice(1).trim() };
|
|
3118
|
+
}
|
|
3119
|
+
if (trimmedStart.startsWith('!')) {
|
|
3120
|
+
return { kind: 'shell', command: trimmedStart.slice(1).trim() };
|
|
3121
|
+
}
|
|
1261
3122
|
const trimmed = line.trim();
|
|
1262
3123
|
if (!trimmed.startsWith('/')) {
|
|
1263
3124
|
return { kind: 'text', prompt: trimmed };
|
|
@@ -1272,11 +3133,14 @@ export function parseSlashInput(line) {
|
|
|
1272
3133
|
const name = match[1];
|
|
1273
3134
|
const rawArgs = (match[2] ?? '').trim();
|
|
1274
3135
|
if (BUILTIN_SLASH_COMMANDS.includes(name)) {
|
|
3136
|
+
const passthroughRawArgBuiltins = new Set(['new', 'compile', 'skill-creator']);
|
|
1275
3137
|
return {
|
|
1276
3138
|
kind: 'builtin',
|
|
1277
3139
|
name: name,
|
|
1278
3140
|
rawArgs,
|
|
1279
|
-
args: name
|
|
3141
|
+
args: passthroughRawArgBuiltins.has(name) && rawArgs
|
|
3142
|
+
? [rawArgs]
|
|
3143
|
+
: parseCommandArgs(rawArgs),
|
|
1280
3144
|
};
|
|
1281
3145
|
}
|
|
1282
3146
|
return {
|
|
@@ -1286,6 +3150,9 @@ export function parseSlashInput(line) {
|
|
|
1286
3150
|
args: parseCommandArgs(rawArgs),
|
|
1287
3151
|
};
|
|
1288
3152
|
}
|
|
3153
|
+
export function parseSlashInput(line) {
|
|
3154
|
+
return parseCommandBarInput(line);
|
|
3155
|
+
}
|
|
1289
3156
|
export function canSubmitParsedInputWhileBusy(parsed) {
|
|
1290
3157
|
return parsed.kind === 'builtin' && (parsed.name === 'cancel' || parsed.name === 'exit' || parsed.name === 'quit');
|
|
1291
3158
|
}
|
|
@@ -1368,21 +3235,117 @@ async function createSessionInteractive(rl, workspaceRoot) {
|
|
|
1368
3235
|
function renderHeader(session) {
|
|
1369
3236
|
console.log(divider('='));
|
|
1370
3237
|
console.log(`${paint('DeepClause', ANSI.bold, ANSI.cyan)} ${paint('interactive conductor', ANSI.dim)}`);
|
|
1371
|
-
console.log(`${paint('commands', ANSI.dim)} /new /sessions /help /exit /skill args`);
|
|
3238
|
+
console.log(`${paint('commands', ANSI.dim)} /compile <spec> /set-model <model> /new /sessions /help /exit /skill args`);
|
|
1372
3239
|
console.log(`${paint('session', ANSI.dim)} ${paint(session.id, ANSI.cyan)}`);
|
|
1373
3240
|
console.log(divider('='));
|
|
1374
3241
|
console.log('');
|
|
1375
3242
|
}
|
|
3243
|
+
export function parseSetModelCommandArgs(rawArgs) {
|
|
3244
|
+
const args = parseCommandArgs(rawArgs);
|
|
3245
|
+
let model;
|
|
3246
|
+
let slot;
|
|
3247
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3248
|
+
const arg = args[index];
|
|
3249
|
+
if (arg === '--slot') {
|
|
3250
|
+
const candidate = args[index + 1];
|
|
3251
|
+
if (!candidate) {
|
|
3252
|
+
throw new Error('Provide a slot after --slot.');
|
|
3253
|
+
}
|
|
3254
|
+
if (!isModelSlot(candidate)) {
|
|
3255
|
+
throw new Error('Slot must be one of: gateway, run, compile.');
|
|
3256
|
+
}
|
|
3257
|
+
if (slot) {
|
|
3258
|
+
throw new Error('Provide --slot only once.');
|
|
3259
|
+
}
|
|
3260
|
+
slot = candidate;
|
|
3261
|
+
index += 1;
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
if (arg.startsWith('--slot=')) {
|
|
3265
|
+
const candidate = arg.slice('--slot='.length);
|
|
3266
|
+
if (!isModelSlot(candidate)) {
|
|
3267
|
+
throw new Error('Slot must be one of: gateway, run, compile.');
|
|
3268
|
+
}
|
|
3269
|
+
if (slot) {
|
|
3270
|
+
throw new Error('Provide --slot only once.');
|
|
3271
|
+
}
|
|
3272
|
+
slot = candidate;
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
if (arg.startsWith('--')) {
|
|
3276
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
3277
|
+
}
|
|
3278
|
+
if (model) {
|
|
3279
|
+
throw new Error('Provide exactly one model for /set-model.');
|
|
3280
|
+
}
|
|
3281
|
+
model = arg;
|
|
3282
|
+
}
|
|
3283
|
+
if (!model) {
|
|
3284
|
+
throw new Error('Provide a model after /set-model.');
|
|
3285
|
+
}
|
|
3286
|
+
return { model, slot };
|
|
3287
|
+
}
|
|
1376
3288
|
function streamLabel(logEvent) {
|
|
1377
3289
|
return logEvent.scope === 'child'
|
|
1378
|
-
?
|
|
3290
|
+
? `${CHILD_EVENT_INDENT}[${logEvent.childSlug ?? '?'}] llm`
|
|
1379
3291
|
: 'llm';
|
|
1380
3292
|
}
|
|
1381
3293
|
function formatEventPrefix(logEvent) {
|
|
1382
3294
|
return logEvent.scope === 'child'
|
|
1383
|
-
?
|
|
3295
|
+
? `${CHILD_EVENT_INDENT}[${logEvent.childSlug ?? '?'}] `
|
|
1384
3296
|
: '';
|
|
1385
3297
|
}
|
|
3298
|
+
function toolScopeKey(logEvent) {
|
|
3299
|
+
return logEvent.scope === 'child'
|
|
3300
|
+
? `child:${logEvent.childSlug ?? '?'}`
|
|
3301
|
+
: 'main';
|
|
3302
|
+
}
|
|
3303
|
+
function toolScopeLabel(logEvent) {
|
|
3304
|
+
return logEvent.scope === 'child'
|
|
3305
|
+
? logEvent.childSlug ?? '?'
|
|
3306
|
+
: 'main';
|
|
3307
|
+
}
|
|
3308
|
+
function parseTaggedPrompt(promptText) {
|
|
3309
|
+
const match = promptText.match(/^\[([^\]]+)\]\s*(.*)$/);
|
|
3310
|
+
if (!match) {
|
|
3311
|
+
return { content: promptText };
|
|
3312
|
+
}
|
|
3313
|
+
return {
|
|
3314
|
+
tag: match[1],
|
|
3315
|
+
content: match[2] || promptText,
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
function sameDisplayMessage(left, right) {
|
|
3319
|
+
return !!left
|
|
3320
|
+
&& left.role === right.role
|
|
3321
|
+
&& left.kind === right.kind
|
|
3322
|
+
&& left.tag === right.tag
|
|
3323
|
+
&& left.content === right.content;
|
|
3324
|
+
}
|
|
3325
|
+
function formatSystemMessageHeader(entry, spinner) {
|
|
3326
|
+
const base = entry.kind === 'output'
|
|
3327
|
+
? 'Output'
|
|
3328
|
+
: entry.kind === 'question'
|
|
3329
|
+
? 'Question'
|
|
3330
|
+
: 'System';
|
|
3331
|
+
return entry.tag
|
|
3332
|
+
? `[${base}: ${entry.tag}${spinner}]`
|
|
3333
|
+
: `[${base}${spinner}]`;
|
|
3334
|
+
}
|
|
3335
|
+
export function formatDisplayMessageHeader(entry, spinner = '') {
|
|
3336
|
+
switch (entry.role) {
|
|
3337
|
+
case 'user':
|
|
3338
|
+
return '[You]';
|
|
3339
|
+
case 'assistant': {
|
|
3340
|
+
const base = entry.error ? 'Assistant Error' : 'Assistant';
|
|
3341
|
+
return entry.tag
|
|
3342
|
+
? `[${base}: ${entry.tag}${spinner}]`
|
|
3343
|
+
: `[${base}${spinner}]`;
|
|
3344
|
+
}
|
|
3345
|
+
case 'system':
|
|
3346
|
+
return formatSystemMessageHeader(entry, spinner);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
1386
3349
|
function formatToolArgs(args) {
|
|
1387
3350
|
if (!args) {
|
|
1388
3351
|
return '';
|
|
@@ -1397,16 +3360,74 @@ function formatToolArgs(args) {
|
|
|
1397
3360
|
}
|
|
1398
3361
|
return parts.join(', ');
|
|
1399
3362
|
}
|
|
3363
|
+
function formatSystemAssetSourcePath(workspaceRoot, filePath) {
|
|
3364
|
+
const relativePath = path.relative(workspaceRoot, filePath);
|
|
3365
|
+
if (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
|
3366
|
+
return relativePath || '.';
|
|
3367
|
+
}
|
|
3368
|
+
return filePath;
|
|
3369
|
+
}
|
|
3370
|
+
function formatToolEventLine(logEvent) {
|
|
3371
|
+
const { event } = logEvent;
|
|
3372
|
+
if (!event.toolName || IGNORED_LIVE_LOG_TOOLS.has(event.toolName)) {
|
|
3373
|
+
return null;
|
|
3374
|
+
}
|
|
3375
|
+
const prefix = formatEventPrefix(logEvent);
|
|
3376
|
+
const callLabel = `${event.toolName}(${formatToolArgs(event.toolArgs)})`;
|
|
3377
|
+
if (!event.toolState) {
|
|
3378
|
+
return `${prefix}tool ${callLabel}`;
|
|
3379
|
+
}
|
|
3380
|
+
return `${prefix}tool ${callLabel} ${event.toolState}${formatToolEventDetails(event)}`;
|
|
3381
|
+
}
|
|
3382
|
+
function formatToolEventDetails(event) {
|
|
3383
|
+
const details = [];
|
|
3384
|
+
if (typeof event.toolPid === 'number') {
|
|
3385
|
+
details.push(`pid=${event.toolPid}`);
|
|
3386
|
+
}
|
|
3387
|
+
else if (event.toolBackend === 'sandbox') {
|
|
3388
|
+
details.push('sandbox');
|
|
3389
|
+
}
|
|
3390
|
+
if (typeof event.toolExitCode === 'number') {
|
|
3391
|
+
details.push(`exit=${event.toolExitCode}`);
|
|
3392
|
+
}
|
|
3393
|
+
const message = event.toolError
|
|
3394
|
+
?? (event.toolSummary && event.toolSummary !== 'Command completed successfully' ? event.toolSummary : undefined);
|
|
3395
|
+
if (message) {
|
|
3396
|
+
details.push(message);
|
|
3397
|
+
}
|
|
3398
|
+
return details.length > 0 ? ` ${details.join(' ')}` : '';
|
|
3399
|
+
}
|
|
3400
|
+
function formatActiveToolStatus(entry) {
|
|
3401
|
+
const details = [];
|
|
3402
|
+
if (typeof entry.toolPid === 'number') {
|
|
3403
|
+
details.push(`pid=${entry.toolPid}`);
|
|
3404
|
+
}
|
|
3405
|
+
else if (entry.toolBackend === 'sandbox') {
|
|
3406
|
+
details.push('sandbox');
|
|
3407
|
+
}
|
|
3408
|
+
const detailSuffix = details.length > 0 ? ` ${details.join(' ')}` : '';
|
|
3409
|
+
return `${entry.scopeLabel} ${entry.toolName} ${entry.toolState}${detailSuffix}`;
|
|
3410
|
+
}
|
|
3411
|
+
function formatShellModeStatus(workspaceRoot, shellWorkspacePath, sandbox) {
|
|
3412
|
+
const displayPath = path.relative(workspaceRoot, shellWorkspacePath) || '.';
|
|
3413
|
+
return `Shell mode: ${sandbox ? 'AgentVM sandbox' : 'host shell'} in ${displayPath}. Enter runs, /cancel stops.`;
|
|
3414
|
+
}
|
|
1400
3415
|
function buildHelpLines() {
|
|
1401
3416
|
return [
|
|
1402
3417
|
'commands',
|
|
1403
3418
|
'/new [title] create a new conductor session',
|
|
1404
3419
|
'/sessions refresh or choose sessions',
|
|
3420
|
+
'/compile <spec> compile and publish a new local skill',
|
|
3421
|
+
'/skill-creator <spec> alias for /compile',
|
|
3422
|
+
'/set-model <model> [--slot <slot>] update the configured model selection',
|
|
1405
3423
|
'/cancel abort the current execution',
|
|
1406
3424
|
'/help show this help',
|
|
1407
3425
|
'/quit exit the TUI',
|
|
1408
3426
|
'/<skill> [args] run a compiled skill directly',
|
|
3427
|
+
'!<command> run a shell command directly in the workspace shell',
|
|
1409
3428
|
'keys',
|
|
3429
|
+
'F10 / Ctrl+G open the menu bar',
|
|
3430
|
+
'Ctrl+P open the action palette',
|
|
1410
3431
|
'Left/Right change focused pane',
|
|
1411
3432
|
'Up/Down select session or scroll focused pane',
|
|
1412
3433
|
'PgUp/PgDn page-scroll the focused pane',
|
|
@@ -1414,6 +3435,19 @@ function buildHelpLines() {
|
|
|
1414
3435
|
'Tab autocomplete /commands and /skills',
|
|
1415
3436
|
];
|
|
1416
3437
|
}
|
|
3438
|
+
function getSlashHelpLines() {
|
|
3439
|
+
const lines = buildHelpLines();
|
|
3440
|
+
const keysIndex = lines.indexOf('keys');
|
|
3441
|
+
return keysIndex === -1 ? lines : lines.slice(0, keysIndex);
|
|
3442
|
+
}
|
|
3443
|
+
function getShortcutHelpLines() {
|
|
3444
|
+
const lines = buildHelpLines();
|
|
3445
|
+
const keysIndex = lines.indexOf('keys');
|
|
3446
|
+
return keysIndex === -1 ? [] : lines.slice(keysIndex);
|
|
3447
|
+
}
|
|
3448
|
+
function isModelSlot(value) {
|
|
3449
|
+
return MODEL_SLOTS.includes(value);
|
|
3450
|
+
}
|
|
1417
3451
|
function computePaneWidths(columns) {
|
|
1418
3452
|
const available = Math.max(36, columns - 2);
|
|
1419
3453
|
let left = Math.max(18, Math.floor(available * 0.2));
|
|
@@ -1468,45 +3502,144 @@ function buildPane(title, body, width, height, kind, active = false) {
|
|
|
1468
3502
|
}
|
|
1469
3503
|
return lines.slice(0, height);
|
|
1470
3504
|
}
|
|
1471
|
-
function wrapPlainText(text, width) {
|
|
3505
|
+
export function wrapPlainText(text, width) {
|
|
1472
3506
|
if (width <= 0) {
|
|
1473
3507
|
return [''];
|
|
1474
3508
|
}
|
|
1475
3509
|
const result = [];
|
|
1476
3510
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
1477
|
-
const line = rawLine || '';
|
|
3511
|
+
const line = expandTabs(rawLine || '', CHILD_EVENT_TAB_WIDTH);
|
|
1478
3512
|
if (!line) {
|
|
1479
3513
|
result.push('');
|
|
1480
3514
|
continue;
|
|
1481
3515
|
}
|
|
1482
3516
|
let remaining = line;
|
|
1483
|
-
while (remaining
|
|
1484
|
-
const slice = remaining
|
|
3517
|
+
while (measureDisplayWidth(remaining) > width) {
|
|
3518
|
+
const { head: slice, tail } = splitByDisplayWidth(remaining, width, { forceOne: true });
|
|
1485
3519
|
const breakIndex = slice.lastIndexOf(' ');
|
|
1486
|
-
|
|
1487
|
-
|
|
3520
|
+
const breakSlice = breakIndex === -1 ? '' : slice.slice(0, breakIndex);
|
|
3521
|
+
if (breakIndex > 0 && measureDisplayWidth(breakSlice) > Math.floor(width * 0.4)) {
|
|
3522
|
+
result.push(breakSlice);
|
|
1488
3523
|
remaining = remaining.slice(breakIndex + 1);
|
|
1489
3524
|
}
|
|
1490
3525
|
else {
|
|
1491
3526
|
result.push(slice);
|
|
1492
|
-
remaining =
|
|
3527
|
+
remaining = tail;
|
|
1493
3528
|
}
|
|
1494
3529
|
}
|
|
1495
3530
|
result.push(remaining);
|
|
1496
3531
|
}
|
|
1497
3532
|
return result;
|
|
1498
3533
|
}
|
|
1499
|
-
function
|
|
1500
|
-
|
|
3534
|
+
function expandTabs(text, tabWidth) {
|
|
3535
|
+
if (!text.includes('\t')) {
|
|
3536
|
+
return text;
|
|
3537
|
+
}
|
|
3538
|
+
return text.replace(/\t/g, ' '.repeat(tabWidth));
|
|
3539
|
+
}
|
|
3540
|
+
export function measureDisplayWidth(text) {
|
|
3541
|
+
return splitGraphemes(text).reduce((total, grapheme) => total + graphemeDisplayWidth(grapheme), 0);
|
|
1501
3542
|
}
|
|
1502
|
-
function
|
|
1503
|
-
|
|
3543
|
+
export function padRight(text, width) {
|
|
3544
|
+
const visibleWidth = measureDisplayWidth(text);
|
|
3545
|
+
return visibleWidth >= width ? text : `${text}${' '.repeat(width - visibleWidth)}`;
|
|
3546
|
+
}
|
|
3547
|
+
export function ellipsize(text, width) {
|
|
3548
|
+
if (measureDisplayWidth(text) <= width) {
|
|
1504
3549
|
return text;
|
|
1505
3550
|
}
|
|
1506
3551
|
if (width <= 3) {
|
|
1507
|
-
return text
|
|
3552
|
+
return splitByDisplayWidth(text, width).head;
|
|
3553
|
+
}
|
|
3554
|
+
return `${splitByDisplayWidth(text, width - 3).head}...`;
|
|
3555
|
+
}
|
|
3556
|
+
function splitGraphemes(text) {
|
|
3557
|
+
if (!text) {
|
|
3558
|
+
return [];
|
|
3559
|
+
}
|
|
3560
|
+
if (!graphemeSegmenter) {
|
|
3561
|
+
return Array.from(text);
|
|
3562
|
+
}
|
|
3563
|
+
return Array.from(graphemeSegmenter.segment(text), (entry) => entry.segment);
|
|
3564
|
+
}
|
|
3565
|
+
function splitByDisplayWidth(text, width, options = {}) {
|
|
3566
|
+
if (width <= 0) {
|
|
3567
|
+
return { head: '', tail: text };
|
|
3568
|
+
}
|
|
3569
|
+
const graphemes = splitGraphemes(text);
|
|
3570
|
+
let consumedWidth = 0;
|
|
3571
|
+
let index = 0;
|
|
3572
|
+
while (index < graphemes.length) {
|
|
3573
|
+
const nextWidth = graphemeDisplayWidth(graphemes[index]);
|
|
3574
|
+
if (consumedWidth + nextWidth > width) {
|
|
3575
|
+
if (index === 0 && options.forceOne) {
|
|
3576
|
+
index = 1;
|
|
3577
|
+
}
|
|
3578
|
+
break;
|
|
3579
|
+
}
|
|
3580
|
+
consumedWidth += nextWidth;
|
|
3581
|
+
index += 1;
|
|
3582
|
+
}
|
|
3583
|
+
return {
|
|
3584
|
+
head: graphemes.slice(0, index).join(''),
|
|
3585
|
+
tail: graphemes.slice(index).join(''),
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
function graphemeDisplayWidth(grapheme) {
|
|
3589
|
+
if (!grapheme) {
|
|
3590
|
+
return 0;
|
|
1508
3591
|
}
|
|
1509
|
-
|
|
3592
|
+
if (isEmojiGrapheme(grapheme)) {
|
|
3593
|
+
return 2;
|
|
3594
|
+
}
|
|
3595
|
+
let width = 0;
|
|
3596
|
+
for (const symbol of Array.from(grapheme)) {
|
|
3597
|
+
const codePoint = symbol.codePointAt(0);
|
|
3598
|
+
if (codePoint === undefined || isZeroWidthCodePoint(codePoint, symbol)) {
|
|
3599
|
+
continue;
|
|
3600
|
+
}
|
|
3601
|
+
width = Math.max(width, isFullWidthCodePoint(codePoint) ? 2 : 1);
|
|
3602
|
+
}
|
|
3603
|
+
return width;
|
|
3604
|
+
}
|
|
3605
|
+
function isEmojiGrapheme(grapheme) {
|
|
3606
|
+
if (EXTENDED_PICTOGRAPHIC_RE.test(grapheme) || grapheme.includes('\u200D') || grapheme.includes('\uFE0F') || grapheme.includes('\u20E3')) {
|
|
3607
|
+
return true;
|
|
3608
|
+
}
|
|
3609
|
+
return Array.from(grapheme).some((symbol) => {
|
|
3610
|
+
const codePoint = symbol.codePointAt(0);
|
|
3611
|
+
return codePoint !== undefined && isRegionalIndicatorCodePoint(codePoint);
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
function isZeroWidthCodePoint(codePoint, symbol) {
|
|
3615
|
+
return COMBINING_MARK_RE.test(symbol)
|
|
3616
|
+
|| codePoint === 0x200D
|
|
3617
|
+
|| (codePoint >= 0xFE00 && codePoint <= 0xFE0F)
|
|
3618
|
+
|| (codePoint >= 0xE0100 && codePoint <= 0xE01EF)
|
|
3619
|
+
|| (codePoint >= 0x1F3FB && codePoint <= 0x1F3FF);
|
|
3620
|
+
}
|
|
3621
|
+
function isRegionalIndicatorCodePoint(codePoint) {
|
|
3622
|
+
return codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF;
|
|
3623
|
+
}
|
|
3624
|
+
function isFullWidthCodePoint(codePoint) {
|
|
3625
|
+
if (codePoint < 0x1100) {
|
|
3626
|
+
return false;
|
|
3627
|
+
}
|
|
3628
|
+
return codePoint <= 0x115F
|
|
3629
|
+
|| codePoint === 0x2329
|
|
3630
|
+
|| codePoint === 0x232A
|
|
3631
|
+
|| (codePoint >= 0x2E80 && codePoint <= 0x3247 && codePoint !== 0x303F)
|
|
3632
|
+
|| (codePoint >= 0x3250 && codePoint <= 0x4DBF)
|
|
3633
|
+
|| (codePoint >= 0x4E00 && codePoint <= 0xA4C6)
|
|
3634
|
+
|| (codePoint >= 0xA960 && codePoint <= 0xA97C)
|
|
3635
|
+
|| (codePoint >= 0xAC00 && codePoint <= 0xD7A3)
|
|
3636
|
+
|| (codePoint >= 0xF900 && codePoint <= 0xFAFF)
|
|
3637
|
+
|| (codePoint >= 0xFE10 && codePoint <= 0xFE19)
|
|
3638
|
+
|| (codePoint >= 0xFE30 && codePoint <= 0xFE6B)
|
|
3639
|
+
|| (codePoint >= 0xFF01 && codePoint <= 0xFF60)
|
|
3640
|
+
|| (codePoint >= 0xFFE0 && codePoint <= 0xFFE6)
|
|
3641
|
+
|| (codePoint >= 0x1F200 && codePoint <= 0x1F251)
|
|
3642
|
+
|| (codePoint >= 0x20000 && codePoint <= 0x3FFFD);
|
|
1510
3643
|
}
|
|
1511
3644
|
function condenseWhitespace(text) {
|
|
1512
3645
|
return text.replace(/\s+/g, ' ').trim();
|
|
@@ -1572,21 +3705,24 @@ function longestCommonPrefix(values) {
|
|
|
1572
3705
|
}
|
|
1573
3706
|
return prefix;
|
|
1574
3707
|
}
|
|
1575
|
-
function renderMenuBar(text, columns) {
|
|
1576
|
-
|
|
3708
|
+
function renderMenuBar(text, columns, activeMenuIndex) {
|
|
3709
|
+
const labels = ['Session', 'Skills', 'Files', 'Run', 'View', 'Help'];
|
|
3710
|
+
let rendered = paint(' DeepClause ', ANSI.black, ANSI.bgWhite, ANSI.bold);
|
|
3711
|
+
let usedWidth = measureDisplayWidth(' DeepClause ');
|
|
3712
|
+
labels.forEach((label, index) => {
|
|
3713
|
+
const raw = ` [${index + 1}] ${label} `;
|
|
3714
|
+
usedWidth += measureDisplayWidth(raw);
|
|
3715
|
+
rendered += paint(raw, ANSI.black, activeMenuIndex === index ? ANSI.bgCyan : ANSI.bgWhite, ANSI.bold);
|
|
3716
|
+
});
|
|
3717
|
+
const trailingWidth = Math.max(0, columns - usedWidth);
|
|
3718
|
+
rendered += paint(padRight(ellipsize(` ${text}`, trailingWidth), trailingWidth), ANSI.black, ANSI.bgWhite);
|
|
3719
|
+
return rendered;
|
|
1577
3720
|
}
|
|
1578
|
-
function renderStatusBar(status, columns,
|
|
1579
|
-
const hotkeys = [
|
|
1580
|
-
['<- ->', 'Pane'],
|
|
1581
|
-
[focusedPane === 'sessions' ? 'Up/Down' : 'PgUp/PgDn', focusedPane === 'sessions' ? 'Select' : 'Scroll'],
|
|
1582
|
-
['End', focusedPane === 'sessions' ? 'Bottom' : 'Follow'],
|
|
1583
|
-
['/cancel', 'Stop'],
|
|
1584
|
-
['^C', 'Quit'],
|
|
1585
|
-
];
|
|
3721
|
+
function renderStatusBar(status, columns, hotkeys) {
|
|
1586
3722
|
let hotkeyWidth = 0;
|
|
1587
3723
|
const selected = [];
|
|
1588
3724
|
for (const hotkey of hotkeys) {
|
|
1589
|
-
const width = hotkey[0]
|
|
3725
|
+
const width = measureDisplayWidth(hotkey[0]) + measureDisplayWidth(hotkey[1]) + 4;
|
|
1590
3726
|
if (hotkeyWidth + width > Math.max(0, columns - 12)) {
|
|
1591
3727
|
break;
|
|
1592
3728
|
}
|
|
@@ -1600,13 +3736,13 @@ function renderStatusBar(status, columns, focusedPane) {
|
|
|
1600
3736
|
}
|
|
1601
3737
|
function renderCommandBar(prefix, value, columns) {
|
|
1602
3738
|
const prefixPart = paint(prefix, ANSI.black, ANSI.bgWhite, ANSI.bold);
|
|
1603
|
-
const remainingWidth = Math.max(0, columns - prefix
|
|
3739
|
+
const remainingWidth = Math.max(0, columns - measureDisplayWidth(prefix));
|
|
1604
3740
|
const valuePart = paint(padRight(ellipsize(value, remainingWidth), remainingWidth), ANSI.brightWhite, ANSI.bgBlue);
|
|
1605
3741
|
return `${prefixPart}${valuePart}`;
|
|
1606
3742
|
}
|
|
1607
3743
|
function renderWindowTop(title, innerWidth, active) {
|
|
1608
3744
|
const safeLabel = ` ${ellipsize(title, Math.max(1, innerWidth - 2))} `;
|
|
1609
|
-
const fillWidth = Math.max(0, innerWidth - safeLabel
|
|
3745
|
+
const fillWidth = Math.max(0, innerWidth - measureDisplayWidth(safeLabel));
|
|
1610
3746
|
const leftFill = Math.floor(fillWidth / 2);
|
|
1611
3747
|
const rightFill = fillWidth - leftFill;
|
|
1612
3748
|
return [
|
|
@@ -1622,6 +3758,9 @@ function renderWindowBody(text, innerWidth, kind) {
|
|
|
1622
3758
|
const normalized = text.trim().toLowerCase();
|
|
1623
3759
|
const content = padRight(ellipsize(text, innerWidth), innerWidth);
|
|
1624
3760
|
const border = paint('║', ANSI.brightCyan, ANSI.bgBlue);
|
|
3761
|
+
if (kind === 'workspace' && text.startsWith('> ')) {
|
|
3762
|
+
return `${border}${paint(content, ANSI.black, ANSI.bgCyan, ANSI.bold)}${border}`;
|
|
3763
|
+
}
|
|
1625
3764
|
if (kind === 'sessions' && text.startsWith('> ')) {
|
|
1626
3765
|
return `${border}${paint(content, ANSI.black, ANSI.bgCyan, ANSI.bold)}${border}`;
|
|
1627
3766
|
}
|
|
@@ -1701,6 +3840,27 @@ function paneDimensionsForKind(kind, columns, rows) {
|
|
|
1701
3840
|
return { width: widths.right, height: rightHeights.context };
|
|
1702
3841
|
}
|
|
1703
3842
|
}
|
|
3843
|
+
export function nextWrappedIndex(currentIndex, delta, length) {
|
|
3844
|
+
if (length <= 0) {
|
|
3845
|
+
return 0;
|
|
3846
|
+
}
|
|
3847
|
+
return (currentIndex + delta + length) % length;
|
|
3848
|
+
}
|
|
3849
|
+
export function selectMenuItemByTypeahead(items, query, currentIndex = -1) {
|
|
3850
|
+
const normalized = query.trim().toLowerCase();
|
|
3851
|
+
if (!normalized || items.length === 0) {
|
|
3852
|
+
return Math.max(0, currentIndex);
|
|
3853
|
+
}
|
|
3854
|
+
const matches = items
|
|
3855
|
+
.map((item, index) => ({ label: item.label.toLowerCase(), index }))
|
|
3856
|
+
.filter((item) => item.label.startsWith(normalized))
|
|
3857
|
+
.map((item) => item.index);
|
|
3858
|
+
if (matches.length === 0) {
|
|
3859
|
+
return Math.max(0, currentIndex);
|
|
3860
|
+
}
|
|
3861
|
+
const next = matches.find((index) => index > currentIndex);
|
|
3862
|
+
return next ?? matches[0];
|
|
3863
|
+
}
|
|
1704
3864
|
function wrapBodyLines(body, innerWidth) {
|
|
1705
3865
|
const lines = [];
|
|
1706
3866
|
for (const line of body) {
|
|
@@ -1708,6 +3868,63 @@ function wrapBodyLines(body, innerWidth) {
|
|
|
1708
3868
|
}
|
|
1709
3869
|
return lines;
|
|
1710
3870
|
}
|
|
3871
|
+
async function listWorkspaceTextFiles(workspaceRoot) {
|
|
3872
|
+
const results = [];
|
|
3873
|
+
const visit = async (relativeDir) => {
|
|
3874
|
+
const absoluteDir = relativeDir ? path.join(workspaceRoot, relativeDir) : workspaceRoot;
|
|
3875
|
+
let entries;
|
|
3876
|
+
try {
|
|
3877
|
+
entries = await fs.readdir(absoluteDir, { withFileTypes: true });
|
|
3878
|
+
}
|
|
3879
|
+
catch {
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
for (const entry of entries) {
|
|
3883
|
+
const relativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name;
|
|
3884
|
+
if (entry.isDirectory()) {
|
|
3885
|
+
if (IGNORED_WORKSPACE_DIRS.has(entry.name)) {
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
await visit(relativePath);
|
|
3889
|
+
continue;
|
|
3890
|
+
}
|
|
3891
|
+
if (entry.isFile() && isEditableTextFile(relativePath)) {
|
|
3892
|
+
results.push(relativePath.replace(/\\/g, '/'));
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
};
|
|
3896
|
+
await visit('');
|
|
3897
|
+
results.sort((left, right) => left.localeCompare(right));
|
|
3898
|
+
return results;
|
|
3899
|
+
}
|
|
3900
|
+
function isEditableTextFile(filePath) {
|
|
3901
|
+
const basename = path.basename(filePath);
|
|
3902
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
3903
|
+
return TEXT_FILE_EXTENSIONS.has(extension) || TEXT_FILE_BASENAMES.has(basename);
|
|
3904
|
+
}
|
|
3905
|
+
function splitEditorContent(content) {
|
|
3906
|
+
const lines = content.split(/\r?\n/);
|
|
3907
|
+
return lines.length > 0 ? lines : [''];
|
|
3908
|
+
}
|
|
3909
|
+
function resolveWorkspaceTextPath(workspaceRoot, requestedPath) {
|
|
3910
|
+
const absolutePath = path.resolve(workspaceRoot, requestedPath);
|
|
3911
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
3912
|
+
if (absolutePath === resolvedRoot || absolutePath.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
3913
|
+
return absolutePath;
|
|
3914
|
+
}
|
|
3915
|
+
return path.join(resolvedRoot, path.basename(requestedPath));
|
|
3916
|
+
}
|
|
3917
|
+
export function filterPickerItems(items, query) {
|
|
3918
|
+
const normalized = query.trim().toLowerCase();
|
|
3919
|
+
if (!normalized) {
|
|
3920
|
+
return [...items];
|
|
3921
|
+
}
|
|
3922
|
+
const tokens = normalized.split(/\s+/).filter(Boolean);
|
|
3923
|
+
return [...items].filter((item) => {
|
|
3924
|
+
const haystack = `${item.label} ${item.description} ${item.detail ?? ''}`.toLowerCase();
|
|
3925
|
+
return tokens.every((token) => haystack.includes(token));
|
|
3926
|
+
});
|
|
3927
|
+
}
|
|
1711
3928
|
function clamp(value, min, max) {
|
|
1712
3929
|
if (value < min) {
|
|
1713
3930
|
return min;
|