@esso0428/pi-subagents 0.17.6 → 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 (260) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/dist/abortable.d.ts +13 -0
  4. package/dist/abortable.d.ts.map +1 -0
  5. package/dist/abortable.js +43 -0
  6. package/dist/abortable.js.map +1 -0
  7. package/dist/agent-color.d.ts +36 -0
  8. package/dist/agent-color.d.ts.map +1 -0
  9. package/dist/agent-color.js +124 -0
  10. package/dist/agent-color.js.map +1 -0
  11. package/dist/agent-file-toggle.d.ts +126 -0
  12. package/dist/agent-file-toggle.d.ts.map +1 -0
  13. package/dist/agent-file-toggle.js +259 -0
  14. package/dist/agent-file-toggle.js.map +1 -0
  15. package/dist/agent-history.d.ts +4 -0
  16. package/dist/agent-history.d.ts.map +1 -1
  17. package/dist/agent-history.js +47 -1
  18. package/dist/agent-history.js.map +1 -1
  19. package/dist/agent-manager.d.ts +370 -56
  20. package/dist/agent-manager.d.ts.map +1 -1
  21. package/dist/agent-manager.js +1123 -409
  22. package/dist/agent-manager.js.map +1 -1
  23. package/dist/agent-runner.d.ts +100 -10
  24. package/dist/agent-runner.d.ts.map +1 -1
  25. package/dist/agent-runner.js +166 -21
  26. package/dist/agent-runner.js.map +1 -1
  27. package/dist/agent-types.d.ts +57 -5
  28. package/dist/agent-types.d.ts.map +1 -1
  29. package/dist/agent-types.js +164 -32
  30. package/dist/agent-types.js.map +1 -1
  31. package/dist/child-context.d.ts +3 -0
  32. package/dist/child-context.d.ts.map +1 -0
  33. package/dist/child-context.js +13 -0
  34. package/dist/child-context.js.map +1 -0
  35. package/dist/cross-extension-rpc.d.ts +23 -3
  36. package/dist/cross-extension-rpc.d.ts.map +1 -1
  37. package/dist/cross-extension-rpc.js +79 -17
  38. package/dist/cross-extension-rpc.js.map +1 -1
  39. package/dist/custom-agents.d.ts +38 -1
  40. package/dist/custom-agents.d.ts.map +1 -1
  41. package/dist/custom-agents.js +164 -12
  42. package/dist/custom-agents.js.map +1 -1
  43. package/dist/index.d.ts +34 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1908 -495
  46. package/dist/index.js.map +1 -1
  47. package/dist/invocation-config.d.ts +87 -2
  48. package/dist/invocation-config.d.ts.map +1 -1
  49. package/dist/invocation-config.js +71 -3
  50. package/dist/invocation-config.js.map +1 -1
  51. package/dist/mention-clone.d.ts +88 -0
  52. package/dist/mention-clone.d.ts.map +1 -0
  53. package/dist/mention-clone.js +154 -0
  54. package/dist/mention-clone.js.map +1 -0
  55. package/dist/mention.d.ts +82 -0
  56. package/dist/mention.d.ts.map +1 -0
  57. package/dist/mention.js +132 -0
  58. package/dist/mention.js.map +1 -0
  59. package/dist/model-resolver.d.ts +17 -0
  60. package/dist/model-resolver.d.ts.map +1 -1
  61. package/dist/model-resolver.js +15 -0
  62. package/dist/model-resolver.js.map +1 -1
  63. package/dist/model-scope.d.ts +50 -0
  64. package/dist/model-scope.d.ts.map +1 -0
  65. package/dist/model-scope.js +49 -0
  66. package/dist/model-scope.js.map +1 -0
  67. package/dist/nested-tools.d.ts +57 -0
  68. package/dist/nested-tools.d.ts.map +1 -0
  69. package/dist/nested-tools.js +301 -0
  70. package/dist/nested-tools.js.map +1 -0
  71. package/dist/output-file.d.ts +22 -3
  72. package/dist/output-file.d.ts.map +1 -1
  73. package/dist/output-file.js +58 -7
  74. package/dist/output-file.js.map +1 -1
  75. package/dist/prompts.d.ts +23 -0
  76. package/dist/prompts.d.ts.map +1 -1
  77. package/dist/prompts.js +20 -2
  78. package/dist/prompts.js.map +1 -1
  79. package/dist/schedule.d.ts.map +1 -1
  80. package/dist/schedule.js +36 -15
  81. package/dist/schedule.js.map +1 -1
  82. package/dist/settings.d.ts +228 -2
  83. package/dist/settings.d.ts.map +1 -1
  84. package/dist/settings.js +94 -0
  85. package/dist/settings.js.map +1 -1
  86. package/dist/status-note.d.ts +49 -1
  87. package/dist/status-note.d.ts.map +1 -1
  88. package/dist/status-note.js +62 -1
  89. package/dist/status-note.js.map +1 -1
  90. package/dist/structured-output.d.ts +62 -0
  91. package/dist/structured-output.d.ts.map +1 -0
  92. package/dist/structured-output.js +113 -0
  93. package/dist/structured-output.js.map +1 -0
  94. package/dist/types.d.ts +176 -10
  95. package/dist/types.d.ts.map +1 -1
  96. package/dist/ui/agent-mention.d.ts +83 -0
  97. package/dist/ui/agent-mention.d.ts.map +1 -0
  98. package/dist/ui/agent-mention.js +188 -0
  99. package/dist/ui/agent-mention.js.map +1 -0
  100. package/dist/ui/agent-widget.d.ts +96 -75
  101. package/dist/ui/agent-widget.d.ts.map +1 -1
  102. package/dist/ui/agent-widget.js +397 -420
  103. package/dist/ui/agent-widget.js.map +1 -1
  104. package/dist/ui/conversation-blocks.d.ts.map +1 -1
  105. package/dist/ui/conversation-blocks.js +6 -0
  106. package/dist/ui/conversation-blocks.js.map +1 -1
  107. package/dist/ui/conversation-timeline.d.ts +10 -2
  108. package/dist/ui/conversation-timeline.d.ts.map +1 -1
  109. package/dist/ui/conversation-timeline.js +130 -23
  110. package/dist/ui/conversation-timeline.js.map +1 -1
  111. package/dist/ui/conversation-viewer.d.ts +15 -5
  112. package/dist/ui/conversation-viewer.d.ts.map +1 -1
  113. package/dist/ui/conversation-viewer.js +202 -50
  114. package/dist/ui/conversation-viewer.js.map +1 -1
  115. package/dist/ui/fleet-list.d.ts +198 -0
  116. package/dist/ui/fleet-list.d.ts.map +1 -0
  117. package/dist/ui/fleet-list.js +487 -0
  118. package/dist/ui/fleet-list.js.map +1 -0
  119. package/dist/ui/schedule-menu.d.ts.map +1 -1
  120. package/dist/ui/schedule-menu.js +6 -7
  121. package/dist/ui/schedule-menu.js.map +1 -1
  122. package/dist/ui/select-item.d.ts +28 -0
  123. package/dist/ui/select-item.d.ts.map +1 -0
  124. package/dist/ui/select-item.js +35 -0
  125. package/dist/ui/select-item.js.map +1 -0
  126. package/dist/ui/workflow-card.d.ts +176 -0
  127. package/dist/ui/workflow-card.d.ts.map +1 -0
  128. package/dist/ui/workflow-card.js +333 -0
  129. package/dist/ui/workflow-card.js.map +1 -0
  130. package/dist/ui/workflow-dialog.d.ts +306 -0
  131. package/dist/ui/workflow-dialog.d.ts.map +1 -0
  132. package/dist/ui/workflow-dialog.js +844 -0
  133. package/dist/ui/workflow-dialog.js.map +1 -0
  134. package/dist/ui/workflow-menu.d.ts +61 -0
  135. package/dist/ui/workflow-menu.d.ts.map +1 -0
  136. package/dist/ui/workflow-menu.js +148 -0
  137. package/dist/ui/workflow-menu.js.map +1 -0
  138. package/dist/usage.d.ts +86 -1
  139. package/dist/usage.d.ts.map +1 -1
  140. package/dist/usage.js +72 -1
  141. package/dist/usage.js.map +1 -1
  142. package/dist/workflow/collisions.d.ts +96 -0
  143. package/dist/workflow/collisions.d.ts.map +1 -0
  144. package/dist/workflow/collisions.js +89 -0
  145. package/dist/workflow/collisions.js.map +1 -0
  146. package/dist/workflow/entry.d.ts +33 -0
  147. package/dist/workflow/entry.d.ts.map +1 -0
  148. package/dist/workflow/entry.js +30 -0
  149. package/dist/workflow/entry.js.map +1 -0
  150. package/dist/workflow/host.d.ts +63 -0
  151. package/dist/workflow/host.d.ts.map +1 -0
  152. package/dist/workflow/host.js +363 -0
  153. package/dist/workflow/host.js.map +1 -0
  154. package/dist/workflow/journal.d.ts +98 -0
  155. package/dist/workflow/journal.d.ts.map +1 -0
  156. package/dist/workflow/journal.js +121 -0
  157. package/dist/workflow/journal.js.map +1 -0
  158. package/dist/workflow/json-schema.d.ts +52 -0
  159. package/dist/workflow/json-schema.d.ts.map +1 -0
  160. package/dist/workflow/json-schema.js +112 -0
  161. package/dist/workflow/json-schema.js.map +1 -0
  162. package/dist/workflow/meta.d.ts +68 -0
  163. package/dist/workflow/meta.d.ts.map +1 -0
  164. package/dist/workflow/meta.js +318 -0
  165. package/dist/workflow/meta.js.map +1 -0
  166. package/dist/workflow/progress.d.ts +225 -0
  167. package/dist/workflow/progress.d.ts.map +1 -0
  168. package/dist/workflow/progress.js +362 -0
  169. package/dist/workflow/progress.js.map +1 -0
  170. package/dist/workflow/runtime.d.ts +335 -0
  171. package/dist/workflow/runtime.d.ts.map +1 -0
  172. package/dist/workflow/runtime.js +831 -0
  173. package/dist/workflow/runtime.js.map +1 -0
  174. package/dist/workflow/saved.d.ts +91 -0
  175. package/dist/workflow/saved.d.ts.map +1 -0
  176. package/dist/workflow/saved.js +204 -0
  177. package/dist/workflow/saved.js.map +1 -0
  178. package/dist/workflow/task.d.ts +137 -0
  179. package/dist/workflow/task.d.ts.map +1 -0
  180. package/dist/workflow/task.js +208 -0
  181. package/dist/workflow/task.js.map +1 -0
  182. package/dist/workflow/tool-description.d.ts +39 -0
  183. package/dist/workflow/tool-description.d.ts.map +1 -0
  184. package/dist/workflow/tool-description.js +200 -0
  185. package/dist/workflow/tool-description.js.map +1 -0
  186. package/dist/workflow/worker-source.d.ts +48 -0
  187. package/dist/workflow/worker-source.d.ts.map +1 -0
  188. package/dist/workflow/worker-source.js +779 -0
  189. package/dist/workflow/worker-source.js.map +1 -0
  190. package/dist/worktree.d.ts +10 -3
  191. package/dist/worktree.d.ts.map +1 -1
  192. package/dist/worktree.js +58 -54
  193. package/dist/worktree.js.map +1 -1
  194. package/dist/xml.d.ts +11 -0
  195. package/dist/xml.d.ts.map +1 -0
  196. package/dist/xml.js +13 -0
  197. package/dist/xml.js.map +1 -0
  198. package/docs/rpc.md +183 -0
  199. package/docs/superpowers/plans/2026-09-30-upstream-event-workflow-partial-history.md +195 -0
  200. package/docs/superpowers/specs/2026-09-30-upstream-event-workflow-partial-history-design.md +49 -0
  201. package/docs/workflows.md +437 -0
  202. package/examples/agent-tool-description.md +7 -7
  203. package/examples/workflows/compose.js +51 -0
  204. package/examples/workflows/fan-out-audit.js +47 -0
  205. package/examples/workflows/gated-fix.js +60 -0
  206. package/examples/workflows/lib/count-child.js +27 -0
  207. package/examples/workflows/review-panel.js +63 -0
  208. package/examples/workflows/structured-findings.js +78 -0
  209. package/package.json +1 -1
  210. package/src/abortable.ts +43 -0
  211. package/src/agent-color.ts +161 -0
  212. package/src/agent-file-toggle.ts +269 -0
  213. package/src/agent-history.ts +54 -2
  214. package/src/agent-manager.ts +1263 -402
  215. package/src/agent-runner.ts +251 -27
  216. package/src/agent-types.ts +188 -32
  217. package/src/child-context.ts +15 -0
  218. package/src/cross-extension-rpc.ts +96 -20
  219. package/src/custom-agents.ts +170 -13
  220. package/src/index.ts +2024 -537
  221. package/src/invocation-config.ts +118 -3
  222. package/src/mention-clone.ts +196 -0
  223. package/src/mention.ts +141 -0
  224. package/src/model-resolver.ts +18 -0
  225. package/src/model-scope.ts +70 -0
  226. package/src/nested-tools.ts +424 -0
  227. package/src/output-file.ts +61 -6
  228. package/src/prompts.ts +45 -2
  229. package/src/schedule.ts +35 -14
  230. package/src/settings.ts +312 -2
  231. package/src/status-note.ts +66 -1
  232. package/src/structured-output.ts +130 -0
  233. package/src/types.ts +177 -10
  234. package/src/ui/agent-mention.ts +216 -0
  235. package/src/ui/agent-widget.ts +389 -441
  236. package/src/ui/conversation-blocks.ts +6 -0
  237. package/src/ui/conversation-timeline.ts +139 -25
  238. package/src/ui/conversation-viewer.ts +212 -48
  239. package/src/ui/fleet-list.ts +558 -0
  240. package/src/ui/schedule-menu.ts +9 -8
  241. package/src/ui/select-item.ts +45 -0
  242. package/src/ui/workflow-card.ts +470 -0
  243. package/src/ui/workflow-dialog.ts +1115 -0
  244. package/src/ui/workflow-menu.ts +193 -0
  245. package/src/usage.ts +109 -2
  246. package/src/workflow/collisions.ts +123 -0
  247. package/src/workflow/entry.ts +47 -0
  248. package/src/workflow/host.ts +403 -0
  249. package/src/workflow/journal.ts +164 -0
  250. package/src/workflow/json-schema.ts +128 -0
  251. package/src/workflow/meta.ts +325 -0
  252. package/src/workflow/progress.ts +550 -0
  253. package/src/workflow/runtime.ts +1219 -0
  254. package/src/workflow/saved.ts +217 -0
  255. package/src/workflow/task.ts +302 -0
  256. package/src/workflow/tool-description.ts +200 -0
  257. package/src/workflow/worker-source.ts +781 -0
  258. package/src/worktree.ts +69 -55
  259. package/src/xml.ts +13 -0
  260. 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;
@@ -234,6 +279,11 @@ export class ConversationViewer implements Component {
234
279
  private steerHistoryDraft = "";
235
280
  private timeline: ConversationTimeline;
236
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;
237
287
 
238
288
  constructor(
239
289
  private tui: TUI,
@@ -248,14 +298,22 @@ export class ConversationViewer implements Component {
248
298
  keybindings?: ViewerKeybindings,
249
299
  /** Send a steering message to the agent. */
250
300
  private onSteer?: (message: string) => void,
251
- /** Optional clipboard/nvim operation context, appended for compatibility. */
252
- 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,
253
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;
254
310
  this.keys = createViewerKeys(keybindings);
255
311
  this.blocks = formatConversationMessages(session.messages);
312
+ this.lastSessionKey = this.sessionKey();
256
313
  this.timeline = new ConversationTimeline(tui, theme, {
257
- cwd: operations?.ctx.cwd,
314
+ cwd: this.operations?.ctx.cwd,
258
315
  record,
316
+ markdownMode: () => this.currentMarkdownMode(),
259
317
  onChange: (change) => this.handleTimelineChange(change),
260
318
  });
261
319
  this.timeline.setBlocks(this.blocks);
@@ -285,6 +343,11 @@ export class ConversationViewer implements Component {
285
343
  this.handleSearchInput(data);
286
344
  return;
287
345
  }
346
+ if (matchesKey(data, Key.ctrl("c"))) {
347
+ this.closed = true;
348
+ this.done(undefined);
349
+ return;
350
+ }
288
351
  if (matchesKey(data, "escape") || matchesKey(data, "q")) {
289
352
  this.closed = true;
290
353
  this.done(undefined);
@@ -296,6 +359,11 @@ export class ConversationViewer implements Component {
296
359
  this.timeline.toggleFocusedTool();
297
360
  return;
298
361
  }
362
+ if (this.canSteer()) {
363
+ this.stopArmed = false;
364
+ this.openComposer();
365
+ return;
366
+ }
299
367
  }
300
368
  if (data === "e" && this.canSteer()) {
301
369
  this.stopArmed = false;
@@ -352,6 +420,8 @@ export class ConversationViewer implements Component {
352
420
  this.gotoMatch(-1);
353
421
  } else if (data === "M") {
354
422
  void this.copyCurrentMessage();
423
+ } else if (data === "m") {
424
+ this.cycleMarkdownMode();
355
425
  } else if (data === "o") {
356
426
  void this.viewCurrentInNvim();
357
427
  } else if (data === "O" && this.isLive()) {
@@ -394,12 +464,13 @@ export class ConversationViewer implements Component {
394
464
  return previewChanged || closeChanged ? { handled: true, render: true } : undefined;
395
465
  }
396
466
  if (event.x === railX) return previewChanged || closeChanged ? { handled: true, render: true } : undefined;
397
- const contentLines = this.buildContentLines(contentWidth);
467
+ this.buildContentLines(contentWidth);
468
+ const contentLines = this.cachedLines;
398
469
  const viewportHeight = this.viewportHeight();
399
470
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
400
471
  if (this.autoScroll) this.scrollOffset = maxScroll;
401
472
  const visibleStart = Math.min(this.scrollOffset, maxScroll);
402
- const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
473
+ const contentOrigin = 4;
403
474
  const contentY = event.y - contentOrigin + visibleStart;
404
475
  const timelineResult = this.timeline.handleMouse({ ...event, y: contentY });
405
476
  return timelineResult ?? (previewChanged || closeChanged ? { handled: true, render: true } : undefined);
@@ -434,7 +505,7 @@ export class ConversationViewer implements Component {
434
505
  const viewportHeight = this.viewportHeight();
435
506
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
436
507
  const visibleStart = Math.min(this.scrollOffset, maxScroll);
437
- const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
508
+ const contentOrigin = 4;
438
509
  if (event.type === "wheel") {
439
510
  if (event.y < contentOrigin || event.y >= contentOrigin + viewportHeight) return undefined;
440
511
  this.scrollBy(Math.trunc(event.wheelDelta ?? 0));
@@ -491,8 +562,12 @@ export class ConversationViewer implements Component {
491
562
  const pad = (s: string, len: number) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
492
563
  const row = (content: string) => th.fg("border", "│") + " " + truncateToWidth(pad(content, innerW), innerW, "...", true) + " " + th.fg("border", "│");
493
564
  const contentRow = (content: string, lineIndex: number, totalLines: number, viewportHeight: number, offset: number) => {
494
- const text = truncateToWidth(pad(content, contentWidth), contentWidth, "...", true);
565
+ const isTruncationNote = stripAnsi(content).includes("... (truncated,");
566
+ const text = isTruncationNote
567
+ ? truncateToWidth(content, contentWidth, "...", true)
568
+ : truncateToWidth(pad(content, contentWidth), contentWidth, "...", true);
495
569
  const rail = renderScrollbarCell(th, totalLines, viewportHeight, offset, lineIndex, false);
570
+ if (isTruncationNote) return th.fg("border", "│") + " " + text + th.fg("border", "│");
496
571
  return th.fg("border", "│") + " " + text + rail + " " + th.fg("border", "│");
497
572
  };
498
573
  const hrTop = th.fg("border", `╭${"─".repeat(Math.max(0, width - 2))}╮`);
@@ -500,7 +575,6 @@ export class ConversationViewer implements Component {
500
575
  const hrMid = row(th.fg("dim", "─".repeat(innerW)));
501
576
 
502
577
  lines.push(hrTop);
503
- const name = getDisplayName(this.record.type);
504
578
  const modeLabel = getPromptModeLabel(this.record.type);
505
579
  const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
506
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", "○");
@@ -508,12 +582,16 @@ export class ConversationViewer implements Component {
508
582
  const headerParts: string[] = [duration];
509
583
  const toolUses = this.activity?.toolUses ?? this.record.toolUses;
510
584
  if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
511
- const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
585
+ const usage = this.activity?.lifetimeUsage ?? this.record.lifetimeUsage;
586
+ const tokens = getLifetimeTotal(usage);
512
587
  if (tokens > 0) {
513
- const percent = getSessionContextPercent(this.activity?.session);
514
- headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
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);
515
592
  }
516
- const headerText = `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
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(" · "))}`;
517
595
  const previewLabel = this.hoveredPreview ? th.fg("text", th.bold("[preview]")) : th.fg("dim", "[preview]");
518
596
  const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[Esc]")) : th.fg("dim", "[Esc]");
519
597
  const actionsWidth = visibleWidth(previewLabel) + 1 + visibleWidth(closeLabel);
@@ -521,10 +599,11 @@ export class ConversationViewer implements Component {
521
599
  const headerGap = Math.max(1, innerW - visibleWidth(headerLeft) - actionsWidth);
522
600
  lines.push(row(headerLeft + " ".repeat(headerGap) + previewLabel + " " + closeLabel));
523
601
  const invocationLine = this.invocationLine();
524
- if (invocationLine) lines.push(row(invocationLine));
602
+ lines.push(row(invocationLine ?? ""));
525
603
  lines.push(hrMid);
526
604
 
527
- const contentLines = this.buildContentLines(contentWidth);
605
+ this.buildContentLines(contentWidth);
606
+ const contentLines = this.cachedLines;
528
607
  const viewportHeight = this.viewportHeight();
529
608
  const maxScroll = Math.max(0, contentLines.length - viewportHeight);
530
609
  if (this.autoScroll) this.scrollOffset = maxScroll;
@@ -538,7 +617,7 @@ export class ConversationViewer implements Component {
538
617
  const block = this.cachedMessageBlocks[this.currentMessageIndex()];
539
618
  if (block) {
540
619
  const header = renderConversationRoleHeader(block, this.theme);
541
- displayed[0] = { text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true };
620
+ displayed[0] = makeRenderLine({ text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true });
542
621
  }
543
622
  }
544
623
  for (let i = 0; i < displayed.length; i++) {
@@ -554,7 +633,10 @@ export class ConversationViewer implements Component {
554
633
  const railGlyph = currentBlock?.kind === "tool" ? "▌" : "▎";
555
634
  text = text.startsWith(" ") ? th.fg(railColor, railGlyph) + text.slice(1) : th.fg(railColor, railGlyph) + text;
556
635
  }
557
- lines.push(contentRow(text, i, contentLines.length, viewportHeight, visibleStart));
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));
558
640
  }
559
641
  for (let i = displayed.length; i < viewportHeight; i++) {
560
642
  lines.push(contentRow("", i, contentLines.length, viewportHeight, visibleStart));
@@ -575,14 +657,21 @@ export class ConversationViewer implements Component {
575
657
  lines.push(row(truncateToWidth(th.fg("accent", query) + th.fg("dim", matchCount) + hints, innerW, "...", true)));
576
658
  } else {
577
659
  const actions: string[] = [];
578
- 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"));
579
661
  if (this.isStoppable()) actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
580
662
  const editHint = this.isLive() ? " O" : "";
581
- const shortcutHints = [
582
- `j/k scroll · J/K messages · [/] tools · w preview · g/G t M o${editHint} /n N q`,
583
- `[/] tools · w preview · M o${editHint} /n N q`,
584
- `[/] tools · w · M o${editHint} /n N q`,
585
- ];
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
+ ];
586
675
  const scrollPct = contentLines.length <= viewportHeight ? "100%" : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
587
676
  const count = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
588
677
  const countWidth = visibleWidth(count);
@@ -623,6 +712,7 @@ export class ConversationViewer implements Component {
623
712
  if (this.closed) return;
624
713
  if (this.hasFocusedBlock) this.selectedBlockId = this.currentBlock()?.id;
625
714
  this.blocks = formatConversationMessages(this.session.messages);
715
+ this.lastSessionKey = this.sessionKey();
626
716
  this.timeline.setBlocks(this.blocks);
627
717
  this.cacheVersion++;
628
718
  this.invalidate();
@@ -641,7 +731,7 @@ export class ConversationViewer implements Component {
641
731
  const end = Math.min(this.cachedLines.length, start + Math.max(0, range.height));
642
732
  for (let index = start; index < end; index++) {
643
733
  const line = snapshotLines[index];
644
- if (line) this.cachedLines[index] = { ...line };
734
+ if (line) this.cachedLines[index] = makeRenderLine({ ...line });
645
735
  }
646
736
  }
647
737
  }
@@ -665,45 +755,119 @@ export class ConversationViewer implements Component {
665
755
  }
666
756
 
667
757
  private chromeLines(): number {
668
- return CHROME_LINES_BASE + (this.invocationLine() ? 1 : 0) + (this.composer ? 1 : 0);
758
+ return CHROME_LINES_BASE + 1 + (this.composer ? 1 : 0);
669
759
  }
670
760
 
671
761
  private invocationLine(): string | undefined {
672
762
  const liveModel = this.isLive() ? this.record.session?.model : undefined;
673
- 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
674
769
  ? {
675
- ...this.record.invocation,
676
- ...(liveModel && { effectiveModelName: liveModel.name ?? liveModel.id }),
677
- ...(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 }),
678
776
  }
679
- : liveModel || (this.isLive() && this.record.session?.thinkingLevel)
680
- ? {
681
- effectiveModelName: liveModel?.name ?? liveModel?.id,
682
- effectiveThinking: this.record.session?.thinkingLevel,
683
- }
777
+ : liveModel
778
+ ? { modelName: liveModel.name ?? liveModel.id, modelId: `${liveModel.provider}/${liveModel.id}`, thinking: this.record.session?.thinkingLevel }
684
779
  : undefined;
685
- if (!invocation) return this.theme.fg("dim", " ↳ model: unknown · thinking: unknown");
686
- const { modelName, tags } = buildInvocationTags(invocation);
687
- const effectiveModelName = invocation.effectiveModelName ?? modelName;
688
- const parts = effectiveModelName ? [`model: ${effectiveModelName}`, ...tags] : ["model: inherit", ...tags];
689
- 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(" · ")}`);
690
788
  }
691
789
 
692
- private buildContentLines(width: number): RenderLine[] {
693
- if (width <= 0) return [];
694
- if (this.cachedVersion === this.cacheVersion && this.cachedWidth === width) return this.cachedLines;
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();
802
+ }
803
+
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
+ }
695
859
  this.timeline.setShowTools(this.showTools);
696
860
  this.timeline.render(width);
697
861
  const timelineSnapshot = this.timeline.getSnapshot();
698
862
  const lines: RenderLine[] = timelineSnapshot.lines.map((line) => {
699
863
  const rawText = typeof line?.text === "string" ? line.text : "";
700
864
  const text = truncateToWidth(rawText, width, "", true);
701
- return {
865
+ return makeRenderLine({
702
866
  text,
703
867
  plain: stripAnsi(text),
704
868
  blockIndex: typeof line?.blockIndex === "number" ? line.blockIndex : -1,
705
869
  ...(line?.railable ? { railable: true } : {}),
706
- };
870
+ });
707
871
  });
708
872
  this.cachedMessageStarts = [...timelineSnapshot.messageStarts];
709
873
  this.cachedMessageBlocks = [...timelineSnapshot.messageBlocks];
@@ -723,13 +887,13 @@ export class ConversationViewer implements Component {
723
887
  if (this.record.status === "running" && this.activity) {
724
888
  const act = describeActivity(this.activity.activeTools, this.activity.responseText);
725
889
  const text = `▍ ${act}`;
726
- 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 }));
727
891
  }
728
892
  this.cachedWidth = width;
729
893
  this.cachedVersion = this.cacheVersion;
730
894
  this.cachedLines = lines;
731
895
  if (this.searchMode || this.searchQuery) this.recomputeMatches();
732
- return lines;
896
+ return publicRenderLines(lines);
733
897
  }
734
898
 
735
899
  private currentMessageIndex(): number {