openhorse 0.2.22 → 0.2.23

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 (111) hide show
  1. package/dist/harness/capsule.d.ts +1 -0
  2. package/dist/harness/capsule.d.ts.map +1 -1
  3. package/dist/harness/capsule.js +38 -9
  4. package/dist/harness/capsule.js.map +1 -1
  5. package/dist/harness/contract.d.ts +2 -0
  6. package/dist/harness/contract.d.ts.map +1 -1
  7. package/dist/harness/contract.js +78 -14
  8. package/dist/harness/contract.js.map +1 -1
  9. package/dist/harness/index.d.ts +2 -2
  10. package/dist/harness/index.d.ts.map +1 -1
  11. package/dist/harness/index.js +4 -1
  12. package/dist/harness/index.js.map +1 -1
  13. package/dist/harness/state.d.ts.map +1 -1
  14. package/dist/harness/state.js +37 -7
  15. package/dist/harness/state.js.map +1 -1
  16. package/dist/runtime/aggregate-tool-presenter.d.ts +13 -0
  17. package/dist/runtime/aggregate-tool-presenter.d.ts.map +1 -0
  18. package/dist/runtime/aggregate-tool-presenter.js +44 -0
  19. package/dist/runtime/aggregate-tool-presenter.js.map +1 -0
  20. package/dist/runtime/chat-controller.d.ts +9 -2
  21. package/dist/runtime/chat-controller.d.ts.map +1 -1
  22. package/dist/runtime/chat-controller.js +129 -17
  23. package/dist/runtime/chat-controller.js.map +1 -1
  24. package/dist/runtime/tool-detail-repository.d.ts +45 -0
  25. package/dist/runtime/tool-detail-repository.d.ts.map +1 -0
  26. package/dist/runtime/tool-detail-repository.js +131 -0
  27. package/dist/runtime/tool-detail-repository.js.map +1 -0
  28. package/dist/runtime/tool-output-presentation.d.ts +89 -0
  29. package/dist/runtime/tool-output-presentation.d.ts.map +1 -0
  30. package/dist/runtime/tool-output-presentation.js +329 -0
  31. package/dist/runtime/tool-output-presentation.js.map +1 -0
  32. package/dist/runtime/ui-events.d.ts +4 -0
  33. package/dist/runtime/ui-events.d.ts.map +1 -1
  34. package/dist/runtime/ui-events.js.map +1 -1
  35. package/dist/services/model-context.d.ts.map +1 -1
  36. package/dist/services/model-context.js +7 -0
  37. package/dist/services/model-context.js.map +1 -1
  38. package/dist/services/session-storage.d.ts +3 -0
  39. package/dist/services/session-storage.d.ts.map +1 -1
  40. package/dist/services/session-storage.js +55 -28
  41. package/dist/services/session-storage.js.map +1 -1
  42. package/dist/services/storage-maintenance.d.ts.map +1 -1
  43. package/dist/services/storage-maintenance.js +3 -2
  44. package/dist/services/storage-maintenance.js.map +1 -1
  45. package/dist/terminal-ui/editor.d.ts.map +1 -1
  46. package/dist/terminal-ui/editor.js +14 -3
  47. package/dist/terminal-ui/editor.js.map +1 -1
  48. package/dist/terminal-ui/launch.d.ts +19 -0
  49. package/dist/terminal-ui/launch.d.ts.map +1 -1
  50. package/dist/terminal-ui/launch.js +138 -24
  51. package/dist/terminal-ui/launch.js.map +1 -1
  52. package/dist/terminal-ui/output-queue.d.ts +43 -0
  53. package/dist/terminal-ui/output-queue.d.ts.map +1 -0
  54. package/dist/terminal-ui/output-queue.js +139 -0
  55. package/dist/terminal-ui/output-queue.js.map +1 -0
  56. package/dist/terminal-ui/raw-editor.d.ts +33 -0
  57. package/dist/terminal-ui/raw-editor.d.ts.map +1 -1
  58. package/dist/terminal-ui/raw-editor.js +142 -23
  59. package/dist/terminal-ui/raw-editor.js.map +1 -1
  60. package/dist/tools/index.d.ts.map +1 -1
  61. package/dist/tools/index.js +133 -58
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/tui-core/input-parser.d.ts +9 -2
  64. package/dist/tui-core/input-parser.d.ts.map +1 -1
  65. package/dist/tui-core/input-parser.js +37 -2
  66. package/dist/tui-core/input-parser.js.map +1 -1
  67. package/dist/tui-ui/inline-surface.d.ts +7 -1
  68. package/dist/tui-ui/inline-surface.d.ts.map +1 -1
  69. package/dist/tui-ui/inline-surface.js +23 -2
  70. package/dist/tui-ui/inline-surface.js.map +1 -1
  71. package/dist/tui-ui/input-ownership.d.ts +46 -0
  72. package/dist/tui-ui/input-ownership.d.ts.map +1 -0
  73. package/dist/tui-ui/input-ownership.js +74 -0
  74. package/dist/tui-ui/input-ownership.js.map +1 -0
  75. package/dist/tui-ui/launch.d.ts +1 -0
  76. package/dist/tui-ui/launch.d.ts.map +1 -1
  77. package/dist/tui-ui/launch.js +104 -2
  78. package/dist/tui-ui/launch.js.map +1 -1
  79. package/dist/tui-ui/layout.d.ts +1 -0
  80. package/dist/tui-ui/layout.d.ts.map +1 -1
  81. package/dist/tui-ui/layout.js +5 -1
  82. package/dist/tui-ui/layout.js.map +1 -1
  83. package/dist/tui-ui/runner.d.ts +32 -0
  84. package/dist/tui-ui/runner.d.ts.map +1 -1
  85. package/dist/tui-ui/runner.js +553 -24
  86. package/dist/tui-ui/runner.js.map +1 -1
  87. package/dist/tui-ui/state.d.ts +65 -0
  88. package/dist/tui-ui/state.d.ts.map +1 -1
  89. package/dist/tui-ui/state.js +187 -2
  90. package/dist/tui-ui/state.js.map +1 -1
  91. package/dist/tui-ui/theme-profile.d.ts +25 -0
  92. package/dist/tui-ui/theme-profile.d.ts.map +1 -0
  93. package/dist/tui-ui/theme-profile.js +130 -0
  94. package/dist/tui-ui/theme-profile.js.map +1 -0
  95. package/dist/tui-ui/transcript-inspector-layout.d.ts +9 -0
  96. package/dist/tui-ui/transcript-inspector-layout.d.ts.map +1 -0
  97. package/dist/tui-ui/transcript-inspector-layout.js +97 -0
  98. package/dist/tui-ui/transcript-inspector-layout.js.map +1 -0
  99. package/dist/tui-ui/transcript-inspector-surface.d.ts +18 -0
  100. package/dist/tui-ui/transcript-inspector-surface.d.ts.map +1 -0
  101. package/dist/tui-ui/transcript-inspector-surface.js +88 -0
  102. package/dist/tui-ui/transcript-inspector-surface.js.map +1 -0
  103. package/dist/tui-ui/transcript-inspector.d.ts +35 -0
  104. package/dist/tui-ui/transcript-inspector.d.ts.map +1 -0
  105. package/dist/tui-ui/transcript-inspector.js +128 -0
  106. package/dist/tui-ui/transcript-inspector.js.map +1 -0
  107. package/dist/tui-ui/transcript-layout.d.ts +1 -0
  108. package/dist/tui-ui/transcript-layout.d.ts.map +1 -1
  109. package/dist/tui-ui/transcript-layout.js +44 -3
  110. package/dist/tui-ui/transcript-layout.js.map +1 -1
  111. package/package.json +1 -1
@@ -8,10 +8,16 @@ const layout_1 = require("./layout");
8
8
  const pickers_1 = require("./pickers");
9
9
  const render_scheduler_1 = require("./render-scheduler");
10
10
  const state_1 = require("./state");
11
+ const input_ownership_1 = require("./input-ownership");
12
+ const transcript_inspector_1 = require("./transcript-inspector");
13
+ const transcript_inspector_layout_1 = require("./transcript-inspector-layout");
11
14
  const transcript_cache_1 = require("./transcript-cache");
12
15
  const transcript_layout_1 = require("./transcript-layout");
13
16
  const theme_1 = require("./theme");
14
17
  const history_1 = require("../runtime/composer/history");
18
+ const promises_1 = require("fs/promises");
19
+ const os_1 = require("os");
20
+ const path_1 = require("path");
15
21
  /** Actions that should use 'stream' priority (FPS-capped). */
16
22
  const STREAM_ACTIONS = new Set([
17
23
  'updateTranscript',
@@ -25,20 +31,36 @@ class TuiRunner {
25
31
  constructor(options) {
26
32
  this.options = options;
27
33
  this.parser = new input_parser_1.TuiInputParser();
34
+ this.inputOwnership = new input_ownership_1.TuiInputOwnershipController();
28
35
  this.transcriptCache = new transcript_cache_1.TranscriptLayoutCache();
29
36
  this.history = history_1.initialHistoryState;
30
37
  this.surfaceFailed = false;
38
+ this.inspectorReady = false;
39
+ this.inspectorSearchActive = false;
40
+ this.modalTransition = Promise.resolve();
41
+ this.commitSequence = 0;
42
+ this.commitInFlight = false;
43
+ this.commitMismatchRetries = 0;
31
44
  this.state = state_1.initialTuiUiState;
32
45
  this.lastFrame = null;
33
46
  this.surfaceResizePending = false;
34
47
  this.resizeEpoch = 0;
35
48
  this.surfaceResizeGeneration = 0;
36
- this.counters = { layoutCount: 0, paintCount: 0, changedRows: 0, commitCount: 0 };
49
+ this.counters = {
50
+ layoutCount: 0,
51
+ paintCount: 0,
52
+ changedRows: 0,
53
+ commitCount: 0,
54
+ };
37
55
  this.width = options.width;
38
56
  this.height = options.height;
39
57
  this.surface = options.surface ?? null;
40
58
  this.theme = (0, theme_1.resolveTuiTheme)(options.theme);
41
59
  this.themeId = options.themeId ?? theme_1.DEFAULT_TUI_THEME_ID;
60
+ this.inspectorSurface = options.inspectorSurface ?? null;
61
+ this.inspectorController = options.detailRepository
62
+ ? new transcript_inspector_1.TranscriptInspectorController(options.detailRepository, options.cwd ?? process.cwd())
63
+ : null;
42
64
  this.scheduler = (0, render_scheduler_1.createTuiRenderScheduler)(() => this.renderLive(), options.schedulerDeps);
43
65
  this.events = (0, state_1.createTuiUiEventSink)(action => this.dispatch(action));
44
66
  // Initial render: paint the live region immediately.
@@ -59,6 +81,8 @@ class TuiRunner {
59
81
  }
60
82
  /** Stop stale-width paints as soon as the terminal reports SIGWINCH. */
61
83
  beginResize(width = this.width) {
84
+ if (this.state.inspector)
85
+ return;
62
86
  if (!this.surface || this.surfaceFailed)
63
87
  return;
64
88
  this.surfaceResizePending = true;
@@ -66,6 +90,12 @@ class TuiRunner {
66
90
  this.surfaceResizeGeneration = this.surface.beginResize(width);
67
91
  }
68
92
  resize(width, height) {
93
+ if (this.state.inspector) {
94
+ this.width = width;
95
+ this.height = height;
96
+ this.paintInspector();
97
+ return;
98
+ }
69
99
  this.beginResize(width);
70
100
  const resizeEpoch = this.resizeEpoch;
71
101
  const surfaceResizeGeneration = this.surfaceResizeGeneration;
@@ -102,19 +132,16 @@ class TuiRunner {
102
132
  dispatch(action) {
103
133
  const prevState = this.state;
104
134
  this.state = (0, state_1.tuiUiReducer)(this.state, action);
135
+ if (action.type === 'setToolOutputViewMode' && action.mode !== prevState.toolOutputViewMode) {
136
+ this.transcriptCache.clear();
137
+ }
105
138
  // Check if any transcript entries became committable (finalized).
106
139
  this.tryCommit(prevState);
107
140
  const priority = STREAM_ACTIONS.has(action.type) ? 'stream' : 'immediate';
108
141
  this.scheduler.request(priority);
109
142
  }
110
143
  feedInput(chunk) {
111
- const raw = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk;
112
- if ((0, input_parser_1.isLikelyUnbracketedMultilinePaste)(raw)) {
113
- const event = { type: 'paste', value: (0, input_parser_1.normalizePastedText)(raw) };
114
- this.applyInputEvent(event);
115
- return [event];
116
- }
117
- const events = this.parser.feed(chunk);
144
+ const events = this.parser.feed(chunk, { detectUnbracketedMultilinePaste: true });
118
145
  for (const event of events) {
119
146
  this.applyInputEvent(event);
120
147
  }
@@ -143,6 +170,10 @@ class TuiRunner {
143
170
  * transcript so tests can inspect complete content via getLastFrame().
144
171
  */
145
172
  renderLive() {
173
+ if (this.state.inspector) {
174
+ this.paintInspector();
175
+ return;
176
+ }
146
177
  if (this.surface && !this.surfaceFailed) {
147
178
  if (this.surfaceResizePending)
148
179
  return;
@@ -188,7 +219,7 @@ class TuiRunner {
188
219
  * but not yet queued.
189
220
  */
190
221
  tryCommit(prevState, force = false) {
191
- if (!this.surface || this.surfaceResizePending)
222
+ if (!this.surface || this.surfaceResizePending || this.state.inspector || this.commitInFlight)
192
223
  return;
193
224
  const prevCommittable = prevState.committableTranscriptCount;
194
225
  const currCommittable = this.state.committableTranscriptCount;
@@ -209,6 +240,7 @@ class TuiRunner {
209
240
  rows: this.layoutTranscriptRecord(entry, this.transcriptWidth),
210
241
  }));
211
242
  const batch = {
243
+ batchId: `tui-commit-${++this.commitSequence}`,
212
244
  generation: this.state.transcriptGeneration,
213
245
  reason: 'finalize',
214
246
  entries: committedEntries,
@@ -218,38 +250,89 @@ class TuiRunner {
218
250
  // live region).
219
251
  const generation = batch.generation;
220
252
  const getLatestLiveFrame = () => this.buildLiveFrame(this.width);
253
+ this.commitInFlight = true;
254
+ let continueCommit = true;
221
255
  // Surface commit is async (serialized FIFO). Fire-and-forget: the
222
256
  // commit's internal getLatestLiveFrame already rebuilds the live region,
223
257
  // so no additional scheduler paint is needed here.
224
258
  void this.surface
225
259
  .commit(batch, getLatestLiveFrame)
226
- .then(() => {
227
- if (this.state.transcriptGeneration !== generation)
260
+ .then(result => {
261
+ if (result.batchId !== batch.batchId ||
262
+ result.generation !== generation ||
263
+ result.committedEntries !== entriesToCommit.length) {
264
+ if (this.state.transcriptGeneration === generation) {
265
+ this.state = {
266
+ ...this.state,
267
+ queuedTranscriptCount: Math.max(0, this.state.queuedTranscriptCount - entriesToCommit.length),
268
+ };
269
+ this.commitMismatchRetries += 1;
270
+ if (this.commitMismatchRetries > 1) {
271
+ continueCommit = false;
272
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
273
+ type: 'setStatus',
274
+ message: 'TUI transcript acknowledgement mismatch; retained content for retry.',
275
+ });
276
+ }
277
+ }
278
+ void this.surface?.forceRedraw(this.buildLiveFrame(this.width));
228
279
  return;
229
- this.state = (0, state_1.markTranscriptCommitted)(this.state, entriesToCommit.length);
280
+ }
281
+ const acknowledgement = (0, state_1.acknowledgeTranscriptCommit)(this.state, {
282
+ generation,
283
+ recordIds: result.displayKeys ?? [],
284
+ });
285
+ if (!acknowledgement.accepted) {
286
+ if (this.state.transcriptGeneration === generation) {
287
+ this.state = {
288
+ ...this.state,
289
+ queuedTranscriptCount: Math.max(0, this.state.queuedTranscriptCount - entriesToCommit.length),
290
+ };
291
+ this.commitMismatchRetries += 1;
292
+ if (this.commitMismatchRetries > 1) {
293
+ continueCommit = false;
294
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
295
+ type: 'setStatus',
296
+ message: 'TUI transcript acknowledgement mismatch; retained content for retry.',
297
+ });
298
+ }
299
+ }
300
+ void this.surface?.forceRedraw(this.buildLiveFrame(this.width));
301
+ return;
302
+ }
303
+ this.state = acknowledgement.state;
304
+ this.commitMismatchRetries = 0;
230
305
  this.counters.commitCount += 1;
231
306
  })
232
- .catch(error => this.handleSurfaceError(error));
307
+ .catch(error => this.handleSurfaceError(error))
308
+ .finally(() => {
309
+ this.commitInFlight = false;
310
+ if (!this.surfaceFailed && continueCommit)
311
+ this.tryCommit(this.state, true);
312
+ });
233
313
  }
234
314
  /** Layout a transcript record once per revision/width/theme combination. */
235
315
  layoutTranscriptRecord(entry, width) {
236
316
  const cached = this.transcriptCache.get(entry.id, entry.revision, this.state.transcriptGeneration, width, this.themeId);
237
317
  if (cached)
238
318
  return cached;
239
- const rows = (0, transcript_layout_1.layoutTranscriptEntry)(entry, { width, theme: this.theme });
319
+ const rows = (0, transcript_layout_1.layoutTranscriptEntry)(entry, {
320
+ width,
321
+ theme: this.theme,
322
+ toolOutputMode: this.state.toolOutputViewMode,
323
+ });
240
324
  this.transcriptCache.set(entry.id, entry.revision, rows, this.state.transcriptGeneration, width, this.themeId);
241
325
  return rows;
242
326
  }
243
327
  get transcriptWidth() {
244
- return this.surface
245
- ? Math.max(1, this.width - 1)
246
- : Math.max(1, this.width);
328
+ return this.surface ? Math.max(1, this.width - 1) : Math.max(1, this.width);
247
329
  }
248
330
  transcriptLayoutOptions() {
249
331
  return {
250
332
  transcriptWidth: this.transcriptWidth,
251
333
  theme: this.theme,
252
- layoutTranscriptRecord: (entry, width) => (this.layoutTranscriptRecord(entry, width)),
334
+ toolOutputMode: this.state.toolOutputViewMode,
335
+ layoutTranscriptRecord: (entry, width) => this.layoutTranscriptRecord(entry, width),
253
336
  };
254
337
  }
255
338
  handleSurfaceError(error) {
@@ -264,6 +347,10 @@ class TuiRunner {
264
347
  this.options.onSurfaceError?.(error);
265
348
  }
266
349
  applyInputEvent(event) {
350
+ if (this.state.inspector) {
351
+ this.applyInspectorInput(event);
352
+ return;
353
+ }
267
354
  if (this.state.overlay?.type === 'permission') {
268
355
  if (event.type === 'text') {
269
356
  const answer = event.value.trim().toLowerCase();
@@ -308,10 +395,16 @@ class TuiRunner {
308
395
  this.dispatch({ type: 'moveOverlaySelection', delta: 1 });
309
396
  return;
310
397
  case 'pageup':
311
- this.dispatch({ type: 'moveOverlaySelection', delta: -Math.max(1, overlay.request.maxVisibleItems ?? 10) });
398
+ this.dispatch({
399
+ type: 'moveOverlaySelection',
400
+ delta: -Math.max(1, overlay.request.maxVisibleItems ?? 10),
401
+ });
312
402
  return;
313
403
  case 'pagedown':
314
- this.dispatch({ type: 'moveOverlaySelection', delta: Math.max(1, overlay.request.maxVisibleItems ?? 10) });
404
+ this.dispatch({
405
+ type: 'moveOverlaySelection',
406
+ delta: Math.max(1, overlay.request.maxVisibleItems ?? 10),
407
+ });
315
408
  return;
316
409
  case 'enter':
317
410
  this.submitPrompt({ allowEmpty: true });
@@ -357,6 +450,11 @@ class TuiRunner {
357
450
  this.completeCommand(overlay.items[overlay.selectedIndex], false);
358
451
  return;
359
452
  case 'enter':
453
+ if (/^\/(?:tool-output|redraw)(?:\s|$)/u.test(this.state.prompt.value.trim())) {
454
+ this.dispatch({ type: 'closeOverlay' });
455
+ this.submitPrompt();
456
+ return;
457
+ }
360
458
  this.completeCommand(overlay.items[overlay.selectedIndex], true);
361
459
  return;
362
460
  case 'escape':
@@ -415,6 +513,12 @@ class TuiRunner {
415
513
  return;
416
514
  }
417
515
  switch (key) {
516
+ case 'ctrl+o':
517
+ this.openToolInspector();
518
+ return;
519
+ case 'ctrl+l':
520
+ this.forceOwnedRedraw();
521
+ return;
418
522
  case 'enter':
419
523
  this.submitPrompt();
420
524
  return;
@@ -472,7 +576,7 @@ class TuiRunner {
472
576
  historyBack() {
473
577
  if (this.state.overlay)
474
578
  return; // overlay handles up/down itself
475
- const { value, cursor } = this.state.prompt;
579
+ const { value } = this.state.prompt;
476
580
  const next = (0, history_1.historyPrevious)(this.history, value);
477
581
  this.history = next;
478
582
  const displayValue = (0, history_1.historyCurrentValue)(next, value);
@@ -537,7 +641,11 @@ class TuiRunner {
537
641
  const { value, cursor } = this.state.prompt;
538
642
  const { line } = lineColOfCursor(value, cursor);
539
643
  const lines = value.split('\n');
540
- this.dispatch({ type: 'setPrompt', value, cursor: cursorOfLineCol(value, line, lines[line].length) });
644
+ this.dispatch({
645
+ type: 'setPrompt',
646
+ value,
647
+ cursor: cursorOfLineCol(value, line, lines[line].length),
648
+ });
541
649
  }
542
650
  submitPrompt(options = {}) {
543
651
  const input = this.state.prompt.value;
@@ -546,6 +654,8 @@ class TuiRunner {
546
654
  // Push to history before clearing.
547
655
  this.history = (0, history_1.pushHistoryEntry)(this.history, input);
548
656
  this.dispatch({ type: 'setPrompt', value: '', cursor: 0 });
657
+ if (this.handleLocalCommand(input))
658
+ return;
549
659
  try {
550
660
  const submission = this.options.onSubmit?.(input);
551
661
  if (submission) {
@@ -559,6 +669,42 @@ class TuiRunner {
559
669
  reportSubmitFailure() {
560
670
  this.events.append({ role: 'error', content: 'Input submission failed.' });
561
671
  }
672
+ handleLocalCommand(input) {
673
+ const command = input.trim();
674
+ if (command === '/redraw') {
675
+ this.forceOwnedRedraw();
676
+ return true;
677
+ }
678
+ const match = command.match(/^\/tool-output(?:\s+(adaptive|collapsed|full))?$/u);
679
+ if (match) {
680
+ const mode = match[1];
681
+ if (mode)
682
+ this.dispatch({ type: 'setToolOutputViewMode', mode });
683
+ this.dispatch({
684
+ type: 'setStatus',
685
+ message: `tool output: ${mode ?? this.state.toolOutputViewMode}`,
686
+ });
687
+ return true;
688
+ }
689
+ if (command.startsWith('/tool-output')) {
690
+ this.dispatch({
691
+ type: 'setStatus',
692
+ message: 'Usage: /tool-output adaptive|collapsed|full',
693
+ });
694
+ return true;
695
+ }
696
+ return false;
697
+ }
698
+ forceOwnedRedraw() {
699
+ if (this.surface && !this.surfaceFailed) {
700
+ void this.surface
701
+ .forceRedraw(this.buildLiveFrame(this.width))
702
+ .catch(error => this.handleSurfaceError(error));
703
+ return;
704
+ }
705
+ this.lastFrame = null;
706
+ this.scheduler.request('immediate');
707
+ }
562
708
  updatePrompt(next) {
563
709
  this.dispatch({ type: 'setPrompt', value: next.value, cursor: next.cursor });
564
710
  this.syncPromptOverlay(next.value);
@@ -571,7 +717,7 @@ class TuiRunner {
571
717
  this.dispatch({
572
718
  type: 'showCommandPalette',
573
719
  query: value.slice(1),
574
- items: (0, pickers_1.visibleCommandItems)(value),
720
+ items: visibleTuiCommandItems(value),
575
721
  });
576
722
  return;
577
723
  }
@@ -593,7 +739,8 @@ class TuiRunner {
593
739
  if (!item)
594
740
  return;
595
741
  const command = (0, commands_1.getCommands)().find(candidate => candidate.name === item.value);
596
- const needsArgs = Boolean(command?.argumentHint || command?.params?.some(param => param.required));
742
+ const needsArgs = item.value === 'tool-output' ||
743
+ Boolean(command?.argumentHint || command?.params?.some(param => param.required));
597
744
  const value = `/${item.value}${needsArgs ? ' ' : ''}`;
598
745
  const promptAlreadyMatchesCommand = this.state.prompt.value.trim() === `/${item.value}`;
599
746
  const nextValue = promptAlreadyMatchesCommand ? `/${item.value}` : value;
@@ -620,6 +767,366 @@ class TuiRunner {
620
767
  this.dispatch({ type: 'closeOverlay' });
621
768
  void this.options.onPermissionDecision?.(overlay.request.id, approved);
622
769
  }
770
+ /** Restore any temporary Inspector terminal ownership before shutdown. */
771
+ async closeModalSurface() {
772
+ if (this.state.inspector || this.inspectorSurface?.isMounted) {
773
+ this.closeToolInspector();
774
+ }
775
+ await this.modalTransition;
776
+ }
777
+ /** Wait until the current Inspector ownership transition is complete. */
778
+ async waitForModalSurface() {
779
+ await this.modalTransition;
780
+ }
781
+ /** Flush every finalized transcript batch, including batches queued by acknowledgements. */
782
+ async flushTranscriptCommits() {
783
+ if (!this.surface || this.surfaceFailed)
784
+ return;
785
+ for (let attempt = 0; attempt < 1024; attempt += 1) {
786
+ await this.surface.whenIdle();
787
+ await Promise.resolve();
788
+ if (!this.commitInFlight && (0, state_1.pendingCommitRecords)(this.state).length === 0)
789
+ return;
790
+ if (!this.commitInFlight)
791
+ this.tryCommit(this.state, true);
792
+ }
793
+ throw new Error('TUI transcript commits did not settle');
794
+ }
795
+ openToolInspector() {
796
+ if (this.state.inspector || !this.inspectorController || !this.inspectorSurface)
797
+ return;
798
+ this.inputOwnership.capture('inspector', {
799
+ value: this.state.prompt.value,
800
+ cursor: this.state.prompt.cursor,
801
+ parserState: this.parser.getState(),
802
+ });
803
+ this.state = (0, state_1.tuiUiReducer)(this.state, { type: 'openToolInspector' });
804
+ this.inspectorReady = false;
805
+ this.inspectorSearchActive = false;
806
+ this.modalTransition = this.modalTransition
807
+ .then(async () => {
808
+ await this.surface?.suspend();
809
+ this.inspectorReady = true;
810
+ await this.inspectorSurface.mount(this.buildInspectorFrame());
811
+ })
812
+ .catch(error => this.handleSurfaceError(error));
813
+ }
814
+ closeToolInspector() {
815
+ if (!this.state.inspector && !this.inspectorSurface?.isMounted)
816
+ return;
817
+ this.inspectorController?.cancel();
818
+ this.state = (0, state_1.tuiUiReducer)(this.state, { type: 'closeToolInspector' });
819
+ const snapshot = this.inputOwnership.restore();
820
+ if (snapshot) {
821
+ this.parser.setState(snapshot.parserState);
822
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
823
+ type: 'setPrompt',
824
+ value: snapshot.value,
825
+ cursor: snapshot.cursor,
826
+ });
827
+ }
828
+ this.inspectorReady = false;
829
+ this.inspectorSearchActive = false;
830
+ this.modalTransition = this.modalTransition
831
+ .then(async () => {
832
+ await this.inspectorSurface?.unmount();
833
+ if (this.surface) {
834
+ await this.surface.restore(() => this.buildLiveFrame(this.width), this.width, this.height);
835
+ }
836
+ this.scheduler.request('immediate');
837
+ })
838
+ .catch(error => this.handleSurfaceError(error));
839
+ }
840
+ applyInspectorInput(event) {
841
+ if (event.type === 'paste')
842
+ return;
843
+ if (event.type === 'text') {
844
+ if (!this.inspectorSearchActive) {
845
+ if (event.value === 'j') {
846
+ this.moveInspectorSelection(1);
847
+ return;
848
+ }
849
+ if (event.value === 'k') {
850
+ this.moveInspectorSelection(-1);
851
+ return;
852
+ }
853
+ if (event.value === ' ') {
854
+ void this.loadSelectedInspectorDetail(false);
855
+ return;
856
+ }
857
+ if (event.value === 'g') {
858
+ this.moveInspectorSelectionTo('first');
859
+ return;
860
+ }
861
+ if (event.value === 'G') {
862
+ this.moveInspectorSelectionTo('last');
863
+ return;
864
+ }
865
+ if (event.value === 'n' && this.state.inspector?.searchQuery) {
866
+ this.moveInspectorSelection(1, true);
867
+ return;
868
+ }
869
+ if (event.value === 'N' && this.state.inspector?.searchQuery) {
870
+ this.moveInspectorSelection(-1, true);
871
+ return;
872
+ }
873
+ if (event.value === 'v') {
874
+ this.openSelectedInspectorDetailInEditor();
875
+ return;
876
+ }
877
+ }
878
+ if (!this.inspectorSearchActive && (event.value === 'q' || event.value === 'Q')) {
879
+ this.closeToolInspector();
880
+ return;
881
+ }
882
+ if (!this.inspectorSearchActive && event.value === '/') {
883
+ this.inspectorSearchActive = true;
884
+ this.state = (0, state_1.tuiUiReducer)(this.state, { type: 'setToolInspectorSearch', query: '' });
885
+ this.paintInspector();
886
+ return;
887
+ }
888
+ if (!this.inspectorSearchActive && event.value === '[') {
889
+ this.exportInspectorResults();
890
+ return;
891
+ }
892
+ if (this.inspectorSearchActive) {
893
+ const query = `${this.state.inspector?.searchQuery ?? ''}${event.value}`;
894
+ this.state = (0, state_1.tuiUiReducer)(this.state, { type: 'setToolInspectorSearch', query });
895
+ this.paintInspector();
896
+ }
897
+ return;
898
+ }
899
+ switch (event.key) {
900
+ case 'ctrl+o':
901
+ case 'escape':
902
+ this.closeToolInspector();
903
+ return;
904
+ case 'ctrl+c':
905
+ this.closeToolInspector();
906
+ return;
907
+ case 'up':
908
+ this.moveInspectorSelection(-1);
909
+ return;
910
+ case 'down':
911
+ this.moveInspectorSelection(1);
912
+ return;
913
+ case 'pageup':
914
+ case 'ctrl+u':
915
+ this.state = (0, state_1.tuiUiReducer)(this.state, { type: 'scrollToolInspector', delta: -10 });
916
+ this.paintInspector();
917
+ return;
918
+ case 'pagedown':
919
+ case 'ctrl+d':
920
+ this.state = (0, state_1.tuiUiReducer)(this.state, { type: 'scrollToolInspector', delta: 10 });
921
+ void this.loadSelectedInspectorDetail(true);
922
+ return;
923
+ case 'home':
924
+ this.moveInspectorSelectionTo('first');
925
+ return;
926
+ case 'end':
927
+ this.moveInspectorSelectionTo('last');
928
+ return;
929
+ case 'backspace': {
930
+ if (!this.inspectorSearchActive)
931
+ return;
932
+ const query = this.state.inspector?.searchQuery ?? '';
933
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
934
+ type: 'setToolInspectorSearch',
935
+ query: query.slice(0, -1),
936
+ });
937
+ this.paintInspector();
938
+ return;
939
+ }
940
+ case 'enter':
941
+ if (this.inspectorSearchActive) {
942
+ this.inspectorSearchActive = false;
943
+ this.paintInspector();
944
+ return;
945
+ }
946
+ void this.loadSelectedInspectorDetail(false);
947
+ return;
948
+ case 'ctrl+e':
949
+ this.state = (0, state_1.tuiUiReducer)(this.state, { type: 'toggleAllToolInspectorEntries' });
950
+ this.paintInspector();
951
+ return;
952
+ case 'ctrl+l':
953
+ this.paintInspector();
954
+ return;
955
+ }
956
+ }
957
+ async loadSelectedInspectorDetail(append) {
958
+ const inspector = this.state.inspector;
959
+ if (!inspector || !this.inspectorController)
960
+ return;
961
+ const view = this.inspectorController.view(this.state.recentToolDetails, inspector);
962
+ const selected = view.selected;
963
+ if (!selected)
964
+ return;
965
+ const expanded = inspector.expandedCallIds.includes(selected.callId);
966
+ if (!append && expanded) {
967
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
968
+ type: 'toggleToolInspectorEntry',
969
+ callId: selected.callId,
970
+ });
971
+ this.paintInspector();
972
+ return;
973
+ }
974
+ if (!expanded) {
975
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
976
+ type: 'toggleToolInspectorEntry',
977
+ callId: selected.callId,
978
+ });
979
+ }
980
+ this.paintInspector();
981
+ await this.inspectorController.load(selected, append && view.detail?.nextOffsetBytes !== undefined);
982
+ if (this.state.inspector)
983
+ this.paintInspector();
984
+ }
985
+ moveInspectorSelection(delta, wrap = false) {
986
+ const inspector = this.state.inspector;
987
+ if (!inspector || !this.inspectorController)
988
+ return;
989
+ const view = this.inspectorController.view(this.state.recentToolDetails, inspector);
990
+ if (view.entries.length === 0)
991
+ return;
992
+ const target = wrap
993
+ ? (view.selectedIndex + delta + view.entries.length) % view.entries.length
994
+ : Math.max(0, Math.min(view.entries.length - 1, view.selectedIndex + delta));
995
+ this.inspectorController.cancel();
996
+ this.state = (0, state_1.tuiUiReducer)(this.state, { type: 'setToolInspectorSelection', index: target });
997
+ this.paintInspector();
998
+ }
999
+ moveInspectorSelectionTo(edge) {
1000
+ const inspector = this.state.inspector;
1001
+ if (!inspector || !this.inspectorController)
1002
+ return;
1003
+ const view = this.inspectorController.view(this.state.recentToolDetails, inspector);
1004
+ if (view.entries.length === 0)
1005
+ return;
1006
+ this.inspectorController.cancel();
1007
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
1008
+ type: 'setToolInspectorSelection',
1009
+ index: edge === 'first' ? 0 : view.entries.length - 1,
1010
+ });
1011
+ this.paintInspector();
1012
+ }
1013
+ exportInspectorResults() {
1014
+ const inspector = this.state.inspector;
1015
+ if (!inspector || !this.inspectorController)
1016
+ return;
1017
+ const entries = this.inspectorController.view(this.state.recentToolDetails, inspector).entries;
1018
+ this.closeToolInspector();
1019
+ this.modalTransition = this.modalTransition
1020
+ .then(async () => {
1021
+ if (!this.surface) {
1022
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
1023
+ type: 'setStatus',
1024
+ message: 'Inspector export requires an inline terminal surface.',
1025
+ });
1026
+ return;
1027
+ }
1028
+ for (const entry of entries) {
1029
+ let pageIndex = 0;
1030
+ for await (const page of this.inspectorController.readPages(entry)) {
1031
+ const heading = pageIndex === 0 ? `#${entry.sequence} ${entry.toolName}\n` : '';
1032
+ const rows = (0, transcript_layout_1.layoutTranscriptEntry)({
1033
+ role: 'tool',
1034
+ content: `${heading}${page.content}`,
1035
+ }, {
1036
+ width: this.transcriptWidth,
1037
+ theme: this.theme,
1038
+ toolOutputMode: 'full',
1039
+ });
1040
+ await this.surface.commit({
1041
+ batchId: `tui-inspector-export-${++this.commitSequence}`,
1042
+ generation: this.state.transcriptGeneration,
1043
+ reason: 'append',
1044
+ entries: [
1045
+ {
1046
+ displayKey: `inspector-export-${entry.callId}-${pageIndex}`,
1047
+ rows,
1048
+ },
1049
+ ],
1050
+ }, () => this.buildLiveFrame(this.width));
1051
+ pageIndex += 1;
1052
+ }
1053
+ }
1054
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
1055
+ type: 'setStatus',
1056
+ message: `Exported ${entries.length} tool result${entries.length === 1 ? '' : 's'} to scrollback.`,
1057
+ });
1058
+ this.scheduler.request('immediate');
1059
+ })
1060
+ .catch(error => this.handleSurfaceError(error));
1061
+ }
1062
+ openSelectedInspectorDetailInEditor() {
1063
+ const inspector = this.state.inspector;
1064
+ if (!inspector || !this.inspectorController)
1065
+ return;
1066
+ const selected = this.inspectorController.view(this.state.recentToolDetails, inspector).selected;
1067
+ if (!selected)
1068
+ return;
1069
+ if (!this.options.onOpenExternalEditor) {
1070
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
1071
+ type: 'setStatus',
1072
+ message: 'External editor is unavailable in this TUI environment.',
1073
+ });
1074
+ this.paintInspector();
1075
+ return;
1076
+ }
1077
+ this.closeToolInspector();
1078
+ this.modalTransition = this.modalTransition
1079
+ .then(async () => {
1080
+ const directory = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'openhorse-tool-'));
1081
+ const filePath = (0, path_1.join)(directory, `${selected.sequence}-${safeFileName(selected.toolName)}.txt`);
1082
+ try {
1083
+ await this.inspectorController.writeDetailToFile(selected, filePath);
1084
+ await this.options.onOpenExternalEditor(filePath);
1085
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
1086
+ type: 'setStatus',
1087
+ message: `Viewed #${selected.sequence} ${selected.toolName} in external editor.`,
1088
+ });
1089
+ }
1090
+ finally {
1091
+ await (0, promises_1.rm)(directory, { recursive: true, force: true });
1092
+ }
1093
+ this.forceOwnedRedraw();
1094
+ })
1095
+ .catch(error => {
1096
+ this.state = (0, state_1.tuiUiReducer)(this.state, {
1097
+ type: 'setStatus',
1098
+ message: `External editor failed: ${error instanceof Error ? error.message : String(error)}`,
1099
+ });
1100
+ this.forceOwnedRedraw();
1101
+ });
1102
+ }
1103
+ paintInspector() {
1104
+ if (!this.state.inspector ||
1105
+ !this.inspectorController ||
1106
+ !this.inspectorSurface ||
1107
+ !this.inspectorReady)
1108
+ return;
1109
+ const frame = this.buildInspectorFrame();
1110
+ this.lastFrame = frame;
1111
+ void this.inspectorSurface.paint(frame).catch(error => this.handleSurfaceError(error));
1112
+ }
1113
+ buildInspectorFrame() {
1114
+ const inspector = this.state.inspector;
1115
+ const view = inspector && this.inspectorController
1116
+ ? this.inspectorController.view(this.state.recentToolDetails, inspector)
1117
+ : {
1118
+ entries: [],
1119
+ selectedIndex: 0,
1120
+ expandedCallIds: [],
1121
+ searchQuery: '',
1122
+ detailOffset: 0,
1123
+ };
1124
+ return (0, transcript_inspector_layout_1.renderTranscriptInspectorFrame)(view, {
1125
+ width: this.width,
1126
+ height: this.height,
1127
+ theme: this.theme,
1128
+ });
1129
+ }
623
1130
  }
624
1131
  exports.TuiRunner = TuiRunner;
625
1132
  function insertAtCursor(value, cursor, text) {
@@ -629,6 +1136,28 @@ function insertAtCursor(value, cursor, text) {
629
1136
  cursor: safeCursor + text.length,
630
1137
  };
631
1138
  }
1139
+ function visibleTuiCommandItems(input) {
1140
+ const query = input.startsWith('/')
1141
+ ? input.slice(1).trim().split(/\s+/u, 1)[0].toLowerCase()
1142
+ : '';
1143
+ const local = [
1144
+ {
1145
+ value: 'tool-output',
1146
+ label: '/tool-output',
1147
+ description: 'Set TUI tool output mode',
1148
+ },
1149
+ {
1150
+ value: 'redraw',
1151
+ label: '/redraw',
1152
+ description: 'Redraw the TUI-owned live region',
1153
+ },
1154
+ ].filter(item => item.value.includes(query));
1155
+ const localNames = new Set(local.map(item => item.value));
1156
+ return [...local, ...(0, pickers_1.visibleCommandItems)(input).filter(item => !localNames.has(item.value))];
1157
+ }
1158
+ function safeFileName(value) {
1159
+ return value.replace(/[^a-zA-Z0-9._-]+/gu, '-').replace(/^-+|-+$/gu, '') || 'tool';
1160
+ }
632
1161
  function deleteBeforeCursor(value, cursor) {
633
1162
  const safeCursor = clampCursor(value, cursor);
634
1163
  if (safeCursor === 0)