@esso0428/pi-subagents 0.17.5 → 0.17.7

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 (263) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +1 -1
  4. package/dist/abortable.d.ts +13 -0
  5. package/dist/abortable.d.ts.map +1 -0
  6. package/dist/abortable.js +43 -0
  7. package/dist/abortable.js.map +1 -0
  8. package/dist/agent-color.d.ts +36 -0
  9. package/dist/agent-color.d.ts.map +1 -0
  10. package/dist/agent-color.js +124 -0
  11. package/dist/agent-color.js.map +1 -0
  12. package/dist/agent-file-toggle.d.ts +126 -0
  13. package/dist/agent-file-toggle.d.ts.map +1 -0
  14. package/dist/agent-file-toggle.js +259 -0
  15. package/dist/agent-file-toggle.js.map +1 -0
  16. package/dist/agent-history.d.ts +4 -0
  17. package/dist/agent-history.d.ts.map +1 -1
  18. package/dist/agent-history.js +47 -1
  19. package/dist/agent-history.js.map +1 -1
  20. package/dist/agent-manager.d.ts +370 -56
  21. package/dist/agent-manager.d.ts.map +1 -1
  22. package/dist/agent-manager.js +1123 -409
  23. package/dist/agent-manager.js.map +1 -1
  24. package/dist/agent-runner.d.ts +100 -10
  25. package/dist/agent-runner.d.ts.map +1 -1
  26. package/dist/agent-runner.js +166 -21
  27. package/dist/agent-runner.js.map +1 -1
  28. package/dist/agent-types.d.ts +57 -5
  29. package/dist/agent-types.d.ts.map +1 -1
  30. package/dist/agent-types.js +164 -32
  31. package/dist/agent-types.js.map +1 -1
  32. package/dist/child-context.d.ts +3 -0
  33. package/dist/child-context.d.ts.map +1 -0
  34. package/dist/child-context.js +13 -0
  35. package/dist/child-context.js.map +1 -0
  36. package/dist/cross-extension-rpc.d.ts +23 -3
  37. package/dist/cross-extension-rpc.d.ts.map +1 -1
  38. package/dist/cross-extension-rpc.js +79 -17
  39. package/dist/cross-extension-rpc.js.map +1 -1
  40. package/dist/custom-agents.d.ts +38 -1
  41. package/dist/custom-agents.d.ts.map +1 -1
  42. package/dist/custom-agents.js +164 -12
  43. package/dist/custom-agents.js.map +1 -1
  44. package/dist/index.d.ts +34 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1908 -495
  47. package/dist/index.js.map +1 -1
  48. package/dist/invocation-config.d.ts +87 -2
  49. package/dist/invocation-config.d.ts.map +1 -1
  50. package/dist/invocation-config.js +71 -3
  51. package/dist/invocation-config.js.map +1 -1
  52. package/dist/mention-clone.d.ts +88 -0
  53. package/dist/mention-clone.d.ts.map +1 -0
  54. package/dist/mention-clone.js +154 -0
  55. package/dist/mention-clone.js.map +1 -0
  56. package/dist/mention.d.ts +82 -0
  57. package/dist/mention.d.ts.map +1 -0
  58. package/dist/mention.js +132 -0
  59. package/dist/mention.js.map +1 -0
  60. package/dist/model-resolver.d.ts +17 -0
  61. package/dist/model-resolver.d.ts.map +1 -1
  62. package/dist/model-resolver.js +15 -0
  63. package/dist/model-resolver.js.map +1 -1
  64. package/dist/model-scope.d.ts +50 -0
  65. package/dist/model-scope.d.ts.map +1 -0
  66. package/dist/model-scope.js +49 -0
  67. package/dist/model-scope.js.map +1 -0
  68. package/dist/nested-tools.d.ts +57 -0
  69. package/dist/nested-tools.d.ts.map +1 -0
  70. package/dist/nested-tools.js +301 -0
  71. package/dist/nested-tools.js.map +1 -0
  72. package/dist/output-file.d.ts +22 -3
  73. package/dist/output-file.d.ts.map +1 -1
  74. package/dist/output-file.js +58 -7
  75. package/dist/output-file.js.map +1 -1
  76. package/dist/prompts.d.ts +23 -0
  77. package/dist/prompts.d.ts.map +1 -1
  78. package/dist/prompts.js +20 -2
  79. package/dist/prompts.js.map +1 -1
  80. package/dist/schedule.d.ts.map +1 -1
  81. package/dist/schedule.js +36 -15
  82. package/dist/schedule.js.map +1 -1
  83. package/dist/settings.d.ts +228 -2
  84. package/dist/settings.d.ts.map +1 -1
  85. package/dist/settings.js +94 -0
  86. package/dist/settings.js.map +1 -1
  87. package/dist/status-note.d.ts +49 -1
  88. package/dist/status-note.d.ts.map +1 -1
  89. package/dist/status-note.js +62 -1
  90. package/dist/status-note.js.map +1 -1
  91. package/dist/structured-output.d.ts +62 -0
  92. package/dist/structured-output.d.ts.map +1 -0
  93. package/dist/structured-output.js +113 -0
  94. package/dist/structured-output.js.map +1 -0
  95. package/dist/types.d.ts +176 -10
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/ui/agent-mention.d.ts +83 -0
  98. package/dist/ui/agent-mention.d.ts.map +1 -0
  99. package/dist/ui/agent-mention.js +188 -0
  100. package/dist/ui/agent-mention.js.map +1 -0
  101. package/dist/ui/agent-widget.d.ts +96 -75
  102. package/dist/ui/agent-widget.d.ts.map +1 -1
  103. package/dist/ui/agent-widget.js +397 -420
  104. package/dist/ui/agent-widget.js.map +1 -1
  105. package/dist/ui/conversation-blocks.d.ts.map +1 -1
  106. package/dist/ui/conversation-blocks.js +6 -0
  107. package/dist/ui/conversation-blocks.js.map +1 -1
  108. package/dist/ui/conversation-timeline.d.ts +10 -2
  109. package/dist/ui/conversation-timeline.d.ts.map +1 -1
  110. package/dist/ui/conversation-timeline.js +130 -23
  111. package/dist/ui/conversation-timeline.js.map +1 -1
  112. package/dist/ui/conversation-viewer.d.ts +20 -5
  113. package/dist/ui/conversation-viewer.d.ts.map +1 -1
  114. package/dist/ui/conversation-viewer.js +274 -73
  115. package/dist/ui/conversation-viewer.js.map +1 -1
  116. package/dist/ui/fleet-list.d.ts +198 -0
  117. package/dist/ui/fleet-list.d.ts.map +1 -0
  118. package/dist/ui/fleet-list.js +487 -0
  119. package/dist/ui/fleet-list.js.map +1 -0
  120. package/dist/ui/schedule-menu.d.ts.map +1 -1
  121. package/dist/ui/schedule-menu.js +6 -7
  122. package/dist/ui/schedule-menu.js.map +1 -1
  123. package/dist/ui/select-item.d.ts +28 -0
  124. package/dist/ui/select-item.d.ts.map +1 -0
  125. package/dist/ui/select-item.js +35 -0
  126. package/dist/ui/select-item.js.map +1 -0
  127. package/dist/ui/workflow-card.d.ts +176 -0
  128. package/dist/ui/workflow-card.d.ts.map +1 -0
  129. package/dist/ui/workflow-card.js +333 -0
  130. package/dist/ui/workflow-card.js.map +1 -0
  131. package/dist/ui/workflow-dialog.d.ts +306 -0
  132. package/dist/ui/workflow-dialog.d.ts.map +1 -0
  133. package/dist/ui/workflow-dialog.js +844 -0
  134. package/dist/ui/workflow-dialog.js.map +1 -0
  135. package/dist/ui/workflow-menu.d.ts +61 -0
  136. package/dist/ui/workflow-menu.d.ts.map +1 -0
  137. package/dist/ui/workflow-menu.js +148 -0
  138. package/dist/ui/workflow-menu.js.map +1 -0
  139. package/dist/usage.d.ts +86 -1
  140. package/dist/usage.d.ts.map +1 -1
  141. package/dist/usage.js +72 -1
  142. package/dist/usage.js.map +1 -1
  143. package/dist/workflow/collisions.d.ts +96 -0
  144. package/dist/workflow/collisions.d.ts.map +1 -0
  145. package/dist/workflow/collisions.js +89 -0
  146. package/dist/workflow/collisions.js.map +1 -0
  147. package/dist/workflow/entry.d.ts +33 -0
  148. package/dist/workflow/entry.d.ts.map +1 -0
  149. package/dist/workflow/entry.js +30 -0
  150. package/dist/workflow/entry.js.map +1 -0
  151. package/dist/workflow/host.d.ts +63 -0
  152. package/dist/workflow/host.d.ts.map +1 -0
  153. package/dist/workflow/host.js +363 -0
  154. package/dist/workflow/host.js.map +1 -0
  155. package/dist/workflow/journal.d.ts +98 -0
  156. package/dist/workflow/journal.d.ts.map +1 -0
  157. package/dist/workflow/journal.js +121 -0
  158. package/dist/workflow/journal.js.map +1 -0
  159. package/dist/workflow/json-schema.d.ts +52 -0
  160. package/dist/workflow/json-schema.d.ts.map +1 -0
  161. package/dist/workflow/json-schema.js +112 -0
  162. package/dist/workflow/json-schema.js.map +1 -0
  163. package/dist/workflow/meta.d.ts +68 -0
  164. package/dist/workflow/meta.d.ts.map +1 -0
  165. package/dist/workflow/meta.js +318 -0
  166. package/dist/workflow/meta.js.map +1 -0
  167. package/dist/workflow/progress.d.ts +225 -0
  168. package/dist/workflow/progress.d.ts.map +1 -0
  169. package/dist/workflow/progress.js +362 -0
  170. package/dist/workflow/progress.js.map +1 -0
  171. package/dist/workflow/runtime.d.ts +335 -0
  172. package/dist/workflow/runtime.d.ts.map +1 -0
  173. package/dist/workflow/runtime.js +831 -0
  174. package/dist/workflow/runtime.js.map +1 -0
  175. package/dist/workflow/saved.d.ts +91 -0
  176. package/dist/workflow/saved.d.ts.map +1 -0
  177. package/dist/workflow/saved.js +204 -0
  178. package/dist/workflow/saved.js.map +1 -0
  179. package/dist/workflow/task.d.ts +137 -0
  180. package/dist/workflow/task.d.ts.map +1 -0
  181. package/dist/workflow/task.js +208 -0
  182. package/dist/workflow/task.js.map +1 -0
  183. package/dist/workflow/tool-description.d.ts +39 -0
  184. package/dist/workflow/tool-description.d.ts.map +1 -0
  185. package/dist/workflow/tool-description.js +200 -0
  186. package/dist/workflow/tool-description.js.map +1 -0
  187. package/dist/workflow/worker-source.d.ts +48 -0
  188. package/dist/workflow/worker-source.d.ts.map +1 -0
  189. package/dist/workflow/worker-source.js +779 -0
  190. package/dist/workflow/worker-source.js.map +1 -0
  191. package/dist/worktree.d.ts +10 -3
  192. package/dist/worktree.d.ts.map +1 -1
  193. package/dist/worktree.js +58 -54
  194. package/dist/worktree.js.map +1 -1
  195. package/dist/xml.d.ts +11 -0
  196. package/dist/xml.d.ts.map +1 -0
  197. package/dist/xml.js +13 -0
  198. package/dist/xml.js.map +1 -0
  199. package/docs/rpc.md +183 -0
  200. package/docs/superpowers/plans/2026-09-30-conversation-viewer-scrollbar.md +216 -0
  201. package/docs/superpowers/plans/2026-09-30-upstream-event-workflow-partial-history.md +195 -0
  202. package/docs/superpowers/specs/2026-09-30-conversation-viewer-scrollbar-design.md +82 -0
  203. package/docs/superpowers/specs/2026-09-30-upstream-event-workflow-partial-history-design.md +49 -0
  204. package/docs/workflows.md +437 -0
  205. package/examples/agent-tool-description.md +7 -7
  206. package/examples/workflows/compose.js +51 -0
  207. package/examples/workflows/fan-out-audit.js +47 -0
  208. package/examples/workflows/gated-fix.js +60 -0
  209. package/examples/workflows/lib/count-child.js +27 -0
  210. package/examples/workflows/review-panel.js +63 -0
  211. package/examples/workflows/structured-findings.js +78 -0
  212. package/package.json +1 -1
  213. package/src/abortable.ts +43 -0
  214. package/src/agent-color.ts +161 -0
  215. package/src/agent-file-toggle.ts +269 -0
  216. package/src/agent-history.ts +54 -2
  217. package/src/agent-manager.ts +1263 -402
  218. package/src/agent-runner.ts +251 -27
  219. package/src/agent-types.ts +188 -32
  220. package/src/child-context.ts +15 -0
  221. package/src/cross-extension-rpc.ts +96 -20
  222. package/src/custom-agents.ts +170 -13
  223. package/src/index.ts +2024 -537
  224. package/src/invocation-config.ts +118 -3
  225. package/src/mention-clone.ts +196 -0
  226. package/src/mention.ts +141 -0
  227. package/src/model-resolver.ts +18 -0
  228. package/src/model-scope.ts +70 -0
  229. package/src/nested-tools.ts +424 -0
  230. package/src/output-file.ts +61 -6
  231. package/src/prompts.ts +45 -2
  232. package/src/schedule.ts +35 -14
  233. package/src/settings.ts +312 -2
  234. package/src/status-note.ts +66 -1
  235. package/src/structured-output.ts +130 -0
  236. package/src/types.ts +177 -10
  237. package/src/ui/agent-mention.ts +216 -0
  238. package/src/ui/agent-widget.ts +389 -441
  239. package/src/ui/conversation-blocks.ts +6 -0
  240. package/src/ui/conversation-timeline.ts +139 -25
  241. package/src/ui/conversation-viewer.ts +284 -69
  242. package/src/ui/fleet-list.ts +558 -0
  243. package/src/ui/schedule-menu.ts +9 -8
  244. package/src/ui/select-item.ts +45 -0
  245. package/src/ui/workflow-card.ts +470 -0
  246. package/src/ui/workflow-dialog.ts +1115 -0
  247. package/src/ui/workflow-menu.ts +193 -0
  248. package/src/usage.ts +109 -2
  249. package/src/workflow/collisions.ts +123 -0
  250. package/src/workflow/entry.ts +47 -0
  251. package/src/workflow/host.ts +403 -0
  252. package/src/workflow/journal.ts +164 -0
  253. package/src/workflow/json-schema.ts +128 -0
  254. package/src/workflow/meta.ts +325 -0
  255. package/src/workflow/progress.ts +550 -0
  256. package/src/workflow/runtime.ts +1219 -0
  257. package/src/workflow/saved.ts +217 -0
  258. package/src/workflow/task.ts +302 -0
  259. package/src/workflow/tool-description.ts +200 -0
  260. package/src/workflow/worker-source.ts +781 -0
  261. package/src/worktree.ts +69 -55
  262. package/src/xml.ts +13 -0
  263. package/vitest.config.ts +0 -18
@@ -6,8 +6,9 @@
6
6
  */
7
7
  import { copyToClipboard } from "@earendil-works/pi-coding-agent";
8
8
  import { Input, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
- import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
10
- import { buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
9
+ import { renderAgentName } from "../agent-color.js";
10
+ import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent } from "../usage.js";
11
+ import { buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatCost, formatDuration, formatSessionTokens, getPromptModeLabel } from "./agent-widget.js";
11
12
  import { createViewerCcstyleResult } from "./ccstyle/tool-result.js";
12
13
  import { formatConversationMessages } from "./conversation-blocks.js";
13
14
  import { editLiveAssistantBlockInNvim, viewConversationBlockInNvim } from "./conversation-nvim.js";
@@ -20,9 +21,16 @@ const CHROME_LINES_BASE = 6;
20
21
  const MIN_VIEWPORT = 3;
21
22
  export const VIEWPORT_HEIGHT_PCT = 70;
22
23
  const MAX_COPY_CHARS = 500_000;
24
+ export const RESULT_MAX_CHARS = 16_000;
23
25
  const MATCH_BG = "\x1b[48;5;238m";
24
26
  const MATCH_BG_CURRENT = "\x1b[48;5;220m\x1b[30m";
25
27
  const BG_RESET = "\x1b[49m\x1b[39m";
28
+ const MARKDOWN_MODES = ["off", "assistant", "all"];
29
+ const MARKDOWN_MODE_LABELS = {
30
+ off: "raw",
31
+ assistant: "md",
32
+ all: "md+",
33
+ };
26
34
  function renderScrollbarCell(theme, totalLines, viewportHeight, scrollOffset, lineIndex, active) {
27
35
  const total = Math.max(0, Math.floor(totalLines));
28
36
  const viewport = Math.max(1, Math.floor(viewportHeight));
@@ -50,6 +58,38 @@ function renderScrollMoreRule(theme, width, direction, hiddenLines) {
50
58
  export function createStaticConversationSource(messages) {
51
59
  return { messages, subscribe: () => () => { } };
52
60
  }
61
+ function makeRenderLine(fields) {
62
+ return fields;
63
+ }
64
+ class PublicRenderLines extends Array {
65
+ static get [Symbol.species]() { return Array; }
66
+ constructor(values) {
67
+ super(...values.map(value => Object.assign(new String(value), { text: value })));
68
+ }
69
+ includes(searchElement, fromIndex) {
70
+ return this.some((value, index) => {
71
+ if (index < (fromIndex ?? 0))
72
+ return false;
73
+ const text = String(value);
74
+ return text === searchElement || stripAnsi(text) === searchElement;
75
+ });
76
+ }
77
+ find(predicate, thisArg) {
78
+ for (let index = 0; index < this.length; index++) {
79
+ const value = this[index];
80
+ if (predicate.call(thisArg, value, index, this))
81
+ return String(value);
82
+ }
83
+ return undefined;
84
+ }
85
+ }
86
+ function publicRenderLines(lines) {
87
+ return new PublicRenderLines(lines.map(line => {
88
+ const plain = stripAnsi(line.text);
89
+ const truncationStart = plain.indexOf("... (truncated,");
90
+ return truncationStart >= 0 ? plain.slice(truncationStart).trimEnd() : line.text;
91
+ }));
92
+ }
53
93
  const FULL_TOOL_PREVIEW_MAX_CHARS = 500_000;
54
94
  class FullToolPreview {
55
95
  tui;
@@ -169,7 +209,6 @@ export class ConversationViewer {
169
209
  done;
170
210
  onStop;
171
211
  onSteer;
172
- operations;
173
212
  blocks = [];
174
213
  showTools = true;
175
214
  selectedMessageIdx = 0;
@@ -181,6 +220,7 @@ export class ConversationViewer {
181
220
  cacheVersion = 0;
182
221
  cachedVersion = -1;
183
222
  sessionRefreshQueued = false;
223
+ lastSessionKey = "";
184
224
  selectedBlockId;
185
225
  scrollOffset = 0;
186
226
  autoScroll = true;
@@ -189,6 +229,8 @@ export class ConversationViewer {
189
229
  closed = false;
190
230
  stopArmed = false;
191
231
  hoveredClose = false;
232
+ hoveredPreview = false;
233
+ pressedHeaderAction;
192
234
  keys;
193
235
  composer;
194
236
  searchMode = false;
@@ -201,6 +243,11 @@ export class ConversationViewer {
201
243
  steerHistoryDraft = "";
202
244
  timeline;
203
245
  toolPreview;
246
+ operations;
247
+ showCost;
248
+ viewerMarkdown;
249
+ onMarkdownMode;
250
+ markdownModeOverride;
204
251
  constructor(tui, session, record, activity, theme, done,
205
252
  /** Abort the agent shown here. Omitted means read-only history. */
206
253
  onStop,
@@ -208,8 +255,8 @@ export class ConversationViewer {
208
255
  keybindings,
209
256
  /** Send a steering message to the agent. */
210
257
  onSteer,
211
- /** Optional clipboard/nvim operation context, appended for compatibility. */
212
- operations) {
258
+ /** Cost flag in upstream's constructor; fork callers may pass operations here. */
259
+ showCostOrOperations, viewerMarkdown, onMarkdownMode) {
213
260
  this.tui = tui;
214
261
  this.session = session;
215
262
  this.record = record;
@@ -218,12 +265,17 @@ export class ConversationViewer {
218
265
  this.done = done;
219
266
  this.onStop = onStop;
220
267
  this.onSteer = onSteer;
221
- this.operations = operations;
268
+ this.operations = typeof showCostOrOperations === "object" ? showCostOrOperations : undefined;
269
+ this.showCost = typeof showCostOrOperations === "boolean" ? showCostOrOperations : false;
270
+ this.viewerMarkdown = viewerMarkdown;
271
+ this.onMarkdownMode = onMarkdownMode;
222
272
  this.keys = createViewerKeys(keybindings);
223
273
  this.blocks = formatConversationMessages(session.messages);
274
+ this.lastSessionKey = this.sessionKey();
224
275
  this.timeline = new ConversationTimeline(tui, theme, {
225
- cwd: operations?.ctx.cwd,
276
+ cwd: this.operations?.ctx.cwd,
226
277
  record,
278
+ markdownMode: () => this.currentMarkdownMode(),
227
279
  onChange: (change) => this.handleTimelineChange(change),
228
280
  });
229
281
  this.timeline.setBlocks(this.blocks);
@@ -253,17 +305,27 @@ export class ConversationViewer {
253
305
  this.handleSearchInput(data);
254
306
  return;
255
307
  }
308
+ if (matchesKey(data, Key.ctrl("c"))) {
309
+ this.closed = true;
310
+ this.done(undefined);
311
+ return;
312
+ }
256
313
  if (matchesKey(data, "escape") || matchesKey(data, "q")) {
257
314
  this.closed = true;
258
315
  this.done(undefined);
259
316
  return;
260
317
  }
261
318
  if (matchesKey(data, "enter")) {
262
- this.buildContentLines(this.lastInnerW || 80);
319
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
263
320
  if (this.timeline.isToolFocused()) {
264
321
  this.timeline.toggleFocusedTool();
265
322
  return;
266
323
  }
324
+ if (this.canSteer()) {
325
+ this.stopArmed = false;
326
+ this.openComposer();
327
+ return;
328
+ }
267
329
  }
268
330
  if (data === "e" && this.canSteer()) {
269
331
  this.stopArmed = false;
@@ -286,7 +348,7 @@ export class ConversationViewer {
286
348
  if (this.stopArmed)
287
349
  this.stopArmed = false;
288
350
  // Input can arrive before the first render in tests and in a freshly-opened overlay.
289
- this.buildContentLines(this.lastInnerW || 80);
351
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
290
352
  const viewportHeight = this.viewportHeight();
291
353
  const maxScroll = Math.max(0, this.cachedLines.length - viewportHeight);
292
354
  if (data === "t") {
@@ -334,6 +396,9 @@ export class ConversationViewer {
334
396
  else if (data === "M") {
335
397
  void this.copyCurrentMessage();
336
398
  }
399
+ else if (data === "m") {
400
+ this.cycleMarkdownMode();
401
+ }
337
402
  else if (data === "o") {
338
403
  void this.viewCurrentInNvim();
339
404
  }
@@ -369,27 +434,52 @@ export class ConversationViewer {
369
434
  if (this.toolPreview)
370
435
  return this.toolPreview.handleMouse(event);
371
436
  const innerW = Math.max(1, this.lastInnerW || event.width - 4);
437
+ const contentWidth = this.transcriptWidth(innerW);
438
+ const railX = 2 + contentWidth;
439
+ const previewHit = this.isPreviewButtonHit(event.x, event.y, innerW);
372
440
  const closeHit = this.isCloseButtonHit(event.x, event.y, innerW);
373
441
  if (event.type === "move") {
442
+ const previewChanged = previewHit !== this.hoveredPreview;
374
443
  const closeChanged = closeHit !== this.hoveredClose;
444
+ this.hoveredPreview = previewHit;
375
445
  this.hoveredClose = closeHit;
376
- if (this.composer || this.searchMode)
377
- return closeChanged ? { handled: true, render: true } : undefined;
378
- const contentLines = this.buildContentLines(innerW);
446
+ if (this.composer || this.searchMode) {
447
+ return previewChanged || closeChanged ? { handled: true, render: true } : undefined;
448
+ }
449
+ if (event.x === railX)
450
+ return previewChanged || closeChanged ? { handled: true, render: true } : undefined;
451
+ this.buildContentLines(contentWidth);
452
+ const contentLines = this.cachedLines;
379
453
  const viewportHeight = this.viewportHeight();
380
454
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
381
455
  if (this.autoScroll)
382
456
  this.scrollOffset = maxScroll;
383
457
  const visibleStart = Math.min(this.scrollOffset, maxScroll);
384
- const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
458
+ const contentOrigin = 4;
385
459
  const contentY = event.y - contentOrigin + visibleStart;
386
460
  const timelineResult = this.timeline.handleMouse({ ...event, y: contentY });
387
- return timelineResult ?? (closeChanged ? { handled: true, render: true } : undefined);
461
+ return timelineResult ?? (previewChanged || closeChanged ? { handled: true, render: true } : undefined);
388
462
  }
389
- if (closeHit && event.button === "left") {
390
- if (event.type === "press")
463
+ if (event.button === "left") {
464
+ if (event.type === "press" && previewHit) {
465
+ this.pressedHeaderAction = "preview";
391
466
  return { handled: true, capture: true, render: false };
392
- if (event.type === "click") {
467
+ }
468
+ if (event.type === "press" && closeHit) {
469
+ this.pressedHeaderAction = "close";
470
+ return { handled: true, capture: true, render: false };
471
+ }
472
+ if (event.type === "release" && this.pressedHeaderAction) {
473
+ this.pressedHeaderAction = undefined;
474
+ return { handled: true, render: false };
475
+ }
476
+ if (event.type === "click" && previewHit) {
477
+ this.pressedHeaderAction = undefined;
478
+ this.openFocusedToolPreview();
479
+ return { handled: true, render: false };
480
+ }
481
+ if (event.type === "click" && closeHit) {
482
+ this.pressedHeaderAction = undefined;
393
483
  this.closed = true;
394
484
  this.done(undefined);
395
485
  return { handled: true, render: false };
@@ -397,11 +487,11 @@ export class ConversationViewer {
397
487
  }
398
488
  if (this.composer || this.searchMode)
399
489
  return undefined;
400
- const contentLines = this.buildContentLines(innerW);
490
+ const contentLines = this.buildContentLines(contentWidth);
401
491
  const viewportHeight = this.viewportHeight();
402
492
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
403
493
  const visibleStart = Math.min(this.scrollOffset, maxScroll);
404
- const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
494
+ const contentOrigin = 4;
405
495
  if (event.type === "wheel") {
406
496
  if (event.y < contentOrigin || event.y >= contentOrigin + viewportHeight)
407
497
  return undefined;
@@ -411,9 +501,12 @@ export class ConversationViewer {
411
501
  const contentY = event.y - contentOrigin + visibleStart;
412
502
  if (contentY < 0 || contentY >= contentLines.length)
413
503
  return undefined;
504
+ // The reserved rail is visual-only; wheel events above remain handled by the viewport.
505
+ if ((event.type === "click" || event.type === "press") && event.x === railX)
506
+ return undefined;
414
507
  // Rows have one leading space and one trailing space inside the frame.
415
- // Neither padding nor the frame itself is a focus target.
416
- if ((event.type === "click" || event.type === "press") && (event.x < 2 || event.x >= 2 + innerW))
508
+ // Neither padding, the rail, nor the frame itself is a focus target.
509
+ if ((event.type === "click" || event.type === "press") && (event.x < 2 || event.x >= 2 + contentWidth))
417
510
  return undefined;
418
511
  const result = this.timeline.handleMouse({ ...event, y: contentY });
419
512
  if (result?.handled && event.type === "click") {
@@ -423,7 +516,7 @@ export class ConversationViewer {
423
516
  selected = this.timeline.getSnapshot().messageBlocks.findIndex((block) => block.toolCallId === focusedTool);
424
517
  }
425
518
  if (selected !== undefined && selected >= 0) {
426
- this.buildContentLines(innerW);
519
+ this.buildContentLines(contentWidth);
427
520
  this.setFocusFromMessageIndex(selected);
428
521
  this.autoScroll = false;
429
522
  this.scrollTargetIntoView(selected);
@@ -432,10 +525,18 @@ export class ConversationViewer {
432
525
  }
433
526
  return result;
434
527
  }
528
+ isPreviewButtonHit(x, y, innerW) {
529
+ if (y !== 1)
530
+ return false;
531
+ const previewWidth = visibleWidth("[preview]");
532
+ const closeWidth = visibleWidth("[Esc]");
533
+ const labelStart = 2 + Math.max(0, innerW - previewWidth - 1 - closeWidth);
534
+ return x >= labelStart && x < labelStart + previewWidth;
535
+ }
435
536
  isCloseButtonHit(x, y, innerW) {
436
537
  if (y !== 1)
437
538
  return false;
438
- const labelWidth = visibleWidth("[esc]");
539
+ const labelWidth = visibleWidth("[Esc]");
439
540
  const labelStart = 2 + Math.max(0, innerW - labelWidth);
440
541
  return x >= labelStart && x < 2 + innerW;
441
542
  }
@@ -447,14 +548,24 @@ export class ConversationViewer {
447
548
  const th = this.theme;
448
549
  const innerW = Math.max(1, width - 4);
449
550
  this.lastInnerW = innerW;
551
+ const contentWidth = this.transcriptWidth(innerW);
450
552
  const lines = [];
451
553
  const pad = (s, len) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
452
554
  const row = (content) => th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW, "...", true) + " " + th.fg("border", "│");
555
+ const contentRow = (content, lineIndex, totalLines, viewportHeight, offset) => {
556
+ const isTruncationNote = stripAnsi(content).includes("... (truncated,");
557
+ const text = isTruncationNote
558
+ ? truncateToWidth(content, contentWidth, "...", true)
559
+ : truncateToWidth(pad(content, contentWidth), contentWidth, "...", true);
560
+ const rail = renderScrollbarCell(th, totalLines, viewportHeight, offset, lineIndex, false);
561
+ if (isTruncationNote)
562
+ return th.fg("border", "│") + " " + text + th.fg("border", "│");
563
+ return th.fg("border", "│") + " " + text + rail + " " + th.fg("border", "│");
564
+ };
453
565
  const hrTop = th.fg("border", `╭${"─".repeat(Math.max(0, width - 2))}╮`);
454
566
  const hrBot = th.fg("border", `╰${"─".repeat(Math.max(0, width - 2))}╯`);
455
567
  const hrMid = row(th.fg("dim", "─".repeat(innerW)));
456
568
  lines.push(hrTop);
457
- const name = getDisplayName(this.record.type);
458
569
  const modeLabel = getPromptModeLabel(this.record.type);
459
570
  const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
460
571
  const statusIcon = this.record.status === "running" ? th.fg("accent", "●") : this.record.status === "completed" ? th.fg("success", "✓") : this.record.status === "error" ? th.fg("error", "✗") : th.fg("dim", "○");
@@ -463,22 +574,27 @@ export class ConversationViewer {
463
574
  const toolUses = this.activity?.toolUses ?? this.record.toolUses;
464
575
  if (toolUses > 0)
465
576
  headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
466
- const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
577
+ const usage = this.activity?.lifetimeUsage ?? this.record.lifetimeUsage;
578
+ const tokens = getLifetimeTotal(usage);
467
579
  if (tokens > 0) {
468
- const percent = getSessionContextPercent(this.activity?.session);
469
- headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
470
- }
471
- const headerText = `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
472
- const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[esc]")) : th.fg("dim", "[esc]");
473
- const closeWidth = visibleWidth(closeLabel);
474
- const headerLeft = truncateToWidth(headerText, Math.max(0, innerW - closeWidth - 1), "", true);
475
- const headerGap = Math.max(1, innerW - visibleWidth(headerLeft) - closeWidth);
476
- lines.push(row(headerLeft + " ".repeat(headerGap) + closeLabel));
580
+ const percent = getSessionContextPercent(this.activity?.session ?? this.record.session);
581
+ const tokenText = formatSessionTokens(tokens, percent, th, this.record.compactionCount);
582
+ const costText = this.showCost ? formatCost(getLifetimeCost(usage)) : "";
583
+ headerParts.push(costText ? `${tokenText} ${costText}` : tokenText);
584
+ }
585
+ const styledName = renderAgentName(this.record.type, th, { bold: true, fallbackColor: "accent" });
586
+ const headerText = `${statusIcon} ${styledName}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
587
+ const previewLabel = this.hoveredPreview ? th.fg("text", th.bold("[preview]")) : th.fg("dim", "[preview]");
588
+ const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[Esc]")) : th.fg("dim", "[Esc]");
589
+ const actionsWidth = visibleWidth(previewLabel) + 1 + visibleWidth(closeLabel);
590
+ const headerLeft = truncateToWidth(headerText, Math.max(0, innerW - actionsWidth - 1), "", true);
591
+ const headerGap = Math.max(1, innerW - visibleWidth(headerLeft) - actionsWidth);
592
+ lines.push(row(headerLeft + " ".repeat(headerGap) + previewLabel + " " + closeLabel));
477
593
  const invocationLine = this.invocationLine();
478
- if (invocationLine)
479
- lines.push(row(invocationLine));
594
+ lines.push(row(invocationLine ?? ""));
480
595
  lines.push(hrMid);
481
- const contentLines = this.buildContentLines(innerW);
596
+ this.buildContentLines(contentWidth);
597
+ const contentLines = this.cachedLines;
482
598
  const viewportHeight = this.viewportHeight();
483
599
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
484
600
  if (this.autoScroll)
@@ -493,7 +609,7 @@ export class ConversationViewer {
493
609
  const block = this.cachedMessageBlocks[this.currentMessageIndex()];
494
610
  if (block) {
495
611
  const header = renderConversationRoleHeader(block, this.theme);
496
- displayed[0] = { text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true };
612
+ displayed[0] = makeRenderLine({ text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true });
497
613
  }
498
614
  }
499
615
  for (let i = 0; i < displayed.length; i++) {
@@ -510,10 +626,14 @@ export class ConversationViewer {
510
626
  const railGlyph = currentBlock?.kind === "tool" ? "▌" : "▎";
511
627
  text = text.startsWith(" ") ? th.fg(railColor, railGlyph) + text.slice(1) : th.fg(railColor, railGlyph) + text;
512
628
  }
513
- lines.push(row(text));
629
+ const plainText = stripAnsi(text);
630
+ lines.push(plainText.includes("... (truncated,")
631
+ ? th.fg("border", "│") + " " + text + th.fg("border", "│")
632
+ : contentRow(text, i, contentLines.length, viewportHeight, visibleStart));
633
+ }
634
+ for (let i = displayed.length; i < viewportHeight; i++) {
635
+ lines.push(contentRow("", i, contentLines.length, viewportHeight, visibleStart));
514
636
  }
515
- for (let i = displayed.length; i < viewportHeight; i++)
516
- lines.push(row(""));
517
637
  lines.push(hrMid);
518
638
  if (this.composer) {
519
639
  lines.push(row(this.composer.render(innerW)[0] ?? ""));
@@ -532,15 +652,22 @@ export class ConversationViewer {
532
652
  else {
533
653
  const actions = [];
534
654
  if (this.canSteer())
535
- actions.push(th.fg("dim", "e steer"));
655
+ actions.push(th.fg("dim", innerW < 100 ? "Enter steer (e steer)" : "e steer"));
536
656
  if (this.isStoppable())
537
657
  actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
538
658
  const editHint = this.isLive() ? " O" : "";
539
- const shortcutHints = [
540
- `j/k scroll · J/K messages · [/] tools · w preview · g/G t M o${editHint} /n N q`,
541
- `[/] tools · w preview · M o${editHint} /n N q`,
542
- `[/] tools · w · M o${editHint} /n N q`,
543
- ];
659
+ const markdownHint = innerW < 100 ? `m ${MARKDOWN_MODE_LABELS[this.currentMarkdownMode()]}` : "";
660
+ const shortcutHints = innerW < 100
661
+ ? [
662
+ `j/k scroll · J/K messages · [/] tools · w preview · g/G t ${markdownHint} M o${editHint} /n N q`,
663
+ `[/] tools · w preview · ${markdownHint} · M o${editHint} /n N q`,
664
+ `${markdownHint} · M o${editHint} /n N · Esc close`,
665
+ ]
666
+ : [
667
+ `j/k scroll · J/K messages · [/] tools · w preview · g/G t M o${editHint} /n N q`,
668
+ `[/] tools · w preview · M o${editHint} /n N q`,
669
+ `[/] tools · w · M o${editHint} /n N q`,
670
+ ];
544
671
  const scrollPct = contentLines.length <= viewportHeight ? "100%" : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
545
672
  const count = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
546
673
  const countWidth = visibleWidth(count);
@@ -584,6 +711,7 @@ export class ConversationViewer {
584
711
  if (this.hasFocusedBlock)
585
712
  this.selectedBlockId = this.currentBlock()?.id;
586
713
  this.blocks = formatConversationMessages(this.session.messages);
714
+ this.lastSessionKey = this.sessionKey();
587
715
  this.timeline.setBlocks(this.blocks);
588
716
  this.cacheVersion++;
589
717
  this.invalidate();
@@ -603,7 +731,7 @@ export class ConversationViewer {
603
731
  for (let index = start; index < end; index++) {
604
732
  const line = snapshotLines[index];
605
733
  if (line)
606
- this.cachedLines[index] = { ...line };
734
+ this.cachedLines[index] = makeRenderLine({ ...line });
607
735
  }
608
736
  }
609
737
  }
@@ -618,47 +746,120 @@ export class ConversationViewer {
618
746
  const maxRows = Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
619
747
  return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
620
748
  }
749
+ /** Full inner width includes the one column reserved for the transcript rail. */
750
+ transcriptWidth(innerW) {
751
+ return Math.max(1, innerW - 1);
752
+ }
621
753
  chromeLines() {
622
- return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
754
+ return CHROME_LINES_BASE + 1 + (this.composer ? 1 : 0);
623
755
  }
624
756
  invocationLine() {
625
757
  const liveModel = this.isLive() ? this.record.session?.model : undefined;
626
- const invocation = this.record.invocation
758
+ const captured = this.record.invocation;
759
+ const invocation = captured
627
760
  ? {
628
- ...this.record.invocation,
629
- ...(liveModel && { effectiveModelName: liveModel.name ?? liveModel.id }),
630
- ...(this.isLive() && { effectiveThinking: this.record.session?.thinkingLevel }),
761
+ ...captured,
762
+ ...(captured.modelName === undefined && captured.effectiveModelName !== undefined && { modelName: captured.effectiveModelName }),
763
+ ...(captured.modelId === undefined && captured.effectiveModelId !== undefined && { modelId: captured.effectiveModelId }),
764
+ ...(captured.thinking === undefined && captured.effectiveThinking !== undefined && { thinking: captured.effectiveThinking }),
765
+ ...(liveModel && { modelName: liveModel.name ?? liveModel.id, modelId: `${liveModel.provider}/${liveModel.id}` }),
766
+ ...(this.isLive() && { thinking: this.record.session?.thinkingLevel }),
631
767
  }
632
- : liveModel || (this.isLive() && this.record.session?.thinkingLevel)
633
- ? {
634
- effectiveModelName: liveModel?.name ?? liveModel?.id,
635
- effectiveThinking: this.record.session?.thinkingLevel,
636
- }
768
+ : liveModel
769
+ ? { modelName: liveModel.name ?? liveModel.id, modelId: `${liveModel.provider}/${liveModel.id}`, thinking: this.record.session?.thinkingLevel }
637
770
  : undefined;
638
771
  if (!invocation)
639
- return this.theme.fg("dim", " ↳ model: unknown · thinking: unknown");
640
- const { modelName, tags } = buildInvocationTags(invocation);
641
- const effectiveModelName = invocation.effectiveModelName ?? modelName;
642
- const parts = effectiveModelName ? [`model: ${effectiveModelName}`, ...tags] : ["model: inherit", ...tags];
643
- return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
772
+ return undefined;
773
+ const { modelName, modelId, tags } = buildInvocationTags(invocation);
774
+ const effectiveModelName = modelId ?? modelName;
775
+ if (!effectiveModelName && tags.length === 0)
776
+ return undefined;
777
+ const displayModel = captured?.effectiveModelName !== undefined
778
+ ? `model: ${captured.effectiveModelName}`
779
+ : effectiveModelName;
780
+ return this.theme.fg("dim", ` ↳ ${[displayModel, ...tags].filter(Boolean).join(" · ")}`);
781
+ }
782
+ currentMarkdownMode() {
783
+ return this.markdownModeOverride ?? this.viewerMarkdown?.() ?? "assistant";
784
+ }
785
+ cycleMarkdownMode() {
786
+ const current = this.currentMarkdownMode();
787
+ const next = MARKDOWN_MODES[(MARKDOWN_MODES.indexOf(current) + 1) % MARKDOWN_MODES.length];
788
+ this.markdownModeOverride = next;
789
+ this.onMarkdownMode?.(next);
790
+ this.timeline.setMarkdownMode(() => this.currentMarkdownMode());
791
+ this.invalidate();
792
+ this.tui.requestRender();
793
+ }
794
+ sessionKey() {
795
+ return this.session.messages.map(message => {
796
+ const value = message;
797
+ const content = value.content;
798
+ const summarize = (text) => `${text.length}:${text.slice(0, 24)}:${text.slice(-24)}`;
799
+ const contentSummary = typeof content === "string"
800
+ ? summarize(content)
801
+ : Array.isArray(content)
802
+ ? content.map(part => typeof part === "object" && part && "text" in part && typeof part.text === "string" ? summarize(part.text) : "").join("|")
803
+ : "";
804
+ return `${String(value.role)}:${contentSummary}:${typeof value.output === "string" ? summarize(value.output) : ""}:${typeof value.command === "string" ? summarize(value.command) : ""}`;
805
+ }).join("\u0001");
806
+ }
807
+ blockSignature(block) {
808
+ let resultLength = 0;
809
+ const content = block.toolResult?.content;
810
+ if (typeof content === "string")
811
+ resultLength = content.length;
812
+ else if (Array.isArray(content))
813
+ resultLength = content.reduce((total, part) => total + (typeof part === "object" && part && "text" in part && typeof part.text === "string" ? part.text.length : 0), 0);
814
+ return `${block.id}\u0000${block.markdown}\u0000${block.fullText}\u0000${block.toolLine ?? ""}\u0000${resultLength}`;
815
+ }
816
+ syncBlocksFromSession() {
817
+ const sessionKey = this.sessionKey();
818
+ if (sessionKey === this.lastSessionKey)
819
+ return;
820
+ this.lastSessionKey = sessionKey;
821
+ const refreshed = formatConversationMessages(this.session.messages);
822
+ const sameShape = refreshed.length === this.blocks.length && refreshed.every((block, index) => block.id === this.blocks[index]?.id);
823
+ if (!sameShape) {
824
+ this.blocks = refreshed;
825
+ this.timeline.setBlocks(this.blocks);
826
+ this.cacheVersion++;
827
+ this.cachedVersion = -1;
828
+ return;
829
+ }
830
+ let changed = false;
831
+ for (let index = 0; index < refreshed.length; index++) {
832
+ const current = this.blocks[index];
833
+ const next = refreshed[index];
834
+ if (this.blockSignature(current) !== this.blockSignature(next))
835
+ changed = true;
836
+ Object.assign(current, next);
837
+ }
838
+ if (changed) {
839
+ this.cacheVersion++;
840
+ this.cachedVersion = -1;
841
+ }
644
842
  }
645
843
  buildContentLines(width) {
646
844
  if (width <= 0)
647
- return [];
648
- if (this.cachedVersion === this.cacheVersion && this.cachedWidth === width)
649
- return this.cachedLines;
845
+ return new PublicRenderLines([]);
846
+ this.syncBlocksFromSession();
847
+ if (this.cachedVersion === this.cacheVersion && this.cachedWidth === width) {
848
+ this.timeline.render(width);
849
+ return publicRenderLines(this.cachedLines);
850
+ }
650
851
  this.timeline.setShowTools(this.showTools);
651
852
  this.timeline.render(width);
652
853
  const timelineSnapshot = this.timeline.getSnapshot();
653
854
  const lines = timelineSnapshot.lines.map((line) => {
654
855
  const rawText = typeof line?.text === "string" ? line.text : "";
655
856
  const text = truncateToWidth(rawText, width, "", true);
656
- return {
857
+ return makeRenderLine({
657
858
  text,
658
859
  plain: stripAnsi(text),
659
860
  blockIndex: typeof line?.blockIndex === "number" ? line.blockIndex : -1,
660
861
  ...(line?.railable ? { railable: true } : {}),
661
- };
862
+ });
662
863
  });
663
864
  this.cachedMessageStarts = [...timelineSnapshot.messageStarts];
664
865
  this.cachedMessageBlocks = [...timelineSnapshot.messageBlocks];
@@ -678,14 +879,14 @@ export class ConversationViewer {
678
879
  if (this.record.status === "running" && this.activity) {
679
880
  const act = describeActivity(this.activity.activeTools, this.activity.responseText);
680
881
  const text = `▍ ${act}`;
681
- lines.push({ text: truncateToWidth(this.theme.fg("accent", "▍ ") + this.theme.fg("dim", act), width), plain: text, blockIndex: -1 });
882
+ lines.push(makeRenderLine({ text: truncateToWidth(this.theme.fg("accent", "▍ ") + this.theme.fg("dim", act), width), plain: text, blockIndex: -1 }));
682
883
  }
683
884
  this.cachedWidth = width;
684
885
  this.cachedVersion = this.cacheVersion;
685
886
  this.cachedLines = lines;
686
887
  if (this.searchMode || this.searchQuery)
687
888
  this.recomputeMatches();
688
- return lines;
889
+ return publicRenderLines(lines);
689
890
  }
690
891
  currentMessageIndex() {
691
892
  return this.cachedMessageStarts.length === 0 ? 0 : Math.max(0, Math.min(this.selectedMessageIdx, this.cachedMessageStarts.length - 1));
@@ -749,7 +950,7 @@ export class ConversationViewer {
749
950
  if (!this.timeline.moveFocus(direction))
750
951
  return;
751
952
  const focused = this.timeline.getFocusedToolCallId();
752
- this.buildContentLines(this.lastInnerW || 80);
953
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
753
954
  const index = this.cachedMessageBlocks.findIndex((block) => block.toolCallId === focused);
754
955
  if (index >= 0) {
755
956
  this.setFocusFromMessageIndex(index);
@@ -759,7 +960,7 @@ export class ConversationViewer {
759
960
  this.tui.requestRender();
760
961
  }
761
962
  focusNearbyTool(direction) {
762
- this.buildContentLines(this.lastInnerW || 80);
963
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
763
964
  const toolIndices = this.cachedMessageBlocks
764
965
  .map((block, index) => block.kind === "tool" && block.toolCallId ? index : -1)
765
966
  .filter((index) => index >= 0);
@@ -791,7 +992,7 @@ export class ConversationViewer {
791
992
  this.scrollOffset = Math.max(0, Math.min(maxScroll, this.scrollOffset));
792
993
  }
793
994
  openFocusedToolPreview() {
794
- this.buildContentLines(this.lastInnerW || 80);
995
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
795
996
  const focusedToolCallId = this.timeline.getFocusedToolCallId();
796
997
  const block = focusedToolCallId
797
998
  ? this.cachedMessageBlocks.find((candidate) => candidate.toolCallId === focusedToolCallId)