deepclause-sdk 0.0.60 → 0.0.62

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