openhorse 0.2.13 → 0.2.15

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 (89) hide show
  1. package/dist/commands/index.d.ts.map +1 -1
  2. package/dist/commands/index.js +217 -49
  3. package/dist/commands/index.js.map +1 -1
  4. package/dist/commands/types.d.ts +4 -1
  5. package/dist/commands/types.d.ts.map +1 -1
  6. package/dist/commands/types.js.map +1 -1
  7. package/dist/core/checkpoint.d.ts +7 -0
  8. package/dist/core/checkpoint.d.ts.map +1 -1
  9. package/dist/core/checkpoint.js +10 -0
  10. package/dist/core/checkpoint.js.map +1 -1
  11. package/dist/core/cost-tracker.d.ts.map +1 -1
  12. package/dist/core/cost-tracker.js +11 -0
  13. package/dist/core/cost-tracker.js.map +1 -1
  14. package/dist/ink-ui/components/PromptInput.d.ts.map +1 -1
  15. package/dist/ink-ui/components/PromptInput.js +5 -3
  16. package/dist/ink-ui/components/PromptInput.js.map +1 -1
  17. package/dist/ink-ui/screens/ReplScreen.d.ts +3 -1
  18. package/dist/ink-ui/screens/ReplScreen.d.ts.map +1 -1
  19. package/dist/ink-ui/screens/ReplScreen.js +40 -93
  20. package/dist/ink-ui/screens/ReplScreen.js.map +1 -1
  21. package/dist/print-ui/launch.d.ts.map +1 -1
  22. package/dist/print-ui/launch.js +1 -0
  23. package/dist/print-ui/launch.js.map +1 -1
  24. package/dist/runtime/agent-runtime-controller.d.ts +5 -0
  25. package/dist/runtime/agent-runtime-controller.d.ts.map +1 -1
  26. package/dist/runtime/agent-runtime-controller.js +15 -3
  27. package/dist/runtime/agent-runtime-controller.js.map +1 -1
  28. package/dist/runtime/agent-status.d.ts +5 -0
  29. package/dist/runtime/agent-status.d.ts.map +1 -1
  30. package/dist/runtime/agent-status.js +28 -2
  31. package/dist/runtime/agent-status.js.map +1 -1
  32. package/dist/runtime/chat-controller.d.ts +4 -0
  33. package/dist/runtime/chat-controller.d.ts.map +1 -1
  34. package/dist/runtime/chat-controller.js +199 -67
  35. package/dist/runtime/chat-controller.js.map +1 -1
  36. package/dist/runtime/turn-controller.d.ts +4 -0
  37. package/dist/runtime/turn-controller.d.ts.map +1 -1
  38. package/dist/runtime/turn-controller.js +8 -0
  39. package/dist/runtime/turn-controller.js.map +1 -1
  40. package/dist/runtime/ui-events.d.ts +7 -0
  41. package/dist/runtime/ui-events.d.ts.map +1 -1
  42. package/dist/runtime/ui-events.js.map +1 -1
  43. package/dist/runtime/ui-view-model.d.ts +287 -0
  44. package/dist/runtime/ui-view-model.d.ts.map +1 -0
  45. package/dist/runtime/ui-view-model.js +642 -0
  46. package/dist/runtime/ui-view-model.js.map +1 -0
  47. package/dist/services/file-glob.d.ts +6 -2
  48. package/dist/services/file-glob.d.ts.map +1 -1
  49. package/dist/services/file-glob.js +10 -4
  50. package/dist/services/file-glob.js.map +1 -1
  51. package/dist/services/model-catalog.d.ts +14 -0
  52. package/dist/services/model-catalog.d.ts.map +1 -0
  53. package/dist/services/model-catalog.js +73 -0
  54. package/dist/services/model-catalog.js.map +1 -0
  55. package/dist/services/model-context.d.ts.map +1 -1
  56. package/dist/services/model-context.js +3 -0
  57. package/dist/services/model-context.js.map +1 -1
  58. package/dist/services/session-storage.d.ts +1 -0
  59. package/dist/services/session-storage.d.ts.map +1 -1
  60. package/dist/services/session-storage.js.map +1 -1
  61. package/dist/services/verification-profile.d.ts +15 -0
  62. package/dist/services/verification-profile.d.ts.map +1 -1
  63. package/dist/services/verification-profile.js +67 -0
  64. package/dist/services/verification-profile.js.map +1 -1
  65. package/dist/terminal-ui/completion.d.ts.map +1 -1
  66. package/dist/terminal-ui/completion.js +9 -6
  67. package/dist/terminal-ui/completion.js.map +1 -1
  68. package/dist/terminal-ui/launch.d.ts +15 -1
  69. package/dist/terminal-ui/launch.d.ts.map +1 -1
  70. package/dist/terminal-ui/launch.js +300 -56
  71. package/dist/terminal-ui/launch.js.map +1 -1
  72. package/dist/terminal-ui/raw-editor.d.ts +4 -0
  73. package/dist/terminal-ui/raw-editor.d.ts.map +1 -1
  74. package/dist/terminal-ui/raw-editor.js +29 -0
  75. package/dist/terminal-ui/raw-editor.js.map +1 -1
  76. package/dist/tui-ui/launch.d.ts.map +1 -1
  77. package/dist/tui-ui/launch.js +1 -0
  78. package/dist/tui-ui/launch.js.map +1 -1
  79. package/dist/tui-ui/layout.d.ts.map +1 -1
  80. package/dist/tui-ui/layout.js +21 -28
  81. package/dist/tui-ui/layout.js.map +1 -1
  82. package/dist/tui-ui/pickers.d.ts.map +1 -1
  83. package/dist/tui-ui/pickers.js +21 -62
  84. package/dist/tui-ui/pickers.js.map +1 -1
  85. package/dist/tui-ui/state.d.ts +2 -4
  86. package/dist/tui-ui/state.d.ts.map +1 -1
  87. package/dist/tui-ui/state.js +13 -11
  88. package/dist/tui-ui/state.js.map +1 -1
  89. package/package.json +1 -1
@@ -5,9 +5,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TerminalInputComposer = exports.TerminalEventSink = void 0;
7
7
  exports.resolveTerminalSessionPickerInput = resolveTerminalSessionPickerInput;
8
+ exports.formatTerminalErrorMessage = formatTerminalErrorMessage;
9
+ exports.inferTerminalErrorLayer = inferTerminalErrorLayer;
10
+ exports.formatTerminalStatusMessage = formatTerminalStatusMessage;
11
+ exports.formatTerminalSessionRestored = formatTerminalSessionRestored;
8
12
  exports.visibleLength = visibleLength;
9
13
  exports.terminalContentWidth = terminalContentWidth;
10
14
  exports.truncateTerminalText = truncateTerminalText;
15
+ exports.renderTerminalCapabilitySummary = renderTerminalCapabilitySummary;
16
+ exports.formatTerminalSessionPickerItem = formatTerminalSessionPickerItem;
17
+ exports.formatTerminalSessionPickerHeader = formatTerminalSessionPickerHeader;
18
+ exports.formatTerminalEditPreviewHeader = formatTerminalEditPreviewHeader;
19
+ exports.formatTerminalEditPreviewCandidate = formatTerminalEditPreviewCandidate;
20
+ exports.renderTerminalBanner = renderTerminalBanner;
21
+ exports.formatTerminalPermissionPrompt = formatTerminalPermissionPrompt;
11
22
  exports.parseEditInput = parseEditInput;
12
23
  exports.renderTerminalShortcuts = renderTerminalShortcuts;
13
24
  exports.normalizeTerminalAnswer = normalizeTerminalAnswer;
@@ -17,7 +28,9 @@ const string_width_1 = __importDefault(require("string-width"));
17
28
  const agent_runtime_controller_1 = require("../runtime/agent-runtime-controller");
18
29
  const agent_runtime_protocol_1 = require("../runtime/agent-runtime-protocol");
19
30
  const ui_events_1 = require("../runtime/ui-events");
31
+ const ui_view_model_1 = require("../runtime/ui-view-model");
20
32
  const format_1 = require("../services/format");
33
+ const redaction_1 = require("../services/redaction");
21
34
  const completion_1 = require("./completion");
22
35
  const editor_1 = require("./editor");
23
36
  const raw_editor_1 = require("./raw-editor");
@@ -29,9 +42,6 @@ const BORDER = chalk_1.default.hex('#38556A');
29
42
  function stripTrailingNewlines(text) {
30
43
  return text.replace(/\n+$/g, '');
31
44
  }
32
- function sessionTitle(session) {
33
- return session.name || session.taskSummary || '(untitled)';
34
- }
35
45
  function normalizePickerText(value) {
36
46
  return value.trim().toLowerCase();
37
47
  }
@@ -62,6 +72,12 @@ function resolveTerminalSessionPickerInput(input, request) {
62
72
  return { type: 'cancelled' };
63
73
  if (trimmed.startsWith('/'))
64
74
  return { type: 'slash', input: trimmed };
75
+ if (trimmed === '--last') {
76
+ const latest = request.sessions[0];
77
+ if (latest)
78
+ return { type: 'selected', sessionId: latest.id };
79
+ return { type: 'error', message: 'No recent session to resume.' };
80
+ }
65
81
  const explicitIndex = trimmed.match(/^#(\d+)$/);
66
82
  if (explicitIndex) {
67
83
  const index = Number(explicitIndex[1]) - 1;
@@ -84,7 +100,7 @@ function resolveTerminalSessionPickerInput(input, request) {
84
100
  if (matches.length > 1) {
85
101
  const preview = matches
86
102
  .slice(0, 3)
87
- .map(session => `${session.id.slice(0, 8)} ${sessionTitle(session)}`)
103
+ .map(session => `${session.id.slice(0, 8)} ${(0, ui_view_model_1.sessionPickerTitle)(session)}`)
88
104
  .join(', ');
89
105
  const suffix = matches.length > 3 ? `, +${matches.length - 3} more` : '';
90
106
  return {
@@ -110,7 +126,7 @@ function formatTranscriptEntry(entry) {
110
126
  case 'tool':
111
127
  return TOOL(content);
112
128
  case 'error':
113
- return ERROR(content);
129
+ return ERROR(formatTerminalErrorMessage(content, entry.errorLayer));
114
130
  case 'status':
115
131
  return DIM(content);
116
132
  case 'command':
@@ -121,9 +137,87 @@ function formatTranscriptEntry(entry) {
121
137
  return content;
122
138
  }
123
139
  }
140
+ const TERMINAL_ERROR_LAYERS = [
141
+ 'renderer',
142
+ 'runtime',
143
+ 'provider',
144
+ 'tool',
145
+ 'session',
146
+ 'memory',
147
+ 'MCP',
148
+ 'skills',
149
+ ];
150
+ function formatTerminalErrorMessage(message, explicitLayer) {
151
+ const trimmed = message.trim();
152
+ if (!trimmed)
153
+ return trimmed;
154
+ if (hasErrorLayerPrefix(trimmed))
155
+ return trimmed;
156
+ if (explicitLayer) {
157
+ return `[${explicitLayer.toUpperCase()}] ${trimmed}`;
158
+ }
159
+ return `[${inferTerminalErrorLayer(trimmed)}] ${trimmed}`;
160
+ }
161
+ function hasErrorLayerPrefix(message) {
162
+ const lower = message.toLowerCase();
163
+ return TERMINAL_ERROR_LAYERS.some(layer => {
164
+ const label = layer.toLowerCase();
165
+ return lower.startsWith(`[${label}] `)
166
+ || lower.startsWith(`error [${label}]:`)
167
+ || lower.startsWith(`error: [${label}] `);
168
+ });
169
+ }
170
+ function inferTerminalErrorLayer(message) {
171
+ const lower = message.toLowerCase();
172
+ if (/\bmcp\b/u.test(lower))
173
+ return 'MCP';
174
+ if (/\b(skill|skills)\b/u.test(lower))
175
+ return 'skills';
176
+ if (/\b(memory|vector store|recall|forget)\b/u.test(lower))
177
+ return 'memory';
178
+ if (/\b(session|resume|compact)\b/u.test(lower))
179
+ return 'session';
180
+ if (/\b(renderer|terminal|tty|prompt|resize|input editor|scrollback)\b/u.test(lower))
181
+ return 'renderer';
182
+ if (/\btool\b/u.test(lower)
183
+ || /\b(exec_command|read_file|write_file|edit_file|grep|glob|list_files)\b/u.test(lower)
184
+ || /command exited with code/u.test(lower)
185
+ || /path is a directory|not a file|enoent|eacces/u.test(lower)) {
186
+ return 'tool';
187
+ }
188
+ if (/\b(provider|model|llm|api|quota|rate limit|rate_limit|provider_busy)\b/u.test(lower)
189
+ || /\b(openai|anthropic|dashscope|bailian|xunfei|glm|qwen)\b/u.test(lower)
190
+ || /\bstatus code\b/u.test(lower)
191
+ || /notEnoughCvError|engineInternalError/i.test(message)) {
192
+ return 'provider';
193
+ }
194
+ return 'runtime';
195
+ }
124
196
  function shouldShowStatus(message) {
125
197
  return Boolean(message.trim());
126
198
  }
199
+ function formatTerminalStatusMessage(message, width = terminalContentWidth(120)) {
200
+ return truncateTerminalText(message.replace(/\s+/g, ' ').trim(), Math.max(1, width));
201
+ }
202
+ function formatTerminalSessionRestored(event) {
203
+ const width = terminalContentWidth(120);
204
+ const view = (0, ui_view_model_1.createSessionRestoredView)(event);
205
+ const fitLine = (prefix, value) => {
206
+ const prefixWidth = visibleLength(prefix);
207
+ if (prefixWidth >= width) {
208
+ return truncateTerminalText(prefix, width);
209
+ }
210
+ return `${prefix}${truncateTerminalText(value, width - prefixWidth)}`;
211
+ };
212
+ const lines = [
213
+ truncateTerminalText(view.headline, width),
214
+ fitLine(` Model: ${view.model} · Project: `, view.projectPath),
215
+ ];
216
+ if (view.summary) {
217
+ lines.push(fitLine(' Summary: ', (0, redaction_1.redactTraceText)(view.summary)));
218
+ }
219
+ return lines.join('\n');
220
+ }
127
221
  class DirectTerminalWriter {
128
222
  write(text) {
129
223
  process.stdout.write(text);
@@ -138,6 +232,10 @@ function terminalContentWidth(fallback = 88) {
138
232
  if (typeof columns === 'number' && columns > 0) {
139
233
  return Math.max(1, Math.min(columns, 200));
140
234
  }
235
+ const envColumns = Number(process.env.COLUMNS);
236
+ if (Number.isFinite(envColumns) && envColumns > 0) {
237
+ return Math.max(1, Math.min(envColumns, 200));
238
+ }
141
239
  return Math.max(60, Math.min(fallback, 200));
142
240
  }
143
241
  function truncateTerminalText(text, maxWidth) {
@@ -163,6 +261,80 @@ function bannerRow(content, width) {
163
261
  const padding = ' '.repeat(Math.max(0, innerWidth - visibleLength(safeContent)));
164
262
  return `${BORDER('│')}${safeContent}${padding}${BORDER('│')}`;
165
263
  }
264
+ function renderTerminalCapabilitySummary(runtime) {
265
+ const snapshot = runtime.store.getSnapshot();
266
+ return (0, ui_view_model_1.createRuntimeCapabilitySummary)({
267
+ projectInstructionsContent: snapshot.projectInstructionsContent,
268
+ skillsContent: snapshot.skillsContent,
269
+ memoryContent: snapshot.memoryContent,
270
+ tools: snapshot.tools,
271
+ webSearchConfigured: Boolean(runtime.config.webSearch),
272
+ }).text;
273
+ }
274
+ function formatTerminalSessionPickerItem(item, width = terminalContentWidth(120)) {
275
+ const safeWidth = Math.max(1, width);
276
+ const prefixPlain = `${String(item.globalIndex).padStart(2, ' ')}. ${item.shortId} `;
277
+ const prefix = `${String(item.globalIndex).padStart(2, ' ')}. ${ACCENT(item.shortId)} `;
278
+ const size = (0, format_1.formatBytes)(item.historySizeBytes);
279
+ const project = item.showProject ? ` ${item.projectPath}` : '';
280
+ const metadataCandidates = [
281
+ `${item.messageCount} msgs ${size} ${item.model}${project}`,
282
+ `${item.messageCount} msgs ${size} ${item.model}`,
283
+ `${item.messageCount} msgs ${size}`,
284
+ `${item.messageCount} msgs`,
285
+ '',
286
+ ];
287
+ for (const metadata of metadataCandidates) {
288
+ const suffixPlain = metadata ? ` ${metadata}` : '';
289
+ const titleBudget = safeWidth - visibleLength(prefixPlain) - visibleLength(suffixPlain);
290
+ if (titleBudget < 8)
291
+ continue;
292
+ const title = truncateTerminalText(item.title, titleBudget);
293
+ const row = `${prefix}${title}${metadata ? ` ${DIM(metadata)}` : ''}`;
294
+ if (visibleLength(row) <= safeWidth)
295
+ return row;
296
+ }
297
+ return truncateTerminalText(`${String(item.globalIndex).padStart(2, ' ')}. ${item.shortId} ${item.title}`, safeWidth);
298
+ }
299
+ function formatTerminalSessionPickerHeader(title, page, pageCount, width = terminalContentWidth(120)) {
300
+ const safeWidth = Math.max(1, width);
301
+ const pageLabelPlain = ` page ${page}/${pageCount}`;
302
+ const titleBudget = safeWidth - visibleLength(pageLabelPlain);
303
+ if (titleBudget < 4) {
304
+ return truncateTerminalText(`${title}${pageLabelPlain}`, safeWidth);
305
+ }
306
+ return `${ACCENT(truncateTerminalText(title, titleBudget))}${DIM(pageLabelPlain)}`;
307
+ }
308
+ function formatSessionPickerInstruction(width) {
309
+ const text = width < 72
310
+ ? 'Select number/id, n/p page, empty cancels.'
311
+ : 'Type number/#number, session id prefix, unique title text, or /resume --last. Empty input cancels.';
312
+ return truncateTerminalText(text, Math.max(1, width));
313
+ }
314
+ function editPreviewKindLabel(request) {
315
+ return request.kind === 'fuzzy' ? `fuzzy (${request.strategy ?? 'match'})` : 'exact';
316
+ }
317
+ function formatTerminalEditPreviewHeader(request, width = terminalContentWidth(120)) {
318
+ return ACCENT(truncateTerminalText(`Edit Preview: ${request.path} (${editPreviewKindLabel(request)})`, Math.max(1, width)));
319
+ }
320
+ function formatTerminalEditPreviewCandidate(candidate, newString, width = terminalContentWidth(120)) {
321
+ const safeWidth = Math.max(1, width);
322
+ const prefix = ` line ${String(candidate.line).padStart(3, ' ')} `;
323
+ const fixedWidth = visibleLength(`${prefix}"" → ""`);
324
+ const contentBudget = safeWidth - fixedWidth;
325
+ if (contentBudget < 8) {
326
+ return truncateTerminalText(`${prefix}"${candidate.match}" → "${newString}"`, safeWidth);
327
+ }
328
+ const matchBudget = Math.max(1, Math.floor(contentBudget * 0.6));
329
+ const replacementBudget = Math.max(1, contentBudget - matchBudget);
330
+ const row = [
331
+ prefix,
332
+ `"${truncateTerminalText(candidate.match, matchBudget)}"`,
333
+ ' → ',
334
+ `"${truncateTerminalText(newString, replacementBudget)}"`,
335
+ ].join('');
336
+ return visibleLength(row) <= safeWidth ? row : truncateTerminalText(row, safeWidth);
337
+ }
166
338
  class TerminalEventSink {
167
339
  constructor(runtime, writer = new DirectTerminalWriter()) {
168
340
  this.runtime = runtime;
@@ -235,7 +407,7 @@ class TerminalEventSink {
235
407
  if (message === this.lastStatusMessage)
236
408
  return;
237
409
  this.lastStatusMessage = message;
238
- this.writer.write(`${DIM(message)}\n`);
410
+ this.writer.write(`${DIM(formatTerminalStatusMessage(message))}\n`);
239
411
  }
240
412
  showSessionPicker(request) {
241
413
  this.pendingPicker = request;
@@ -244,18 +416,23 @@ class TerminalEventSink {
244
416
  }
245
417
  showEditPreview(request) {
246
418
  this.pendingEditPreview = request;
247
- const kindLabel = request.kind === 'fuzzy' ? `fuzzy (${request.strategy ?? 'match'})` : 'exact';
248
- this.writer.write(`\n${ACCENT(`Edit Preview: ${request.path} (${kindLabel})`)}\n`);
419
+ const rowWidth = terminalContentWidth(120);
420
+ this.writer.write(`\n${formatTerminalEditPreviewHeader(request, rowWidth)}\n`);
249
421
  this.writer.write(`${BORDER('─'.repeat(terminalContentWidth(80)))}\n`);
250
422
  request.candidates.slice(0, 10).forEach(c => {
251
- const matchPreview = c.match.length > 60 ? c.match.slice(0, 57) + '...' : c.match;
252
- const newPreview = request.newString.length > 40 ? request.newString.slice(0, 37) + '...' : request.newString;
253
- this.writer.write(` line ${String(c.line).padStart(3, ' ')} "${matchPreview}" → "${newPreview}"\n`);
423
+ this.writer.write(`${formatTerminalEditPreviewCandidate(c, request.newString, rowWidth)}\n`);
254
424
  });
255
425
  if (request.candidates.length > 10) {
256
- this.writer.write(`${DIM(` ... ${request.candidates.length - 10} more candidates`)}\n`);
426
+ this.writer.write(`${DIM(truncateTerminalText(` ... ${request.candidates.length - 10} more candidates`, rowWidth))}\n`);
257
427
  }
258
- this.writer.write(`${DIM('Press Enter to dismiss.')}\n`);
428
+ this.writer.write(`${DIM(truncateTerminalText('Press Enter to dismiss.', rowWidth))}\n`);
429
+ }
430
+ sessionRestored(event) {
431
+ this.append({
432
+ role: 'status',
433
+ title: 'resume',
434
+ content: formatTerminalSessionRestored(event),
435
+ });
259
436
  }
260
437
  setProcessing(_processing) {
261
438
  // The stable terminal UI is append-only, so there is no live spinner state.
@@ -307,38 +484,28 @@ class TerminalEventSink {
307
484
  const request = this.pendingPicker;
308
485
  if (!request)
309
486
  return;
310
- const total = request.sessions.length;
311
- const visibleLimit = Math.max(1, Math.min(request.maxVisibleItems ?? 10, Math.max(1, total)));
312
- const maxOffset = Math.max(0, Math.ceil(total / visibleLimit) - 1) * visibleLimit;
313
- this.pickerOffset = Math.max(0, Math.min(this.pickerOffset, maxOffset));
314
- const visibleSessions = request.sessions.slice(this.pickerOffset, this.pickerOffset + visibleLimit);
315
- const page = total === 0 ? 1 : Math.floor(this.pickerOffset / visibleLimit) + 1;
316
- const pages = total === 0 ? 1 : Math.ceil(total / visibleLimit);
317
- this.writer.write(`\n${ACCENT(request.title)} ${DIM(`page ${page}/${pages}`)}\n`);
487
+ const state = (0, ui_view_model_1.createSessionPickerState)(request, this.pickerOffset);
488
+ this.pickerOffset = state.visibleStart;
489
+ const rowWidth = terminalContentWidth(120);
490
+ this.writer.write(`\n${formatTerminalSessionPickerHeader(state.title, state.page, state.pageCount, rowWidth)}\n`);
318
491
  this.writer.write(`${BORDER('─'.repeat(terminalContentWidth(80)))}\n`);
319
- if (visibleSessions.length === 0) {
492
+ if (state.visibleItems.length === 0) {
320
493
  this.writer.write(`${DIM('No saved sessions found.')}\n`);
321
494
  }
322
- visibleSessions.forEach((session, index) => {
323
- const globalIndex = this.pickerOffset + index + 1;
324
- const id = session.id.slice(0, 8);
325
- const size = (0, format_1.formatBytes)(session.historySizeBytes ?? 0);
326
- const messages = session.messageCount ?? 0;
327
- const project = request.showProject ? ` ${session.projectPath}` : '';
328
- this.writer.write(`${String(globalIndex).padStart(2, ' ')}. ${ACCENT(id)} ${sessionTitle(session)} ${DIM(`${messages} msgs ${size} ${session.model}${project}`)}\n`);
495
+ state.visibleItems.forEach(item => {
496
+ this.writer.write(`${formatTerminalSessionPickerItem(item, rowWidth)}\n`);
329
497
  });
330
- if (total > visibleLimit) {
331
- this.writer.write(`${DIM(`Showing ${this.pickerOffset + 1}-${this.pickerOffset + visibleSessions.length} of ${total}. Type n/next or p/prev to page.`)}\n`);
498
+ if (state.totalItems > state.visibleLimit) {
499
+ this.writer.write(`${DIM(truncateTerminalText(`Showing ${state.visibleStart + 1}-${state.visibleStart + state.visibleItems.length} of ${state.totalItems}. Type n/next or p/prev to page.`, rowWidth))}\n`);
332
500
  }
333
- this.writer.write(`${DIM('Type number/#number, session id prefix, unique title text, or /resume --last. Empty input cancels.')}\n`);
501
+ this.writer.write(`${DIM(formatSessionPickerInstruction(rowWidth))}\n`);
334
502
  }
335
503
  moveSessionPickerPage(delta) {
336
504
  const request = this.pendingPicker;
337
505
  if (!request)
338
506
  return;
339
- const visibleLimit = Math.max(1, Math.min(request.maxVisibleItems ?? 10, Math.max(1, request.sessions.length)));
340
- const maxOffset = Math.max(0, Math.ceil(request.sessions.length / visibleLimit) - 1) * visibleLimit;
341
- const nextOffset = Math.max(0, Math.min(this.pickerOffset + delta * visibleLimit, maxOffset));
507
+ const state = (0, ui_view_model_1.createSessionPickerState)(request, this.pickerOffset);
508
+ const nextOffset = (0, ui_view_model_1.movePickerPageOffset)(state, delta);
342
509
  if (nextOffset === this.pickerOffset) {
343
510
  this.writer.write(`${DIM(delta > 0 ? 'Already at last session page.' : 'Already at first session page.')}\n`);
344
511
  return;
@@ -385,29 +552,83 @@ class TerminalEventSink {
385
552
  }
386
553
  }
387
554
  exports.TerminalEventSink = TerminalEventSink;
388
- function printBanner(runtime) {
389
- const width = terminalContentWidth(88);
390
- const line = '─'.repeat(Math.max(20, width - 2));
555
+ function renderTerminalBanner(runtime) {
556
+ const width = Math.max(2, terminalContentWidth(88));
557
+ const line = '─'.repeat(Math.max(0, width - 2));
391
558
  const firstLine = ` ${ACCENT.bold('OPENHORSE')} ${DIM(`v${runtime.version}`)} ${DIM('stable terminal UI')}`;
392
559
  const projectPrefix = ` ${DIM('Model')} ${ACCENT(runtime.config.model)} ${DIM('Project')} `;
393
560
  const project = truncateTerminalText(runtime.cwd, Math.max(10, width - 2 - visibleLength(projectPrefix)));
394
- process.stdout.write('\n');
395
- process.stdout.write(`${BORDER(`╭${line}╮`)}\n`);
396
- process.stdout.write(`${bannerRow(firstLine, width)}\n`);
397
- process.stdout.write(`${bannerRow(`${projectPrefix}${project}`, width)}\n`);
398
- process.stdout.write(`${BORDER(`╰${line}╯`)}\n\n`);
561
+ const session = runtime.getSession()?.id.slice(0, 8) ?? 'new';
562
+ const renderer = runtime.config.ui?.renderer ?? 'terminal';
563
+ const rendererLine = ` ${DIM('Session')} ${ACCENT(session)} ${DIM('Renderer')} ${ACCENT(renderer)}`;
564
+ const capabilityPrefix = ` ${DIM('Capabilities')} `;
565
+ const capabilityText = renderTerminalCapabilitySummary(runtime);
566
+ const capabilities = truncateTerminalText(capabilityText, Math.max(10, width - 2 - visibleLength(capabilityPrefix)));
567
+ return [
568
+ '',
569
+ BORDER(`╭${line}╮`),
570
+ bannerRow(firstLine, width),
571
+ bannerRow(`${projectPrefix}${project}`, width),
572
+ bannerRow(rendererLine, width),
573
+ bannerRow(`${capabilityPrefix}${capabilities}`, width),
574
+ BORDER(`╰${line}╯`),
575
+ '',
576
+ '',
577
+ ].join('\n');
578
+ }
579
+ function printBanner(runtime) {
580
+ process.stdout.write(renderTerminalBanner(runtime));
399
581
  }
400
582
  function promptText(runtime) {
401
583
  const session = runtime.getSession()?.id.slice(0, 8) ?? 'new';
402
584
  return `${DIM(`[${session}]`)} ${ACCENT('›')} `;
403
585
  }
404
- function summarizeToolArgs(args) {
405
- const entries = Object.entries(args)
406
- .slice(0, 4)
407
- .map(([key, value]) => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
408
- const summary = entries.join(' ');
409
- const maxLength = Math.max(120, terminalContentWidth(120) - 24);
410
- return summary.length > maxLength ? `${summary.slice(0, maxLength - 3)}...` : summary;
586
+ function compactPermissionValue(value, maxWidth) {
587
+ const singleLine = (0, redaction_1.redactTraceText)(value).replace(/\s+/g, ' ').trim();
588
+ return truncateTerminalText(singleLine, Math.max(8, maxWidth));
589
+ }
590
+ function formatTerminalPermissionScope(state) {
591
+ const width = terminalContentWidth(120);
592
+ const maxScopeWidth = Math.max(24, Math.min(72, Math.floor(width * 0.35)));
593
+ return compactPermissionValue((0, ui_view_model_1.permissionScopeDisplayValue)(state.scope), maxScopeWidth);
594
+ }
595
+ function formatTerminalPermissionPrompt(request, cwd) {
596
+ const state = (0, ui_view_model_1.createPermissionPromptState)(request, cwd);
597
+ const width = terminalContentWidth(120);
598
+ const budget = Math.max(1, width);
599
+ const base = `${ACCENT('?')} Allow tool ${ACCENT(state.toolName)}?`;
600
+ const options = DIM(`[${state.options.approve} ${state.options.deny}]`);
601
+ const scope = DIM(formatTerminalPermissionScope(state));
602
+ const cwdLabel = DIM(`cwd=${compactPermissionValue(state.cwd, Math.max(12, Math.min(48, Math.floor(width * 0.22))))}`);
603
+ const risk = DIM(`risk=${compactPermissionValue((0, ui_view_model_1.permissionRiskDisplayValue)(state.risk), Math.max(12, Math.min(48, Math.floor(width * 0.24))))}`);
604
+ const parts = [base, scope, cwdLabel, risk, options];
605
+ while (parts.length > 3 && visibleLength(`${parts.join(' ')} `) > budget) {
606
+ const riskIndex = parts.indexOf(risk);
607
+ if (riskIndex >= 0) {
608
+ parts.splice(riskIndex, 1);
609
+ continue;
610
+ }
611
+ const cwdIndex = parts.indexOf(cwdLabel);
612
+ if (cwdIndex >= 0) {
613
+ parts.splice(cwdIndex, 1);
614
+ continue;
615
+ }
616
+ break;
617
+ }
618
+ let prompt = `${parts.join(' ')} `;
619
+ if (visibleLength(prompt) <= budget)
620
+ return prompt;
621
+ const fixed = `${base} ${options} `;
622
+ const scopeBudget = Math.max(8, budget - visibleLength(fixed) - 1);
623
+ prompt = `${base} ${DIM(truncateTerminalText(formatTerminalPermissionScope(state), scopeBudget))} ${options} `;
624
+ if (visibleLength(prompt) <= budget)
625
+ return prompt;
626
+ const optionWidth = visibleLength(options);
627
+ if (budget <= optionWidth + 1) {
628
+ return truncateTerminalText(`${stripTrailingNewlines(base)} ${stripTrailingNewlines(options)} `, budget);
629
+ }
630
+ const baseBudget = Math.max(1, budget - optionWidth - 2);
631
+ return `${truncateTerminalText(stripTrailingNewlines(base), baseBudget)} ${options} `;
411
632
  }
412
633
  function isExitInput(input) {
413
634
  return ['/exit', '/quit', '/q'].includes(input.trim());
@@ -422,6 +643,29 @@ function parseEditInput(input) {
422
643
  return { isEdit: false, initialContent: '' };
423
644
  }
424
645
  function renderTerminalShortcuts() {
646
+ const width = terminalContentWidth(88);
647
+ if (width < 44) {
648
+ const rows = [
649
+ `${ACCENT('Shortcuts')}`,
650
+ `${BORDER('─'.repeat(Math.min(width, 32)))}`,
651
+ `${DIM('Enter')} send ${DIM('Tab')} complete`,
652
+ `${DIM('Ctrl+C')} interrupt; twice exits`,
653
+ `${DIM('/paste')} ${DIM('/edit')} ${DIM('/resume')}`,
654
+ `${DIM('/last-tool')} ${DIM('/trace')}`,
655
+ ];
656
+ return `\n${rows.join('\n')}\n`;
657
+ }
658
+ if (width < 72) {
659
+ const rows = [
660
+ `${ACCENT('Shortcuts')}`,
661
+ `${BORDER('─'.repeat(Math.min(width, 40)))}`,
662
+ `${DIM('Enter')} send ${DIM('Tab')} complete ${DIM('↑/↓')} history`,
663
+ `${DIM('Ctrl+U/W')} edit ${DIM('Ctrl+C')} interrupt; twice exits`,
664
+ `${DIM('/paste')} multiline ${DIM('/edit')} editor ${DIM('/resume')} sessions`,
665
+ `${DIM('/last-tool')} tool detail ${DIM('/trace')} timeline`,
666
+ ];
667
+ return `\n${rows.join('\n')}\n`;
668
+ }
425
669
  const rows = [
426
670
  `${ACCENT('Terminal shortcuts')}`,
427
671
  `${BORDER('─'.repeat(48))}`,
@@ -434,6 +678,8 @@ function renderTerminalShortcuts() {
434
678
  `${DIM('/paste')} collect multiline input until /end`,
435
679
  `${DIM('/edit')} open $VISUAL or $EDITOR for long input`,
436
680
  `${DIM('/resume')} pick a session; use n/p to page results`,
681
+ `${DIM('/last-tool')} inspect the latest tool call/result`,
682
+ `${DIM('/trace')} show the ordered event timeline`,
437
683
  ];
438
684
  return `\n${rows.join('\n')}\n`;
439
685
  }
@@ -560,10 +806,7 @@ async function launchTerminalUI(runtime) {
560
806
  return undefined;
561
807
  }
562
808
  confirmingTool = true;
563
- const detail = summarizeToolArgs(event.request.args);
564
- const reason = event.request.reason ? ` ${DIM(event.request.reason)}` : '';
565
- const suffix = detail ? ` ${DIM(detail)}` : '';
566
- void editor.ask(`${ACCENT('?')} Allow tool ${ACCENT(event.request.name)}?${suffix}${reason} ${DIM('[y/N]')} `, event.request.abortSignal).then(answer => {
809
+ void editor.ask(formatTerminalPermissionPrompt(event.request, runtime.cwd), event.request.abortSignal).then(answer => {
567
810
  agentController.handle({
568
811
  type: 'permission_decision',
569
812
  requestId: event.request.id,
@@ -588,6 +831,7 @@ async function launchTerminalUI(runtime) {
588
831
  runtime,
589
832
  eventSink,
590
833
  uiCapabilities: (0, ui_events_1.resolveUiRendererCapabilities)(undefined, 'terminal'),
834
+ uiRenderer: 'terminal',
591
835
  useRuntimeToolPermissions: true,
592
836
  echoSubmittedInput: false,
593
837
  beforeTurn: () => writer.write('\n'),
@@ -597,7 +841,7 @@ async function launchTerminalUI(runtime) {
597
841
  },
598
842
  onTurnError: error => {
599
843
  const message = error instanceof Error ? error.message : String(error);
600
- events.append({ role: 'error', content: `Error: ${message}` });
844
+ events.append({ role: 'error', content: message });
601
845
  },
602
846
  });
603
847
  editor = new raw_editor_1.RawTerminalEditor({
@@ -676,7 +920,7 @@ async function launchTerminalUI(runtime) {
676
920
  if (!stopping)
677
921
  editor.start();
678
922
  if (result.error) {
679
- events.append({ role: 'error', content: `Editor failed: ${result.error}` });
923
+ events.append({ role: 'error', content: `Editor failed: ${result.error}`, errorLayer: 'renderer' });
680
924
  prompt();
681
925
  return;
682
926
  }