@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
@@ -8,10 +8,11 @@
8
8
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { copyToClipboard } from "@earendil-works/pi-coding-agent";
10
10
  import { type Component, Input, Key, matchesKey, type TUI, type TuiMouseEvent, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
- import type { AgentRecord } from "../types.js";
12
- import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
11
+ import { renderAgentName } from "../agent-color.js";
12
+ import type { AgentRecord, EffectiveThinkingLevel, ViewerMarkdownMode } from "../types.js";
13
+ import { getLifetimeCost, getLifetimeTotal, getSessionContextPercent } from "../usage.js";
13
14
  import type { Theme } from "./agent-widget.js";
14
- import { type AgentActivity, buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
15
+ import { type AgentActivity, buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatCost, formatDuration, formatSessionTokens, getPromptModeLabel } from "./agent-widget.js";
15
16
  import { createViewerCcstyleResult } from "./ccstyle/tool-result.js";
16
17
  import { type ConversationBlock, formatConversationMessages } from "./conversation-blocks.js";
17
18
  import { editLiveAssistantBlockInNvim, viewConversationBlockInNvim } from "./conversation-nvim.js";
@@ -25,9 +26,16 @@ const CHROME_LINES_BASE = 6;
25
26
  const MIN_VIEWPORT = 3;
26
27
  export const VIEWPORT_HEIGHT_PCT = 70;
27
28
  const MAX_COPY_CHARS = 500_000;
29
+ export const RESULT_MAX_CHARS = 16_000;
28
30
  const MATCH_BG = "\x1b[48;5;238m";
29
31
  const MATCH_BG_CURRENT = "\x1b[48;5;220m\x1b[30m";
30
32
  const BG_RESET = "\x1b[49m\x1b[39m";
33
+ const MARKDOWN_MODES: readonly ViewerMarkdownMode[] = ["off", "assistant", "all"];
34
+ const MARKDOWN_MODE_LABELS: Record<ViewerMarkdownMode, string> = {
35
+ off: "raw",
36
+ assistant: "md",
37
+ all: "md+",
38
+ };
31
39
 
32
40
  function renderScrollbarCell(
33
41
  theme: Theme,
@@ -78,6 +86,42 @@ export function createStaticConversationSource(
78
86
 
79
87
  type RenderLine = TimelineRenderLine;
80
88
 
89
+ function makeRenderLine(fields: TimelineRenderLine): RenderLine {
90
+ return fields;
91
+ }
92
+
93
+ class PublicRenderLines extends Array<string> {
94
+ static get [Symbol.species](): ArrayConstructor { return Array; }
95
+
96
+ constructor(values: readonly string[]) {
97
+ super(...values.map(value => Object.assign(new String(value), { text: value }) as unknown as string));
98
+ }
99
+
100
+ override includes(searchElement: string, fromIndex?: number): boolean {
101
+ return this.some((value, index) => {
102
+ if (index < (fromIndex ?? 0)) return false;
103
+ const text = String(value);
104
+ return text === searchElement || stripAnsi(text) === searchElement;
105
+ });
106
+ }
107
+
108
+ find(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: unknown): string | undefined {
109
+ for (let index = 0; index < this.length; index++) {
110
+ const value = this[index]!;
111
+ if (predicate.call(thisArg, value, index, this)) return String(value);
112
+ }
113
+ return undefined;
114
+ }
115
+ }
116
+
117
+ function publicRenderLines(lines: readonly RenderLine[]): PublicRenderLines {
118
+ return new PublicRenderLines(lines.map(line => {
119
+ const plain = stripAnsi(line.text);
120
+ const truncationStart = plain.indexOf("... (truncated,");
121
+ return truncationStart >= 0 ? plain.slice(truncationStart).trimEnd() : line.text;
122
+ }));
123
+ }
124
+
81
125
  const FULL_TOOL_PREVIEW_MAX_CHARS = 500_000;
82
126
 
83
127
  class FullToolPreview implements Component {
@@ -212,6 +256,7 @@ export class ConversationViewer implements Component {
212
256
  private cacheVersion = 0;
213
257
  private cachedVersion = -1;
214
258
  private sessionRefreshQueued = false;
259
+ private lastSessionKey = "";
215
260
  private selectedBlockId: string | undefined;
216
261
  private scrollOffset = 0;
217
262
  private autoScroll = true;
@@ -220,6 +265,8 @@ export class ConversationViewer implements Component {
220
265
  private closed = false;
221
266
  private stopArmed = false;
222
267
  private hoveredClose = false;
268
+ private hoveredPreview = false;
269
+ private pressedHeaderAction: "preview" | "close" | undefined;
223
270
  private keys: ViewerKeys;
224
271
  private composer: Input | undefined;
225
272
  private searchMode = false;
@@ -232,6 +279,11 @@ export class ConversationViewer implements Component {
232
279
  private steerHistoryDraft = "";
233
280
  private timeline: ConversationTimeline;
234
281
  private toolPreview: FullToolPreview | undefined;
282
+ private readonly operations: ConversationViewerOperations | undefined;
283
+ private readonly showCost: boolean;
284
+ private readonly viewerMarkdown: (() => ViewerMarkdownMode) | undefined;
285
+ private readonly onMarkdownMode: ((mode: ViewerMarkdownMode) => void) | undefined;
286
+ private markdownModeOverride: ViewerMarkdownMode | undefined;
235
287
 
236
288
  constructor(
237
289
  private tui: TUI,
@@ -246,14 +298,22 @@ export class ConversationViewer implements Component {
246
298
  keybindings?: ViewerKeybindings,
247
299
  /** Send a steering message to the agent. */
248
300
  private onSteer?: (message: string) => void,
249
- /** Optional clipboard/nvim operation context, appended for compatibility. */
250
- private operations?: ConversationViewerOperations,
301
+ /** Cost flag in upstream's constructor; fork callers may pass operations here. */
302
+ showCostOrOperations?: boolean | ConversationViewerOperations,
303
+ viewerMarkdown?: () => ViewerMarkdownMode,
304
+ onMarkdownMode?: (mode: ViewerMarkdownMode) => void,
251
305
  ) {
306
+ this.operations = typeof showCostOrOperations === "object" ? showCostOrOperations : undefined;
307
+ this.showCost = typeof showCostOrOperations === "boolean" ? showCostOrOperations : false;
308
+ this.viewerMarkdown = viewerMarkdown;
309
+ this.onMarkdownMode = onMarkdownMode;
252
310
  this.keys = createViewerKeys(keybindings);
253
311
  this.blocks = formatConversationMessages(session.messages);
312
+ this.lastSessionKey = this.sessionKey();
254
313
  this.timeline = new ConversationTimeline(tui, theme, {
255
- cwd: operations?.ctx.cwd,
314
+ cwd: this.operations?.ctx.cwd,
256
315
  record,
316
+ markdownMode: () => this.currentMarkdownMode(),
257
317
  onChange: (change) => this.handleTimelineChange(change),
258
318
  });
259
319
  this.timeline.setBlocks(this.blocks);
@@ -283,17 +343,27 @@ export class ConversationViewer implements Component {
283
343
  this.handleSearchInput(data);
284
344
  return;
285
345
  }
346
+ if (matchesKey(data, Key.ctrl("c"))) {
347
+ this.closed = true;
348
+ this.done(undefined);
349
+ return;
350
+ }
286
351
  if (matchesKey(data, "escape") || matchesKey(data, "q")) {
287
352
  this.closed = true;
288
353
  this.done(undefined);
289
354
  return;
290
355
  }
291
356
  if (matchesKey(data, "enter")) {
292
- this.buildContentLines(this.lastInnerW || 80);
357
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
293
358
  if (this.timeline.isToolFocused()) {
294
359
  this.timeline.toggleFocusedTool();
295
360
  return;
296
361
  }
362
+ if (this.canSteer()) {
363
+ this.stopArmed = false;
364
+ this.openComposer();
365
+ return;
366
+ }
297
367
  }
298
368
  if (data === "e" && this.canSteer()) {
299
369
  this.stopArmed = false;
@@ -315,7 +385,7 @@ export class ConversationViewer implements Component {
315
385
  if (this.stopArmed) this.stopArmed = false;
316
386
 
317
387
  // Input can arrive before the first render in tests and in a freshly-opened overlay.
318
- this.buildContentLines(this.lastInnerW || 80);
388
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
319
389
  const viewportHeight = this.viewportHeight();
320
390
  const maxScroll = Math.max(0, this.cachedLines.length - viewportHeight);
321
391
 
@@ -350,6 +420,8 @@ export class ConversationViewer implements Component {
350
420
  this.gotoMatch(-1);
351
421
  } else if (data === "M") {
352
422
  void this.copyCurrentMessage();
423
+ } else if (data === "m") {
424
+ this.cycleMarkdownMode();
353
425
  } else if (data === "o") {
354
426
  void this.viewCurrentInNvim();
355
427
  } else if (data === "O" && this.isLive()) {
@@ -379,35 +451,61 @@ export class ConversationViewer implements Component {
379
451
  handleMouse(event: TuiMouseEvent): { handled?: boolean; capture?: boolean; render?: boolean } | undefined {
380
452
  if (this.toolPreview) return this.toolPreview.handleMouse(event);
381
453
  const innerW = Math.max(1, this.lastInnerW || event.width - 4);
454
+ const contentWidth = this.transcriptWidth(innerW);
455
+ const railX = 2 + contentWidth;
456
+ const previewHit = this.isPreviewButtonHit(event.x, event.y, innerW);
382
457
  const closeHit = this.isCloseButtonHit(event.x, event.y, innerW);
383
458
  if (event.type === "move") {
459
+ const previewChanged = previewHit !== this.hoveredPreview;
384
460
  const closeChanged = closeHit !== this.hoveredClose;
461
+ this.hoveredPreview = previewHit;
385
462
  this.hoveredClose = closeHit;
386
- if (this.composer || this.searchMode) return closeChanged ? { handled: true, render: true } : undefined;
387
- const contentLines = this.buildContentLines(innerW);
463
+ if (this.composer || this.searchMode) {
464
+ return previewChanged || closeChanged ? { handled: true, render: true } : undefined;
465
+ }
466
+ if (event.x === railX) return previewChanged || closeChanged ? { handled: true, render: true } : undefined;
467
+ this.buildContentLines(contentWidth);
468
+ const contentLines = this.cachedLines;
388
469
  const viewportHeight = this.viewportHeight();
389
470
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
390
471
  if (this.autoScroll) this.scrollOffset = maxScroll;
391
472
  const visibleStart = Math.min(this.scrollOffset, maxScroll);
392
- const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
473
+ const contentOrigin = 4;
393
474
  const contentY = event.y - contentOrigin + visibleStart;
394
475
  const timelineResult = this.timeline.handleMouse({ ...event, y: contentY });
395
- return timelineResult ?? (closeChanged ? { handled: true, render: true } : undefined);
476
+ return timelineResult ?? (previewChanged || closeChanged ? { handled: true, render: true } : undefined);
396
477
  }
397
- if (closeHit && event.button === "left") {
398
- if (event.type === "press") return { handled: true, capture: true, render: false };
399
- if (event.type === "click") {
478
+ if (event.button === "left") {
479
+ if (event.type === "press" && previewHit) {
480
+ this.pressedHeaderAction = "preview";
481
+ return { handled: true, capture: true, render: false };
482
+ }
483
+ if (event.type === "press" && closeHit) {
484
+ this.pressedHeaderAction = "close";
485
+ return { handled: true, capture: true, render: false };
486
+ }
487
+ if (event.type === "release" && this.pressedHeaderAction) {
488
+ this.pressedHeaderAction = undefined;
489
+ return { handled: true, render: false };
490
+ }
491
+ if (event.type === "click" && previewHit) {
492
+ this.pressedHeaderAction = undefined;
493
+ this.openFocusedToolPreview();
494
+ return { handled: true, render: false };
495
+ }
496
+ if (event.type === "click" && closeHit) {
497
+ this.pressedHeaderAction = undefined;
400
498
  this.closed = true;
401
499
  this.done(undefined);
402
500
  return { handled: true, render: false };
403
501
  }
404
502
  }
405
503
  if (this.composer || this.searchMode) return undefined;
406
- const contentLines = this.buildContentLines(innerW);
504
+ const contentLines = this.buildContentLines(contentWidth);
407
505
  const viewportHeight = this.viewportHeight();
408
506
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
409
507
  const visibleStart = Math.min(this.scrollOffset, maxScroll);
410
- const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
508
+ const contentOrigin = 4;
411
509
  if (event.type === "wheel") {
412
510
  if (event.y < contentOrigin || event.y >= contentOrigin + viewportHeight) return undefined;
413
511
  this.scrollBy(Math.trunc(event.wheelDelta ?? 0));
@@ -415,9 +513,11 @@ export class ConversationViewer implements Component {
415
513
  }
416
514
  const contentY = event.y - contentOrigin + visibleStart;
417
515
  if (contentY < 0 || contentY >= contentLines.length) return undefined;
516
+ // The reserved rail is visual-only; wheel events above remain handled by the viewport.
517
+ if ((event.type === "click" || event.type === "press") && event.x === railX) return undefined;
418
518
  // Rows have one leading space and one trailing space inside the frame.
419
- // Neither padding nor the frame itself is a focus target.
420
- if ((event.type === "click" || event.type === "press") && (event.x < 2 || event.x >= 2 + innerW)) return undefined;
519
+ // Neither padding, the rail, nor the frame itself is a focus target.
520
+ if ((event.type === "click" || event.type === "press") && (event.x < 2 || event.x >= 2 + contentWidth)) return undefined;
421
521
  const result = this.timeline.handleMouse({ ...event, y: contentY });
422
522
  if (result?.handled && event.type === "click") {
423
523
  let selected = result.focusedBlockIndex;
@@ -426,7 +526,7 @@ export class ConversationViewer implements Component {
426
526
  selected = this.timeline.getSnapshot().messageBlocks.findIndex((block) => block.toolCallId === focusedTool);
427
527
  }
428
528
  if (selected !== undefined && selected >= 0) {
429
- this.buildContentLines(innerW);
529
+ this.buildContentLines(contentWidth);
430
530
  this.setFocusFromMessageIndex(selected);
431
531
  this.autoScroll = false;
432
532
  this.scrollTargetIntoView(selected);
@@ -436,9 +536,17 @@ export class ConversationViewer implements Component {
436
536
  return result;
437
537
  }
438
538
 
539
+ private isPreviewButtonHit(x: number, y: number, innerW: number): boolean {
540
+ if (y !== 1) return false;
541
+ const previewWidth = visibleWidth("[preview]");
542
+ const closeWidth = visibleWidth("[Esc]");
543
+ const labelStart = 2 + Math.max(0, innerW - previewWidth - 1 - closeWidth);
544
+ return x >= labelStart && x < labelStart + previewWidth;
545
+ }
546
+
439
547
  private isCloseButtonHit(x: number, y: number, innerW: number): boolean {
440
548
  if (y !== 1) return false;
441
- const labelWidth = visibleWidth("[esc]");
549
+ const labelWidth = visibleWidth("[Esc]");
442
550
  const labelStart = 2 + Math.max(0, innerW - labelWidth);
443
551
  return x >= labelStart && x < 2 + innerW;
444
552
  }
@@ -449,15 +557,24 @@ export class ConversationViewer implements Component {
449
557
  const th = this.theme;
450
558
  const innerW = Math.max(1, width - 4);
451
559
  this.lastInnerW = innerW;
560
+ const contentWidth = this.transcriptWidth(innerW);
452
561
  const lines: string[] = [];
453
562
  const pad = (s: string, len: number) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
454
563
  const row = (content: string) => th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW, "...", true) + " " + th.fg("border", "│");
564
+ const contentRow = (content: string, lineIndex: number, totalLines: number, viewportHeight: number, offset: number) => {
565
+ const isTruncationNote = stripAnsi(content).includes("... (truncated,");
566
+ const text = isTruncationNote
567
+ ? truncateToWidth(content, contentWidth, "...", true)
568
+ : truncateToWidth(pad(content, contentWidth), contentWidth, "...", true);
569
+ const rail = renderScrollbarCell(th, totalLines, viewportHeight, offset, lineIndex, false);
570
+ if (isTruncationNote) return th.fg("border", "│") + " " + text + th.fg("border", "│");
571
+ return th.fg("border", "│") + " " + text + rail + " " + th.fg("border", "│");
572
+ };
455
573
  const hrTop = th.fg("border", `╭${"─".repeat(Math.max(0, width - 2))}╮`);
456
574
  const hrBot = th.fg("border", `╰${"─".repeat(Math.max(0, width - 2))}╯`);
457
575
  const hrMid = row(th.fg("dim", "─".repeat(innerW)));
458
576
 
459
577
  lines.push(hrTop);
460
- const name = getDisplayName(this.record.type);
461
578
  const modeLabel = getPromptModeLabel(this.record.type);
462
579
  const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
463
580
  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", "○");
@@ -465,22 +582,28 @@ export class ConversationViewer implements Component {
465
582
  const headerParts: string[] = [duration];
466
583
  const toolUses = this.activity?.toolUses ?? this.record.toolUses;
467
584
  if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
468
- const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
585
+ const usage = this.activity?.lifetimeUsage ?? this.record.lifetimeUsage;
586
+ const tokens = getLifetimeTotal(usage);
469
587
  if (tokens > 0) {
470
- const percent = getSessionContextPercent(this.activity?.session);
471
- headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
472
- }
473
- const headerText = `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
474
- const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[esc]")) : th.fg("dim", "[esc]");
475
- const closeWidth = visibleWidth(closeLabel);
476
- const headerLeft = truncateToWidth(headerText, Math.max(0, innerW - closeWidth - 1), "", true);
477
- const headerGap = Math.max(1, innerW - visibleWidth(headerLeft) - closeWidth);
478
- lines.push(row(headerLeft + " ".repeat(headerGap) + closeLabel));
588
+ const percent = getSessionContextPercent(this.activity?.session ?? this.record.session);
589
+ const tokenText = formatSessionTokens(tokens, percent, th, this.record.compactionCount);
590
+ const costText = this.showCost ? formatCost(getLifetimeCost(usage)) : "";
591
+ headerParts.push(costText ? `${tokenText} ${costText}` : tokenText);
592
+ }
593
+ const styledName = renderAgentName(this.record.type, th, { bold: true, fallbackColor: "accent" });
594
+ const headerText = `${statusIcon} ${styledName}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
595
+ const previewLabel = this.hoveredPreview ? th.fg("text", th.bold("[preview]")) : th.fg("dim", "[preview]");
596
+ const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[Esc]")) : th.fg("dim", "[Esc]");
597
+ const actionsWidth = visibleWidth(previewLabel) + 1 + visibleWidth(closeLabel);
598
+ const headerLeft = truncateToWidth(headerText, Math.max(0, innerW - actionsWidth - 1), "", true);
599
+ const headerGap = Math.max(1, innerW - visibleWidth(headerLeft) - actionsWidth);
600
+ lines.push(row(headerLeft + " ".repeat(headerGap) + previewLabel + " " + closeLabel));
479
601
  const invocationLine = this.invocationLine();
480
- if (invocationLine) lines.push(row(invocationLine));
602
+ lines.push(row(invocationLine ?? ""));
481
603
  lines.push(hrMid);
482
604
 
483
- const contentLines = this.buildContentLines(innerW);
605
+ this.buildContentLines(contentWidth);
606
+ const contentLines = this.cachedLines;
484
607
  const viewportHeight = this.viewportHeight();
485
608
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
486
609
  if (this.autoScroll) this.scrollOffset = maxScroll;
@@ -494,7 +617,7 @@ export class ConversationViewer implements Component {
494
617
  const block = this.cachedMessageBlocks[this.currentMessageIndex()];
495
618
  if (block) {
496
619
  const header = renderConversationRoleHeader(block, this.theme);
497
- displayed[0] = { text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true };
620
+ displayed[0] = makeRenderLine({ text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true });
498
621
  }
499
622
  }
500
623
  for (let i = 0; i < displayed.length; i++) {
@@ -510,9 +633,14 @@ export class ConversationViewer implements Component {
510
633
  const railGlyph = currentBlock?.kind === "tool" ? "▌" : "▎";
511
634
  text = text.startsWith(" ") ? th.fg(railColor, railGlyph) + text.slice(1) : th.fg(railColor, railGlyph) + text;
512
635
  }
513
- lines.push(row(text));
636
+ const plainText = stripAnsi(text);
637
+ lines.push(plainText.includes("... (truncated,")
638
+ ? th.fg("border", "│") + " " + text + th.fg("border", "│")
639
+ : contentRow(text, i, contentLines.length, viewportHeight, visibleStart));
640
+ }
641
+ for (let i = displayed.length; i < viewportHeight; i++) {
642
+ lines.push(contentRow("", i, contentLines.length, viewportHeight, visibleStart));
514
643
  }
515
- for (let i = displayed.length; i < viewportHeight; i++) lines.push(row(""));
516
644
 
517
645
  lines.push(hrMid);
518
646
  if (this.composer) {
@@ -529,14 +657,21 @@ export class ConversationViewer implements Component {
529
657
  lines.push(row(truncateToWidth(th.fg("accent", query) + th.fg("dim", matchCount) + hints, innerW, "...", true)));
530
658
  } else {
531
659
  const actions: string[] = [];
532
- if (this.canSteer()) actions.push(th.fg("dim", "e steer"));
660
+ if (this.canSteer()) actions.push(th.fg("dim", innerW < 100 ? "Enter steer (e steer)" : "e steer"));
533
661
  if (this.isStoppable()) actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
534
662
  const editHint = this.isLive() ? " O" : "";
535
- const shortcutHints = [
536
- `j/k scroll · J/K messages · [/] tools · w preview · g/G t M o${editHint} /n N q`,
537
- `[/] tools · w preview · M o${editHint} /n N q`,
538
- `[/] tools · w · M o${editHint} /n N q`,
539
- ];
663
+ const markdownHint = innerW < 100 ? `m ${MARKDOWN_MODE_LABELS[this.currentMarkdownMode()]}` : "";
664
+ const shortcutHints = innerW < 100
665
+ ? [
666
+ `j/k scroll · J/K messages · [/] tools · w preview · g/G t ${markdownHint} M o${editHint} /n N q`,
667
+ `[/] tools · w preview · ${markdownHint} · M o${editHint} /n N q`,
668
+ `${markdownHint} · M o${editHint} /n N · Esc close`,
669
+ ]
670
+ : [
671
+ `j/k scroll · J/K messages · [/] tools · w preview · g/G t M o${editHint} /n N q`,
672
+ `[/] tools · w preview · M o${editHint} /n N q`,
673
+ `[/] tools · w · M o${editHint} /n N q`,
674
+ ];
540
675
  const scrollPct = contentLines.length <= viewportHeight ? "100%" : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
541
676
  const count = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
542
677
  const countWidth = visibleWidth(count);
@@ -577,6 +712,7 @@ export class ConversationViewer implements Component {
577
712
  if (this.closed) return;
578
713
  if (this.hasFocusedBlock) this.selectedBlockId = this.currentBlock()?.id;
579
714
  this.blocks = formatConversationMessages(this.session.messages);
715
+ this.lastSessionKey = this.sessionKey();
580
716
  this.timeline.setBlocks(this.blocks);
581
717
  this.cacheVersion++;
582
718
  this.invalidate();
@@ -595,7 +731,7 @@ export class ConversationViewer implements Component {
595
731
  const end = Math.min(this.cachedLines.length, start + Math.max(0, range.height));
596
732
  for (let index = start; index < end; index++) {
597
733
  const line = snapshotLines[index];
598
- if (line) this.cachedLines[index] = { ...line };
734
+ if (line) this.cachedLines[index] = makeRenderLine({ ...line });
599
735
  }
600
736
  }
601
737
  }
@@ -613,46 +749,125 @@ export class ConversationViewer implements Component {
613
749
  return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
614
750
  }
615
751
 
752
+ /** Full inner width includes the one column reserved for the transcript rail. */
753
+ private transcriptWidth(innerW: number): number {
754
+ return Math.max(1, innerW - 1);
755
+ }
756
+
616
757
  private chromeLines(): number {
617
- return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
758
+ return CHROME_LINES_BASE + 1 + (this.composer ? 1 : 0);
618
759
  }
619
760
 
620
761
  private invocationLine(): string | undefined {
621
762
  const liveModel = this.isLive() ? this.record.session?.model : undefined;
622
- const invocation = this.record.invocation
763
+ const captured = this.record.invocation as (AgentRecord["invocation"] & {
764
+ effectiveModelName?: string;
765
+ effectiveModelId?: string;
766
+ effectiveThinking?: EffectiveThinkingLevel;
767
+ }) | undefined;
768
+ const invocation = captured
623
769
  ? {
624
- ...this.record.invocation,
625
- ...(liveModel && { effectiveModelName: liveModel.name ?? liveModel.id }),
626
- ...(this.isLive() && { effectiveThinking: this.record.session?.thinkingLevel }),
770
+ ...captured,
771
+ ...(captured.modelName === undefined && captured.effectiveModelName !== undefined && { modelName: captured.effectiveModelName }),
772
+ ...(captured.modelId === undefined && captured.effectiveModelId !== undefined && { modelId: captured.effectiveModelId }),
773
+ ...(captured.thinking === undefined && captured.effectiveThinking !== undefined && { thinking: captured.effectiveThinking }),
774
+ ...(liveModel && { modelName: liveModel.name ?? liveModel.id, modelId: `${liveModel.provider}/${liveModel.id}` }),
775
+ ...(this.isLive() && { thinking: this.record.session?.thinkingLevel }),
627
776
  }
628
- : liveModel || (this.isLive() && this.record.session?.thinkingLevel)
629
- ? {
630
- effectiveModelName: liveModel?.name ?? liveModel?.id,
631
- effectiveThinking: this.record.session?.thinkingLevel,
632
- }
777
+ : liveModel
778
+ ? { modelName: liveModel.name ?? liveModel.id, modelId: `${liveModel.provider}/${liveModel.id}`, thinking: this.record.session?.thinkingLevel }
633
779
  : undefined;
634
- if (!invocation) return this.theme.fg("dim", " ↳ model: unknown · thinking: unknown");
635
- const { modelName, tags } = buildInvocationTags(invocation);
636
- const effectiveModelName = invocation.effectiveModelName ?? modelName;
637
- const parts = effectiveModelName ? [`model: ${effectiveModelName}`, ...tags] : ["model: inherit", ...tags];
638
- return this.theme.fg("dim", ` ↳ ${parts.join(" · ")}`);
780
+ if (!invocation) return undefined;
781
+ const { modelName, modelId, tags } = buildInvocationTags(invocation);
782
+ const effectiveModelName = modelId ?? modelName;
783
+ if (!effectiveModelName && tags.length === 0) return undefined;
784
+ const displayModel = captured?.effectiveModelName !== undefined
785
+ ? `model: ${captured.effectiveModelName}`
786
+ : effectiveModelName;
787
+ return this.theme.fg("dim", ` ↳ ${[displayModel, ...tags].filter(Boolean).join(" · ")}`);
788
+ }
789
+
790
+ private currentMarkdownMode(): ViewerMarkdownMode {
791
+ return this.markdownModeOverride ?? this.viewerMarkdown?.() ?? "assistant";
792
+ }
793
+
794
+ private cycleMarkdownMode(): void {
795
+ const current = this.currentMarkdownMode();
796
+ const next = MARKDOWN_MODES[(MARKDOWN_MODES.indexOf(current) + 1) % MARKDOWN_MODES.length]!;
797
+ this.markdownModeOverride = next;
798
+ this.onMarkdownMode?.(next);
799
+ this.timeline.setMarkdownMode(() => this.currentMarkdownMode());
800
+ this.invalidate();
801
+ this.tui.requestRender();
639
802
  }
640
803
 
641
- private buildContentLines(width: number): RenderLine[] {
642
- if (width <= 0) return [];
643
- if (this.cachedVersion === this.cacheVersion && this.cachedWidth === width) return this.cachedLines;
804
+ private sessionKey(): string {
805
+ return this.session.messages.map(message => {
806
+ const value = message as unknown as { role?: unknown; content?: unknown; output?: unknown; command?: unknown };
807
+ const content = value.content;
808
+ const summarize = (text: string): string => `${text.length}:${text.slice(0, 24)}:${text.slice(-24)}`;
809
+ const contentSummary = typeof content === "string"
810
+ ? summarize(content)
811
+ : Array.isArray(content)
812
+ ? content.map(part => typeof part === "object" && part && "text" in part && typeof part.text === "string" ? summarize(part.text) : "").join("|")
813
+ : "";
814
+ return `${String(value.role)}:${contentSummary}:${typeof value.output === "string" ? summarize(value.output) : ""}:${typeof value.command === "string" ? summarize(value.command) : ""}`;
815
+ }).join("\u0001");
816
+ }
817
+
818
+ private blockSignature(block: ConversationBlock): string {
819
+ let resultLength = 0;
820
+ const content = block.toolResult?.content;
821
+ if (typeof content === "string") resultLength = content.length;
822
+ else if (Array.isArray(content)) resultLength = content.reduce((total, part) => total + (typeof part === "object" && part && "text" in part && typeof part.text === "string" ? part.text.length : 0), 0);
823
+ return `${block.id}\u0000${block.markdown}\u0000${block.fullText}\u0000${block.toolLine ?? ""}\u0000${resultLength}`;
824
+ }
825
+
826
+ private syncBlocksFromSession(): void {
827
+ const sessionKey = this.sessionKey();
828
+ if (sessionKey === this.lastSessionKey) return;
829
+ this.lastSessionKey = sessionKey;
830
+ const refreshed = formatConversationMessages(this.session.messages);
831
+ const sameShape = refreshed.length === this.blocks.length && refreshed.every((block, index) => block.id === this.blocks[index]?.id);
832
+ if (!sameShape) {
833
+ this.blocks = refreshed;
834
+ this.timeline.setBlocks(this.blocks);
835
+ this.cacheVersion++;
836
+ this.cachedVersion = -1;
837
+ return;
838
+ }
839
+ let changed = false;
840
+ for (let index = 0; index < refreshed.length; index++) {
841
+ const current = this.blocks[index]!;
842
+ const next = refreshed[index]!;
843
+ if (this.blockSignature(current) !== this.blockSignature(next)) changed = true;
844
+ Object.assign(current, next);
845
+ }
846
+ if (changed) {
847
+ this.cacheVersion++;
848
+ this.cachedVersion = -1;
849
+ }
850
+ }
851
+
852
+ private buildContentLines(width: number): PublicRenderLines {
853
+ if (width <= 0) return new PublicRenderLines([]);
854
+ this.syncBlocksFromSession();
855
+ if (this.cachedVersion === this.cacheVersion && this.cachedWidth === width) {
856
+ this.timeline.render(width);
857
+ return publicRenderLines(this.cachedLines);
858
+ }
644
859
  this.timeline.setShowTools(this.showTools);
645
860
  this.timeline.render(width);
646
861
  const timelineSnapshot = this.timeline.getSnapshot();
647
862
  const lines: RenderLine[] = timelineSnapshot.lines.map((line) => {
648
863
  const rawText = typeof line?.text === "string" ? line.text : "";
649
864
  const text = truncateToWidth(rawText, width, "", true);
650
- return {
865
+ return makeRenderLine({
651
866
  text,
652
867
  plain: stripAnsi(text),
653
868
  blockIndex: typeof line?.blockIndex === "number" ? line.blockIndex : -1,
654
869
  ...(line?.railable ? { railable: true } : {}),
655
- };
870
+ });
656
871
  });
657
872
  this.cachedMessageStarts = [...timelineSnapshot.messageStarts];
658
873
  this.cachedMessageBlocks = [...timelineSnapshot.messageBlocks];
@@ -672,13 +887,13 @@ export class ConversationViewer implements Component {
672
887
  if (this.record.status === "running" && this.activity) {
673
888
  const act = describeActivity(this.activity.activeTools, this.activity.responseText);
674
889
  const text = `▍ ${act}`;
675
- lines.push({ text: truncateToWidth(this.theme.fg("accent", "▍ ") + this.theme.fg("dim", act), width), plain: text, blockIndex: -1 });
890
+ lines.push(makeRenderLine({ text: truncateToWidth(this.theme.fg("accent", "▍ ") + this.theme.fg("dim", act), width), plain: text, blockIndex: -1 }));
676
891
  }
677
892
  this.cachedWidth = width;
678
893
  this.cachedVersion = this.cacheVersion;
679
894
  this.cachedLines = lines;
680
895
  if (this.searchMode || this.searchQuery) this.recomputeMatches();
681
- return lines;
896
+ return publicRenderLines(lines);
682
897
  }
683
898
 
684
899
  private currentMessageIndex(): number {
@@ -744,7 +959,7 @@ export class ConversationViewer implements Component {
744
959
  private moveToolFocus(direction: 1 | -1): void {
745
960
  if (!this.timeline.moveFocus(direction)) return;
746
961
  const focused = this.timeline.getFocusedToolCallId();
747
- this.buildContentLines(this.lastInnerW || 80);
962
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
748
963
  const index = this.cachedMessageBlocks.findIndex((block) => block.toolCallId === focused);
749
964
  if (index >= 0) {
750
965
  this.setFocusFromMessageIndex(index);
@@ -755,7 +970,7 @@ export class ConversationViewer implements Component {
755
970
  }
756
971
 
757
972
  private focusNearbyTool(direction: 1 | -1): void {
758
- this.buildContentLines(this.lastInnerW || 80);
973
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
759
974
  const toolIndices = this.cachedMessageBlocks
760
975
  .map((block, index) => block.kind === "tool" && block.toolCallId ? index : -1)
761
976
  .filter((index): index is number => index >= 0);
@@ -786,7 +1001,7 @@ export class ConversationViewer implements Component {
786
1001
  }
787
1002
 
788
1003
  private openFocusedToolPreview(): void {
789
- this.buildContentLines(this.lastInnerW || 80);
1004
+ this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
790
1005
  const focusedToolCallId = this.timeline.getFocusedToolCallId();
791
1006
  const block = focusedToolCallId
792
1007
  ? this.cachedMessageBlocks.find((candidate) => candidate.toolCallId === focusedToolCallId)