deepclause-sdk 0.0.60 → 0.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +311 -94
- package/dist/cli/config.d.ts +1 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +11 -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 +16 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/run.d.ts +1 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +18 -62
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/tui.d.ts +26 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +1936 -126
- package/dist/cli/tui.js.map +1 -1
- package/dist/prolog/bridge.js +1 -1
- package/dist/prolog/bridge.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +5 -0
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +16 -6
- package/dist/system/assets/skills/skill-creator.dml +6 -2
- 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 +11 -61
- 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 +48 -0
- 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 +4 -0
- package/dist/system/runtime/runtime-tools.d.ts.map +1 -1
- package/dist/system/runtime/runtime-tools.js +24 -0
- package/dist/system/runtime/runtime-tools.js.map +1 -1
- package/dist/system/runtime/skill-creator.d.ts.map +1 -1
- package/dist/system/runtime/skill-creator.js +51 -105
- package/dist/system/runtime/skill-creator.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/tui.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
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 } 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';
|
|
7
10
|
const IGNORED_LIVE_LOG_TOOLS = new Set(['set_result', 'update_memory']);
|
|
8
|
-
const BUILTIN_SLASH_COMMANDS = ['new', 'sessions', 'help', 'cancel', 'exit', 'quit'];
|
|
11
|
+
const BUILTIN_SLASH_COMMANDS = ['new', 'sessions', 'help', 'compile', 'skill-creator', 'cancel', 'exit', 'quit'];
|
|
12
|
+
const CHILD_EVENT_INDENT = '\t';
|
|
13
|
+
const CHILD_EVENT_TAB_WIDTH = 4;
|
|
9
14
|
const MAX_ACTIVITY_LINES = 400;
|
|
10
15
|
const SPINNER_FRAMES = ['|', '/', '-', '\\'];
|
|
11
16
|
const IDLE_RENDER_INTERVAL_MS = 16;
|
|
12
17
|
const BUSY_RENDER_INTERVAL_MS = 40;
|
|
13
18
|
const TYPING_RENDER_INTERVAL_MS = 90;
|
|
19
|
+
const COMBINING_MARK_RE = /\p{Mark}/u;
|
|
20
|
+
const EXTENDED_PICTOGRAPHIC_RE = /\p{Extended_Pictographic}/u;
|
|
21
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
22
|
+
'.dml', '.md', '.txt', '.json', '.yaml', '.yml', '.toml', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
23
|
+
'.css', '.scss', '.html', '.sql', '.py', '.sh', '.bash', '.zsh', '.pro', '.pl', '.csv', '.env', '.example',
|
|
24
|
+
]);
|
|
25
|
+
const TEXT_FILE_BASENAMES = new Set(['README', 'README.md', 'AGENTS.md', 'SPEC.md', 'ARCHITECTURE.md', 'CMD_LINE.md']);
|
|
26
|
+
const IGNORED_WORKSPACE_DIRS = new Set(['.git', 'node_modules', 'dist', '.next', 'coverage', 'agentvm-cache']);
|
|
27
|
+
const graphemeSegmenter = (() => {
|
|
28
|
+
const Segmenter = Intl.Segmenter;
|
|
29
|
+
return Segmenter ? new Segmenter(undefined, { granularity: 'grapheme' }) : null;
|
|
30
|
+
})();
|
|
14
31
|
const ANSI = {
|
|
15
32
|
reset: '\u001b[0m',
|
|
16
33
|
bold: '\u001b[1m',
|
|
@@ -40,7 +57,7 @@ export class LiveExecutionPrinter {
|
|
|
40
57
|
}
|
|
41
58
|
handle(logEvent) {
|
|
42
59
|
const { event } = logEvent;
|
|
43
|
-
if (event.type === 'usage' || event.type === 'finished'
|
|
60
|
+
if (event.type === 'usage' || event.type === 'finished') {
|
|
44
61
|
return;
|
|
45
62
|
}
|
|
46
63
|
if (event.type === 'stream') {
|
|
@@ -75,6 +92,11 @@ export class LiveExecutionPrinter {
|
|
|
75
92
|
this.writeLine(`${formatEventPrefix(logEvent)}log ${event.content}`);
|
|
76
93
|
}
|
|
77
94
|
break;
|
|
95
|
+
case 'input_required':
|
|
96
|
+
if (event.prompt) {
|
|
97
|
+
this.writeLine(`${formatEventPrefix(logEvent)}clarify ${event.prompt}`);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
78
100
|
}
|
|
79
101
|
}
|
|
80
102
|
finish() {
|
|
@@ -111,7 +133,7 @@ class ActivityBuffer {
|
|
|
111
133
|
activeStreamLine = '';
|
|
112
134
|
handle(logEvent) {
|
|
113
135
|
const { event } = logEvent;
|
|
114
|
-
if (event.type === 'usage' || event.type === 'finished'
|
|
136
|
+
if (event.type === 'usage' || event.type === 'finished') {
|
|
115
137
|
return;
|
|
116
138
|
}
|
|
117
139
|
if (event.type === 'stream') {
|
|
@@ -146,6 +168,11 @@ class ActivityBuffer {
|
|
|
146
168
|
this.pushLine(`${formatEventPrefix(logEvent)}log ${event.content}`);
|
|
147
169
|
}
|
|
148
170
|
break;
|
|
171
|
+
case 'input_required':
|
|
172
|
+
if (event.prompt) {
|
|
173
|
+
this.pushLine(`${formatEventPrefix(logEvent)}clarify ${event.prompt}`);
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
149
176
|
}
|
|
150
177
|
}
|
|
151
178
|
pushLine(line) {
|
|
@@ -159,6 +186,11 @@ class ActivityBuffer {
|
|
|
159
186
|
finish() {
|
|
160
187
|
this.flushStream();
|
|
161
188
|
}
|
|
189
|
+
clear() {
|
|
190
|
+
this.lines.length = 0;
|
|
191
|
+
this.activeStreamKey = null;
|
|
192
|
+
this.activeStreamLine = '';
|
|
193
|
+
}
|
|
162
194
|
snapshot() {
|
|
163
195
|
return this.activeStreamLine
|
|
164
196
|
? [...this.lines, this.activeStreamLine]
|
|
@@ -204,6 +236,14 @@ class FullscreenTui {
|
|
|
204
236
|
activityBySessionId = new Map();
|
|
205
237
|
ephemeralMessagesBySessionId = new Map();
|
|
206
238
|
focusedPane = 'messages';
|
|
239
|
+
uiMode = 'command';
|
|
240
|
+
menuReturnMode = 'command';
|
|
241
|
+
menuState = {
|
|
242
|
+
activeIndex: 0,
|
|
243
|
+
selectedIndex: 0,
|
|
244
|
+
typeahead: '',
|
|
245
|
+
typeaheadAt: 0,
|
|
246
|
+
};
|
|
207
247
|
paneScroll = {
|
|
208
248
|
sessions: 0,
|
|
209
249
|
messages: 0,
|
|
@@ -217,7 +257,12 @@ class FullscreenTui {
|
|
|
217
257
|
busy = false;
|
|
218
258
|
exitRequested = false;
|
|
219
259
|
statusLine = 'Loading...';
|
|
220
|
-
|
|
260
|
+
pendingPrompt = null;
|
|
261
|
+
pickerState = null;
|
|
262
|
+
viewerState = null;
|
|
263
|
+
editorState = null;
|
|
264
|
+
recentFiles = [];
|
|
265
|
+
lastSubmittedInput = null;
|
|
221
266
|
renderTimer = null;
|
|
222
267
|
animationTimer = null;
|
|
223
268
|
closeResolver = null;
|
|
@@ -232,7 +277,7 @@ class FullscreenTui {
|
|
|
232
277
|
try {
|
|
233
278
|
await this.refreshCommands();
|
|
234
279
|
await this.refreshSessions({ createIfMissing: true });
|
|
235
|
-
this.statusLine = 'Ready.
|
|
280
|
+
this.statusLine = 'Ready. F10 or Ctrl+G opens menus. Left/right changes pane when the menu is closed.';
|
|
236
281
|
emitKeypressEvents(input);
|
|
237
282
|
if (input.isTTY) {
|
|
238
283
|
input.setRawMode(true);
|
|
@@ -266,15 +311,28 @@ class FullscreenTui {
|
|
|
266
311
|
void this.handleKeypress(text, key);
|
|
267
312
|
};
|
|
268
313
|
async handleKeypress(text, key) {
|
|
314
|
+
if ((key.ctrl && key.name === 'g') || key.name === 'f10') {
|
|
315
|
+
this.toggleMenuMode();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (key.ctrl && key.name === 'p' && !this.pendingPrompt && !this.busy) {
|
|
319
|
+
this.openActionPalette();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
269
322
|
if (key.ctrl && key.name === 'w') {
|
|
270
323
|
this.cyclePaneFocus(1);
|
|
271
324
|
return;
|
|
272
325
|
}
|
|
273
326
|
if (key.ctrl && key.name === 'c') {
|
|
274
|
-
if (this.
|
|
275
|
-
const pending = this.
|
|
276
|
-
this.
|
|
327
|
+
if (this.pendingPrompt) {
|
|
328
|
+
const pending = this.pendingPrompt;
|
|
329
|
+
this.pendingPrompt = null;
|
|
277
330
|
pending.resolve('');
|
|
331
|
+
if (!this.busy) {
|
|
332
|
+
this.statusLine = 'Input cancelled.';
|
|
333
|
+
this.requestRender();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
278
336
|
}
|
|
279
337
|
if (this.busy) {
|
|
280
338
|
this.exitRequested = true;
|
|
@@ -284,6 +342,22 @@ class FullscreenTui {
|
|
|
284
342
|
this.close();
|
|
285
343
|
return;
|
|
286
344
|
}
|
|
345
|
+
if (this.uiMode === 'menu') {
|
|
346
|
+
await this.handleMenuKeypress(text, key);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
350
|
+
await this.handlePickerKeypress(text, key);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (this.uiMode === 'viewer') {
|
|
354
|
+
await this.handleViewerKeypress(key);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (this.uiMode === 'editor') {
|
|
358
|
+
await this.handleEditorKeypress(text, key);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
287
361
|
switch (key.name) {
|
|
288
362
|
case 'return':
|
|
289
363
|
await this.submitInput();
|
|
@@ -365,16 +439,271 @@ class FullscreenTui {
|
|
|
365
439
|
}
|
|
366
440
|
this.requestRender();
|
|
367
441
|
}
|
|
442
|
+
async handleMenuKeypress(text, key) {
|
|
443
|
+
const menus = this.getMenuDefinitions();
|
|
444
|
+
const activeMenu = menus[this.menuState.activeIndex];
|
|
445
|
+
const activeItems = activeMenu?.items ?? [];
|
|
446
|
+
if (key.name === 'escape') {
|
|
447
|
+
this.closeMenuMode();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (key.name && /^[1-6]$/.test(key.name)) {
|
|
451
|
+
this.selectMenu(Number.parseInt(key.name, 10) - 1);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
switch (key.name) {
|
|
455
|
+
case 'left':
|
|
456
|
+
this.moveMenu(-1);
|
|
457
|
+
return;
|
|
458
|
+
case 'right':
|
|
459
|
+
this.moveMenu(1);
|
|
460
|
+
return;
|
|
461
|
+
case 'up':
|
|
462
|
+
this.moveMenuItem(-1, activeItems.length);
|
|
463
|
+
return;
|
|
464
|
+
case 'down':
|
|
465
|
+
this.moveMenuItem(1, activeItems.length);
|
|
466
|
+
return;
|
|
467
|
+
case 'home':
|
|
468
|
+
this.menuState.selectedIndex = 0;
|
|
469
|
+
this.statusLine = `${activeMenu?.label ?? 'Menu'} menu.`;
|
|
470
|
+
this.requestRender();
|
|
471
|
+
return;
|
|
472
|
+
case 'end':
|
|
473
|
+
this.menuState.selectedIndex = Math.max(0, activeItems.length - 1);
|
|
474
|
+
this.statusLine = `${activeMenu?.label ?? 'Menu'} menu.`;
|
|
475
|
+
this.requestRender();
|
|
476
|
+
return;
|
|
477
|
+
case 'return':
|
|
478
|
+
case 'space': {
|
|
479
|
+
const selected = activeItems[this.menuState.selectedIndex];
|
|
480
|
+
if (selected) {
|
|
481
|
+
if (selected.enabled === false) {
|
|
482
|
+
this.statusLine = `${selected.label} is not available right now.`;
|
|
483
|
+
this.requestRender();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
await this.runMenuAction(selected.id);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
case 'backspace':
|
|
491
|
+
if (this.menuState.typeahead) {
|
|
492
|
+
this.menuState.typeahead = this.menuState.typeahead.slice(0, -1);
|
|
493
|
+
this.menuState.typeaheadAt = Date.now();
|
|
494
|
+
this.requestRender();
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
default:
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
if (text && !key.ctrl && !key.meta && /\S/.test(text)) {
|
|
501
|
+
const now = Date.now();
|
|
502
|
+
this.menuState.typeahead = now - this.menuState.typeaheadAt > 900
|
|
503
|
+
? text.toLowerCase()
|
|
504
|
+
: `${this.menuState.typeahead}${text.toLowerCase()}`;
|
|
505
|
+
this.menuState.typeaheadAt = now;
|
|
506
|
+
this.menuState.selectedIndex = selectMenuItemByTypeahead(activeItems, this.menuState.typeahead, this.menuState.selectedIndex);
|
|
507
|
+
this.requestRender();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async handlePickerKeypress(text, key) {
|
|
511
|
+
const picker = this.pickerState;
|
|
512
|
+
if (!picker) {
|
|
513
|
+
this.closeWorkspaceMode();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
switch (key.name) {
|
|
517
|
+
case 'escape':
|
|
518
|
+
this.closeWorkspaceMode();
|
|
519
|
+
return;
|
|
520
|
+
case 'return':
|
|
521
|
+
await this.activatePickerSelection();
|
|
522
|
+
return;
|
|
523
|
+
case 'up':
|
|
524
|
+
this.movePickerSelection(-1);
|
|
525
|
+
return;
|
|
526
|
+
case 'down':
|
|
527
|
+
this.movePickerSelection(1);
|
|
528
|
+
return;
|
|
529
|
+
case 'pageup':
|
|
530
|
+
this.movePickerSelection(-Math.max(3, Math.floor(this.workspaceViewportLines() / 2)));
|
|
531
|
+
return;
|
|
532
|
+
case 'pagedown':
|
|
533
|
+
this.movePickerSelection(Math.max(3, Math.floor(this.workspaceViewportLines() / 2)));
|
|
534
|
+
return;
|
|
535
|
+
case 'home':
|
|
536
|
+
picker.selectedIndex = 0;
|
|
537
|
+
this.ensurePickerSelectionVisible();
|
|
538
|
+
this.requestRender();
|
|
539
|
+
return;
|
|
540
|
+
case 'end':
|
|
541
|
+
picker.selectedIndex = Math.max(0, picker.filteredItems.length - 1);
|
|
542
|
+
this.ensurePickerSelectionVisible();
|
|
543
|
+
this.requestRender();
|
|
544
|
+
return;
|
|
545
|
+
case 'backspace':
|
|
546
|
+
this.deleteBackward();
|
|
547
|
+
this.refreshPickerFilter();
|
|
548
|
+
return;
|
|
549
|
+
case 'delete':
|
|
550
|
+
this.deleteForward();
|
|
551
|
+
this.refreshPickerFilter();
|
|
552
|
+
return;
|
|
553
|
+
case 'left':
|
|
554
|
+
if (this.cursor > 0) {
|
|
555
|
+
this.cursor -= 1;
|
|
556
|
+
this.requestRender();
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
case 'right':
|
|
560
|
+
if (this.cursor < this.inputValue.length) {
|
|
561
|
+
this.cursor += 1;
|
|
562
|
+
this.requestRender();
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
default:
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
if (key.ctrl && key.name === 'e') {
|
|
569
|
+
await this.activatePickerSelection('edit');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (key.ctrl && key.name === 'r') {
|
|
573
|
+
await this.activatePickerSelection('run');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (key.ctrl && key.name === 'm') {
|
|
577
|
+
await this.activatePickerSelection('meta');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (key.ctrl && key.name === 's') {
|
|
581
|
+
await this.activatePickerSelection('source');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (text && !key.ctrl && !key.meta) {
|
|
585
|
+
this.insertText(text);
|
|
586
|
+
this.refreshPickerFilter();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async handleViewerKeypress(key) {
|
|
590
|
+
if (!this.viewerState) {
|
|
591
|
+
this.closeWorkspaceMode();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (key.ctrl && key.name === 'e' && this.viewerState.canEdit && this.viewerState.path) {
|
|
595
|
+
await this.openEditorForFile(this.viewerState.path, { title: this.viewerState.title, preferredExtension: this.viewerState.preferredExtension });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (key.ctrl && key.name === 'r' && this.viewerState.skillName) {
|
|
599
|
+
this.closeWorkspaceMode();
|
|
600
|
+
await this.executeSkill(this.viewerState.skillName, []);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
switch (key.name) {
|
|
604
|
+
case 'escape':
|
|
605
|
+
this.closeWorkspaceMode();
|
|
606
|
+
return;
|
|
607
|
+
case 'up':
|
|
608
|
+
this.viewerState.scrollTop = Math.max(0, this.viewerState.scrollTop - 1);
|
|
609
|
+
this.requestRender();
|
|
610
|
+
return;
|
|
611
|
+
case 'down':
|
|
612
|
+
this.viewerState.scrollTop += 1;
|
|
613
|
+
this.requestRender();
|
|
614
|
+
return;
|
|
615
|
+
case 'pageup':
|
|
616
|
+
this.viewerState.scrollTop = Math.max(0, this.viewerState.scrollTop - this.workspaceViewportLines());
|
|
617
|
+
this.requestRender();
|
|
618
|
+
return;
|
|
619
|
+
case 'pagedown':
|
|
620
|
+
this.viewerState.scrollTop += this.workspaceViewportLines();
|
|
621
|
+
this.requestRender();
|
|
622
|
+
return;
|
|
623
|
+
case 'home':
|
|
624
|
+
this.viewerState.scrollTop = 0;
|
|
625
|
+
this.requestRender();
|
|
626
|
+
return;
|
|
627
|
+
default:
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async handleEditorKeypress(text, key) {
|
|
632
|
+
if (!this.editorState) {
|
|
633
|
+
this.closeWorkspaceMode();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (key.ctrl && key.name === 's') {
|
|
637
|
+
await this.saveCurrentEditor(false);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
switch (key.name) {
|
|
641
|
+
case 'escape':
|
|
642
|
+
if (this.editorState.dirty && !this.editorState.discardPending) {
|
|
643
|
+
this.editorState.discardPending = true;
|
|
644
|
+
this.statusLine = 'Unsaved changes. Press Esc again to discard or Ctrl+S to save.';
|
|
645
|
+
this.requestRender();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.closeWorkspaceMode(true);
|
|
649
|
+
return;
|
|
650
|
+
case 'return':
|
|
651
|
+
this.splitEditorLine();
|
|
652
|
+
return;
|
|
653
|
+
case 'backspace':
|
|
654
|
+
this.deleteEditorBackward();
|
|
655
|
+
return;
|
|
656
|
+
case 'delete':
|
|
657
|
+
this.deleteEditorForward();
|
|
658
|
+
return;
|
|
659
|
+
case 'left':
|
|
660
|
+
this.moveEditorCursor('left');
|
|
661
|
+
return;
|
|
662
|
+
case 'right':
|
|
663
|
+
this.moveEditorCursor('right');
|
|
664
|
+
return;
|
|
665
|
+
case 'up':
|
|
666
|
+
this.moveEditorCursor('up');
|
|
667
|
+
return;
|
|
668
|
+
case 'down':
|
|
669
|
+
this.moveEditorCursor('down');
|
|
670
|
+
return;
|
|
671
|
+
case 'pageup':
|
|
672
|
+
this.pageEditorCursor(-1);
|
|
673
|
+
return;
|
|
674
|
+
case 'pagedown':
|
|
675
|
+
this.pageEditorCursor(1);
|
|
676
|
+
return;
|
|
677
|
+
case 'home':
|
|
678
|
+
this.editorState.cursorColumn = 0;
|
|
679
|
+
this.ensureEditorCursorVisible();
|
|
680
|
+
this.requestRender();
|
|
681
|
+
return;
|
|
682
|
+
case 'end':
|
|
683
|
+
this.editorState.cursorColumn = this.currentEditorLine().length;
|
|
684
|
+
this.ensureEditorCursorVisible();
|
|
685
|
+
this.requestRender();
|
|
686
|
+
return;
|
|
687
|
+
case 'tab':
|
|
688
|
+
this.insertEditorText(' ');
|
|
689
|
+
return;
|
|
690
|
+
default:
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
if (text && !key.ctrl && !key.meta) {
|
|
694
|
+
this.insertEditorText(text);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
368
697
|
async submitInput() {
|
|
369
698
|
const rawInput = this.inputValue;
|
|
370
699
|
const trimmedInput = rawInput.trim();
|
|
371
700
|
this.inputValue = '';
|
|
372
701
|
this.cursor = 0;
|
|
373
|
-
if (this.
|
|
374
|
-
const pending = this.
|
|
375
|
-
this.
|
|
702
|
+
if (this.pendingPrompt) {
|
|
703
|
+
const pending = this.pendingPrompt;
|
|
704
|
+
this.pendingPrompt = null;
|
|
376
705
|
pending.resolve(trimmedInput);
|
|
377
|
-
this.statusLine = 'Continuing...';
|
|
706
|
+
this.statusLine = pending.kind === 'clarify' ? 'Continuing...' : 'Working...';
|
|
378
707
|
this.requestRender();
|
|
379
708
|
return;
|
|
380
709
|
}
|
|
@@ -383,7 +712,7 @@ class FullscreenTui {
|
|
|
383
712
|
return;
|
|
384
713
|
}
|
|
385
714
|
const parsed = parseSlashInput(trimmedInput);
|
|
386
|
-
if (this.busy && !this.
|
|
715
|
+
if (this.busy && !this.pendingPrompt && !canSubmitParsedInputWhileBusy(parsed)) {
|
|
387
716
|
this.statusLine = 'Execution is still running. Use /cancel or Ctrl+C to stop it.';
|
|
388
717
|
this.requestRender();
|
|
389
718
|
return;
|
|
@@ -416,6 +745,10 @@ class FullscreenTui {
|
|
|
416
745
|
this.requestRender();
|
|
417
746
|
return;
|
|
418
747
|
}
|
|
748
|
+
case 'compile':
|
|
749
|
+
case 'skill-creator':
|
|
750
|
+
await this.executeSkillCreator(parsed.name, parsed.rawArgs);
|
|
751
|
+
return;
|
|
419
752
|
case 'sessions':
|
|
420
753
|
await this.refreshSessions({ createIfMissing: true, selectedSessionId: this.selectedSessionId });
|
|
421
754
|
await this.refreshCommands();
|
|
@@ -434,6 +767,7 @@ class FullscreenTui {
|
|
|
434
767
|
async executeTask(promptText) {
|
|
435
768
|
const sessionId = this.selectedSessionId;
|
|
436
769
|
const activity = this.activityFor(sessionId);
|
|
770
|
+
this.lastSubmittedInput = { kind: 'text', prompt: promptText };
|
|
437
771
|
this.focusedPane = 'messages';
|
|
438
772
|
this.paneScroll.messages = 0;
|
|
439
773
|
this.paneScroll.process = 0;
|
|
@@ -488,10 +822,11 @@ class FullscreenTui {
|
|
|
488
822
|
const sessionId = this.selectedSessionId || 'global';
|
|
489
823
|
const activity = this.activityFor(sessionId);
|
|
490
824
|
const commandText = `/${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`;
|
|
825
|
+
this.lastSubmittedInput = { kind: 'skill', name: skillName, rawArgs: args.join(' '), args: [...args] };
|
|
491
826
|
this.focusedPane = 'messages';
|
|
492
827
|
this.paneScroll.messages = 0;
|
|
493
828
|
this.paneScroll.process = 0;
|
|
494
|
-
this.beginExecutionPreview(sessionId, 'skill', { role: 'system', content: commandText });
|
|
829
|
+
this.beginExecutionPreview(sessionId, 'skill', { role: 'system', content: commandText }, skillName);
|
|
495
830
|
activity.pushLine(`skill /${skillName}${args.length > 0 ? ` ${args.join(' ')}` : ''}`);
|
|
496
831
|
this.busy = true;
|
|
497
832
|
this.currentAbortController = new AbortController();
|
|
@@ -502,7 +837,7 @@ class FullscreenTui {
|
|
|
502
837
|
const result = await runSkillCommand(this.workspaceRoot, skillName, args, {
|
|
503
838
|
sandbox: this.options.sandbox,
|
|
504
839
|
signal: this.currentAbortController.signal,
|
|
505
|
-
onUserInput: (question) => this.requestClarification(
|
|
840
|
+
onUserInput: (question) => this.requestClarification(question),
|
|
506
841
|
onEvent: (event) => {
|
|
507
842
|
activity.handle(event);
|
|
508
843
|
this.updatePreviewFromEvent(event);
|
|
@@ -518,80 +853,971 @@ class FullscreenTui {
|
|
|
518
853
|
});
|
|
519
854
|
this.statusLine = result.error ? `/${skillName} failed.` : `/${skillName} finished.`;
|
|
520
855
|
}
|
|
521
|
-
catch (error) {
|
|
522
|
-
activity.finish();
|
|
523
|
-
const message = error.message;
|
|
524
|
-
activity.pushLine(`[${skillName}] error ${message}`);
|
|
525
|
-
this.finishExecutionPreview({ persist: true, error: message });
|
|
526
|
-
this.statusLine = `/${skillName} failed.`;
|
|
856
|
+
catch (error) {
|
|
857
|
+
activity.finish();
|
|
858
|
+
const message = error.message;
|
|
859
|
+
activity.pushLine(`[${skillName}] error ${message}`);
|
|
860
|
+
this.finishExecutionPreview({ persist: true, error: message });
|
|
861
|
+
this.statusLine = `/${skillName} failed.`;
|
|
862
|
+
}
|
|
863
|
+
finally {
|
|
864
|
+
this.busy = false;
|
|
865
|
+
this.currentAbortController = null;
|
|
866
|
+
this.stopAnimation();
|
|
867
|
+
if (this.exitRequested) {
|
|
868
|
+
this.close();
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
this.requestRender();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async executeSkillCreator(commandName, specText) {
|
|
876
|
+
const trimmedSpec = specText.trim();
|
|
877
|
+
if (!trimmedSpec) {
|
|
878
|
+
this.statusLine = `Provide a skill specification after /${commandName}.`;
|
|
879
|
+
this.requestRender();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const sessionId = this.selectedSessionId;
|
|
883
|
+
const activity = this.activityFor(sessionId);
|
|
884
|
+
const commandText = `/${commandName} ${trimmedSpec}`;
|
|
885
|
+
this.lastSubmittedInput = { kind: 'builtin', name: commandName, rawArgs: trimmedSpec, args: [trimmedSpec] };
|
|
886
|
+
this.focusedPane = 'messages';
|
|
887
|
+
this.paneScroll.messages = 0;
|
|
888
|
+
this.paneScroll.process = 0;
|
|
889
|
+
this.beginExecutionPreview(sessionId, 'skill', { role: 'system', content: commandText }, 'skill-creator');
|
|
890
|
+
activity.pushLine(`skill ${commandText}`);
|
|
891
|
+
this.busy = true;
|
|
892
|
+
this.currentAbortController = new AbortController();
|
|
893
|
+
this.startAnimation();
|
|
894
|
+
this.statusLine = `Running /${commandName}...`;
|
|
895
|
+
this.requestRender();
|
|
896
|
+
try {
|
|
897
|
+
const result = await runSkillCreatorCommand(this.workspaceRoot, sessionId, trimmedSpec, {
|
|
898
|
+
sandbox: this.options.sandbox,
|
|
899
|
+
signal: this.currentAbortController.signal,
|
|
900
|
+
onUserInput: (question) => this.requestClarification(question),
|
|
901
|
+
onEvent: (event) => {
|
|
902
|
+
activity.handle(event);
|
|
903
|
+
this.updatePreviewFromEvent(event);
|
|
904
|
+
this.requestRender();
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
activity.finish();
|
|
908
|
+
await this.refreshCommands();
|
|
909
|
+
this.finishExecutionPreview({
|
|
910
|
+
persist: true,
|
|
911
|
+
finalText: formatSkillCreatorSummary(this.workspaceRoot, result),
|
|
912
|
+
});
|
|
913
|
+
this.statusLine = `/${commandName} finished.`;
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
activity.finish();
|
|
917
|
+
const message = error.message;
|
|
918
|
+
activity.pushLine(`[skill-creator] error ${message}`);
|
|
919
|
+
this.finishExecutionPreview({ persist: true, error: message });
|
|
920
|
+
this.statusLine = `/${commandName} failed.`;
|
|
921
|
+
}
|
|
922
|
+
finally {
|
|
923
|
+
this.busy = false;
|
|
924
|
+
this.currentAbortController = null;
|
|
925
|
+
this.stopAnimation();
|
|
926
|
+
if (this.exitRequested) {
|
|
927
|
+
this.close();
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
this.requestRender();
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async requestClarification(promptText) {
|
|
935
|
+
this.ensureClarificationVisible(promptText);
|
|
936
|
+
return this.promptForValue(promptText, 'clarify');
|
|
937
|
+
}
|
|
938
|
+
async promptForValue(promptText, kind = 'input') {
|
|
939
|
+
this.inputValue = '';
|
|
940
|
+
this.cursor = 0;
|
|
941
|
+
this.statusLine = kind === 'clarify' ? 'Awaiting clarification...' : promptText;
|
|
942
|
+
this.requestRender();
|
|
943
|
+
return new Promise((resolve) => {
|
|
944
|
+
this.pendingPrompt = { kind, prompt: promptText, resolve };
|
|
945
|
+
this.requestRender();
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
ensureClarificationVisible(promptText) {
|
|
949
|
+
if (!this.currentPreview) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const entry = previewQuestionMessage(promptText);
|
|
953
|
+
const entries = this.currentPreview.entries;
|
|
954
|
+
const pendingAssistantIndex = entries.length > 0 && entries[entries.length - 1].role === 'assistant'
|
|
955
|
+
? entries.length - 1
|
|
956
|
+
: entries.length;
|
|
957
|
+
const previous = pendingAssistantIndex > 0 ? entries[pendingAssistantIndex - 1] : undefined;
|
|
958
|
+
if (sameDisplayMessage(previous, entry)) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
this.insertPreviewMessage(entry);
|
|
962
|
+
}
|
|
963
|
+
async applyCompletion() {
|
|
964
|
+
if (!this.inputValue.startsWith('/')) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
await this.refreshCommands();
|
|
968
|
+
const completion = completeSlashCommand(this.inputValue, [
|
|
969
|
+
...BUILTIN_SLASH_COMMANDS,
|
|
970
|
+
...this.commands.map((command) => command.name),
|
|
971
|
+
]);
|
|
972
|
+
if (completion.applied) {
|
|
973
|
+
this.inputValue = completion.value;
|
|
974
|
+
this.cursor = this.inputValue.length;
|
|
975
|
+
}
|
|
976
|
+
if (completion.matches.length === 0) {
|
|
977
|
+
this.statusLine = 'No matching commands or skills.';
|
|
978
|
+
}
|
|
979
|
+
else if (completion.matches.length === 1) {
|
|
980
|
+
this.statusLine = `Completed /${completion.matches[0]}`;
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
const preview = completion.matches.slice(0, 6).join(', ');
|
|
984
|
+
this.statusLine = `Matches: ${preview}${completion.matches.length > 6 ? ', ...' : ''}`;
|
|
985
|
+
}
|
|
986
|
+
this.requestRender();
|
|
987
|
+
}
|
|
988
|
+
async handleUpDown(offset) {
|
|
989
|
+
if (this.focusedPane === 'sessions') {
|
|
990
|
+
await this.moveSession(offset);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
this.scrollFocusedPane(offset < 0 ? 'up' : 'down', 1);
|
|
994
|
+
}
|
|
995
|
+
cyclePaneFocus(direction) {
|
|
996
|
+
const order = ['sessions', 'messages', 'process', 'context'];
|
|
997
|
+
const currentIndex = order.indexOf(this.focusedPane);
|
|
998
|
+
const nextIndex = (currentIndex + direction + order.length) % order.length;
|
|
999
|
+
this.focusedPane = order[nextIndex];
|
|
1000
|
+
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}. Left/right switches panes; PgUp/PgDn scrolls.`;
|
|
1001
|
+
this.requestRender();
|
|
1002
|
+
}
|
|
1003
|
+
toggleMenuMode() {
|
|
1004
|
+
if (this.pendingPrompt) {
|
|
1005
|
+
this.statusLine = this.pendingPrompt.kind === 'clarify'
|
|
1006
|
+
? 'Finish the clarification prompt before opening menus.'
|
|
1007
|
+
: 'Finish the current input prompt before opening menus.';
|
|
1008
|
+
this.requestRender();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (this.uiMode === 'menu') {
|
|
1012
|
+
this.closeMenuMode();
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
this.menuReturnMode = this.uiMode;
|
|
1016
|
+
this.uiMode = 'menu';
|
|
1017
|
+
this.menuState.typeahead = '';
|
|
1018
|
+
this.menuState.typeaheadAt = 0;
|
|
1019
|
+
this.menuState.selectedIndex = 0;
|
|
1020
|
+
this.statusLine = `${this.getMenuDefinitions()[this.menuState.activeIndex]?.label ?? 'Menu'} menu.`;
|
|
1021
|
+
this.requestRender();
|
|
1022
|
+
}
|
|
1023
|
+
closeMenuMode() {
|
|
1024
|
+
if (this.uiMode !== 'menu') {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
this.uiMode = this.menuReturnMode;
|
|
1028
|
+
this.menuState.typeahead = '';
|
|
1029
|
+
this.menuState.typeaheadAt = 0;
|
|
1030
|
+
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}. F10 or Ctrl+G reopens the menu bar.`;
|
|
1031
|
+
this.requestRender();
|
|
1032
|
+
}
|
|
1033
|
+
moveMenu(direction) {
|
|
1034
|
+
const menus = this.getMenuDefinitions();
|
|
1035
|
+
this.menuState.activeIndex = nextWrappedIndex(this.menuState.activeIndex, direction, menus.length);
|
|
1036
|
+
this.menuState.selectedIndex = 0;
|
|
1037
|
+
this.menuState.typeahead = '';
|
|
1038
|
+
this.menuState.typeaheadAt = 0;
|
|
1039
|
+
this.statusLine = `${menus[this.menuState.activeIndex]?.label ?? 'Menu'} menu.`;
|
|
1040
|
+
this.requestRender();
|
|
1041
|
+
}
|
|
1042
|
+
selectMenu(index) {
|
|
1043
|
+
const menus = this.getMenuDefinitions();
|
|
1044
|
+
if (index < 0 || index >= menus.length) {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
this.menuState.activeIndex = index;
|
|
1048
|
+
this.menuState.selectedIndex = 0;
|
|
1049
|
+
this.menuState.typeahead = '';
|
|
1050
|
+
this.menuState.typeaheadAt = 0;
|
|
1051
|
+
this.statusLine = `${menus[index]?.label ?? 'Menu'} menu.`;
|
|
1052
|
+
this.requestRender();
|
|
1053
|
+
}
|
|
1054
|
+
moveMenuItem(direction, itemCount) {
|
|
1055
|
+
this.menuState.selectedIndex = nextWrappedIndex(this.menuState.selectedIndex, direction, itemCount);
|
|
1056
|
+
this.menuState.typeahead = '';
|
|
1057
|
+
this.menuState.typeaheadAt = 0;
|
|
1058
|
+
this.requestRender();
|
|
1059
|
+
}
|
|
1060
|
+
getMenuDefinitions() {
|
|
1061
|
+
const busy = this.busy || !!this.pendingPrompt;
|
|
1062
|
+
const menus = [
|
|
1063
|
+
{
|
|
1064
|
+
id: 'session',
|
|
1065
|
+
label: 'Session',
|
|
1066
|
+
items: [
|
|
1067
|
+
{ id: 'session.new', label: 'New Session', description: 'Create and switch to a fresh conductor session.', shortcut: 'Enter' },
|
|
1068
|
+
{ id: 'session.refresh', label: 'Refresh Sessions', description: 'Reload the session list and command catalog.' },
|
|
1069
|
+
{ id: 'session.next', label: 'Next Session', description: 'Move to the next session in the left pane.', shortcut: 'Down' },
|
|
1070
|
+
{ id: 'session.previous', label: 'Previous Session', description: 'Move to the previous session in the left pane.', shortcut: 'Up' },
|
|
1071
|
+
{ id: 'session.quit', label: 'Quit', description: 'Leave the fullscreen TUI.', shortcut: 'Ctrl+C' },
|
|
1072
|
+
],
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
id: 'skills',
|
|
1076
|
+
label: 'Skills',
|
|
1077
|
+
items: [
|
|
1078
|
+
{ id: 'skills.browse', label: 'Browse All Skills', description: 'Open a searchable catalog of compiled skills.', shortcut: 'Enter' },
|
|
1079
|
+
{ id: 'skills.run', label: 'Run Skill…', description: 'Pick a skill and execute it from the menu bar.' },
|
|
1080
|
+
{ id: 'skills.inspect', label: 'Inspect Skill Code', description: 'View the compiled DML for a skill.' },
|
|
1081
|
+
{ id: 'skills.edit', label: 'Edit Skill Code', description: 'Open a skill in the inline editor.' },
|
|
1082
|
+
{ id: 'skills.meta', label: 'Inspect Skill Metadata', description: 'View the metadata JSON for a compiled skill.' },
|
|
1083
|
+
{ id: 'skills.source', label: 'Open Skill Source Spec', description: 'Jump from a compiled skill back to its source Markdown.' },
|
|
1084
|
+
],
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
id: 'files',
|
|
1088
|
+
label: 'Files',
|
|
1089
|
+
items: [
|
|
1090
|
+
{ id: 'files.open', label: 'Open Text File…', description: 'Browse workspace text files in a searchable picker.', shortcut: 'Enter' },
|
|
1091
|
+
{ id: 'files.recent', label: 'Recent Files', description: 'Reopen a recently inspected or edited file.' },
|
|
1092
|
+
{ id: 'files.newMarkdown', label: 'New Markdown Note', description: 'Create a new Markdown buffer inside the workspace.' },
|
|
1093
|
+
{ id: 'files.save', label: 'Save', description: 'Save the current editor buffer.', shortcut: 'Ctrl+S', enabled: !!this.editorState },
|
|
1094
|
+
{ id: 'files.saveAs', label: 'Save As…', description: 'Save the current editor buffer to a new path.', enabled: !!this.editorState },
|
|
1095
|
+
{ id: 'files.readme', label: 'Open Workspace README', description: 'Inspect the top-level README.md.' },
|
|
1096
|
+
{ id: 'files.config', label: 'Open .deepclause Config', description: 'Inspect the local DeepClause config file.' },
|
|
1097
|
+
],
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
id: 'run',
|
|
1101
|
+
label: 'Run',
|
|
1102
|
+
items: [
|
|
1103
|
+
{ id: 'run.prompt', label: 'Send Prompt…', description: 'Return to the command bar and enter a freeform prompt.', shortcut: 'Enter' },
|
|
1104
|
+
{ id: 'run.repeat', label: 'Repeat Last Command', description: 'Re-run the last prompt or skill invocation.', enabled: !!this.lastSubmittedInput },
|
|
1105
|
+
{ id: 'run.cancel', label: 'Cancel Current Execution', description: 'Abort the running task or skill.', shortcut: '/cancel', enabled: this.busy },
|
|
1106
|
+
{ id: 'run.clear', label: 'Clear Execution Pane', description: 'Discard the current session execution log.' },
|
|
1107
|
+
{ id: 'run.refreshSkills', label: 'Refresh Skill Catalog', description: 'Re-scan the workspace for compiled skills.' },
|
|
1108
|
+
],
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
id: 'view',
|
|
1112
|
+
label: 'View',
|
|
1113
|
+
items: [
|
|
1114
|
+
{ id: 'view.focus.sessions', label: 'Focus Sessions Pane', description: 'Focus the session list pane.' },
|
|
1115
|
+
{ id: 'view.focus.messages', label: 'Focus Messages Pane', description: 'Focus the conversation history pane.' },
|
|
1116
|
+
{ id: 'view.focus.process', label: 'Focus Execution Pane', description: 'Focus the live execution log pane.' },
|
|
1117
|
+
{ id: 'view.focus.context', label: 'Focus Context Pane', description: 'Focus the session context and token usage pane.' },
|
|
1118
|
+
{ id: 'view.follow', label: 'Follow Focused Pane', description: 'Jump back to the newest visible content in the active pane.' },
|
|
1119
|
+
{ id: 'view.palette', label: 'Open Action Palette', description: 'Search all menu actions from one picker.', shortcut: 'Ctrl+P' },
|
|
1120
|
+
],
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
id: 'help',
|
|
1124
|
+
label: 'Help',
|
|
1125
|
+
items: [
|
|
1126
|
+
{ id: 'help.shortcuts', label: 'Keyboard Shortcuts', description: 'Show the fullscreen keyboard map.' },
|
|
1127
|
+
{ id: 'help.slash', label: 'Slash Commands', description: 'Show available slash commands and direct skill invocation.' },
|
|
1128
|
+
{ id: 'help.about', label: 'About This TUI', description: 'Show a short explanation of the fullscreen workspace.' },
|
|
1129
|
+
],
|
|
1130
|
+
},
|
|
1131
|
+
];
|
|
1132
|
+
return menus.map((menu) => ({
|
|
1133
|
+
...menu,
|
|
1134
|
+
items: menu.items.map((item) => ({
|
|
1135
|
+
...item,
|
|
1136
|
+
enabled: item.enabled ?? (!busy || item.id === 'run.cancel'),
|
|
1137
|
+
})),
|
|
1138
|
+
}));
|
|
1139
|
+
}
|
|
1140
|
+
async runMenuAction(actionId) {
|
|
1141
|
+
switch (actionId) {
|
|
1142
|
+
case 'session.new': {
|
|
1143
|
+
const created = await createConductorSession(this.workspaceRoot);
|
|
1144
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: created.id });
|
|
1145
|
+
this.closeMenuMode();
|
|
1146
|
+
this.statusLine = `Created session ${created.title}.`;
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
case 'session.refresh':
|
|
1150
|
+
await this.refreshSessions({ createIfMissing: true, selectedSessionId: this.selectedSessionId });
|
|
1151
|
+
await this.refreshCommands();
|
|
1152
|
+
this.closeMenuMode();
|
|
1153
|
+
this.statusLine = `Loaded ${this.sessions.length} session${this.sessions.length === 1 ? '' : 's'}.`;
|
|
1154
|
+
return;
|
|
1155
|
+
case 'session.next':
|
|
1156
|
+
await this.moveSession(1);
|
|
1157
|
+
return;
|
|
1158
|
+
case 'session.previous':
|
|
1159
|
+
await this.moveSession(-1);
|
|
1160
|
+
return;
|
|
1161
|
+
case 'session.quit':
|
|
1162
|
+
this.close();
|
|
1163
|
+
return;
|
|
1164
|
+
case 'skills.browse':
|
|
1165
|
+
await this.openSkillPicker('skills.browse');
|
|
1166
|
+
return;
|
|
1167
|
+
case 'skills.run':
|
|
1168
|
+
await this.openSkillPicker('skills.run');
|
|
1169
|
+
return;
|
|
1170
|
+
case 'skills.inspect':
|
|
1171
|
+
await this.openSkillPicker('skills.inspect');
|
|
1172
|
+
return;
|
|
1173
|
+
case 'skills.edit':
|
|
1174
|
+
await this.openSkillPicker('skills.edit');
|
|
1175
|
+
return;
|
|
1176
|
+
case 'skills.meta':
|
|
1177
|
+
await this.openSkillPicker('skills.meta');
|
|
1178
|
+
return;
|
|
1179
|
+
case 'skills.source':
|
|
1180
|
+
await this.openSkillPicker('skills.source');
|
|
1181
|
+
return;
|
|
1182
|
+
case 'files.open':
|
|
1183
|
+
await this.openFilePicker('files.inspect');
|
|
1184
|
+
return;
|
|
1185
|
+
case 'files.recent':
|
|
1186
|
+
await this.openFilePicker('files.recent');
|
|
1187
|
+
return;
|
|
1188
|
+
case 'files.newMarkdown':
|
|
1189
|
+
this.openEditorBuffer('', { title: 'New Markdown Note', preferredExtension: '.md' });
|
|
1190
|
+
return;
|
|
1191
|
+
case 'files.save':
|
|
1192
|
+
await this.saveCurrentEditor(false);
|
|
1193
|
+
return;
|
|
1194
|
+
case 'files.saveAs':
|
|
1195
|
+
await this.saveCurrentEditor(true);
|
|
1196
|
+
return;
|
|
1197
|
+
case 'files.readme':
|
|
1198
|
+
await this.openFileViewer(path.join(this.workspaceRoot, 'README.md'), { title: 'README.md' });
|
|
1199
|
+
return;
|
|
1200
|
+
case 'files.config':
|
|
1201
|
+
await this.openFileViewer(getConfigPath(this.workspaceRoot), { title: '.deepclause/config.json' });
|
|
1202
|
+
return;
|
|
1203
|
+
case 'run.prompt':
|
|
1204
|
+
this.closeMenuMode();
|
|
1205
|
+
this.statusLine = 'Command bar ready.';
|
|
1206
|
+
return;
|
|
1207
|
+
case 'run.repeat':
|
|
1208
|
+
await this.repeatLastCommand();
|
|
1209
|
+
return;
|
|
1210
|
+
case 'run.cancel':
|
|
1211
|
+
this.closeMenuMode();
|
|
1212
|
+
this.cancelCurrentExecution('Cancellation requested.');
|
|
1213
|
+
return;
|
|
1214
|
+
case 'run.clear':
|
|
1215
|
+
this.currentProcessActivity().clear();
|
|
1216
|
+
this.closeMenuMode();
|
|
1217
|
+
this.statusLine = 'Execution pane cleared.';
|
|
1218
|
+
return;
|
|
1219
|
+
case 'run.refreshSkills':
|
|
1220
|
+
await this.refreshCommands();
|
|
1221
|
+
this.closeMenuMode();
|
|
1222
|
+
this.statusLine = `Loaded ${this.commands.length} skill${this.commands.length === 1 ? '' : 's'}.`;
|
|
1223
|
+
return;
|
|
1224
|
+
case 'view.focus.sessions':
|
|
1225
|
+
case 'view.focus.messages':
|
|
1226
|
+
case 'view.focus.process':
|
|
1227
|
+
case 'view.focus.context': {
|
|
1228
|
+
const pane = actionId.slice('view.focus.'.length);
|
|
1229
|
+
this.focusedPane = pane;
|
|
1230
|
+
this.closeMenuMode();
|
|
1231
|
+
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}.`;
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
case 'view.follow':
|
|
1235
|
+
if (this.focusedPane !== 'sessions') {
|
|
1236
|
+
this.paneScroll[this.focusedPane] = 0;
|
|
1237
|
+
}
|
|
1238
|
+
this.closeMenuMode();
|
|
1239
|
+
this.statusLine = `${paneDisplayName(this.focusedPane)} following newest content.`;
|
|
1240
|
+
return;
|
|
1241
|
+
case 'view.palette':
|
|
1242
|
+
this.openActionPalette();
|
|
1243
|
+
return;
|
|
1244
|
+
case 'help.shortcuts':
|
|
1245
|
+
this.openTextViewer('Keyboard Shortcuts', getShortcutHelpLines().join('\n'), { preferredExtension: '.txt' });
|
|
1246
|
+
return;
|
|
1247
|
+
case 'help.slash':
|
|
1248
|
+
this.openTextViewer('Slash Commands', getSlashHelpLines().join('\n'), { preferredExtension: '.txt' });
|
|
1249
|
+
return;
|
|
1250
|
+
case 'help.about':
|
|
1251
|
+
this.openTextViewer('About This TUI', [
|
|
1252
|
+
'DeepClause fullscreen TUI',
|
|
1253
|
+
'',
|
|
1254
|
+
'Use F10 or Ctrl+G for the menu bar, Ctrl+W to cycle panes, and /cancel to stop a running execution.',
|
|
1255
|
+
'The Skills and Files menus open searchable pickers, and Ctrl+P opens the action palette directly.',
|
|
1256
|
+
].join('\n'), { preferredExtension: '.txt' });
|
|
1257
|
+
return;
|
|
1258
|
+
default:
|
|
1259
|
+
this.closeMenuMode();
|
|
1260
|
+
this.statusLine = 'This menu action is being wired up next.';
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
closeWorkspaceMode(force = false) {
|
|
1265
|
+
if (!force && !this.canReplaceWorkspaceSurface()) {
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
this.uiMode = 'command';
|
|
1269
|
+
this.pickerState = null;
|
|
1270
|
+
this.viewerState = null;
|
|
1271
|
+
this.editorState = null;
|
|
1272
|
+
this.inputValue = '';
|
|
1273
|
+
this.cursor = 0;
|
|
1274
|
+
this.requestRender();
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
canReplaceWorkspaceSurface() {
|
|
1278
|
+
if (this.editorState?.dirty) {
|
|
1279
|
+
this.statusLine = 'Save the current editor with Ctrl+S or press Esc again in the editor to discard changes.';
|
|
1280
|
+
this.requestRender();
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
workspaceViewportLines() {
|
|
1286
|
+
return Math.max(1, (output.rows ?? 40) - 5);
|
|
1287
|
+
}
|
|
1288
|
+
openActionPalette() {
|
|
1289
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const items = this.getMenuDefinitions()
|
|
1293
|
+
.flatMap((menu) => menu.items)
|
|
1294
|
+
.filter((item) => item.enabled !== false)
|
|
1295
|
+
.map((item) => ({
|
|
1296
|
+
id: item.id,
|
|
1297
|
+
label: item.label,
|
|
1298
|
+
description: item.description,
|
|
1299
|
+
detail: item.shortcut,
|
|
1300
|
+
actionId: item.id,
|
|
1301
|
+
}));
|
|
1302
|
+
this.openPicker({
|
|
1303
|
+
mode: 'palette',
|
|
1304
|
+
title: 'Action Palette',
|
|
1305
|
+
emptyText: 'No actions match the current query.',
|
|
1306
|
+
items,
|
|
1307
|
+
filteredItems: items,
|
|
1308
|
+
selectedIndex: 0,
|
|
1309
|
+
scrollTop: 0,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
async openSkillPicker(mode) {
|
|
1313
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
await this.refreshCommands();
|
|
1317
|
+
const items = await Promise.all(this.commands.map(async (command) => {
|
|
1318
|
+
const skillPath = this.resolveCommandDmlPath(command);
|
|
1319
|
+
const metaPath = this.resolveCommandMetaPath(command);
|
|
1320
|
+
let sourcePath;
|
|
1321
|
+
try {
|
|
1322
|
+
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
|
|
1323
|
+
if (meta.source) {
|
|
1324
|
+
sourcePath = path.resolve(this.workspaceRoot, meta.source);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
// Metadata is optional while browsing.
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
id: command.name,
|
|
1332
|
+
label: command.name,
|
|
1333
|
+
description: command.description,
|
|
1334
|
+
detail: command.path,
|
|
1335
|
+
path: skillPath,
|
|
1336
|
+
skillName: command.name,
|
|
1337
|
+
metaPath,
|
|
1338
|
+
sourcePath,
|
|
1339
|
+
};
|
|
1340
|
+
}));
|
|
1341
|
+
const titles = {
|
|
1342
|
+
'skills.browse': 'Browse Skills',
|
|
1343
|
+
'skills.run': 'Run Skill',
|
|
1344
|
+
'skills.inspect': 'Inspect Skill Code',
|
|
1345
|
+
'skills.edit': 'Edit Skill Code',
|
|
1346
|
+
'skills.meta': 'Inspect Skill Metadata',
|
|
1347
|
+
'skills.source': 'Open Skill Source Spec',
|
|
1348
|
+
};
|
|
1349
|
+
this.openPicker({
|
|
1350
|
+
mode,
|
|
1351
|
+
title: titles[mode],
|
|
1352
|
+
emptyText: 'No skills match the current query.',
|
|
1353
|
+
items,
|
|
1354
|
+
filteredItems: items,
|
|
1355
|
+
selectedIndex: 0,
|
|
1356
|
+
scrollTop: 0,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
async openFilePicker(mode) {
|
|
1360
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const relativePaths = mode === 'files.recent'
|
|
1364
|
+
? [...this.recentFiles]
|
|
1365
|
+
: await listWorkspaceTextFiles(this.workspaceRoot);
|
|
1366
|
+
const items = relativePaths.map((relativePath) => ({
|
|
1367
|
+
id: relativePath,
|
|
1368
|
+
label: path.basename(relativePath),
|
|
1369
|
+
description: relativePath,
|
|
1370
|
+
detail: path.extname(relativePath) || 'text',
|
|
1371
|
+
path: path.join(this.workspaceRoot, relativePath),
|
|
1372
|
+
}));
|
|
1373
|
+
const titles = {
|
|
1374
|
+
'files.inspect': 'Open Text File',
|
|
1375
|
+
'files.edit': 'Edit Text File',
|
|
1376
|
+
'files.recent': 'Recent Files',
|
|
1377
|
+
};
|
|
1378
|
+
this.openPicker({
|
|
1379
|
+
mode,
|
|
1380
|
+
title: titles[mode],
|
|
1381
|
+
emptyText: mode === 'files.recent'
|
|
1382
|
+
? 'No recent files yet. Open or edit a file first.'
|
|
1383
|
+
: 'No text files match the current query.',
|
|
1384
|
+
items,
|
|
1385
|
+
filteredItems: items,
|
|
1386
|
+
selectedIndex: 0,
|
|
1387
|
+
scrollTop: 0,
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
openPicker(state) {
|
|
1391
|
+
this.closeMenuMode();
|
|
1392
|
+
this.uiMode = state.mode === 'palette' ? 'palette' : 'picker';
|
|
1393
|
+
this.pickerState = state;
|
|
1394
|
+
this.viewerState = null;
|
|
1395
|
+
this.editorState = null;
|
|
1396
|
+
this.inputValue = '';
|
|
1397
|
+
this.cursor = 0;
|
|
1398
|
+
this.ensurePickerSelectionVisible();
|
|
1399
|
+
this.statusLine = `${state.title}.`;
|
|
1400
|
+
this.requestRender();
|
|
1401
|
+
}
|
|
1402
|
+
refreshPickerFilter() {
|
|
1403
|
+
if (!this.pickerState) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
this.pickerState.filteredItems = filterPickerItems(this.pickerState.items, this.inputValue);
|
|
1407
|
+
this.pickerState.selectedIndex = clamp(this.pickerState.selectedIndex, 0, Math.max(0, this.pickerState.filteredItems.length - 1));
|
|
1408
|
+
this.pickerState.scrollTop = 0;
|
|
1409
|
+
this.ensurePickerSelectionVisible();
|
|
1410
|
+
this.requestRender();
|
|
1411
|
+
}
|
|
1412
|
+
movePickerSelection(delta) {
|
|
1413
|
+
if (!this.pickerState || this.pickerState.filteredItems.length === 0) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
this.pickerState.selectedIndex = clamp(this.pickerState.selectedIndex + delta, 0, Math.max(0, this.pickerState.filteredItems.length - 1));
|
|
1417
|
+
this.ensurePickerSelectionVisible();
|
|
1418
|
+
this.requestRender();
|
|
1419
|
+
}
|
|
1420
|
+
ensurePickerSelectionVisible() {
|
|
1421
|
+
if (!this.pickerState) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const targetLine = 3 + (this.pickerState.selectedIndex * 3);
|
|
1425
|
+
const viewport = this.workspaceViewportLines();
|
|
1426
|
+
if (targetLine < this.pickerState.scrollTop) {
|
|
1427
|
+
this.pickerState.scrollTop = targetLine;
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
if (targetLine >= this.pickerState.scrollTop + viewport) {
|
|
1431
|
+
this.pickerState.scrollTop = Math.max(0, targetLine - viewport + 1);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
async activatePickerSelection(override) {
|
|
1435
|
+
const picker = this.pickerState;
|
|
1436
|
+
const item = picker?.filteredItems[picker.selectedIndex];
|
|
1437
|
+
if (!picker || !item) {
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
if (picker.mode === 'palette') {
|
|
1441
|
+
await this.runMenuAction(item.actionId ?? 'help.about');
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
const action = override ?? picker.mode;
|
|
1445
|
+
switch (action) {
|
|
1446
|
+
case 'skills.browse':
|
|
1447
|
+
case 'skills.inspect':
|
|
1448
|
+
await this.openSkillViewer(item, 'dml');
|
|
1449
|
+
return;
|
|
1450
|
+
case 'skills.run': {
|
|
1451
|
+
this.closeWorkspaceMode();
|
|
1452
|
+
const rawArgs = await this.promptForValue(`Arguments for /${item.skillName} (optional):`);
|
|
1453
|
+
await this.executeSkill(item.skillName ?? item.label, parseCommandArgs(rawArgs));
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
case 'run':
|
|
1457
|
+
if (item.skillName) {
|
|
1458
|
+
this.closeWorkspaceMode();
|
|
1459
|
+
const rawArgs = await this.promptForValue(`Arguments for /${item.skillName} (optional):`);
|
|
1460
|
+
await this.executeSkill(item.skillName, parseCommandArgs(rawArgs));
|
|
1461
|
+
}
|
|
1462
|
+
return;
|
|
1463
|
+
case 'edit':
|
|
1464
|
+
case 'skills.edit':
|
|
1465
|
+
case 'files.edit':
|
|
1466
|
+
if (item.path) {
|
|
1467
|
+
await this.openEditorForFile(item.path, { title: item.description || item.label });
|
|
1468
|
+
}
|
|
1469
|
+
return;
|
|
1470
|
+
case 'meta':
|
|
1471
|
+
case 'skills.meta':
|
|
1472
|
+
if (item.metaPath) {
|
|
1473
|
+
await this.openSkillViewer(item, 'meta');
|
|
1474
|
+
}
|
|
1475
|
+
return;
|
|
1476
|
+
case 'source':
|
|
1477
|
+
case 'skills.source':
|
|
1478
|
+
if (item.sourcePath) {
|
|
1479
|
+
await this.openSkillViewer(item, 'source');
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
this.statusLine = `No source spec recorded for ${item.label}.`;
|
|
1483
|
+
this.requestRender();
|
|
1484
|
+
}
|
|
1485
|
+
return;
|
|
1486
|
+
case 'files.inspect':
|
|
1487
|
+
case 'files.recent':
|
|
1488
|
+
if (item.path) {
|
|
1489
|
+
await this.openFileViewer(item.path, { title: item.description || item.label });
|
|
1490
|
+
}
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
async openSkillViewer(item, target) {
|
|
1495
|
+
const filePath = target === 'dml' ? item.path : target === 'meta' ? item.metaPath : item.sourcePath;
|
|
1496
|
+
if (!filePath) {
|
|
1497
|
+
this.statusLine = `No ${target} file available for ${item.label}.`;
|
|
1498
|
+
this.requestRender();
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const title = target === 'dml'
|
|
1502
|
+
? `Skill: ${item.label}`
|
|
1503
|
+
: target === 'meta'
|
|
1504
|
+
? `Metadata: ${item.label}`
|
|
1505
|
+
: `Source Spec: ${item.label}`;
|
|
1506
|
+
await this.openFileViewer(filePath, {
|
|
1507
|
+
title,
|
|
1508
|
+
skillName: item.skillName,
|
|
1509
|
+
canEdit: target !== 'meta',
|
|
1510
|
+
preferredExtension: target === 'meta' ? '.json' : path.extname(filePath) || '.txt',
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
openTextViewer(title, content, options = {}) {
|
|
1514
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
this.closeMenuMode();
|
|
1518
|
+
this.uiMode = 'viewer';
|
|
1519
|
+
this.pickerState = null;
|
|
1520
|
+
this.editorState = null;
|
|
1521
|
+
this.viewerState = {
|
|
1522
|
+
title,
|
|
1523
|
+
content,
|
|
1524
|
+
path: options.path,
|
|
1525
|
+
canEdit: options.canEdit ?? false,
|
|
1526
|
+
scrollTop: 0,
|
|
1527
|
+
skillName: options.skillName,
|
|
1528
|
+
metaPath: options.metaPath,
|
|
1529
|
+
sourcePath: options.sourcePath,
|
|
1530
|
+
preferredExtension: options.preferredExtension,
|
|
1531
|
+
};
|
|
1532
|
+
this.inputValue = '';
|
|
1533
|
+
this.cursor = 0;
|
|
1534
|
+
if (options.path) {
|
|
1535
|
+
this.pushRecentFile(options.path);
|
|
1536
|
+
}
|
|
1537
|
+
this.statusLine = `Viewing ${title}.`;
|
|
1538
|
+
this.requestRender();
|
|
1539
|
+
}
|
|
1540
|
+
async openFileViewer(filePath, options = {}) {
|
|
1541
|
+
try {
|
|
1542
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1543
|
+
this.openTextViewer(options.title ?? path.relative(this.workspaceRoot, filePath), content, {
|
|
1544
|
+
path: filePath,
|
|
1545
|
+
canEdit: options.canEdit ?? isEditableTextFile(filePath),
|
|
1546
|
+
skillName: options.skillName,
|
|
1547
|
+
preferredExtension: options.preferredExtension ?? (path.extname(filePath) || '.txt'),
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
this.closeMenuMode();
|
|
1552
|
+
this.statusLine = `Unable to open ${path.relative(this.workspaceRoot, filePath)}: ${error.message}`;
|
|
1553
|
+
this.requestRender();
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async openEditorForFile(filePath, options = {}) {
|
|
1557
|
+
try {
|
|
1558
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1559
|
+
this.openEditorBuffer(content, {
|
|
1560
|
+
title: options.title ?? path.relative(this.workspaceRoot, filePath),
|
|
1561
|
+
path: filePath,
|
|
1562
|
+
preferredExtension: options.preferredExtension ?? (path.extname(filePath) || '.txt'),
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
catch (error) {
|
|
1566
|
+
this.statusLine = `Unable to edit ${path.relative(this.workspaceRoot, filePath)}: ${error.message}`;
|
|
1567
|
+
this.requestRender();
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
openEditorBuffer(content, options) {
|
|
1571
|
+
if (!this.canReplaceWorkspaceSurface()) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
this.closeMenuMode();
|
|
1575
|
+
this.uiMode = 'editor';
|
|
1576
|
+
this.pickerState = null;
|
|
1577
|
+
this.viewerState = null;
|
|
1578
|
+
this.editorState = {
|
|
1579
|
+
title: options.title,
|
|
1580
|
+
path: options.path,
|
|
1581
|
+
lines: splitEditorContent(content),
|
|
1582
|
+
cursorLine: 0,
|
|
1583
|
+
cursorColumn: 0,
|
|
1584
|
+
scrollTop: 0,
|
|
1585
|
+
scrollLeft: 0,
|
|
1586
|
+
dirty: false,
|
|
1587
|
+
preferredExtension: options.preferredExtension ?? '.txt',
|
|
1588
|
+
discardPending: false,
|
|
1589
|
+
};
|
|
1590
|
+
this.inputValue = '';
|
|
1591
|
+
this.cursor = 0;
|
|
1592
|
+
if (options.path) {
|
|
1593
|
+
this.pushRecentFile(options.path);
|
|
1594
|
+
}
|
|
1595
|
+
this.ensureEditorCursorVisible();
|
|
1596
|
+
this.statusLine = `Editing ${options.title}.`;
|
|
1597
|
+
this.requestRender();
|
|
1598
|
+
}
|
|
1599
|
+
currentEditorLine() {
|
|
1600
|
+
return this.editorState?.lines[this.editorState.cursorLine] ?? '';
|
|
1601
|
+
}
|
|
1602
|
+
updateCurrentEditorLine(nextLine) {
|
|
1603
|
+
if (!this.editorState) {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
this.editorState.lines[this.editorState.cursorLine] = nextLine;
|
|
1607
|
+
this.editorState.dirty = true;
|
|
1608
|
+
this.editorState.discardPending = false;
|
|
1609
|
+
}
|
|
1610
|
+
insertEditorText(text) {
|
|
1611
|
+
if (!this.editorState) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
const line = this.currentEditorLine();
|
|
1615
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn)}${text}${line.slice(this.editorState.cursorColumn)}`);
|
|
1616
|
+
this.editorState.cursorColumn += text.length;
|
|
1617
|
+
this.ensureEditorCursorVisible();
|
|
1618
|
+
this.requestRender();
|
|
1619
|
+
}
|
|
1620
|
+
splitEditorLine() {
|
|
1621
|
+
if (!this.editorState) {
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
const line = this.currentEditorLine();
|
|
1625
|
+
const before = line.slice(0, this.editorState.cursorColumn);
|
|
1626
|
+
const after = line.slice(this.editorState.cursorColumn);
|
|
1627
|
+
this.editorState.lines.splice(this.editorState.cursorLine, 1, before, after);
|
|
1628
|
+
this.editorState.cursorLine += 1;
|
|
1629
|
+
this.editorState.cursorColumn = 0;
|
|
1630
|
+
this.editorState.dirty = true;
|
|
1631
|
+
this.editorState.discardPending = false;
|
|
1632
|
+
this.ensureEditorCursorVisible();
|
|
1633
|
+
this.requestRender();
|
|
1634
|
+
}
|
|
1635
|
+
deleteEditorBackward() {
|
|
1636
|
+
if (!this.editorState) {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (this.editorState.cursorColumn > 0) {
|
|
1640
|
+
const line = this.currentEditorLine();
|
|
1641
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn - 1)}${line.slice(this.editorState.cursorColumn)}`);
|
|
1642
|
+
this.editorState.cursorColumn -= 1;
|
|
1643
|
+
}
|
|
1644
|
+
else if (this.editorState.cursorLine > 0) {
|
|
1645
|
+
const previous = this.editorState.lines[this.editorState.cursorLine - 1] ?? '';
|
|
1646
|
+
const current = this.currentEditorLine();
|
|
1647
|
+
this.editorState.lines.splice(this.editorState.cursorLine - 1, 2, `${previous}${current}`);
|
|
1648
|
+
this.editorState.cursorLine -= 1;
|
|
1649
|
+
this.editorState.cursorColumn = previous.length;
|
|
1650
|
+
this.editorState.dirty = true;
|
|
1651
|
+
this.editorState.discardPending = false;
|
|
1652
|
+
}
|
|
1653
|
+
this.ensureEditorCursorVisible();
|
|
1654
|
+
this.requestRender();
|
|
1655
|
+
}
|
|
1656
|
+
deleteEditorForward() {
|
|
1657
|
+
if (!this.editorState) {
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
const line = this.currentEditorLine();
|
|
1661
|
+
if (this.editorState.cursorColumn < line.length) {
|
|
1662
|
+
this.updateCurrentEditorLine(`${line.slice(0, this.editorState.cursorColumn)}${line.slice(this.editorState.cursorColumn + 1)}`);
|
|
1663
|
+
}
|
|
1664
|
+
else if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1665
|
+
const next = this.editorState.lines[this.editorState.cursorLine + 1] ?? '';
|
|
1666
|
+
this.editorState.lines.splice(this.editorState.cursorLine, 2, `${line}${next}`);
|
|
1667
|
+
this.editorState.dirty = true;
|
|
1668
|
+
this.editorState.discardPending = false;
|
|
1669
|
+
}
|
|
1670
|
+
this.ensureEditorCursorVisible();
|
|
1671
|
+
this.requestRender();
|
|
1672
|
+
}
|
|
1673
|
+
moveEditorCursor(direction) {
|
|
1674
|
+
if (!this.editorState) {
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
switch (direction) {
|
|
1678
|
+
case 'left':
|
|
1679
|
+
if (this.editorState.cursorColumn > 0) {
|
|
1680
|
+
this.editorState.cursorColumn -= 1;
|
|
1681
|
+
}
|
|
1682
|
+
else if (this.editorState.cursorLine > 0) {
|
|
1683
|
+
this.editorState.cursorLine -= 1;
|
|
1684
|
+
this.editorState.cursorColumn = this.currentEditorLine().length;
|
|
1685
|
+
}
|
|
1686
|
+
break;
|
|
1687
|
+
case 'right':
|
|
1688
|
+
if (this.editorState.cursorColumn < this.currentEditorLine().length) {
|
|
1689
|
+
this.editorState.cursorColumn += 1;
|
|
1690
|
+
}
|
|
1691
|
+
else if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1692
|
+
this.editorState.cursorLine += 1;
|
|
1693
|
+
this.editorState.cursorColumn = 0;
|
|
1694
|
+
}
|
|
1695
|
+
break;
|
|
1696
|
+
case 'up':
|
|
1697
|
+
if (this.editorState.cursorLine > 0) {
|
|
1698
|
+
this.editorState.cursorLine -= 1;
|
|
1699
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1700
|
+
}
|
|
1701
|
+
break;
|
|
1702
|
+
case 'down':
|
|
1703
|
+
if (this.editorState.cursorLine < this.editorState.lines.length - 1) {
|
|
1704
|
+
this.editorState.cursorLine += 1;
|
|
1705
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1706
|
+
}
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1709
|
+
this.ensureEditorCursorVisible();
|
|
1710
|
+
this.requestRender();
|
|
1711
|
+
}
|
|
1712
|
+
pageEditorCursor(direction) {
|
|
1713
|
+
if (!this.editorState) {
|
|
1714
|
+
return;
|
|
527
1715
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1716
|
+
const nextLine = clamp(this.editorState.cursorLine + (direction * this.workspaceViewportLines()), 0, Math.max(0, this.editorState.lines.length - 1));
|
|
1717
|
+
this.editorState.cursorLine = nextLine;
|
|
1718
|
+
this.editorState.cursorColumn = Math.min(this.editorState.cursorColumn, this.currentEditorLine().length);
|
|
1719
|
+
this.ensureEditorCursorVisible();
|
|
1720
|
+
this.requestRender();
|
|
1721
|
+
}
|
|
1722
|
+
ensureEditorCursorVisible() {
|
|
1723
|
+
if (!this.editorState) {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const viewportLines = this.workspaceViewportLines();
|
|
1727
|
+
if (this.editorState.cursorLine < this.editorState.scrollTop) {
|
|
1728
|
+
this.editorState.scrollTop = this.editorState.cursorLine;
|
|
1729
|
+
}
|
|
1730
|
+
else if (this.editorState.cursorLine >= this.editorState.scrollTop + viewportLines) {
|
|
1731
|
+
this.editorState.scrollTop = Math.max(0, this.editorState.cursorLine - viewportLines + 1);
|
|
1732
|
+
}
|
|
1733
|
+
const width = output.columns ?? 120;
|
|
1734
|
+
const innerWidth = Math.max(1, width - 2);
|
|
1735
|
+
const lineNumberWidth = Math.max(2, String(Math.max(1, this.editorState.lines.length)).length);
|
|
1736
|
+
const prefixWidth = 2 + lineNumberWidth + 3;
|
|
1737
|
+
const contentWidth = Math.max(1, innerWidth - prefixWidth);
|
|
1738
|
+
if (this.editorState.cursorColumn < this.editorState.scrollLeft) {
|
|
1739
|
+
this.editorState.scrollLeft = this.editorState.cursorColumn;
|
|
1740
|
+
}
|
|
1741
|
+
else if (this.editorState.cursorColumn >= this.editorState.scrollLeft + contentWidth) {
|
|
1742
|
+
this.editorState.scrollLeft = Math.max(0, this.editorState.cursorColumn - contentWidth + 1);
|
|
538
1743
|
}
|
|
539
1744
|
}
|
|
540
|
-
async
|
|
541
|
-
this.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
this.statusLine = 'Awaiting clarification...';
|
|
545
|
-
this.requestRender();
|
|
546
|
-
return new Promise((resolve) => {
|
|
547
|
-
this.pendingQuestion = { prompt: promptText, resolve };
|
|
1745
|
+
async saveCurrentEditor(saveAs) {
|
|
1746
|
+
if (!this.editorState) {
|
|
1747
|
+
this.closeMenuMode();
|
|
1748
|
+
this.statusLine = 'No editor buffer is active.';
|
|
548
1749
|
this.requestRender();
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
async applyCompletion() {
|
|
552
|
-
if (!this.inputValue.startsWith('/')) {
|
|
553
1750
|
return;
|
|
554
1751
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
this.cursor = this.inputValue.length;
|
|
1752
|
+
let targetPath = this.editorState.path;
|
|
1753
|
+
if (saveAs || !targetPath) {
|
|
1754
|
+
const suggested = this.editorState.path
|
|
1755
|
+
? path.relative(this.workspaceRoot, this.editorState.path)
|
|
1756
|
+
: `notes/${Date.now()}.${this.editorState.preferredExtension.replace(/^\./, '')}`;
|
|
1757
|
+
const requestedPath = (await this.promptForValue(`Save file as (relative path) [${suggested}]:`)).trim() || suggested;
|
|
1758
|
+
targetPath = resolveWorkspaceTextPath(this.workspaceRoot, requestedPath);
|
|
563
1759
|
}
|
|
564
|
-
|
|
565
|
-
|
|
1760
|
+
try {
|
|
1761
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
1762
|
+
await fs.writeFile(targetPath, this.editorState.lines.join('\n'), 'utf8');
|
|
1763
|
+
this.editorState.path = targetPath;
|
|
1764
|
+
this.editorState.title = path.relative(this.workspaceRoot, targetPath);
|
|
1765
|
+
this.editorState.dirty = false;
|
|
1766
|
+
this.editorState.discardPending = false;
|
|
1767
|
+
this.pushRecentFile(targetPath);
|
|
1768
|
+
this.statusLine = `Saved ${path.relative(this.workspaceRoot, targetPath)}.`;
|
|
1769
|
+
this.requestRender();
|
|
566
1770
|
}
|
|
567
|
-
|
|
568
|
-
this.statusLine = `
|
|
1771
|
+
catch (error) {
|
|
1772
|
+
this.statusLine = `Unable to save file: ${error.message}`;
|
|
1773
|
+
this.requestRender();
|
|
569
1774
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1775
|
+
}
|
|
1776
|
+
pushRecentFile(filePath) {
|
|
1777
|
+
const relativePath = path.relative(this.workspaceRoot, filePath).replace(/\\/g, '/');
|
|
1778
|
+
const existingIndex = this.recentFiles.indexOf(relativePath);
|
|
1779
|
+
if (existingIndex >= 0) {
|
|
1780
|
+
this.recentFiles.splice(existingIndex, 1);
|
|
1781
|
+
}
|
|
1782
|
+
this.recentFiles.unshift(relativePath);
|
|
1783
|
+
if (this.recentFiles.length > 20) {
|
|
1784
|
+
this.recentFiles.length = 20;
|
|
573
1785
|
}
|
|
574
|
-
this.requestRender();
|
|
575
1786
|
}
|
|
576
|
-
async
|
|
577
|
-
if (this.
|
|
578
|
-
|
|
1787
|
+
async repeatLastCommand() {
|
|
1788
|
+
if (!this.lastSubmittedInput) {
|
|
1789
|
+
this.closeMenuMode();
|
|
1790
|
+
this.statusLine = 'No previous prompt or skill to repeat yet.';
|
|
1791
|
+
this.requestRender();
|
|
579
1792
|
return;
|
|
580
1793
|
}
|
|
581
|
-
this.
|
|
1794
|
+
if (!this.closeWorkspaceMode()) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (this.lastSubmittedInput.kind === 'text') {
|
|
1798
|
+
await this.executeTask(this.lastSubmittedInput.prompt);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (this.lastSubmittedInput.kind === 'skill') {
|
|
1802
|
+
await this.executeSkill(this.lastSubmittedInput.name, this.lastSubmittedInput.args);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (this.lastSubmittedInput.kind === 'builtin') {
|
|
1806
|
+
await this.executeBuiltin(this.lastSubmittedInput);
|
|
1807
|
+
}
|
|
582
1808
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const currentIndex = order.indexOf(this.focusedPane);
|
|
586
|
-
const nextIndex = (currentIndex + direction + order.length) % order.length;
|
|
587
|
-
this.focusedPane = order[nextIndex];
|
|
588
|
-
this.statusLine = `Focus: ${paneDisplayName(this.focusedPane)}. Left/right switches panes; PgUp/PgDn scrolls.`;
|
|
589
|
-
this.requestRender();
|
|
1809
|
+
resolveCommandDmlPath(command) {
|
|
1810
|
+
return path.join(this.workspaceRoot, `${command.path}.dml`);
|
|
590
1811
|
}
|
|
591
|
-
|
|
1812
|
+
resolveCommandMetaPath(command) {
|
|
1813
|
+
return path.join(this.workspaceRoot, `${command.path}.meta.json`);
|
|
1814
|
+
}
|
|
1815
|
+
beginExecutionPreview(sessionId, kind, lead, rootTag) {
|
|
592
1816
|
this.currentPreview = {
|
|
593
1817
|
sessionId,
|
|
594
1818
|
kind,
|
|
1819
|
+
rootTag,
|
|
1820
|
+
activeChildTag: kind === 'skill' ? rootTag : undefined,
|
|
595
1821
|
entries: [lead, { role: 'assistant', content: '', pending: true }],
|
|
596
1822
|
};
|
|
597
1823
|
}
|
|
@@ -599,7 +1825,12 @@ class FullscreenTui {
|
|
|
599
1825
|
if (!this.currentPreview) {
|
|
600
1826
|
return;
|
|
601
1827
|
}
|
|
602
|
-
|
|
1828
|
+
this.updatePreviewChildState(logEvent);
|
|
1829
|
+
const previewMessage = previewMessageFromEvent(logEvent);
|
|
1830
|
+
if (previewMessage) {
|
|
1831
|
+
this.insertPreviewMessage(previewMessage);
|
|
1832
|
+
}
|
|
1833
|
+
if (!this.isPrimaryPreviewEvent(logEvent)) {
|
|
603
1834
|
return;
|
|
604
1835
|
}
|
|
605
1836
|
const message = this.ensurePreviewAssistantMessage();
|
|
@@ -625,15 +1856,61 @@ class FullscreenTui {
|
|
|
625
1856
|
break;
|
|
626
1857
|
}
|
|
627
1858
|
}
|
|
1859
|
+
isPrimaryPreviewEvent(logEvent) {
|
|
1860
|
+
if (!this.currentPreview) {
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
if (this.currentPreview.kind === 'task') {
|
|
1864
|
+
return logEvent.scope === 'main';
|
|
1865
|
+
}
|
|
1866
|
+
return logEvent.scope === 'child' && logEvent.childSlug === this.currentPreview.rootTag;
|
|
1867
|
+
}
|
|
1868
|
+
insertPreviewMessage(entry) {
|
|
1869
|
+
if (!this.currentPreview) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const entries = this.currentPreview.entries;
|
|
1873
|
+
const last = entries[entries.length - 1];
|
|
1874
|
+
if (last && last.role === 'assistant' && last.pending && !last.content) {
|
|
1875
|
+
entries.splice(entries.length - 1, 0, entry);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
entries.push(entry);
|
|
1879
|
+
}
|
|
1880
|
+
updatePreviewChildState(logEvent) {
|
|
1881
|
+
if (!this.currentPreview || this.currentPreview.kind !== 'task') {
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (logEvent.scope === 'child' && logEvent.childSlug) {
|
|
1885
|
+
this.currentPreview.activeChildTag = logEvent.childSlug;
|
|
1886
|
+
this.ensurePreviewChildSkillMessage(logEvent.childSlug);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (logEvent.scope === 'main' && (logEvent.event.type === 'stream' || logEvent.event.type === 'answer' || logEvent.event.type === 'error')) {
|
|
1890
|
+
this.currentPreview.activeChildTag = undefined;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
ensurePreviewChildSkillMessage(childSlug) {
|
|
1894
|
+
if (!this.currentPreview) {
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const message = previewChildSkillActivityMessage(childSlug);
|
|
1898
|
+
const exists = this.currentPreview.entries.some((entry) => sameDisplayMessage(entry, message));
|
|
1899
|
+
if (!exists) {
|
|
1900
|
+
this.insertPreviewMessage(message);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
628
1903
|
ensurePreviewAssistantMessage() {
|
|
629
1904
|
if (!this.currentPreview) {
|
|
630
1905
|
return { role: 'assistant', content: '', pending: false };
|
|
631
1906
|
}
|
|
1907
|
+
const desiredTag = this.currentPreview.rootTag ?? this.currentPreview.activeChildTag;
|
|
632
1908
|
const last = this.currentPreview.entries[this.currentPreview.entries.length - 1];
|
|
633
1909
|
if (last && last.role === 'assistant') {
|
|
1910
|
+
last.tag = desiredTag;
|
|
634
1911
|
return last;
|
|
635
1912
|
}
|
|
636
|
-
const created = { role: 'assistant', content: '', pending: true };
|
|
1913
|
+
const created = { role: 'assistant', content: '', pending: true, tag: desiredTag };
|
|
637
1914
|
this.currentPreview.entries.push(created);
|
|
638
1915
|
return created;
|
|
639
1916
|
}
|
|
@@ -641,6 +1918,7 @@ class FullscreenTui {
|
|
|
641
1918
|
if (!this.currentPreview) {
|
|
642
1919
|
return;
|
|
643
1920
|
}
|
|
1921
|
+
this.currentPreview.activeChildTag = undefined;
|
|
644
1922
|
const assistant = this.ensurePreviewAssistantMessage();
|
|
645
1923
|
if (typeof options.finalText === 'string' && options.finalText.trim()) {
|
|
646
1924
|
assistant.content = options.finalText;
|
|
@@ -824,21 +2102,34 @@ class FullscreenTui {
|
|
|
824
2102
|
const messagesPane = this.buildPaneView('messages', 'Messages', widths.center, contentHeight);
|
|
825
2103
|
const processPane = this.buildPaneView('process', 'Execution', widths.right, rightHeights.process);
|
|
826
2104
|
const contextPane = this.buildPaneView('context', 'Context', widths.right, rightHeights.context);
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
2105
|
+
const workspaceView = this.uiMode === 'command'
|
|
2106
|
+
? null
|
|
2107
|
+
: this.buildWorkspacePaneView(columns, contentHeight);
|
|
2108
|
+
const header = renderMenuBar(this.buildHeaderLine(), columns, this.uiMode === 'menu' ? this.menuState.activeIndex : null);
|
|
2109
|
+
const status = renderStatusBar(this.buildStatusLine(), columns, this.buildHotkeys());
|
|
2110
|
+
const inputPrefix = this.buildInputPrefix();
|
|
2111
|
+
const inputValue = this.buildInputValue();
|
|
2112
|
+
const inputLine = renderCommandBar(inputPrefix, inputValue, columns);
|
|
831
2113
|
const gap = paint(' ', ANSI.bgBlue);
|
|
832
2114
|
const lines = [header];
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
2115
|
+
if (workspaceView) {
|
|
2116
|
+
lines.push(...workspaceView.lines);
|
|
2117
|
+
}
|
|
2118
|
+
else {
|
|
2119
|
+
for (let index = 0; index < contentHeight; index += 1) {
|
|
2120
|
+
const rightLine = index < rightHeights.process
|
|
2121
|
+
? processPane[index]
|
|
2122
|
+
: contextPane[index - rightHeights.process];
|
|
2123
|
+
lines.push(`${sessionsPane[index]}${gap}${messagesPane[index]}${gap}${rightLine}`);
|
|
2124
|
+
}
|
|
838
2125
|
}
|
|
839
2126
|
lines.push(status);
|
|
840
2127
|
lines.push(inputLine);
|
|
841
2128
|
this.paintFrame(lines, { columns, rows });
|
|
2129
|
+
if (workspaceView?.cursor && !this.pendingPrompt) {
|
|
2130
|
+
output.write(`\u001b[${workspaceView.cursor.row};${workspaceView.cursor.column}H`);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
842
2133
|
const cursorColumn = Math.min(columns, inputPrefix.length + this.cursor + 1);
|
|
843
2134
|
output.write(`\u001b[${rows};${cursorColumn}H`);
|
|
844
2135
|
}
|
|
@@ -869,19 +2160,66 @@ class FullscreenTui {
|
|
|
869
2160
|
? `${this.commands.length} skill${this.commands.length === 1 ? '' : 's'}`
|
|
870
2161
|
: 'no skills';
|
|
871
2162
|
const mode = this.busy ? '[Running]' : '[Idle]';
|
|
872
|
-
return
|
|
2163
|
+
return `${sessionLabel} ${mode} ${skillLabel}`;
|
|
873
2164
|
}
|
|
874
2165
|
buildStatusLine() {
|
|
2166
|
+
if (this.uiMode === 'menu') {
|
|
2167
|
+
const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
|
|
2168
|
+
return `Menu ${menu?.label ?? ''} | ${menu?.items[this.menuState.selectedIndex]?.description ?? 'Choose an action.'}`;
|
|
2169
|
+
}
|
|
2170
|
+
if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
|
|
2171
|
+
const selected = this.pickerState.filteredItems[this.pickerState.selectedIndex];
|
|
2172
|
+
return `${this.pickerState.title} | ${selected?.description ?? this.pickerState.emptyText}`;
|
|
2173
|
+
}
|
|
2174
|
+
if (this.uiMode === 'viewer' && this.viewerState) {
|
|
2175
|
+
return `${this.viewerState.title} | ${this.viewerState.path ? path.relative(this.workspaceRoot, this.viewerState.path) : 'read-only view'}`;
|
|
2176
|
+
}
|
|
2177
|
+
if (this.uiMode === 'editor' && this.editorState) {
|
|
2178
|
+
const dirty = this.editorState.dirty ? 'modified' : 'saved';
|
|
2179
|
+
return `${this.editorState.title} | ${dirty} | Ln ${this.editorState.cursorLine + 1}, Col ${this.editorState.cursorColumn + 1}`;
|
|
2180
|
+
}
|
|
875
2181
|
const focus = paneDisplayName(this.focusedPane);
|
|
876
|
-
if (!this.
|
|
2182
|
+
if (!this.pendingPrompt && this.focusedPane !== 'sessions') {
|
|
877
2183
|
const follow = this.paneScroll[this.focusedPane] === 0 ? 'follow' : `scroll ${this.paneScroll[this.focusedPane]}`;
|
|
878
2184
|
return `${this.statusLine} | Focus ${focus} | ${follow}`;
|
|
879
2185
|
}
|
|
880
|
-
if (this.
|
|
881
|
-
return
|
|
2186
|
+
if (this.pendingPrompt) {
|
|
2187
|
+
return this.pendingPrompt.kind === 'clarify'
|
|
2188
|
+
? `Clarify: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`
|
|
2189
|
+
: `Input: ${condenseWhitespace(this.pendingPrompt.prompt)} | Focus ${focus}`;
|
|
882
2190
|
}
|
|
883
2191
|
return `${this.statusLine} | Focus ${focus}`;
|
|
884
2192
|
}
|
|
2193
|
+
buildInputPrefix() {
|
|
2194
|
+
if (this.uiMode === 'menu') {
|
|
2195
|
+
return ' Menu > ';
|
|
2196
|
+
}
|
|
2197
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
2198
|
+
return ' Search > ';
|
|
2199
|
+
}
|
|
2200
|
+
if (this.pendingPrompt?.kind === 'clarify') {
|
|
2201
|
+
return ' Clarify ? ';
|
|
2202
|
+
}
|
|
2203
|
+
if (this.pendingPrompt) {
|
|
2204
|
+
return ' Input > ';
|
|
2205
|
+
}
|
|
2206
|
+
if (this.uiMode === 'editor') {
|
|
2207
|
+
return ' Editor > ';
|
|
2208
|
+
}
|
|
2209
|
+
if (this.uiMode === 'viewer') {
|
|
2210
|
+
return ' Viewer > ';
|
|
2211
|
+
}
|
|
2212
|
+
return ' Command > ';
|
|
2213
|
+
}
|
|
2214
|
+
buildInputValue() {
|
|
2215
|
+
if (this.uiMode === 'editor') {
|
|
2216
|
+
return 'Ctrl+S save Esc close arrows move';
|
|
2217
|
+
}
|
|
2218
|
+
if (this.uiMode === 'viewer') {
|
|
2219
|
+
return this.viewerState?.canEdit ? 'Ctrl+E edit Esc close PgUp/PgDn scroll' : 'Esc close PgUp/PgDn scroll';
|
|
2220
|
+
}
|
|
2221
|
+
return this.inputValue;
|
|
2222
|
+
}
|
|
885
2223
|
buildSessionPaneBody() {
|
|
886
2224
|
if (this.sessions.length === 0) {
|
|
887
2225
|
return ['No sessions yet.'];
|
|
@@ -987,6 +2325,141 @@ class FullscreenTui {
|
|
|
987
2325
|
const paneTitle = this.buildPaneTitle(title, kind, metrics);
|
|
988
2326
|
return buildPane(paneTitle, metrics.lines.slice(metrics.start, metrics.start + metrics.viewportLines), width, height, kind, this.focusedPane === kind);
|
|
989
2327
|
}
|
|
2328
|
+
buildWorkspacePaneView(width, height) {
|
|
2329
|
+
if (this.uiMode === 'editor' && this.editorState) {
|
|
2330
|
+
return this.buildEditorPaneView(width, height, this.editorState);
|
|
2331
|
+
}
|
|
2332
|
+
if ((this.uiMode === 'picker' || this.uiMode === 'palette') && this.pickerState) {
|
|
2333
|
+
return {
|
|
2334
|
+
lines: buildPane(this.pickerState.title, this.buildPickerPaneBody(this.pickerState), width, height, 'workspace', true),
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
if (this.uiMode === 'viewer' && this.viewerState) {
|
|
2338
|
+
return {
|
|
2339
|
+
lines: buildPane(this.viewerState.title, this.buildViewerPaneBody(this.viewerState), width, height, 'workspace', true),
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
const menu = this.getMenuDefinitions()[this.menuState.activeIndex];
|
|
2343
|
+
return {
|
|
2344
|
+
lines: buildPane(`Menu: ${menu?.label ?? 'Menu'}`, this.buildMenuPaneBody(menu), width, height, 'workspace', true),
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
buildMenuPaneBody(menu) {
|
|
2348
|
+
if (!menu) {
|
|
2349
|
+
return ['No menu available.'];
|
|
2350
|
+
}
|
|
2351
|
+
const lines = [
|
|
2352
|
+
'Use Left/Right or 1-6 to move across the menu bar, Up/Down to choose an item, and Enter to run it.',
|
|
2353
|
+
'',
|
|
2354
|
+
];
|
|
2355
|
+
menu.items.forEach((item, index) => {
|
|
2356
|
+
const prefix = index === this.menuState.selectedIndex ? '>' : ' ';
|
|
2357
|
+
const suffix = item.enabled === false ? ' [disabled]' : item.shortcut ? ` [${item.shortcut}]` : '';
|
|
2358
|
+
lines.push(`${prefix} ${item.label}${suffix}`);
|
|
2359
|
+
lines.push(` ${item.description}`);
|
|
2360
|
+
lines.push('');
|
|
2361
|
+
});
|
|
2362
|
+
return lines;
|
|
2363
|
+
}
|
|
2364
|
+
buildPickerPaneBody(state) {
|
|
2365
|
+
const lines = [
|
|
2366
|
+
`Search: ${this.inputValue || '(type to filter)'}`,
|
|
2367
|
+
`Results: ${state.filteredItems.length}/${state.items.length}`,
|
|
2368
|
+
'',
|
|
2369
|
+
];
|
|
2370
|
+
if (state.filteredItems.length === 0) {
|
|
2371
|
+
lines.push(state.emptyText);
|
|
2372
|
+
return lines;
|
|
2373
|
+
}
|
|
2374
|
+
state.filteredItems.forEach((item, index) => {
|
|
2375
|
+
const prefix = index === state.selectedIndex ? '>' : ' ';
|
|
2376
|
+
lines.push(`${prefix} ${item.label}${item.detail ? ` ${item.detail}` : ''}`);
|
|
2377
|
+
lines.push(` ${item.description}`);
|
|
2378
|
+
lines.push('');
|
|
2379
|
+
});
|
|
2380
|
+
return lines.slice(state.scrollTop);
|
|
2381
|
+
}
|
|
2382
|
+
buildViewerPaneBody(state) {
|
|
2383
|
+
const lines = [
|
|
2384
|
+
state.path ? `Path: ${path.relative(this.workspaceRoot, state.path)}` : 'Read-only view',
|
|
2385
|
+
state.canEdit ? 'Ctrl+E opens the editor for this file.' : 'Read-only content.',
|
|
2386
|
+
'',
|
|
2387
|
+
...state.content.split(/\r?\n/).map((line, index) => `${String(index + 1).padStart(4, ' ')} | ${line}`),
|
|
2388
|
+
];
|
|
2389
|
+
return lines.slice(state.scrollTop);
|
|
2390
|
+
}
|
|
2391
|
+
buildEditorPaneView(width, height, state) {
|
|
2392
|
+
const innerWidth = Math.max(1, width - 2);
|
|
2393
|
+
const viewportLines = Math.max(0, height - 2);
|
|
2394
|
+
const lineNumberWidth = Math.max(2, String(Math.max(1, state.lines.length)).length);
|
|
2395
|
+
const prefixWidth = 2 + lineNumberWidth + 3;
|
|
2396
|
+
const contentWidth = Math.max(1, innerWidth - prefixWidth);
|
|
2397
|
+
const maxStart = Math.max(0, state.lines.length - viewportLines);
|
|
2398
|
+
state.scrollTop = clamp(state.scrollTop, 0, maxStart);
|
|
2399
|
+
const lines = [renderWindowTop(`${state.title}${state.dirty ? ' [modified]' : ''}`, innerWidth, true)];
|
|
2400
|
+
for (let offset = 0; offset < viewportLines; offset += 1) {
|
|
2401
|
+
const lineIndex = state.scrollTop + offset;
|
|
2402
|
+
const lineNumber = String(lineIndex + 1).padStart(lineNumberWidth, ' ');
|
|
2403
|
+
const content = state.lines[lineIndex] ?? '';
|
|
2404
|
+
const visibleContent = content.slice(state.scrollLeft, state.scrollLeft + contentWidth);
|
|
2405
|
+
const prefix = lineIndex === state.cursorLine ? '> ' : ' ';
|
|
2406
|
+
const rowText = `${prefix}${lineNumber} | ${visibleContent}`;
|
|
2407
|
+
lines.push(renderWindowBody(padRight(rowText, innerWidth), innerWidth, 'workspace'));
|
|
2408
|
+
}
|
|
2409
|
+
if (height >= 2) {
|
|
2410
|
+
lines.push(renderWindowBottom(innerWidth, true));
|
|
2411
|
+
}
|
|
2412
|
+
const cursorRow = Math.min(2 + viewportLines, 3 + Math.max(0, state.cursorLine - state.scrollTop));
|
|
2413
|
+
const cursorColumn = Math.min(width, 2 + prefixWidth + Math.max(0, state.cursorColumn - state.scrollLeft));
|
|
2414
|
+
return {
|
|
2415
|
+
lines: lines.slice(0, height),
|
|
2416
|
+
cursor: { row: cursorRow, column: cursorColumn },
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
buildHotkeys() {
|
|
2420
|
+
if (this.uiMode === 'menu') {
|
|
2421
|
+
return [
|
|
2422
|
+
['1-6', 'Menu'],
|
|
2423
|
+
['<- ->', 'Switch'],
|
|
2424
|
+
['Up/Down', 'Item'],
|
|
2425
|
+
['Enter', 'Run'],
|
|
2426
|
+
['Esc', 'Close'],
|
|
2427
|
+
];
|
|
2428
|
+
}
|
|
2429
|
+
if (this.uiMode === 'picker' || this.uiMode === 'palette') {
|
|
2430
|
+
return [
|
|
2431
|
+
['type', 'Filter'],
|
|
2432
|
+
['Up/Down', 'Pick'],
|
|
2433
|
+
['Enter', 'Open'],
|
|
2434
|
+
['Ctrl+E', 'Edit'],
|
|
2435
|
+
['Esc', 'Close'],
|
|
2436
|
+
];
|
|
2437
|
+
}
|
|
2438
|
+
if (this.uiMode === 'viewer') {
|
|
2439
|
+
return [
|
|
2440
|
+
['PgUp/PgDn', 'Scroll'],
|
|
2441
|
+
['Ctrl+E', 'Edit'],
|
|
2442
|
+
['Ctrl+R', 'Run'],
|
|
2443
|
+
['Esc', 'Close'],
|
|
2444
|
+
];
|
|
2445
|
+
}
|
|
2446
|
+
if (this.uiMode === 'editor') {
|
|
2447
|
+
return [
|
|
2448
|
+
['Arrows', 'Move'],
|
|
2449
|
+
['Enter', 'Split'],
|
|
2450
|
+
['Ctrl+S', 'Save'],
|
|
2451
|
+
['Esc', 'Close'],
|
|
2452
|
+
];
|
|
2453
|
+
}
|
|
2454
|
+
return [
|
|
2455
|
+
['F10', 'Menu'],
|
|
2456
|
+
['Ctrl+P', 'Palette'],
|
|
2457
|
+
['<- ->', 'Pane'],
|
|
2458
|
+
[this.focusedPane === 'sessions' ? 'Up/Down' : 'PgUp/PgDn', this.focusedPane === 'sessions' ? 'Select' : 'Scroll'],
|
|
2459
|
+
['End', this.focusedPane === 'sessions' ? 'Bottom' : 'Follow'],
|
|
2460
|
+
['/cancel', 'Stop'],
|
|
2461
|
+
];
|
|
2462
|
+
}
|
|
990
2463
|
buildPaneTitle(title, kind, metrics) {
|
|
991
2464
|
if (kind === 'sessions') {
|
|
992
2465
|
return this.focusedPane === kind ? `${title} [focus]` : title;
|
|
@@ -1047,14 +2520,7 @@ class FullscreenTui {
|
|
|
1047
2520
|
}
|
|
1048
2521
|
formatMessageHeader(entry) {
|
|
1049
2522
|
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
|
-
}
|
|
2523
|
+
return formatDisplayMessageHeader(entry, spinner);
|
|
1058
2524
|
}
|
|
1059
2525
|
close() {
|
|
1060
2526
|
if (!this.closeResolver) {
|
|
@@ -1065,7 +2531,7 @@ class FullscreenTui {
|
|
|
1065
2531
|
resolve();
|
|
1066
2532
|
}
|
|
1067
2533
|
isEditingInput() {
|
|
1068
|
-
return
|
|
2534
|
+
return this.uiMode !== 'editor' && this.inputValue.length > 0;
|
|
1069
2535
|
}
|
|
1070
2536
|
}
|
|
1071
2537
|
export async function startTui(workspaceRoot = process.cwd(), options = {}) {
|
|
@@ -1112,6 +2578,39 @@ async function startLineTui(workspaceRoot = process.cwd(), options = {}) {
|
|
|
1112
2578
|
console.log(helpLine);
|
|
1113
2579
|
}
|
|
1114
2580
|
continue;
|
|
2581
|
+
case 'compile':
|
|
2582
|
+
case 'skill-creator':
|
|
2583
|
+
if (!parsed.rawArgs.trim()) {
|
|
2584
|
+
console.log(`Provide a skill specification after /${parsed.name}.`);
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
console.log(divider('-'));
|
|
2588
|
+
console.log(`${paint('skill', ANSI.dim)} /${parsed.name} ${parsed.rawArgs}`);
|
|
2589
|
+
console.log(divider('-'));
|
|
2590
|
+
{
|
|
2591
|
+
const printer = new LiveExecutionPrinter();
|
|
2592
|
+
try {
|
|
2593
|
+
const result = await runSkillCreatorCommand(workspaceRoot, session.id, parsed.rawArgs, {
|
|
2594
|
+
sandbox: options.sandbox,
|
|
2595
|
+
onUserInput: async (question) => {
|
|
2596
|
+
printer.finish();
|
|
2597
|
+
console.log(`${paint('clarify', ANSI.yellow)} ${question}`);
|
|
2598
|
+
return (await rl.question('> ')).trim();
|
|
2599
|
+
},
|
|
2600
|
+
onEvent: (event) => printer.handle(event),
|
|
2601
|
+
});
|
|
2602
|
+
printer.finish();
|
|
2603
|
+
console.log(formatSkillCreatorSummary(workspaceRoot, result));
|
|
2604
|
+
}
|
|
2605
|
+
catch (error) {
|
|
2606
|
+
printer.finish();
|
|
2607
|
+
console.log(`${paint('error', ANSI.red)} ${error.message}`);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
console.log('');
|
|
2611
|
+
console.log(divider('-'));
|
|
2612
|
+
console.log('');
|
|
2613
|
+
continue;
|
|
1115
2614
|
}
|
|
1116
2615
|
}
|
|
1117
2616
|
if (parsed.kind === 'skill') {
|
|
@@ -1209,9 +2708,79 @@ async function runSkillCommand(workspaceRoot, skillName, args, options = {}) {
|
|
|
1209
2708
|
signal: options.signal,
|
|
1210
2709
|
onUserInput: options.onUserInput,
|
|
1211
2710
|
onEvent: (event) => options.onEvent?.({ scope: 'child', childSlug: skillName, event }),
|
|
2711
|
+
onChildEvent: (childSlug, event) => options.onEvent?.({ scope: 'child', childSlug, event }),
|
|
1212
2712
|
});
|
|
1213
2713
|
return result;
|
|
1214
2714
|
}
|
|
2715
|
+
async function runSkillCreatorCommand(workspaceRoot, sessionId, spec, options = {}) {
|
|
2716
|
+
const trimmedSpec = spec.trim();
|
|
2717
|
+
if (!trimmedSpec) {
|
|
2718
|
+
throw new Error('spec is required');
|
|
2719
|
+
}
|
|
2720
|
+
const config = await loadConfig(workspaceRoot);
|
|
2721
|
+
const compileSelection = resolveModelSlot(config, 'compile');
|
|
2722
|
+
const runSelection = resolveModelSlot(config, 'run');
|
|
2723
|
+
return createLocalSkill({
|
|
2724
|
+
spec: trimmedSpec,
|
|
2725
|
+
workspaceRoot,
|
|
2726
|
+
workspacePath: path.resolve(workspaceRoot, config.workspace || './workspace'),
|
|
2727
|
+
config,
|
|
2728
|
+
compileSelection,
|
|
2729
|
+
runSelection,
|
|
2730
|
+
sessionId,
|
|
2731
|
+
sandbox: options.sandbox ?? false,
|
|
2732
|
+
signal: options.signal,
|
|
2733
|
+
onUserInput: options.onUserInput ?? (async () => ''),
|
|
2734
|
+
onEvent: options.onEvent,
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
function formatSkillCreatorSummary(workspaceRoot, result) {
|
|
2738
|
+
const slug = typeof result.slug === 'string' ? result.slug : 'unknown-skill';
|
|
2739
|
+
const outputPath = typeof result.output_path === 'string' ? result.output_path : '';
|
|
2740
|
+
if (!outputPath) {
|
|
2741
|
+
return `Published /${slug}.`;
|
|
2742
|
+
}
|
|
2743
|
+
const relativeOutputPath = path.relative(workspaceRoot, outputPath);
|
|
2744
|
+
return `Published /${slug} to ${relativeOutputPath || outputPath}.`;
|
|
2745
|
+
}
|
|
2746
|
+
export function previewMessageFromEvent(logEvent) {
|
|
2747
|
+
switch (logEvent.event.type) {
|
|
2748
|
+
case 'output':
|
|
2749
|
+
if (!logEvent.event.content) {
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
role: 'system',
|
|
2754
|
+
kind: 'output',
|
|
2755
|
+
tag: logEvent.scope === 'child' ? logEvent.childSlug : undefined,
|
|
2756
|
+
content: logEvent.event.content,
|
|
2757
|
+
};
|
|
2758
|
+
case 'input_required':
|
|
2759
|
+
if (!logEvent.event.prompt) {
|
|
2760
|
+
return null;
|
|
2761
|
+
}
|
|
2762
|
+
return previewQuestionMessage(logEvent.event.prompt, logEvent.scope === 'child' ? logEvent.childSlug : undefined);
|
|
2763
|
+
default:
|
|
2764
|
+
return null;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
export function previewChildSkillActivityMessage(childSlug) {
|
|
2768
|
+
return {
|
|
2769
|
+
role: 'system',
|
|
2770
|
+
kind: 'output',
|
|
2771
|
+
tag: childSlug,
|
|
2772
|
+
content: 'Running child skill...',
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
export function previewQuestionMessage(promptText, explicitTag) {
|
|
2776
|
+
const parsed = explicitTag ? { tag: explicitTag, content: promptText } : parseTaggedPrompt(promptText);
|
|
2777
|
+
return {
|
|
2778
|
+
role: 'system',
|
|
2779
|
+
kind: 'question',
|
|
2780
|
+
tag: parsed.tag,
|
|
2781
|
+
content: parsed.content,
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
1215
2784
|
export function parseCommandArgs(rawArgs) {
|
|
1216
2785
|
const args = [];
|
|
1217
2786
|
let current = '';
|
|
@@ -1272,11 +2841,14 @@ export function parseSlashInput(line) {
|
|
|
1272
2841
|
const name = match[1];
|
|
1273
2842
|
const rawArgs = (match[2] ?? '').trim();
|
|
1274
2843
|
if (BUILTIN_SLASH_COMMANDS.includes(name)) {
|
|
2844
|
+
const passthroughRawArgBuiltins = new Set(['new', 'compile', 'skill-creator']);
|
|
1275
2845
|
return {
|
|
1276
2846
|
kind: 'builtin',
|
|
1277
2847
|
name: name,
|
|
1278
2848
|
rawArgs,
|
|
1279
|
-
args: name
|
|
2849
|
+
args: passthroughRawArgBuiltins.has(name) && rawArgs
|
|
2850
|
+
? [rawArgs]
|
|
2851
|
+
: parseCommandArgs(rawArgs),
|
|
1280
2852
|
};
|
|
1281
2853
|
}
|
|
1282
2854
|
return {
|
|
@@ -1368,21 +2940,62 @@ async function createSessionInteractive(rl, workspaceRoot) {
|
|
|
1368
2940
|
function renderHeader(session) {
|
|
1369
2941
|
console.log(divider('='));
|
|
1370
2942
|
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`);
|
|
2943
|
+
console.log(`${paint('commands', ANSI.dim)} /compile <spec> /new /sessions /help /exit /skill args`);
|
|
1372
2944
|
console.log(`${paint('session', ANSI.dim)} ${paint(session.id, ANSI.cyan)}`);
|
|
1373
2945
|
console.log(divider('='));
|
|
1374
2946
|
console.log('');
|
|
1375
2947
|
}
|
|
1376
2948
|
function streamLabel(logEvent) {
|
|
1377
2949
|
return logEvent.scope === 'child'
|
|
1378
|
-
?
|
|
2950
|
+
? `${CHILD_EVENT_INDENT}[${logEvent.childSlug ?? '?'}] llm`
|
|
1379
2951
|
: 'llm';
|
|
1380
2952
|
}
|
|
1381
2953
|
function formatEventPrefix(logEvent) {
|
|
1382
2954
|
return logEvent.scope === 'child'
|
|
1383
|
-
?
|
|
2955
|
+
? `${CHILD_EVENT_INDENT}[${logEvent.childSlug ?? '?'}] `
|
|
1384
2956
|
: '';
|
|
1385
2957
|
}
|
|
2958
|
+
function parseTaggedPrompt(promptText) {
|
|
2959
|
+
const match = promptText.match(/^\[([^\]]+)\]\s*(.*)$/);
|
|
2960
|
+
if (!match) {
|
|
2961
|
+
return { content: promptText };
|
|
2962
|
+
}
|
|
2963
|
+
return {
|
|
2964
|
+
tag: match[1],
|
|
2965
|
+
content: match[2] || promptText,
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function sameDisplayMessage(left, right) {
|
|
2969
|
+
return !!left
|
|
2970
|
+
&& left.role === right.role
|
|
2971
|
+
&& left.kind === right.kind
|
|
2972
|
+
&& left.tag === right.tag
|
|
2973
|
+
&& left.content === right.content;
|
|
2974
|
+
}
|
|
2975
|
+
function formatSystemMessageHeader(entry, spinner) {
|
|
2976
|
+
const base = entry.kind === 'output'
|
|
2977
|
+
? 'Output'
|
|
2978
|
+
: entry.kind === 'question'
|
|
2979
|
+
? 'Question'
|
|
2980
|
+
: 'System';
|
|
2981
|
+
return entry.tag
|
|
2982
|
+
? `[${base}: ${entry.tag}${spinner}]`
|
|
2983
|
+
: `[${base}${spinner}]`;
|
|
2984
|
+
}
|
|
2985
|
+
export function formatDisplayMessageHeader(entry, spinner = '') {
|
|
2986
|
+
switch (entry.role) {
|
|
2987
|
+
case 'user':
|
|
2988
|
+
return '[You]';
|
|
2989
|
+
case 'assistant': {
|
|
2990
|
+
const base = entry.error ? 'Assistant Error' : 'Assistant';
|
|
2991
|
+
return entry.tag
|
|
2992
|
+
? `[${base}: ${entry.tag}${spinner}]`
|
|
2993
|
+
: `[${base}${spinner}]`;
|
|
2994
|
+
}
|
|
2995
|
+
case 'system':
|
|
2996
|
+
return formatSystemMessageHeader(entry, spinner);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
1386
2999
|
function formatToolArgs(args) {
|
|
1387
3000
|
if (!args) {
|
|
1388
3001
|
return '';
|
|
@@ -1402,11 +3015,15 @@ function buildHelpLines() {
|
|
|
1402
3015
|
'commands',
|
|
1403
3016
|
'/new [title] create a new conductor session',
|
|
1404
3017
|
'/sessions refresh or choose sessions',
|
|
3018
|
+
'/compile <spec> compile and publish a new local skill',
|
|
3019
|
+
'/skill-creator <spec> alias for /compile',
|
|
1405
3020
|
'/cancel abort the current execution',
|
|
1406
3021
|
'/help show this help',
|
|
1407
3022
|
'/quit exit the TUI',
|
|
1408
3023
|
'/<skill> [args] run a compiled skill directly',
|
|
1409
3024
|
'keys',
|
|
3025
|
+
'F10 / Ctrl+G open the menu bar',
|
|
3026
|
+
'Ctrl+P open the action palette',
|
|
1410
3027
|
'Left/Right change focused pane',
|
|
1411
3028
|
'Up/Down select session or scroll focused pane',
|
|
1412
3029
|
'PgUp/PgDn page-scroll the focused pane',
|
|
@@ -1414,6 +3031,16 @@ function buildHelpLines() {
|
|
|
1414
3031
|
'Tab autocomplete /commands and /skills',
|
|
1415
3032
|
];
|
|
1416
3033
|
}
|
|
3034
|
+
function getSlashHelpLines() {
|
|
3035
|
+
const lines = buildHelpLines();
|
|
3036
|
+
const keysIndex = lines.indexOf('keys');
|
|
3037
|
+
return keysIndex === -1 ? lines : lines.slice(0, keysIndex);
|
|
3038
|
+
}
|
|
3039
|
+
function getShortcutHelpLines() {
|
|
3040
|
+
const lines = buildHelpLines();
|
|
3041
|
+
const keysIndex = lines.indexOf('keys');
|
|
3042
|
+
return keysIndex === -1 ? [] : lines.slice(keysIndex);
|
|
3043
|
+
}
|
|
1417
3044
|
function computePaneWidths(columns) {
|
|
1418
3045
|
const available = Math.max(36, columns - 2);
|
|
1419
3046
|
let left = Math.max(18, Math.floor(available * 0.2));
|
|
@@ -1468,45 +3095,144 @@ function buildPane(title, body, width, height, kind, active = false) {
|
|
|
1468
3095
|
}
|
|
1469
3096
|
return lines.slice(0, height);
|
|
1470
3097
|
}
|
|
1471
|
-
function wrapPlainText(text, width) {
|
|
3098
|
+
export function wrapPlainText(text, width) {
|
|
1472
3099
|
if (width <= 0) {
|
|
1473
3100
|
return [''];
|
|
1474
3101
|
}
|
|
1475
3102
|
const result = [];
|
|
1476
3103
|
for (const rawLine of text.split(/\r?\n/)) {
|
|
1477
|
-
const line = rawLine || '';
|
|
3104
|
+
const line = expandTabs(rawLine || '', CHILD_EVENT_TAB_WIDTH);
|
|
1478
3105
|
if (!line) {
|
|
1479
3106
|
result.push('');
|
|
1480
3107
|
continue;
|
|
1481
3108
|
}
|
|
1482
3109
|
let remaining = line;
|
|
1483
|
-
while (remaining
|
|
1484
|
-
const slice = remaining
|
|
3110
|
+
while (measureDisplayWidth(remaining) > width) {
|
|
3111
|
+
const { head: slice, tail } = splitByDisplayWidth(remaining, width, { forceOne: true });
|
|
1485
3112
|
const breakIndex = slice.lastIndexOf(' ');
|
|
1486
|
-
|
|
1487
|
-
|
|
3113
|
+
const breakSlice = breakIndex === -1 ? '' : slice.slice(0, breakIndex);
|
|
3114
|
+
if (breakIndex > 0 && measureDisplayWidth(breakSlice) > Math.floor(width * 0.4)) {
|
|
3115
|
+
result.push(breakSlice);
|
|
1488
3116
|
remaining = remaining.slice(breakIndex + 1);
|
|
1489
3117
|
}
|
|
1490
3118
|
else {
|
|
1491
3119
|
result.push(slice);
|
|
1492
|
-
remaining =
|
|
3120
|
+
remaining = tail;
|
|
1493
3121
|
}
|
|
1494
3122
|
}
|
|
1495
3123
|
result.push(remaining);
|
|
1496
3124
|
}
|
|
1497
3125
|
return result;
|
|
1498
3126
|
}
|
|
1499
|
-
function
|
|
1500
|
-
|
|
3127
|
+
function expandTabs(text, tabWidth) {
|
|
3128
|
+
if (!text.includes('\t')) {
|
|
3129
|
+
return text;
|
|
3130
|
+
}
|
|
3131
|
+
return text.replace(/\t/g, ' '.repeat(tabWidth));
|
|
3132
|
+
}
|
|
3133
|
+
export function measureDisplayWidth(text) {
|
|
3134
|
+
return splitGraphemes(text).reduce((total, grapheme) => total + graphemeDisplayWidth(grapheme), 0);
|
|
1501
3135
|
}
|
|
1502
|
-
function
|
|
1503
|
-
|
|
3136
|
+
export function padRight(text, width) {
|
|
3137
|
+
const visibleWidth = measureDisplayWidth(text);
|
|
3138
|
+
return visibleWidth >= width ? text : `${text}${' '.repeat(width - visibleWidth)}`;
|
|
3139
|
+
}
|
|
3140
|
+
export function ellipsize(text, width) {
|
|
3141
|
+
if (measureDisplayWidth(text) <= width) {
|
|
1504
3142
|
return text;
|
|
1505
3143
|
}
|
|
1506
3144
|
if (width <= 3) {
|
|
1507
|
-
return text
|
|
3145
|
+
return splitByDisplayWidth(text, width).head;
|
|
3146
|
+
}
|
|
3147
|
+
return `${splitByDisplayWidth(text, width - 3).head}...`;
|
|
3148
|
+
}
|
|
3149
|
+
function splitGraphemes(text) {
|
|
3150
|
+
if (!text) {
|
|
3151
|
+
return [];
|
|
3152
|
+
}
|
|
3153
|
+
if (!graphemeSegmenter) {
|
|
3154
|
+
return Array.from(text);
|
|
3155
|
+
}
|
|
3156
|
+
return Array.from(graphemeSegmenter.segment(text), (entry) => entry.segment);
|
|
3157
|
+
}
|
|
3158
|
+
function splitByDisplayWidth(text, width, options = {}) {
|
|
3159
|
+
if (width <= 0) {
|
|
3160
|
+
return { head: '', tail: text };
|
|
3161
|
+
}
|
|
3162
|
+
const graphemes = splitGraphemes(text);
|
|
3163
|
+
let consumedWidth = 0;
|
|
3164
|
+
let index = 0;
|
|
3165
|
+
while (index < graphemes.length) {
|
|
3166
|
+
const nextWidth = graphemeDisplayWidth(graphemes[index]);
|
|
3167
|
+
if (consumedWidth + nextWidth > width) {
|
|
3168
|
+
if (index === 0 && options.forceOne) {
|
|
3169
|
+
index = 1;
|
|
3170
|
+
}
|
|
3171
|
+
break;
|
|
3172
|
+
}
|
|
3173
|
+
consumedWidth += nextWidth;
|
|
3174
|
+
index += 1;
|
|
3175
|
+
}
|
|
3176
|
+
return {
|
|
3177
|
+
head: graphemes.slice(0, index).join(''),
|
|
3178
|
+
tail: graphemes.slice(index).join(''),
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
function graphemeDisplayWidth(grapheme) {
|
|
3182
|
+
if (!grapheme) {
|
|
3183
|
+
return 0;
|
|
1508
3184
|
}
|
|
1509
|
-
|
|
3185
|
+
if (isEmojiGrapheme(grapheme)) {
|
|
3186
|
+
return 2;
|
|
3187
|
+
}
|
|
3188
|
+
let width = 0;
|
|
3189
|
+
for (const symbol of Array.from(grapheme)) {
|
|
3190
|
+
const codePoint = symbol.codePointAt(0);
|
|
3191
|
+
if (codePoint === undefined || isZeroWidthCodePoint(codePoint, symbol)) {
|
|
3192
|
+
continue;
|
|
3193
|
+
}
|
|
3194
|
+
width = Math.max(width, isFullWidthCodePoint(codePoint) ? 2 : 1);
|
|
3195
|
+
}
|
|
3196
|
+
return width;
|
|
3197
|
+
}
|
|
3198
|
+
function isEmojiGrapheme(grapheme) {
|
|
3199
|
+
if (EXTENDED_PICTOGRAPHIC_RE.test(grapheme) || grapheme.includes('\u200D') || grapheme.includes('\uFE0F') || grapheme.includes('\u20E3')) {
|
|
3200
|
+
return true;
|
|
3201
|
+
}
|
|
3202
|
+
return Array.from(grapheme).some((symbol) => {
|
|
3203
|
+
const codePoint = symbol.codePointAt(0);
|
|
3204
|
+
return codePoint !== undefined && isRegionalIndicatorCodePoint(codePoint);
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
function isZeroWidthCodePoint(codePoint, symbol) {
|
|
3208
|
+
return COMBINING_MARK_RE.test(symbol)
|
|
3209
|
+
|| codePoint === 0x200D
|
|
3210
|
+
|| (codePoint >= 0xFE00 && codePoint <= 0xFE0F)
|
|
3211
|
+
|| (codePoint >= 0xE0100 && codePoint <= 0xE01EF)
|
|
3212
|
+
|| (codePoint >= 0x1F3FB && codePoint <= 0x1F3FF);
|
|
3213
|
+
}
|
|
3214
|
+
function isRegionalIndicatorCodePoint(codePoint) {
|
|
3215
|
+
return codePoint >= 0x1F1E6 && codePoint <= 0x1F1FF;
|
|
3216
|
+
}
|
|
3217
|
+
function isFullWidthCodePoint(codePoint) {
|
|
3218
|
+
if (codePoint < 0x1100) {
|
|
3219
|
+
return false;
|
|
3220
|
+
}
|
|
3221
|
+
return codePoint <= 0x115F
|
|
3222
|
+
|| codePoint === 0x2329
|
|
3223
|
+
|| codePoint === 0x232A
|
|
3224
|
+
|| (codePoint >= 0x2E80 && codePoint <= 0x3247 && codePoint !== 0x303F)
|
|
3225
|
+
|| (codePoint >= 0x3250 && codePoint <= 0x4DBF)
|
|
3226
|
+
|| (codePoint >= 0x4E00 && codePoint <= 0xA4C6)
|
|
3227
|
+
|| (codePoint >= 0xA960 && codePoint <= 0xA97C)
|
|
3228
|
+
|| (codePoint >= 0xAC00 && codePoint <= 0xD7A3)
|
|
3229
|
+
|| (codePoint >= 0xF900 && codePoint <= 0xFAFF)
|
|
3230
|
+
|| (codePoint >= 0xFE10 && codePoint <= 0xFE19)
|
|
3231
|
+
|| (codePoint >= 0xFE30 && codePoint <= 0xFE6B)
|
|
3232
|
+
|| (codePoint >= 0xFF01 && codePoint <= 0xFF60)
|
|
3233
|
+
|| (codePoint >= 0xFFE0 && codePoint <= 0xFFE6)
|
|
3234
|
+
|| (codePoint >= 0x1F200 && codePoint <= 0x1F251)
|
|
3235
|
+
|| (codePoint >= 0x20000 && codePoint <= 0x3FFFD);
|
|
1510
3236
|
}
|
|
1511
3237
|
function condenseWhitespace(text) {
|
|
1512
3238
|
return text.replace(/\s+/g, ' ').trim();
|
|
@@ -1572,21 +3298,24 @@ function longestCommonPrefix(values) {
|
|
|
1572
3298
|
}
|
|
1573
3299
|
return prefix;
|
|
1574
3300
|
}
|
|
1575
|
-
function renderMenuBar(text, columns) {
|
|
1576
|
-
|
|
3301
|
+
function renderMenuBar(text, columns, activeMenuIndex) {
|
|
3302
|
+
const labels = ['Session', 'Skills', 'Files', 'Run', 'View', 'Help'];
|
|
3303
|
+
let rendered = paint(' DeepClause ', ANSI.black, ANSI.bgWhite, ANSI.bold);
|
|
3304
|
+
let usedWidth = measureDisplayWidth(' DeepClause ');
|
|
3305
|
+
labels.forEach((label, index) => {
|
|
3306
|
+
const raw = ` [${index + 1}] ${label} `;
|
|
3307
|
+
usedWidth += measureDisplayWidth(raw);
|
|
3308
|
+
rendered += paint(raw, ANSI.black, activeMenuIndex === index ? ANSI.bgCyan : ANSI.bgWhite, ANSI.bold);
|
|
3309
|
+
});
|
|
3310
|
+
const trailingWidth = Math.max(0, columns - usedWidth);
|
|
3311
|
+
rendered += paint(padRight(ellipsize(` ${text}`, trailingWidth), trailingWidth), ANSI.black, ANSI.bgWhite);
|
|
3312
|
+
return rendered;
|
|
1577
3313
|
}
|
|
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
|
-
];
|
|
3314
|
+
function renderStatusBar(status, columns, hotkeys) {
|
|
1586
3315
|
let hotkeyWidth = 0;
|
|
1587
3316
|
const selected = [];
|
|
1588
3317
|
for (const hotkey of hotkeys) {
|
|
1589
|
-
const width = hotkey[0]
|
|
3318
|
+
const width = measureDisplayWidth(hotkey[0]) + measureDisplayWidth(hotkey[1]) + 4;
|
|
1590
3319
|
if (hotkeyWidth + width > Math.max(0, columns - 12)) {
|
|
1591
3320
|
break;
|
|
1592
3321
|
}
|
|
@@ -1600,13 +3329,13 @@ function renderStatusBar(status, columns, focusedPane) {
|
|
|
1600
3329
|
}
|
|
1601
3330
|
function renderCommandBar(prefix, value, columns) {
|
|
1602
3331
|
const prefixPart = paint(prefix, ANSI.black, ANSI.bgWhite, ANSI.bold);
|
|
1603
|
-
const remainingWidth = Math.max(0, columns - prefix
|
|
3332
|
+
const remainingWidth = Math.max(0, columns - measureDisplayWidth(prefix));
|
|
1604
3333
|
const valuePart = paint(padRight(ellipsize(value, remainingWidth), remainingWidth), ANSI.brightWhite, ANSI.bgBlue);
|
|
1605
3334
|
return `${prefixPart}${valuePart}`;
|
|
1606
3335
|
}
|
|
1607
3336
|
function renderWindowTop(title, innerWidth, active) {
|
|
1608
3337
|
const safeLabel = ` ${ellipsize(title, Math.max(1, innerWidth - 2))} `;
|
|
1609
|
-
const fillWidth = Math.max(0, innerWidth - safeLabel
|
|
3338
|
+
const fillWidth = Math.max(0, innerWidth - measureDisplayWidth(safeLabel));
|
|
1610
3339
|
const leftFill = Math.floor(fillWidth / 2);
|
|
1611
3340
|
const rightFill = fillWidth - leftFill;
|
|
1612
3341
|
return [
|
|
@@ -1622,6 +3351,9 @@ function renderWindowBody(text, innerWidth, kind) {
|
|
|
1622
3351
|
const normalized = text.trim().toLowerCase();
|
|
1623
3352
|
const content = padRight(ellipsize(text, innerWidth), innerWidth);
|
|
1624
3353
|
const border = paint('║', ANSI.brightCyan, ANSI.bgBlue);
|
|
3354
|
+
if (kind === 'workspace' && text.startsWith('> ')) {
|
|
3355
|
+
return `${border}${paint(content, ANSI.black, ANSI.bgCyan, ANSI.bold)}${border}`;
|
|
3356
|
+
}
|
|
1625
3357
|
if (kind === 'sessions' && text.startsWith('> ')) {
|
|
1626
3358
|
return `${border}${paint(content, ANSI.black, ANSI.bgCyan, ANSI.bold)}${border}`;
|
|
1627
3359
|
}
|
|
@@ -1701,6 +3433,27 @@ function paneDimensionsForKind(kind, columns, rows) {
|
|
|
1701
3433
|
return { width: widths.right, height: rightHeights.context };
|
|
1702
3434
|
}
|
|
1703
3435
|
}
|
|
3436
|
+
export function nextWrappedIndex(currentIndex, delta, length) {
|
|
3437
|
+
if (length <= 0) {
|
|
3438
|
+
return 0;
|
|
3439
|
+
}
|
|
3440
|
+
return (currentIndex + delta + length) % length;
|
|
3441
|
+
}
|
|
3442
|
+
export function selectMenuItemByTypeahead(items, query, currentIndex = -1) {
|
|
3443
|
+
const normalized = query.trim().toLowerCase();
|
|
3444
|
+
if (!normalized || items.length === 0) {
|
|
3445
|
+
return Math.max(0, currentIndex);
|
|
3446
|
+
}
|
|
3447
|
+
const matches = items
|
|
3448
|
+
.map((item, index) => ({ label: item.label.toLowerCase(), index }))
|
|
3449
|
+
.filter((item) => item.label.startsWith(normalized))
|
|
3450
|
+
.map((item) => item.index);
|
|
3451
|
+
if (matches.length === 0) {
|
|
3452
|
+
return Math.max(0, currentIndex);
|
|
3453
|
+
}
|
|
3454
|
+
const next = matches.find((index) => index > currentIndex);
|
|
3455
|
+
return next ?? matches[0];
|
|
3456
|
+
}
|
|
1704
3457
|
function wrapBodyLines(body, innerWidth) {
|
|
1705
3458
|
const lines = [];
|
|
1706
3459
|
for (const line of body) {
|
|
@@ -1708,6 +3461,63 @@ function wrapBodyLines(body, innerWidth) {
|
|
|
1708
3461
|
}
|
|
1709
3462
|
return lines;
|
|
1710
3463
|
}
|
|
3464
|
+
async function listWorkspaceTextFiles(workspaceRoot) {
|
|
3465
|
+
const results = [];
|
|
3466
|
+
const visit = async (relativeDir) => {
|
|
3467
|
+
const absoluteDir = relativeDir ? path.join(workspaceRoot, relativeDir) : workspaceRoot;
|
|
3468
|
+
let entries;
|
|
3469
|
+
try {
|
|
3470
|
+
entries = await fs.readdir(absoluteDir, { withFileTypes: true });
|
|
3471
|
+
}
|
|
3472
|
+
catch {
|
|
3473
|
+
return;
|
|
3474
|
+
}
|
|
3475
|
+
for (const entry of entries) {
|
|
3476
|
+
const relativePath = relativeDir ? path.join(relativeDir, entry.name) : entry.name;
|
|
3477
|
+
if (entry.isDirectory()) {
|
|
3478
|
+
if (IGNORED_WORKSPACE_DIRS.has(entry.name)) {
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
await visit(relativePath);
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
if (entry.isFile() && isEditableTextFile(relativePath)) {
|
|
3485
|
+
results.push(relativePath.replace(/\\/g, '/'));
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
};
|
|
3489
|
+
await visit('');
|
|
3490
|
+
results.sort((left, right) => left.localeCompare(right));
|
|
3491
|
+
return results;
|
|
3492
|
+
}
|
|
3493
|
+
function isEditableTextFile(filePath) {
|
|
3494
|
+
const basename = path.basename(filePath);
|
|
3495
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
3496
|
+
return TEXT_FILE_EXTENSIONS.has(extension) || TEXT_FILE_BASENAMES.has(basename);
|
|
3497
|
+
}
|
|
3498
|
+
function splitEditorContent(content) {
|
|
3499
|
+
const lines = content.split(/\r?\n/);
|
|
3500
|
+
return lines.length > 0 ? lines : [''];
|
|
3501
|
+
}
|
|
3502
|
+
function resolveWorkspaceTextPath(workspaceRoot, requestedPath) {
|
|
3503
|
+
const absolutePath = path.resolve(workspaceRoot, requestedPath);
|
|
3504
|
+
const resolvedRoot = path.resolve(workspaceRoot);
|
|
3505
|
+
if (absolutePath === resolvedRoot || absolutePath.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
3506
|
+
return absolutePath;
|
|
3507
|
+
}
|
|
3508
|
+
return path.join(resolvedRoot, path.basename(requestedPath));
|
|
3509
|
+
}
|
|
3510
|
+
export function filterPickerItems(items, query) {
|
|
3511
|
+
const normalized = query.trim().toLowerCase();
|
|
3512
|
+
if (!normalized) {
|
|
3513
|
+
return [...items];
|
|
3514
|
+
}
|
|
3515
|
+
const tokens = normalized.split(/\s+/).filter(Boolean);
|
|
3516
|
+
return [...items].filter((item) => {
|
|
3517
|
+
const haystack = `${item.label} ${item.description} ${item.detail ?? ''}`.toLowerCase();
|
|
3518
|
+
return tokens.every((token) => haystack.includes(token));
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
1711
3521
|
function clamp(value, min, max) {
|
|
1712
3522
|
if (value < min) {
|
|
1713
3523
|
return min;
|