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.
Files changed (50) hide show
  1. package/README.md +311 -94
  2. package/dist/cli/config.d.ts +1 -0
  3. package/dist/cli/config.d.ts.map +1 -1
  4. package/dist/cli/config.js +11 -0
  5. package/dist/cli/config.js.map +1 -1
  6. package/dist/cli/default-skills.d.ts +9 -0
  7. package/dist/cli/default-skills.d.ts.map +1 -0
  8. package/dist/cli/default-skills.js +175 -0
  9. package/dist/cli/default-skills.js.map +1 -0
  10. package/dist/cli/index.js +16 -1
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/run.d.ts +1 -0
  13. package/dist/cli/run.d.ts.map +1 -1
  14. package/dist/cli/run.js +18 -62
  15. package/dist/cli/run.js.map +1 -1
  16. package/dist/cli/tui.d.ts +26 -1
  17. package/dist/cli/tui.d.ts.map +1 -1
  18. package/dist/cli/tui.js +1936 -126
  19. package/dist/cli/tui.js.map +1 -1
  20. package/dist/prolog/bridge.js +1 -1
  21. package/dist/prolog/bridge.js.map +1 -1
  22. package/dist/runner.d.ts.map +1 -1
  23. package/dist/runner.js +5 -0
  24. package/dist/runner.js.map +1 -1
  25. package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +16 -6
  26. package/dist/system/assets/skills/skill-creator.dml +6 -2
  27. package/dist/system/runtime/catalog-skills.d.ts +45 -0
  28. package/dist/system/runtime/catalog-skills.d.ts.map +1 -0
  29. package/dist/system/runtime/catalog-skills.js +110 -0
  30. package/dist/system/runtime/catalog-skills.js.map +1 -0
  31. package/dist/system/runtime/conductor.d.ts +15 -1
  32. package/dist/system/runtime/conductor.d.ts.map +1 -1
  33. package/dist/system/runtime/conductor.js +11 -61
  34. package/dist/system/runtime/conductor.js.map +1 -1
  35. package/dist/system/runtime/dml-executor.d.ts +8 -0
  36. package/dist/system/runtime/dml-executor.d.ts.map +1 -1
  37. package/dist/system/runtime/dml-executor.js +48 -0
  38. package/dist/system/runtime/dml-executor.js.map +1 -1
  39. package/dist/system/runtime/dml-params.d.ts +11 -0
  40. package/dist/system/runtime/dml-params.d.ts.map +1 -0
  41. package/dist/system/runtime/dml-params.js +45 -0
  42. package/dist/system/runtime/dml-params.js.map +1 -0
  43. package/dist/system/runtime/runtime-tools.d.ts +4 -0
  44. package/dist/system/runtime/runtime-tools.d.ts.map +1 -1
  45. package/dist/system/runtime/runtime-tools.js +24 -0
  46. package/dist/system/runtime/runtime-tools.js.map +1 -1
  47. package/dist/system/runtime/skill-creator.d.ts.map +1 -1
  48. package/dist/system/runtime/skill-creator.js +51 -105
  49. package/dist/system/runtime/skill-creator.js.map +1 -1
  50. 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' || event.type === 'input_required') {
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' || event.type === 'input_required') {
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
- pendingQuestion = null;
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. Left/right changes pane. Up/down and PgUp/PgDn scroll the focused pane when input is empty.';
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.pendingQuestion) {
275
- const pending = this.pendingQuestion;
276
- this.pendingQuestion = null;
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.pendingQuestion) {
374
- const pending = this.pendingQuestion;
375
- this.pendingQuestion = null;
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.pendingQuestion && !canSubmitParsedInputWhileBusy(parsed)) {
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(`[${skillName}] ${question}`),
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
- finally {
529
- this.busy = false;
530
- this.currentAbortController = null;
531
- this.stopAnimation();
532
- if (this.exitRequested) {
533
- this.close();
534
- }
535
- else {
536
- this.requestRender();
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 requestClarification(promptText) {
541
- this.currentProcessActivity().pushLine(`clarify ${condenseWhitespace(promptText)}`);
542
- this.inputValue = '';
543
- this.cursor = 0;
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
- await this.refreshCommands();
556
- const completion = completeSlashCommand(this.inputValue, [
557
- ...BUILTIN_SLASH_COMMANDS,
558
- ...this.commands.map((command) => command.name),
559
- ]);
560
- if (completion.applied) {
561
- this.inputValue = completion.value;
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
- if (completion.matches.length === 0) {
565
- this.statusLine = 'No matching commands or skills.';
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
- else if (completion.matches.length === 1) {
568
- this.statusLine = `Completed /${completion.matches[0]}`;
1771
+ catch (error) {
1772
+ this.statusLine = `Unable to save file: ${error.message}`;
1773
+ this.requestRender();
569
1774
  }
570
- else {
571
- const preview = completion.matches.slice(0, 6).join(', ');
572
- this.statusLine = `Matches: ${preview}${completion.matches.length > 6 ? ', ...' : ''}`;
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 handleUpDown(offset) {
577
- if (this.focusedPane === 'sessions') {
578
- await this.moveSession(offset);
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.scrollFocusedPane(offset < 0 ? 'up' : 'down', 1);
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
- cyclePaneFocus(direction) {
584
- const order = ['sessions', 'messages', 'process', 'context'];
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
- beginExecutionPreview(sessionId, kind, lead) {
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
- if (this.currentPreview.kind === 'task' && logEvent.scope !== 'main') {
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 header = renderMenuBar(this.buildHeaderLine(), columns);
828
- const status = renderStatusBar(this.buildStatusLine(), columns, this.focusedPane);
829
- const inputPrefix = this.pendingQuestion ? ' Clarify ? ' : ' Command > ';
830
- const inputLine = renderCommandBar(inputPrefix, this.inputValue, columns);
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
- for (let index = 0; index < contentHeight; index += 1) {
834
- const rightLine = index < rightHeights.process
835
- ? processPane[index]
836
- : contextPane[index - rightHeights.process];
837
- lines.push(`${sessionsPane[index]}${gap}${messagesPane[index]}${gap}${rightLine}`);
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 ` DeepClause Session Skills Run Help ${sessionLabel} ${mode} ${skillLabel}`;
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.pendingQuestion && this.focusedPane !== 'sessions') {
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.pendingQuestion) {
881
- return `Clarify: ${condenseWhitespace(this.pendingQuestion.prompt)} | Focus ${focus}`;
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
- switch (entry.role) {
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 !this.pendingQuestion && this.inputValue.length > 0;
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 === 'new' && rawArgs ? [rawArgs] : parseCommandArgs(rawArgs),
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
- ? `[${logEvent.childSlug ?? '?'}] llm`
2950
+ ? `${CHILD_EVENT_INDENT}[${logEvent.childSlug ?? '?'}] llm`
1379
2951
  : 'llm';
1380
2952
  }
1381
2953
  function formatEventPrefix(logEvent) {
1382
2954
  return logEvent.scope === 'child'
1383
- ? `[${logEvent.childSlug ?? '?'}] `
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.length > width) {
1484
- const slice = remaining.slice(0, width);
3110
+ while (measureDisplayWidth(remaining) > width) {
3111
+ const { head: slice, tail } = splitByDisplayWidth(remaining, width, { forceOne: true });
1485
3112
  const breakIndex = slice.lastIndexOf(' ');
1486
- if (breakIndex > Math.floor(width * 0.4)) {
1487
- result.push(slice.slice(0, breakIndex));
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 = remaining.slice(width);
3120
+ remaining = tail;
1493
3121
  }
1494
3122
  }
1495
3123
  result.push(remaining);
1496
3124
  }
1497
3125
  return result;
1498
3126
  }
1499
- function padRight(text, width) {
1500
- return text.length >= width ? text : `${text}${' '.repeat(width - text.length)}`;
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 ellipsize(text, width) {
1503
- if (text.length <= width) {
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.slice(0, width);
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
- return `${text.slice(0, width - 3)}...`;
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
- return paint(padRight(ellipsize(text, columns), columns), ANSI.black, ANSI.bgWhite, ANSI.bold);
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, focusedPane) {
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].length + hotkey[1].length + 4;
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.length);
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.length);
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;