@esso0428/pi-subagents 0.17.16 → 0.17.17

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 (359) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/ROADMAP.md +87 -0
  3. package/docs/post-0.17.6-agents-roster-viewer-spec.md +42 -0
  4. package/docs/post-0.17.6-durable-history-spec.md +41 -0
  5. package/docs/post-0.17.6-feature-specs.md +50 -0
  6. package/docs/post-0.17.6-nested-agents-spec.md +38 -0
  7. package/docs/post-0.17.6-recovery-shutdown-spec.md +46 -0
  8. package/docs/post-0.17.6-ui-latency-baseline-spec.md +46 -0
  9. package/docs/post-0.17.6-workflow-rpc-lifecycle-spec.md +54 -0
  10. package/package.json +1 -1
  11. package/src/agent-history.ts +2 -54
  12. package/src/agent-manager.ts +406 -1288
  13. package/src/agent-runner.ts +27 -251
  14. package/src/agent-types.ts +32 -188
  15. package/src/cross-extension-rpc.ts +20 -96
  16. package/src/custom-agents.ts +13 -170
  17. package/src/index.ts +480 -1920
  18. package/src/invocation-config.ts +3 -118
  19. package/src/model-resolver.ts +0 -18
  20. package/src/output-file.ts +6 -61
  21. package/src/prompts.ts +2 -45
  22. package/src/schedule.ts +14 -35
  23. package/src/settings.ts +2 -301
  24. package/src/status-note.ts +1 -66
  25. package/src/types.ts +10 -177
  26. package/src/ui/agent-widget.ts +57 -278
  27. package/src/ui/conversation-blocks.ts +0 -6
  28. package/src/ui/conversation-timeline.ts +25 -139
  29. package/src/ui/conversation-viewer.ts +48 -212
  30. package/src/ui/schedule-menu.ts +8 -9
  31. package/src/usage.ts +2 -109
  32. package/src/worktree.ts +55 -69
  33. package/dist/abortable.d.ts +0 -13
  34. package/dist/abortable.d.ts.map +0 -1
  35. package/dist/abortable.js +0 -43
  36. package/dist/abortable.js.map +0 -1
  37. package/dist/agent-color.d.ts +0 -36
  38. package/dist/agent-color.d.ts.map +0 -1
  39. package/dist/agent-color.js +0 -124
  40. package/dist/agent-color.js.map +0 -1
  41. package/dist/agent-file-toggle.d.ts +0 -126
  42. package/dist/agent-file-toggle.d.ts.map +0 -1
  43. package/dist/agent-file-toggle.js +0 -259
  44. package/dist/agent-file-toggle.js.map +0 -1
  45. package/dist/agent-history-list.d.ts +0 -19
  46. package/dist/agent-history-list.d.ts.map +0 -1
  47. package/dist/agent-history-list.js +0 -69
  48. package/dist/agent-history-list.js.map +0 -1
  49. package/dist/agent-history.d.ts +0 -35
  50. package/dist/agent-history.d.ts.map +0 -1
  51. package/dist/agent-history.js +0 -188
  52. package/dist/agent-history.js.map +0 -1
  53. package/dist/agent-manager.d.ts +0 -503
  54. package/dist/agent-manager.d.ts.map +0 -1
  55. package/dist/agent-manager.js +0 -1572
  56. package/dist/agent-manager.js.map +0 -1
  57. package/dist/agent-recovery.d.ts +0 -37
  58. package/dist/agent-recovery.d.ts.map +0 -1
  59. package/dist/agent-recovery.js +0 -171
  60. package/dist/agent-recovery.js.map +0 -1
  61. package/dist/agent-runner.d.ts +0 -303
  62. package/dist/agent-runner.d.ts.map +0 -1
  63. package/dist/agent-runner.js +0 -1003
  64. package/dist/agent-runner.js.map +0 -1
  65. package/dist/agent-types.d.ts +0 -120
  66. package/dist/agent-types.d.ts.map +0 -1
  67. package/dist/agent-types.js +0 -301
  68. package/dist/agent-types.js.map +0 -1
  69. package/dist/child-context.d.ts +0 -3
  70. package/dist/child-context.d.ts.map +0 -1
  71. package/dist/child-context.js +0 -13
  72. package/dist/child-context.js.map +0 -1
  73. package/dist/context.d.ts +0 -13
  74. package/dist/context.d.ts.map +0 -1
  75. package/dist/context.js +0 -57
  76. package/dist/context.js.map +0 -1
  77. package/dist/cross-extension-rpc.d.ts +0 -67
  78. package/dist/cross-extension-rpc.d.ts.map +0 -1
  79. package/dist/cross-extension-rpc.js +0 -139
  80. package/dist/cross-extension-rpc.js.map +0 -1
  81. package/dist/custom-agents.d.ts +0 -55
  82. package/dist/custom-agents.d.ts.map +0 -1
  83. package/dist/custom-agents.js +0 -309
  84. package/dist/custom-agents.js.map +0 -1
  85. package/dist/default-agents.d.ts +0 -8
  86. package/dist/default-agents.d.ts.map +0 -1
  87. package/dist/default-agents.js +0 -123
  88. package/dist/default-agents.js.map +0 -1
  89. package/dist/enabled-models.d.ts +0 -50
  90. package/dist/enabled-models.d.ts.map +0 -1
  91. package/dist/enabled-models.js +0 -146
  92. package/dist/enabled-models.js.map +0 -1
  93. package/dist/env.d.ts +0 -7
  94. package/dist/env.d.ts.map +0 -1
  95. package/dist/env.js +0 -29
  96. package/dist/env.js.map +0 -1
  97. package/dist/group-join.d.ts +0 -33
  98. package/dist/group-join.d.ts.map +0 -1
  99. package/dist/group-join.js +0 -117
  100. package/dist/group-join.js.map +0 -1
  101. package/dist/index.d.ts +0 -51
  102. package/dist/index.d.ts.map +0 -1
  103. package/dist/index.js +0 -3687
  104. package/dist/index.js.map +0 -1
  105. package/dist/invocation-config.d.ts +0 -108
  106. package/dist/invocation-config.d.ts.map +0 -1
  107. package/dist/invocation-config.js +0 -84
  108. package/dist/invocation-config.js.map +0 -1
  109. package/dist/memory.d.ts +0 -54
  110. package/dist/memory.d.ts.map +0 -1
  111. package/dist/memory.js +0 -166
  112. package/dist/memory.js.map +0 -1
  113. package/dist/mention-clone.d.ts +0 -88
  114. package/dist/mention-clone.d.ts.map +0 -1
  115. package/dist/mention-clone.js +0 -154
  116. package/dist/mention-clone.js.map +0 -1
  117. package/dist/mention.d.ts +0 -82
  118. package/dist/mention.d.ts.map +0 -1
  119. package/dist/mention.js +0 -132
  120. package/dist/mention.js.map +0 -1
  121. package/dist/model-resolver.d.ts +0 -37
  122. package/dist/model-resolver.d.ts.map +0 -1
  123. package/dist/model-resolver.js +0 -96
  124. package/dist/model-resolver.js.map +0 -1
  125. package/dist/model-scope.d.ts +0 -50
  126. package/dist/model-scope.d.ts.map +0 -1
  127. package/dist/model-scope.js +0 -49
  128. package/dist/model-scope.js.map +0 -1
  129. package/dist/nested-tools.d.ts +0 -57
  130. package/dist/nested-tools.d.ts.map +0 -1
  131. package/dist/nested-tools.js +0 -301
  132. package/dist/nested-tools.js.map +0 -1
  133. package/dist/nico-overrides.d.ts +0 -54
  134. package/dist/nico-overrides.d.ts.map +0 -1
  135. package/dist/nico-overrides.js +0 -170
  136. package/dist/nico-overrides.js.map +0 -1
  137. package/dist/output-file.d.ts +0 -44
  138. package/dist/output-file.d.ts.map +0 -1
  139. package/dist/output-file.js +0 -156
  140. package/dist/output-file.js.map +0 -1
  141. package/dist/prompts.d.ts +0 -56
  142. package/dist/prompts.d.ts.map +0 -1
  143. package/dist/prompts.js +0 -92
  144. package/dist/prompts.js.map +0 -1
  145. package/dist/schedule-store.d.ts +0 -39
  146. package/dist/schedule-store.d.ts.map +0 -1
  147. package/dist/schedule-store.js +0 -156
  148. package/dist/schedule-store.js.map +0 -1
  149. package/dist/schedule.d.ts +0 -110
  150. package/dist/schedule.d.ts.map +0 -1
  151. package/dist/schedule.js +0 -360
  152. package/dist/schedule.js.map +0 -1
  153. package/dist/settings.d.ts +0 -354
  154. package/dist/settings.d.ts.map +0 -1
  155. package/dist/settings.js +0 -247
  156. package/dist/settings.js.map +0 -1
  157. package/dist/skill-loader.d.ts +0 -25
  158. package/dist/skill-loader.d.ts.map +0 -1
  159. package/dist/skill-loader.js +0 -94
  160. package/dist/skill-loader.js.map +0 -1
  161. package/dist/status-note.d.ts +0 -62
  162. package/dist/status-note.d.ts.map +0 -1
  163. package/dist/status-note.js +0 -86
  164. package/dist/status-note.js.map +0 -1
  165. package/dist/structured-output.d.ts +0 -62
  166. package/dist/structured-output.d.ts.map +0 -1
  167. package/dist/structured-output.js +0 -113
  168. package/dist/structured-output.js.map +0 -1
  169. package/dist/types.d.ts +0 -372
  170. package/dist/types.d.ts.map +0 -1
  171. package/dist/types.js +0 -6
  172. package/dist/types.js.map +0 -1
  173. package/dist/ui/agent-mention.d.ts +0 -83
  174. package/dist/ui/agent-mention.d.ts.map +0 -1
  175. package/dist/ui/agent-mention.js +0 -188
  176. package/dist/ui/agent-mention.js.map +0 -1
  177. package/dist/ui/agent-widget.d.ts +0 -241
  178. package/dist/ui/agent-widget.d.ts.map +0 -1
  179. package/dist/ui/agent-widget.js +0 -992
  180. package/dist/ui/agent-widget.js.map +0 -1
  181. package/dist/ui/ccstyle/diff/ansi-utils.d.ts +0 -11
  182. package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +0 -1
  183. package/dist/ui/ccstyle/diff/ansi-utils.js +0 -145
  184. package/dist/ui/ccstyle/diff/ansi-utils.js.map +0 -1
  185. package/dist/ui/ccstyle/diff/diff-presentation.d.ts +0 -12
  186. package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +0 -1
  187. package/dist/ui/ccstyle/diff/diff-presentation.js +0 -48
  188. package/dist/ui/ccstyle/diff/diff-presentation.js.map +0 -1
  189. package/dist/ui/ccstyle/diff/diff-renderer.d.ts +0 -46
  190. package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +0 -1
  191. package/dist/ui/ccstyle/diff/diff-renderer.js +0 -2049
  192. package/dist/ui/ccstyle/diff/diff-renderer.js.map +0 -1
  193. package/dist/ui/ccstyle/diff/line-width-safety.d.ts +0 -12
  194. package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +0 -1
  195. package/dist/ui/ccstyle/diff/line-width-safety.js +0 -58
  196. package/dist/ui/ccstyle/diff/line-width-safety.js.map +0 -1
  197. package/dist/ui/ccstyle/diff/render-utils.d.ts +0 -6
  198. package/dist/ui/ccstyle/diff/render-utils.d.ts.map +0 -1
  199. package/dist/ui/ccstyle/diff/render-utils.js +0 -25
  200. package/dist/ui/ccstyle/diff/render-utils.js.map +0 -1
  201. package/dist/ui/ccstyle/diff/shiki-highlight.d.ts +0 -19
  202. package/dist/ui/ccstyle/diff/shiki-highlight.d.ts.map +0 -1
  203. package/dist/ui/ccstyle/diff/shiki-highlight.js +0 -85
  204. package/dist/ui/ccstyle/diff/shiki-highlight.js.map +0 -1
  205. package/dist/ui/ccstyle/diff/types.d.ts +0 -22
  206. package/dist/ui/ccstyle/diff/types.d.ts.map +0 -1
  207. package/dist/ui/ccstyle/diff/types.js +0 -10
  208. package/dist/ui/ccstyle/diff/types.js.map +0 -1
  209. package/dist/ui/ccstyle/diff/write-display-utils.d.ts +0 -2
  210. package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +0 -1
  211. package/dist/ui/ccstyle/diff/write-display-utils.js +0 -12
  212. package/dist/ui/ccstyle/diff/write-display-utils.js.map +0 -1
  213. package/dist/ui/ccstyle/tool-renderer.d.ts +0 -17
  214. package/dist/ui/ccstyle/tool-renderer.d.ts.map +0 -1
  215. package/dist/ui/ccstyle/tool-renderer.js +0 -74
  216. package/dist/ui/ccstyle/tool-renderer.js.map +0 -1
  217. package/dist/ui/ccstyle/tool-result.d.ts +0 -44
  218. package/dist/ui/ccstyle/tool-result.d.ts.map +0 -1
  219. package/dist/ui/ccstyle/tool-result.js +0 -423
  220. package/dist/ui/ccstyle/tool-result.js.map +0 -1
  221. package/dist/ui/conversation-blocks.d.ts +0 -46
  222. package/dist/ui/conversation-blocks.d.ts.map +0 -1
  223. package/dist/ui/conversation-blocks.js +0 -313
  224. package/dist/ui/conversation-blocks.js.map +0 -1
  225. package/dist/ui/conversation-nvim.d.ts +0 -8
  226. package/dist/ui/conversation-nvim.d.ts.map +0 -1
  227. package/dist/ui/conversation-nvim.js +0 -117
  228. package/dist/ui/conversation-nvim.js.map +0 -1
  229. package/dist/ui/conversation-role.d.ts +0 -13
  230. package/dist/ui/conversation-role.d.ts.map +0 -1
  231. package/dist/ui/conversation-role.js +0 -39
  232. package/dist/ui/conversation-role.js.map +0 -1
  233. package/dist/ui/conversation-search.d.ts +0 -39
  234. package/dist/ui/conversation-search.d.ts.map +0 -1
  235. package/dist/ui/conversation-search.js +0 -124
  236. package/dist/ui/conversation-search.js.map +0 -1
  237. package/dist/ui/conversation-timeline.d.ts +0 -102
  238. package/dist/ui/conversation-timeline.d.ts.map +0 -1
  239. package/dist/ui/conversation-timeline.js +0 -555
  240. package/dist/ui/conversation-timeline.js.map +0 -1
  241. package/dist/ui/conversation-viewer.d.ts +0 -138
  242. package/dist/ui/conversation-viewer.d.ts.map +0 -1
  243. package/dist/ui/conversation-viewer.js +0 -1175
  244. package/dist/ui/conversation-viewer.js.map +0 -1
  245. package/dist/ui/schedule-menu.d.ts +0 -17
  246. package/dist/ui/schedule-menu.d.ts.map +0 -1
  247. package/dist/ui/schedule-menu.js +0 -95
  248. package/dist/ui/schedule-menu.js.map +0 -1
  249. package/dist/ui/select-item.d.ts +0 -28
  250. package/dist/ui/select-item.d.ts.map +0 -1
  251. package/dist/ui/select-item.js +0 -35
  252. package/dist/ui/select-item.js.map +0 -1
  253. package/dist/ui/viewer-keys.d.ts +0 -21
  254. package/dist/ui/viewer-keys.d.ts.map +0 -1
  255. package/dist/ui/viewer-keys.js +0 -18
  256. package/dist/ui/viewer-keys.js.map +0 -1
  257. package/dist/ui/workflow-card.d.ts +0 -176
  258. package/dist/ui/workflow-card.d.ts.map +0 -1
  259. package/dist/ui/workflow-card.js +0 -333
  260. package/dist/ui/workflow-card.js.map +0 -1
  261. package/dist/ui/workflow-dialog.d.ts +0 -306
  262. package/dist/ui/workflow-dialog.d.ts.map +0 -1
  263. package/dist/ui/workflow-dialog.js +0 -844
  264. package/dist/ui/workflow-dialog.js.map +0 -1
  265. package/dist/ui/workflow-menu.d.ts +0 -42
  266. package/dist/ui/workflow-menu.d.ts.map +0 -1
  267. package/dist/ui/workflow-menu.js +0 -127
  268. package/dist/ui/workflow-menu.js.map +0 -1
  269. package/dist/usage.d.ts +0 -136
  270. package/dist/usage.d.ts.map +0 -1
  271. package/dist/usage.js +0 -121
  272. package/dist/usage.js.map +0 -1
  273. package/dist/workflow/collisions.d.ts +0 -96
  274. package/dist/workflow/collisions.d.ts.map +0 -1
  275. package/dist/workflow/collisions.js +0 -89
  276. package/dist/workflow/collisions.js.map +0 -1
  277. package/dist/workflow/entry.d.ts +0 -33
  278. package/dist/workflow/entry.d.ts.map +0 -1
  279. package/dist/workflow/entry.js +0 -30
  280. package/dist/workflow/entry.js.map +0 -1
  281. package/dist/workflow/host.d.ts +0 -63
  282. package/dist/workflow/host.d.ts.map +0 -1
  283. package/dist/workflow/host.js +0 -363
  284. package/dist/workflow/host.js.map +0 -1
  285. package/dist/workflow/journal.d.ts +0 -98
  286. package/dist/workflow/journal.d.ts.map +0 -1
  287. package/dist/workflow/journal.js +0 -121
  288. package/dist/workflow/journal.js.map +0 -1
  289. package/dist/workflow/json-schema.d.ts +0 -52
  290. package/dist/workflow/json-schema.d.ts.map +0 -1
  291. package/dist/workflow/json-schema.js +0 -112
  292. package/dist/workflow/json-schema.js.map +0 -1
  293. package/dist/workflow/meta.d.ts +0 -68
  294. package/dist/workflow/meta.d.ts.map +0 -1
  295. package/dist/workflow/meta.js +0 -318
  296. package/dist/workflow/meta.js.map +0 -1
  297. package/dist/workflow/progress.d.ts +0 -225
  298. package/dist/workflow/progress.d.ts.map +0 -1
  299. package/dist/workflow/progress.js +0 -362
  300. package/dist/workflow/progress.js.map +0 -1
  301. package/dist/workflow/runtime.d.ts +0 -335
  302. package/dist/workflow/runtime.d.ts.map +0 -1
  303. package/dist/workflow/runtime.js +0 -831
  304. package/dist/workflow/runtime.js.map +0 -1
  305. package/dist/workflow/saved.d.ts +0 -91
  306. package/dist/workflow/saved.d.ts.map +0 -1
  307. package/dist/workflow/saved.js +0 -204
  308. package/dist/workflow/saved.js.map +0 -1
  309. package/dist/workflow/task.d.ts +0 -137
  310. package/dist/workflow/task.d.ts.map +0 -1
  311. package/dist/workflow/task.js +0 -208
  312. package/dist/workflow/task.js.map +0 -1
  313. package/dist/workflow/tool-description.d.ts +0 -39
  314. package/dist/workflow/tool-description.d.ts.map +0 -1
  315. package/dist/workflow/tool-description.js +0 -200
  316. package/dist/workflow/tool-description.js.map +0 -1
  317. package/dist/workflow/worker-source.d.ts +0 -48
  318. package/dist/workflow/worker-source.d.ts.map +0 -1
  319. package/dist/workflow/worker-source.js +0 -779
  320. package/dist/workflow/worker-source.js.map +0 -1
  321. package/dist/worktree.d.ts +0 -53
  322. package/dist/worktree.d.ts.map +0 -1
  323. package/dist/worktree.js +0 -165
  324. package/dist/worktree.js.map +0 -1
  325. package/dist/write-execution.d.ts +0 -42
  326. package/dist/write-execution.d.ts.map +0 -1
  327. package/dist/write-execution.js +0 -138
  328. package/dist/write-execution.js.map +0 -1
  329. package/dist/xml.d.ts +0 -11
  330. package/dist/xml.d.ts.map +0 -1
  331. package/dist/xml.js +0 -13
  332. package/dist/xml.js.map +0 -1
  333. package/src/abortable.ts +0 -43
  334. package/src/agent-color.ts +0 -161
  335. package/src/agent-file-toggle.ts +0 -269
  336. package/src/child-context.ts +0 -15
  337. package/src/mention-clone.ts +0 -196
  338. package/src/mention.ts +0 -141
  339. package/src/model-scope.ts +0 -70
  340. package/src/nested-tools.ts +0 -424
  341. package/src/structured-output.ts +0 -130
  342. package/src/ui/agent-mention.ts +0 -216
  343. package/src/ui/select-item.ts +0 -45
  344. package/src/ui/workflow-card.ts +0 -470
  345. package/src/ui/workflow-dialog.ts +0 -1115
  346. package/src/ui/workflow-menu.ts +0 -166
  347. package/src/workflow/collisions.ts +0 -123
  348. package/src/workflow/entry.ts +0 -47
  349. package/src/workflow/host.ts +0 -403
  350. package/src/workflow/journal.ts +0 -164
  351. package/src/workflow/json-schema.ts +0 -128
  352. package/src/workflow/meta.ts +0 -325
  353. package/src/workflow/progress.ts +0 -550
  354. package/src/workflow/runtime.ts +0 -1219
  355. package/src/workflow/saved.ts +0 -217
  356. package/src/workflow/task.ts +0 -302
  357. package/src/workflow/tool-description.ts +0 -200
  358. package/src/workflow/worker-source.ts +0 -781
  359. package/src/xml.ts +0 -13
@@ -1,1175 +0,0 @@
1
- /**
2
- * conversation-viewer.ts — Live and historical conversation overlay.
3
- *
4
- * The viewer deliberately keeps rendering state separate from the transcript:
5
- * blocks are formatted data, while Markdown/ANSI lines are a bounded cache.
6
- */
7
- import { copyToClipboard } from "@earendil-works/pi-coding-agent";
8
- import { Input, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
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";
12
- import { createViewerCcstyleResult } from "./ccstyle/tool-result.js";
13
- import { formatConversationMessages } from "./conversation-blocks.js";
14
- import { editLiveAssistantBlockInNvim, viewConversationBlockInNvim } from "./conversation-nvim.js";
15
- import { renderConversationRoleHeader } from "./conversation-role.js";
16
- import { findConversationMatches, highlightConversationLine, nearestConversationMatchAfter, nearestConversationMatchBefore, stripAnsi } from "./conversation-search.js";
17
- import { ConversationTimeline } from "./conversation-timeline.js";
18
- import { createViewerKeys } from "./viewer-keys.js";
19
- /** Base lines consumed by the overlay: borders, header, separator and footer. */
20
- const CHROME_LINES_BASE = 6;
21
- const MIN_VIEWPORT = 3;
22
- export const VIEWPORT_HEIGHT_PCT = 70;
23
- const MAX_COPY_CHARS = 500_000;
24
- export const RESULT_MAX_CHARS = 16_000;
25
- const MATCH_BG = "\x1b[48;5;238m";
26
- const MATCH_BG_CURRENT = "\x1b[48;5;220m\x1b[30m";
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
- };
34
- function renderScrollbarCell(theme, totalLines, viewportHeight, scrollOffset, lineIndex, active) {
35
- const total = Math.max(0, Math.floor(totalLines));
36
- const viewport = Math.max(1, Math.floor(viewportHeight));
37
- if (total <= viewport)
38
- return theme.fg("scrollbarTrack", " ");
39
- const maxOffset = Math.max(0, total - viewport);
40
- const offset = Math.max(0, Math.min(maxOffset, Math.floor(scrollOffset)));
41
- const thumbHeight = Math.max(1, Math.round((viewport * viewport) / total));
42
- const thumbStart = Math.round(((viewport - thumbHeight) * offset) / maxOffset);
43
- const inThumb = lineIndex >= thumbStart && lineIndex < thumbStart + thumbHeight;
44
- return theme.fg(inThumb ? "scrollbarThumb" : "scrollbarTrack", inThumb ? (active ? "█" : "┃") : "│");
45
- }
46
- function renderScrollMoreRule(theme, width, direction, hiddenLines) {
47
- const safeWidth = Math.max(1, Math.floor(width));
48
- if (hiddenLines <= 0)
49
- return theme.fg("border", `├${"─".repeat(safeWidth)}┤`);
50
- const label = `${direction === "top" ? "↑" : "↓"} ${hiddenLines} more`;
51
- const labelWidth = visibleWidth(label);
52
- if (labelWidth + 2 > safeWidth)
53
- return theme.fg("border", `├${"─".repeat(safeWidth)}┤`);
54
- const left = Math.floor((safeWidth - labelWidth) / 2);
55
- const right = Math.max(0, safeWidth - left - labelWidth);
56
- return `${theme.fg("border", `├${"─".repeat(left)}`)}${theme.fg("dim", label)}${theme.fg("border", `${"─".repeat(right)}┤`)}`;
57
- }
58
- export function createStaticConversationSource(messages) {
59
- return { messages, subscribe: () => () => { } };
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
- }
93
- const FULL_TOOL_PREVIEW_MAX_CHARS = 500_000;
94
- class FullToolPreview {
95
- tui;
96
- theme;
97
- title;
98
- body;
99
- done;
100
- scrollOffset = 0;
101
- pageSize = 1;
102
- totalLines = 1;
103
- pendingTop = false;
104
- hoveredClose = false;
105
- scrollbarActive = false;
106
- constructor(tui, theme, title, body, done) {
107
- this.tui = tui;
108
- this.theme = theme;
109
- this.title = title;
110
- this.body = body;
111
- this.done = done;
112
- }
113
- handleInput(data) {
114
- if (matchesKey(data, Key.escape) || matchesKey(data, "q")) {
115
- this.done(undefined);
116
- return;
117
- }
118
- if (data === "g") {
119
- if (this.pendingTop)
120
- this.scrollTo(0);
121
- this.pendingTop = true;
122
- return;
123
- }
124
- const delta = data === "j" || matchesKey(data, Key.down) ? 1 :
125
- data === "k" || matchesKey(data, Key.up) ? -1 :
126
- matchesKey(data, "pageUp") ? -this.pageSize : matchesKey(data, "pageDown") ? this.pageSize :
127
- matchesKey(data, Key.home) ? -this.totalLines : data === "G" || matchesKey(data, Key.end) ? this.totalLines : 0;
128
- this.pendingTop = false;
129
- if (delta !== 0)
130
- this.scrollTo(this.scrollOffset + delta);
131
- }
132
- handleMouse(event) {
133
- const closeHit = event.y === 1 && event.x >= Math.max(0, event.width - 7) && event.x < Math.max(0, event.width - 1);
134
- if (event.type === "move") {
135
- if (closeHit === this.hoveredClose)
136
- return undefined;
137
- this.hoveredClose = closeHit;
138
- return { handled: true, render: true };
139
- }
140
- if (closeHit && event.button === "left") {
141
- if (event.type === "press")
142
- return { handled: true, capture: true, render: false };
143
- if (event.type === "click") {
144
- this.done(undefined);
145
- return { handled: true, render: false };
146
- }
147
- }
148
- if (event.type === "wheel") {
149
- this.scrollTo(this.scrollOffset + Math.trunc(event.wheelDelta ?? 0));
150
- return { handled: true, render: true };
151
- }
152
- return undefined;
153
- }
154
- render(width) {
155
- const safeWidth = Math.max(12, Math.floor(Number.isFinite(width) ? width : 12));
156
- const inner = Math.max(1, safeWidth - 2);
157
- // Reserve the rightmost inner cell for the same track/thumb rail used by
158
- // pi-tui's native ScrollView layout.
159
- const bodyWidth = Math.max(1, inner - 1);
160
- const terminalRows = Math.max(1, this.tui.terminal.rows);
161
- const viewport = Math.max(1, Math.min(30, Math.floor(terminalRows * 0.8), terminalRows - 6));
162
- const wrapped = this.body.render(Math.max(1, bodyWidth - 1));
163
- this.totalLines = wrapped.length;
164
- this.pageSize = viewport;
165
- this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, this.totalLines - this.pageSize));
166
- const visible = wrapped.slice(this.scrollOffset, this.scrollOffset + this.pageSize);
167
- const border = (text) => this.theme.fg("border", text);
168
- const pad = (text, rowWidth = inner) => {
169
- const clipped = truncateToWidth(text, rowWidth, "…", true);
170
- return clipped + " ".repeat(Math.max(0, rowWidth - visibleWidth(clipped)));
171
- };
172
- const hiddenAbove = this.scrollOffset;
173
- const hiddenBelow = Math.max(0, this.totalLines - this.scrollOffset - this.pageSize);
174
- const close = this.theme.fg(this.hoveredClose ? "text" : "dim", "[esc]");
175
- const header = this.theme.bold(this.theme.fg("accent", this.title));
176
- const status = `${this.scrollOffset + 1}-${Math.min(this.totalLines, this.scrollOffset + this.pageSize)} / ${this.totalLines} lines · j/k gg/G ↑↓ PgUp/PgDn · [esc] close`;
177
- return [
178
- border(`╭${"─".repeat(inner)}╮`),
179
- `${border("│")}${pad(` ${header}`, inner - visibleWidth(close))}${close}${border("│")}`,
180
- renderScrollMoreRule(this.theme, inner, "top", hiddenAbove),
181
- ...Array.from({ length: viewport }, (_, index) => {
182
- const content = pad(` ${visible[index] ?? ""}`, bodyWidth);
183
- const rail = renderScrollbarCell(this.theme, this.totalLines, this.pageSize, this.scrollOffset, index, this.scrollbarActive);
184
- return `${border("│")}${content}${rail}${border("│")}`;
185
- }),
186
- renderScrollMoreRule(this.theme, inner, "bottom", hiddenBelow),
187
- `${border("│")}${pad(this.theme.fg("dim", ` ${status}`))}${border("│")}`,
188
- border(`╰${"─".repeat(inner)}╯`),
189
- ];
190
- }
191
- invalidate() {
192
- this.body.invalidate();
193
- }
194
- scrollTo(offset) {
195
- const next = Math.max(0, Math.min(Math.max(0, this.totalLines - this.pageSize), offset));
196
- if (next === this.scrollOffset)
197
- return;
198
- this.scrollOffset = next;
199
- this.scrollbarActive = true;
200
- this.tui.requestRender();
201
- }
202
- }
203
- export class ConversationViewer {
204
- tui;
205
- session;
206
- record;
207
- activity;
208
- theme;
209
- done;
210
- onStop;
211
- onSteer;
212
- blocks = [];
213
- showTools = true;
214
- selectedMessageIdx = 0;
215
- hasFocusedBlock = false;
216
- cachedWidth = 0;
217
- cachedLines = [];
218
- cachedMessageStarts = [];
219
- cachedMessageBlocks = [];
220
- cacheVersion = 0;
221
- cachedVersion = -1;
222
- sessionRefreshQueued = false;
223
- lastSessionKey = "";
224
- selectedBlockId;
225
- scrollOffset = 0;
226
- autoScroll = true;
227
- unsubscribe;
228
- lastInnerW = 0;
229
- closed = false;
230
- stopArmed = false;
231
- hoveredClose = false;
232
- hoveredPreview = false;
233
- pressedHeaderAction;
234
- keys;
235
- composer;
236
- searchMode = false;
237
- searchQuery = "";
238
- searchMatches = [];
239
- currentMatchIdx = -1;
240
- /** Follow-up drafts, newest first, matching pi editor history semantics. */
241
- steerHistory = [];
242
- steerHistoryIndex = -1;
243
- steerHistoryDraft = "";
244
- timeline;
245
- toolPreview;
246
- operations;
247
- showCost;
248
- viewerMarkdown;
249
- onMarkdownMode;
250
- markdownModeOverride;
251
- constructor(tui, session, record, activity, theme, done,
252
- /** Abort the agent shown here. Omitted means read-only history. */
253
- onStop,
254
- /** User keybindings from ctx.ui.custom(). */
255
- keybindings,
256
- /** Send a steering message to the agent. */
257
- onSteer,
258
- /** Cost flag in upstream's constructor; fork callers may pass operations here. */
259
- showCostOrOperations, viewerMarkdown, onMarkdownMode) {
260
- this.tui = tui;
261
- this.session = session;
262
- this.record = record;
263
- this.activity = activity;
264
- this.theme = theme;
265
- this.done = done;
266
- this.onStop = onStop;
267
- this.onSteer = onSteer;
268
- this.operations = typeof showCostOrOperations === "object" ? showCostOrOperations : undefined;
269
- this.showCost = typeof showCostOrOperations === "boolean" ? showCostOrOperations : false;
270
- this.viewerMarkdown = viewerMarkdown;
271
- this.onMarkdownMode = onMarkdownMode;
272
- this.keys = createViewerKeys(keybindings);
273
- this.blocks = formatConversationMessages(session.messages);
274
- this.lastSessionKey = this.sessionKey();
275
- this.timeline = new ConversationTimeline(tui, theme, {
276
- cwd: this.operations?.ctx.cwd,
277
- record,
278
- markdownMode: () => this.currentMarkdownMode(),
279
- onChange: (change) => this.handleTimelineChange(change),
280
- });
281
- this.timeline.setBlocks(this.blocks);
282
- this.unsubscribe = session.subscribe(() => this.scheduleSessionRefresh());
283
- }
284
- handleInput(data) {
285
- if (this.toolPreview) {
286
- this.toolPreview.handleInput(data);
287
- return;
288
- }
289
- if (this.composer) {
290
- if (matchesKey(data, Key.alt("up")) || data === "a-up" || data === "alt+up") {
291
- this.recallSteerDraft();
292
- }
293
- else {
294
- const before = this.composer.getValue();
295
- this.composer.handleInput(data);
296
- if (this.composer && before !== this.composer.getValue()) {
297
- this.steerHistoryIndex = -1;
298
- this.steerHistoryDraft = "";
299
- }
300
- }
301
- this.tui.requestRender();
302
- return;
303
- }
304
- if (this.searchMode) {
305
- this.handleSearchInput(data);
306
- return;
307
- }
308
- if (matchesKey(data, Key.ctrl("c"))) {
309
- this.closed = true;
310
- this.done(undefined);
311
- return;
312
- }
313
- if (matchesKey(data, "escape") || matchesKey(data, "q")) {
314
- this.closed = true;
315
- this.done(undefined);
316
- return;
317
- }
318
- if (matchesKey(data, "enter")) {
319
- this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
320
- if (this.timeline.isToolFocused()) {
321
- this.timeline.toggleFocusedTool();
322
- return;
323
- }
324
- if (this.canSteer()) {
325
- this.stopArmed = false;
326
- this.openComposer();
327
- return;
328
- }
329
- }
330
- if (data === "e" && this.canSteer()) {
331
- this.stopArmed = false;
332
- this.openComposer();
333
- return;
334
- }
335
- if (matchesKey(data, "x")) {
336
- if (this.isStoppable()) {
337
- if (this.stopArmed) {
338
- this.stopArmed = false;
339
- this.onStop?.();
340
- }
341
- else {
342
- this.stopArmed = true;
343
- }
344
- this.tui.requestRender();
345
- }
346
- return;
347
- }
348
- if (this.stopArmed)
349
- this.stopArmed = false;
350
- // Input can arrive before the first render in tests and in a freshly-opened overlay.
351
- this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
352
- const viewportHeight = this.viewportHeight();
353
- const maxScroll = Math.max(0, this.cachedLines.length - viewportHeight);
354
- if (data === "t") {
355
- this.showTools = !this.showTools;
356
- this.timeline.setShowTools(this.showTools);
357
- this.invalidate();
358
- this.tui.requestRender();
359
- }
360
- else if (data === "\t") {
361
- this.moveToolFocus(1);
362
- }
363
- else if (matchesKey(data, Key.shift("tab"))) {
364
- this.moveToolFocus(-1);
365
- }
366
- else if (data === "J" || matchesKey(data, Key.shift("j"))) {
367
- this.jumpToMessage(1);
368
- }
369
- else if (data === "K" || matchesKey(data, Key.shift("k"))) {
370
- this.jumpToMessage(-1);
371
- }
372
- else if (data === "]") {
373
- this.focusNearbyTool(1);
374
- }
375
- else if (data === "[") {
376
- this.focusNearbyTool(-1);
377
- }
378
- else if (data === "w") {
379
- this.openFocusedToolPreview();
380
- }
381
- else if (data === "g") {
382
- this.scrollToTop();
383
- }
384
- else if (data === "G" || matchesKey(data, Key.shift("g"))) {
385
- this.scrollToBottom();
386
- }
387
- else if (data === "/") {
388
- this.enterSearch();
389
- }
390
- else if (data === "n") {
391
- this.gotoMatch(1);
392
- }
393
- else if (data === "N") {
394
- this.gotoMatch(-1);
395
- }
396
- else if (data === "M") {
397
- void this.copyCurrentMessage();
398
- }
399
- else if (data === "m") {
400
- this.cycleMarkdownMode();
401
- }
402
- else if (data === "o") {
403
- void this.viewCurrentInNvim();
404
- }
405
- else if (data === "O" && this.isLive()) {
406
- void this.editCurrentInNvim();
407
- }
408
- else if (this.keys.scrollUp(data)) {
409
- this.scrollBy(-1);
410
- }
411
- else if (this.keys.scrollDown(data)) {
412
- this.scrollBy(1);
413
- }
414
- else if (this.keys.pageUp(data)) {
415
- this.scrollBy(-viewportHeight);
416
- }
417
- else if (this.keys.pageDown(data)) {
418
- this.scrollBy(viewportHeight);
419
- }
420
- else if (matchesKey(data, "home")) {
421
- this.scrollOffset = 0;
422
- this.setFocusFromMessageIndex(0);
423
- this.autoScroll = false;
424
- this.invalidate();
425
- this.tui.requestRender();
426
- }
427
- else if (matchesKey(data, "end")) {
428
- this.scrollToBottom();
429
- }
430
- // Keep the computed max alive for the key path even when the cache is empty.
431
- void maxScroll;
432
- }
433
- handleMouse(event) {
434
- if (this.toolPreview)
435
- return this.toolPreview.handleMouse(event);
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);
440
- const closeHit = this.isCloseButtonHit(event.x, event.y, innerW);
441
- if (event.type === "move") {
442
- const previewChanged = previewHit !== this.hoveredPreview;
443
- const closeChanged = closeHit !== this.hoveredClose;
444
- this.hoveredPreview = previewHit;
445
- this.hoveredClose = closeHit;
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;
453
- const viewportHeight = this.viewportHeight();
454
- const maxScroll = Math.max(0, contentLines.length - viewportHeight);
455
- if (this.autoScroll)
456
- this.scrollOffset = maxScroll;
457
- const visibleStart = Math.min(this.scrollOffset, maxScroll);
458
- const contentOrigin = 4;
459
- const contentY = event.y - contentOrigin + visibleStart;
460
- const timelineResult = this.timeline.handleMouse({ ...event, y: contentY });
461
- return timelineResult ?? (previewChanged || closeChanged ? { handled: true, render: true } : undefined);
462
- }
463
- if (event.button === "left") {
464
- if (event.type === "press" && previewHit) {
465
- this.pressedHeaderAction = "preview";
466
- return { handled: true, capture: true, render: false };
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;
483
- this.closed = true;
484
- this.done(undefined);
485
- return { handled: true, render: false };
486
- }
487
- }
488
- if (this.composer || this.searchMode)
489
- return undefined;
490
- const contentLines = this.buildContentLines(contentWidth);
491
- const viewportHeight = this.viewportHeight();
492
- const maxScroll = Math.max(0, contentLines.length - viewportHeight);
493
- const visibleStart = Math.min(this.scrollOffset, maxScroll);
494
- const contentOrigin = 4;
495
- if (event.type === "wheel") {
496
- if (event.y < contentOrigin || event.y >= contentOrigin + viewportHeight)
497
- return undefined;
498
- this.scrollBy(Math.trunc(event.wheelDelta ?? 0));
499
- return { handled: true, render: true };
500
- }
501
- const contentY = event.y - contentOrigin + visibleStart;
502
- if (contentY < 0 || contentY >= contentLines.length)
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;
507
- // Rows have one leading space and one trailing space inside the frame.
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))
510
- return undefined;
511
- const result = this.timeline.handleMouse({ ...event, y: contentY });
512
- if (result?.handled && event.type === "click") {
513
- let selected = result.focusedBlockIndex;
514
- if (selected === undefined) {
515
- const focusedTool = this.timeline.getFocusedToolCallId();
516
- selected = this.timeline.getSnapshot().messageBlocks.findIndex((block) => block.toolCallId === focusedTool);
517
- }
518
- if (selected !== undefined && selected >= 0) {
519
- this.buildContentLines(contentWidth);
520
- this.setFocusFromMessageIndex(selected);
521
- this.autoScroll = false;
522
- this.scrollTargetIntoView(selected);
523
- this.tui.requestRender();
524
- }
525
- }
526
- return result;
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
- }
536
- isCloseButtonHit(x, y, innerW) {
537
- if (y !== 1)
538
- return false;
539
- const labelWidth = visibleWidth("[Esc]");
540
- const labelStart = 2 + Math.max(0, innerW - labelWidth);
541
- return x >= labelStart && x < 2 + innerW;
542
- }
543
- render(width) {
544
- if (this.toolPreview)
545
- return this.toolPreview.render(width);
546
- if (width < 6)
547
- return [];
548
- const th = this.theme;
549
- const innerW = Math.max(1, width - 4);
550
- this.lastInnerW = innerW;
551
- const contentWidth = this.transcriptWidth(innerW);
552
- const lines = [];
553
- const pad = (s, len) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
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
- };
565
- const hrTop = th.fg("border", `╭${"─".repeat(Math.max(0, width - 2))}╮`);
566
- const hrBot = th.fg("border", `╰${"─".repeat(Math.max(0, width - 2))}╯`);
567
- const hrMid = row(th.fg("dim", "─".repeat(innerW)));
568
- lines.push(hrTop);
569
- const modeLabel = getPromptModeLabel(this.record.type);
570
- const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
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", "○");
572
- const duration = formatDuration(this.record.startedAt, this.record.completedAt);
573
- const headerParts = [duration];
574
- const toolUses = this.activity?.toolUses ?? this.record.toolUses;
575
- if (toolUses > 0)
576
- headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
577
- const usage = this.activity?.lifetimeUsage ?? this.record.lifetimeUsage;
578
- const tokens = getLifetimeTotal(usage);
579
- if (tokens > 0) {
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));
593
- const invocationLine = this.invocationLine();
594
- lines.push(row(invocationLine ?? ""));
595
- lines.push(hrMid);
596
- this.buildContentLines(contentWidth);
597
- const contentLines = this.cachedLines;
598
- const viewportHeight = this.viewportHeight();
599
- const maxScroll = Math.max(0, contentLines.length - viewportHeight);
600
- if (this.autoScroll)
601
- this.scrollOffset = maxScroll;
602
- const visibleStart = Math.min(this.scrollOffset, maxScroll);
603
- const visible = contentLines.slice(visibleStart, visibleStart + viewportHeight);
604
- const currentBlockIdx = this.currentBlockGlobalIndex();
605
- const headerLine = this.activeHeaderLine();
606
- const showSticky = headerLine >= 0 && headerLine < visibleStart && visible.length > 0;
607
- const displayed = visible.slice();
608
- if (showSticky) {
609
- const block = this.cachedMessageBlocks[this.currentMessageIndex()];
610
- if (block) {
611
- const header = renderConversationRoleHeader(block, this.theme);
612
- displayed[0] = makeRenderLine({ text: ` ${header}`, plain: header, blockIndex: currentBlockIdx, railable: true });
613
- }
614
- }
615
- for (let i = 0; i < displayed.length; i++) {
616
- const line = displayed[i];
617
- let text = line.text;
618
- if (this.searchMatches.length > 0 && !(showSticky && i === 0)) {
619
- const hits = this.searchMatches.filter((match) => match.lineIdx === visibleStart + i).map((match) => ({ lineIdx: match.lineIdx, col: match.col, len: match.len, current: this.searchMatches.indexOf(match) === this.currentMatchIdx }));
620
- if (hits.length > 0)
621
- text = highlightConversationLine(text, hits, MATCH_BG, MATCH_BG_CURRENT, BG_RESET);
622
- }
623
- if (line.railable && line.blockIndex === currentBlockIdx) {
624
- const currentBlock = this.cachedMessageBlocks[this.currentMessageIndex()];
625
- const railColor = currentBlock?.kind === "tool" ? "toolTitle" : "accent";
626
- const railGlyph = currentBlock?.kind === "tool" ? "▌" : "▎";
627
- text = text.startsWith(" ") ? th.fg(railColor, railGlyph) + text.slice(1) : th.fg(railColor, railGlyph) + text;
628
- }
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));
636
- }
637
- lines.push(hrMid);
638
- if (this.composer) {
639
- lines.push(row(this.composer.render(innerW)[0] ?? ""));
640
- const hint = th.fg("dim", "Enter send · Esc cancel · Alt+Up recall");
641
- const label = th.fg("accent", "✎ steer");
642
- lines.push(row(label + " ".repeat(Math.max(1, innerW - visibleWidth(label) - visibleWidth(hint))) + hint));
643
- }
644
- else if (this.searchMode) {
645
- const query = `/${this.searchQuery}`;
646
- const matchCount = this.searchMatches.length > 0
647
- ? ` · ${this.searchMatches.length} match${this.searchMatches.length === 1 ? "" : "es"}`
648
- : "";
649
- const hints = th.fg("dim", " · Enter apply · Esc cancel");
650
- lines.push(row(truncateToWidth(th.fg("accent", query) + th.fg("dim", matchCount) + hints, innerW, "...", true)));
651
- }
652
- else {
653
- const actions = [];
654
- if (this.canSteer())
655
- actions.push(th.fg("dim", innerW < 100 ? "Enter steer (e steer)" : "e steer"));
656
- if (this.isStoppable())
657
- actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
658
- const editHint = this.isLive() ? " O" : "";
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
- ];
671
- const scrollPct = contentLines.length <= viewportHeight ? "100%" : `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
672
- const count = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
673
- const countWidth = visibleWidth(count);
674
- let status = "";
675
- for (const hint of shortcutHints) {
676
- const styledHint = th.fg("dim", hint);
677
- const withActions = [styledHint, ...actions].join(" ");
678
- if (visibleWidth(withActions) <= innerW) {
679
- status = withActions;
680
- break;
681
- }
682
- if (visibleWidth(styledHint) <= innerW)
683
- status ||= styledHint;
684
- }
685
- if (!status)
686
- status = th.fg("dim", shortcutHints[shortcutHints.length - 1]);
687
- const statusWidth = visibleWidth(status);
688
- const footer = statusWidth + countWidth + 1 <= innerW
689
- ? status + " ".repeat(Math.max(1, innerW - statusWidth - countWidth)) + count
690
- : status;
691
- lines.push(row(truncateToWidth(footer, innerW, "...", true)));
692
- }
693
- lines.push(hrBot);
694
- return lines;
695
- }
696
- invalidate() {
697
- this.cachedWidth = 0;
698
- this.cachedLines = [];
699
- this.cachedMessageStarts = [];
700
- this.cachedMessageBlocks = [];
701
- this.cachedVersion = -1;
702
- }
703
- scheduleSessionRefresh() {
704
- if (this.closed || this.sessionRefreshQueued)
705
- return;
706
- this.sessionRefreshQueued = true;
707
- queueMicrotask(() => {
708
- this.sessionRefreshQueued = false;
709
- if (this.closed)
710
- return;
711
- if (this.hasFocusedBlock)
712
- this.selectedBlockId = this.currentBlock()?.id;
713
- this.blocks = formatConversationMessages(this.session.messages);
714
- this.lastSessionKey = this.sessionKey();
715
- this.timeline.setBlocks(this.blocks);
716
- this.cacheVersion++;
717
- this.invalidate();
718
- this.tui.requestRender();
719
- });
720
- }
721
- handleTimelineChange(change) {
722
- if (change.kind === "content") {
723
- this.cacheVersion++;
724
- this.invalidate();
725
- }
726
- else if (change.changedRanges && this.cachedWidth > 0 && this.cachedVersion === this.cacheVersion) {
727
- const snapshotLines = this.timeline.getSnapshot().lines;
728
- for (const range of change.changedRanges) {
729
- const start = Math.max(0, range.startLine);
730
- const end = Math.min(this.cachedLines.length, start + Math.max(0, range.height));
731
- for (let index = start; index < end; index++) {
732
- const line = snapshotLines[index];
733
- if (line)
734
- this.cachedLines[index] = makeRenderLine({ ...line });
735
- }
736
- }
737
- }
738
- this.tui.requestRender();
739
- }
740
- dispose() {
741
- this.closed = true;
742
- this.unsubscribe?.();
743
- this.unsubscribe = undefined;
744
- }
745
- viewportHeight() {
746
- const maxRows = Math.floor((this.tui.terminal.rows * VIEWPORT_HEIGHT_PCT) / 100);
747
- return Math.max(MIN_VIEWPORT, maxRows - this.chromeLines());
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
- }
753
- chromeLines() {
754
- return CHROME_LINES_BASE + 1 + (this.composer ? 1 : 0);
755
- }
756
- invocationLine() {
757
- const liveModel = this.isLive() ? this.record.session?.model : undefined;
758
- const captured = this.record.invocation;
759
- const invocation = captured
760
- ? {
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 }),
767
- }
768
- : liveModel
769
- ? { modelName: liveModel.name ?? liveModel.id, modelId: `${liveModel.provider}/${liveModel.id}`, thinking: this.record.session?.thinkingLevel }
770
- : undefined;
771
- if (!invocation)
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
- }
842
- }
843
- buildContentLines(width) {
844
- if (width <= 0)
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
- }
851
- this.timeline.setShowTools(this.showTools);
852
- this.timeline.render(width);
853
- const timelineSnapshot = this.timeline.getSnapshot();
854
- const lines = timelineSnapshot.lines.map((line) => {
855
- const rawText = typeof line?.text === "string" ? line.text : "";
856
- const text = truncateToWidth(rawText, width, "", true);
857
- return makeRenderLine({
858
- text,
859
- plain: stripAnsi(text),
860
- blockIndex: typeof line?.blockIndex === "number" ? line.blockIndex : -1,
861
- ...(line?.railable ? { railable: true } : {}),
862
- });
863
- });
864
- this.cachedMessageStarts = [...timelineSnapshot.messageStarts];
865
- this.cachedMessageBlocks = [...timelineSnapshot.messageBlocks];
866
- if (this.hasFocusedBlock) {
867
- const selectedIndex = this.selectedBlockId
868
- ? this.cachedMessageBlocks.findIndex((block) => block.id === this.selectedBlockId)
869
- : -1;
870
- const focusedToolIndex = this.timeline.getFocusedToolCallId()
871
- ? this.cachedMessageBlocks.findIndex((block) => block.toolCallId === this.timeline.getFocusedToolCallId())
872
- : -1;
873
- const restoredIndex = selectedIndex >= 0 ? selectedIndex : focusedToolIndex;
874
- if (restoredIndex >= 0) {
875
- this.selectedMessageIdx = restoredIndex;
876
- this.selectedBlockId = this.cachedMessageBlocks[restoredIndex]?.id;
877
- }
878
- }
879
- if (this.record.status === "running" && this.activity) {
880
- const act = describeActivity(this.activity.activeTools, this.activity.responseText);
881
- const text = `▍ ${act}`;
882
- lines.push(makeRenderLine({ text: truncateToWidth(this.theme.fg("accent", "▍ ") + this.theme.fg("dim", act), width), plain: text, blockIndex: -1 }));
883
- }
884
- this.cachedWidth = width;
885
- this.cachedVersion = this.cacheVersion;
886
- this.cachedLines = lines;
887
- if (this.searchMode || this.searchQuery)
888
- this.recomputeMatches();
889
- return publicRenderLines(lines);
890
- }
891
- currentMessageIndex() {
892
- return this.cachedMessageStarts.length === 0 ? 0 : Math.max(0, Math.min(this.selectedMessageIdx, this.cachedMessageStarts.length - 1));
893
- }
894
- /** Keep the selected transcript block and the tool focus as one target. */
895
- setFocusFromMessageIndex(index) {
896
- if (index < 0 || index >= this.cachedMessageBlocks.length)
897
- return false;
898
- this.selectedMessageIdx = index;
899
- this.hasFocusedBlock = true;
900
- const block = this.cachedMessageBlocks[index];
901
- this.selectedBlockId = block.id;
902
- this.timeline.setFocusedToolCallId(block.kind === "tool" ? block.toolCallId : undefined);
903
- return true;
904
- }
905
- currentBlockGlobalIndex() {
906
- const block = this.cachedMessageBlocks[this.currentMessageIndex()];
907
- return block ? this.blocks.indexOf(block) : -1;
908
- }
909
- activeHeaderLine() {
910
- const block = this.cachedMessageBlocks[this.currentMessageIndex()];
911
- return block?.role === "user" || block?.role === "assistant"
912
- ? this.cachedMessageStarts[this.currentMessageIndex()] ?? -1
913
- : -1;
914
- }
915
- currentBlock() {
916
- return this.cachedMessageBlocks[this.currentMessageIndex()];
917
- }
918
- messageIndexForOffset(offset) {
919
- let result = 0;
920
- for (let i = 0; i < this.cachedMessageStarts.length; i++) {
921
- if (this.cachedMessageStarts[i] <= offset)
922
- result = i;
923
- else
924
- break;
925
- }
926
- return result;
927
- }
928
- jumpToMessage(dir) {
929
- const messageIndices = this.cachedMessageBlocks
930
- .map((block, index) => block.role === "user" || block.role === "assistant" ? index : -1)
931
- .filter((index) => index >= 0);
932
- if (messageIndices.length === 0)
933
- return;
934
- const current = this.currentMessageIndex();
935
- const currentPosition = messageIndices.indexOf(current);
936
- const precedingPosition = messageIndices.reduce((last, index, position) => index < current ? position : last, -1);
937
- const nextPosition = messageIndices.findIndex((index) => index > current);
938
- const position = currentPosition >= 0
939
- ? currentPosition + dir
940
- : dir > 0 ? nextPosition : precedingPosition;
941
- if (position < 0 || position >= messageIndices.length)
942
- return;
943
- const target = messageIndices[position];
944
- this.setFocusFromMessageIndex(target);
945
- this.scrollOffset = Math.min(this.cachedMessageStarts[target], Math.max(0, this.cachedLines.length - this.viewportHeight()));
946
- this.autoScroll = false;
947
- this.tui.requestRender();
948
- }
949
- moveToolFocus(direction) {
950
- if (!this.timeline.moveFocus(direction))
951
- return;
952
- const focused = this.timeline.getFocusedToolCallId();
953
- this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
954
- const index = this.cachedMessageBlocks.findIndex((block) => block.toolCallId === focused);
955
- if (index >= 0) {
956
- this.setFocusFromMessageIndex(index);
957
- this.autoScroll = false;
958
- this.scrollTargetIntoView(index);
959
- }
960
- this.tui.requestRender();
961
- }
962
- focusNearbyTool(direction) {
963
- this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
964
- const toolIndices = this.cachedMessageBlocks
965
- .map((block, index) => block.kind === "tool" && block.toolCallId ? index : -1)
966
- .filter((index) => index >= 0);
967
- if (toolIndices.length === 0)
968
- return;
969
- const current = this.currentMessageIndex();
970
- const resolvedTarget = !this.hasFocusedBlock
971
- ? direction > 0 ? toolIndices[0] : toolIndices[toolIndices.length - 1]
972
- : direction > 0
973
- ? toolIndices.find((index) => index > current)
974
- : [...toolIndices].reverse().find((index) => index < current);
975
- if (resolvedTarget === undefined)
976
- return;
977
- this.setFocusFromMessageIndex(resolvedTarget);
978
- this.autoScroll = false;
979
- this.scrollTargetIntoView(resolvedTarget);
980
- this.tui.requestRender();
981
- }
982
- scrollTargetIntoView(index) {
983
- if (index < 0 || index >= this.cachedMessageStarts.length)
984
- return;
985
- const targetLine = this.cachedMessageStarts[index];
986
- const viewportHeight = this.viewportHeight();
987
- const maxScroll = Math.max(0, this.cachedLines.length - viewportHeight);
988
- if (targetLine < this.scrollOffset)
989
- this.scrollOffset = targetLine;
990
- else if (targetLine >= this.scrollOffset + viewportHeight)
991
- this.scrollOffset = targetLine - viewportHeight + 1;
992
- this.scrollOffset = Math.max(0, Math.min(maxScroll, this.scrollOffset));
993
- }
994
- openFocusedToolPreview() {
995
- this.buildContentLines(this.transcriptWidth(this.lastInnerW || 80));
996
- const focusedToolCallId = this.timeline.getFocusedToolCallId();
997
- const block = focusedToolCallId
998
- ? this.cachedMessageBlocks.find((candidate) => candidate.toolCallId === focusedToolCallId)
999
- : this.currentBlock();
1000
- if (block?.kind !== "tool")
1001
- return;
1002
- const toolName = block.toolName || "tool";
1003
- const args = block.args ?? block.toolArguments;
1004
- const result = block.toolResult ?? block.result ?? { content: [] };
1005
- const title = `${toolName} · full preview`;
1006
- const body = createViewerCcstyleResult(toolName, result, { expanded: true, isError: result.isError === true }, this.theme, { args, maxChars: FULL_TOOL_PREVIEW_MAX_CHARS, diffConfig: { expandedPreviewMaxLines: Number.MAX_SAFE_INTEGER } });
1007
- this.toolPreview = new FullToolPreview(this.tui, this.theme, title, body, () => {
1008
- this.toolPreview = undefined;
1009
- this.tui.requestRender();
1010
- });
1011
- this.tui.requestRender();
1012
- }
1013
- scrollBy(delta) {
1014
- const max = Math.max(0, this.cachedLines.length - this.viewportHeight());
1015
- this.scrollOffset = Math.max(0, Math.min(max, this.scrollOffset + delta));
1016
- const selected = this.messageIndexForOffset(this.scrollOffset);
1017
- this.setFocusFromMessageIndex(selected);
1018
- this.autoScroll = this.scrollOffset >= max;
1019
- this.tui.requestRender();
1020
- }
1021
- scrollToTop() {
1022
- this.scrollOffset = 0;
1023
- this.setFocusFromMessageIndex(0);
1024
- this.autoScroll = false;
1025
- this.tui.requestRender();
1026
- }
1027
- scrollToBottom() {
1028
- this.scrollOffset = Math.max(0, this.cachedLines.length - this.viewportHeight());
1029
- this.setFocusFromMessageIndex(Math.max(0, this.cachedMessageStarts.length - 1));
1030
- this.autoScroll = true;
1031
- this.tui.requestRender();
1032
- }
1033
- enterSearch() {
1034
- this.searchMode = true;
1035
- this.searchQuery = "";
1036
- this.searchMatches = [];
1037
- this.currentMatchIdx = -1;
1038
- this.tui.requestRender();
1039
- }
1040
- handleSearchInput(data) {
1041
- if (matchesKey(data, "escape") || matchesKey(data, Key.ctrl("c"))) {
1042
- this.searchMode = false;
1043
- this.searchQuery = "";
1044
- this.searchMatches = [];
1045
- this.currentMatchIdx = -1;
1046
- }
1047
- else if (matchesKey(data, "enter") || data === "\r" || data === "\n") {
1048
- this.searchMode = false;
1049
- if (this.searchMatches.length > 0)
1050
- this.scrollToMatch(this.currentMatchIdx < 0 ? 0 : this.currentMatchIdx);
1051
- }
1052
- else if (matchesKey(data, "backspace") || data === "\x7f" || data === "\b") {
1053
- this.searchQuery = this.searchQuery.slice(0, -1);
1054
- this.recomputeMatches();
1055
- }
1056
- else if (data.length === 1 && data >= " " && data !== "\x7f") {
1057
- this.searchQuery += data;
1058
- this.recomputeMatches();
1059
- }
1060
- this.tui.requestRender();
1061
- }
1062
- recomputeMatches() {
1063
- this.searchMatches = findConversationMatches(this.cachedLines, this.searchQuery);
1064
- this.currentMatchIdx = this.searchMatches.length > 0 ? Math.min(Math.max(this.currentMatchIdx, 0), this.searchMatches.length - 1) : -1;
1065
- }
1066
- gotoMatch(dir) {
1067
- if (this.searchMatches.length === 0)
1068
- return;
1069
- this.currentMatchIdx = this.currentMatchIdx < 0
1070
- ? (dir === 1 ? nearestConversationMatchAfter(this.searchMatches, this.scrollOffset) : nearestConversationMatchBefore(this.searchMatches, this.scrollOffset))
1071
- : (this.currentMatchIdx + dir + this.searchMatches.length) % this.searchMatches.length;
1072
- this.scrollToMatch(this.currentMatchIdx);
1073
- this.tui.requestRender();
1074
- }
1075
- scrollToMatch(index) {
1076
- const match = this.searchMatches[index];
1077
- if (!match)
1078
- return;
1079
- const max = Math.max(0, this.cachedLines.length - this.viewportHeight());
1080
- this.scrollOffset = Math.max(0, Math.min(max, match.lineIdx - Math.floor(this.viewportHeight() / 2)));
1081
- this.setFocusFromMessageIndex(this.messageIndexForOffset(this.scrollOffset));
1082
- this.autoScroll = false;
1083
- }
1084
- async copyCurrentMessage() {
1085
- const block = this.currentBlock();
1086
- if (!block || !this.operations) {
1087
- if (!this.operations)
1088
- return;
1089
- return;
1090
- }
1091
- const source = `${stripAnsi(block.header)}\n${block.copyText}`.trimEnd();
1092
- const clipped = source.length > MAX_COPY_CHARS ? `${source.slice(0, MAX_COPY_CHARS)}\n… [truncated by subagent copy]` : source;
1093
- try {
1094
- await copyToClipboard(clipped);
1095
- this.operations.ctx.ui.notify(`Copied current message (${clipped.length} chars)`, "info");
1096
- }
1097
- catch (error) {
1098
- this.operations.ctx.ui.notify(`Copy failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1099
- }
1100
- }
1101
- async viewCurrentInNvim() {
1102
- const block = this.currentBlock();
1103
- if (!block || !this.operations)
1104
- return;
1105
- await viewConversationBlockInNvim(this.tui, this.operations.ctx, block);
1106
- this.invalidate();
1107
- this.tui.requestRender(true);
1108
- }
1109
- async editCurrentInNvim() {
1110
- const block = this.currentBlock();
1111
- if (!block || !this.operations || !this.isLive() || block.role !== "assistant")
1112
- return;
1113
- const sent = await editLiveAssistantBlockInNvim(this.operations.pi, this.tui, this.operations.ctx, block);
1114
- if (sent)
1115
- this.done(undefined);
1116
- else {
1117
- this.invalidate();
1118
- this.tui.requestRender(true);
1119
- }
1120
- }
1121
- isReadOnly() {
1122
- return this.operations?.readOnly === true;
1123
- }
1124
- isLive() {
1125
- return !this.isReadOnly() && this.record.session !== undefined;
1126
- }
1127
- isStoppable() {
1128
- return !this.isReadOnly() && !!this.onStop && (this.record.status === "running" || this.record.status === "queued");
1129
- }
1130
- canSteer() {
1131
- return !this.isReadOnly() && !!this.onSteer && (this.record.status === "running" || this.record.status === "queued");
1132
- }
1133
- openComposer() {
1134
- const input = new Input();
1135
- input.focused = true;
1136
- this.steerHistoryIndex = -1;
1137
- this.steerHistoryDraft = "";
1138
- input.onSubmit = (value) => {
1139
- const message = value.trim();
1140
- this.composer = undefined;
1141
- this.steerHistoryIndex = -1;
1142
- this.steerHistoryDraft = "";
1143
- if (message) {
1144
- if (this.steerHistory[0] !== message)
1145
- this.steerHistory.unshift(message);
1146
- if (this.steerHistory.length > 100)
1147
- this.steerHistory.pop();
1148
- this.onSteer?.(message);
1149
- }
1150
- this.tui.requestRender();
1151
- };
1152
- input.onEscape = () => {
1153
- this.composer = undefined;
1154
- this.steerHistoryIndex = -1;
1155
- this.steerHistoryDraft = "";
1156
- this.tui.requestRender();
1157
- };
1158
- this.composer = input;
1159
- this.tui.requestRender();
1160
- }
1161
- /** Recall the newest prior steer, preserving the draft for future editing. */
1162
- recallSteerDraft() {
1163
- if (!this.composer || this.steerHistory.length === 0)
1164
- return;
1165
- if (this.steerHistoryIndex < 0) {
1166
- this.steerHistoryDraft = this.composer.getValue();
1167
- this.steerHistoryIndex = 0;
1168
- }
1169
- else if (this.steerHistoryIndex < this.steerHistory.length - 1) {
1170
- this.steerHistoryIndex++;
1171
- }
1172
- this.composer.setValue(this.steerHistory[this.steerHistoryIndex] ?? this.steerHistoryDraft);
1173
- }
1174
- }
1175
- //# sourceMappingURL=conversation-viewer.js.map