@gajae-code/coding-agent 0.11.0 → 0.11.2

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 (418) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/types/async/job-manager.d.ts +46 -7
  3. package/dist/types/cli/config-cli.d.ts +11 -1
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +56 -1
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +11 -0
  54. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  55. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  56. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  57. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  58. package/dist/types/goals/runtime.d.ts +1 -0
  59. package/dist/types/goals/state.d.ts +8 -0
  60. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  61. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  62. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  63. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  64. package/dist/types/hindsight/state.d.ts +11 -4
  65. package/dist/types/lsp/lspmux.d.ts +5 -3
  66. package/dist/types/lsp/path-trust.d.ts +1 -0
  67. package/dist/types/main.d.ts +3 -2
  68. package/dist/types/memories/storage.d.ts +1 -0
  69. package/dist/types/modes/action-registry.d.ts +29 -0
  70. package/dist/types/modes/components/chrome.d.ts +14 -0
  71. package/dist/types/modes/components/command-palette.d.ts +27 -0
  72. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  73. package/dist/types/modes/components/index.d.ts +2 -1
  74. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  75. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  76. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  77. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  78. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  79. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  80. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  81. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  82. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  83. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  84. package/dist/types/modes/components/status-line.d.ts +1 -94
  85. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  86. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  87. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  88. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  89. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  90. package/dist/types/modes/controllers/command-controller.d.ts +2 -2
  91. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -0
  92. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  93. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  94. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  95. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  96. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  97. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  98. package/dist/types/modes/interactive-mode.d.ts +21 -19
  99. package/dist/types/modes/jobs-observer.d.ts +2 -0
  100. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  101. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +21 -2
  102. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  103. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  104. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  105. package/dist/types/modes/types.d.ts +22 -15
  106. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  107. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  108. package/dist/types/sdk/broker/broker.d.ts +78 -12
  109. package/dist/types/sdk/broker/ensure.d.ts +9 -0
  110. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  111. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  112. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  113. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  114. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  115. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  116. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  117. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  120. package/dist/types/sdk/bus/discord-daemon.d.ts +5 -0
  121. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  122. package/dist/types/sdk/bus/index.d.ts +27 -0
  123. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  124. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  125. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  126. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  127. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  128. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  129. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  130. package/dist/types/sdk/bus/telegram-daemon.d.ts +69 -3
  131. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  132. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  133. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  134. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  135. package/dist/types/sdk/host/host.d.ts +2 -0
  136. package/dist/types/sdk/index.d.ts +1 -0
  137. package/dist/types/sdk/session-directory.d.ts +56 -0
  138. package/dist/types/session/agent-session.d.ts +82 -47
  139. package/dist/types/session/agent-storage.d.ts +7 -0
  140. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  141. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  142. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  143. package/dist/types/session/messages.d.ts +16 -0
  144. package/dist/types/session/session-manager-internal.d.ts +14 -0
  145. package/dist/types/session/session-manager.d.ts +82 -12
  146. package/dist/types/session/session-storage.d.ts +43 -3
  147. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  148. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  149. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  150. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  151. package/dist/types/system-prompt.d.ts +2 -11
  152. package/dist/types/task/executor.d.ts +3 -1
  153. package/dist/types/thinking.d.ts +7 -0
  154. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  155. package/dist/types/tools/ask.d.ts +64 -2
  156. package/dist/types/tools/bash.d.ts +3 -0
  157. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  158. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  159. package/dist/types/tools/cron.d.ts +2 -0
  160. package/dist/types/tools/fetch.d.ts +1 -0
  161. package/dist/types/tools/index.d.ts +30 -15
  162. package/dist/types/tools/output-meta.d.ts +1 -0
  163. package/dist/types/tools/read.d.ts +3 -0
  164. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  165. package/dist/types/tools/subagent-render.d.ts +1 -0
  166. package/dist/types/tools/subagent.d.ts +6 -1
  167. package/dist/types/utils/event-bus.d.ts +1 -1
  168. package/dist/types/web/search/index.d.ts +3 -1
  169. package/package.json +21 -9
  170. package/scripts/build-sdk-package-smoke.ts +39 -1
  171. package/scripts/generate-hotkeys-docs.ts +74 -0
  172. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  173. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  174. package/src/async/job-manager.ts +455 -69
  175. package/src/cli/config-cli.ts +75 -2
  176. package/src/cli/notify-cli.ts +92 -26
  177. package/src/cli/session-picker.ts +20 -3
  178. package/src/cli/setup-cli.ts +1 -1
  179. package/src/cli.ts +1 -1
  180. package/src/commands/config.ts +1 -1
  181. package/src/commands/harness.ts +34 -106
  182. package/src/commands/launch.ts +6 -4
  183. package/src/commands/mcp-serve.ts +85 -8
  184. package/src/commands/mcp.ts +2 -2
  185. package/src/commands/notify.ts +8 -4
  186. package/src/commands/sdk.ts +61 -40
  187. package/src/commands/ultragoal.ts +4 -1
  188. package/src/config/atomic-yaml-patch.ts +80 -13
  189. package/src/config/file-lock.ts +105 -43
  190. package/src/config/keybindings.ts +64 -20
  191. package/src/config/model-auth.ts +9 -0
  192. package/src/config/model-bindings-applier.ts +139 -0
  193. package/src/config/model-discovery-manager.ts +224 -0
  194. package/src/config/model-equivalence.ts +66 -0
  195. package/src/config/model-registry.ts +169 -302
  196. package/src/config/model-resolver.ts +361 -369
  197. package/src/config/models-config-schema.ts +5 -4
  198. package/src/config/settings-schema.ts +159 -2
  199. package/src/config/settings.ts +458 -482
  200. package/src/coordinator-mcp/policy.ts +76 -11
  201. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  202. package/src/coordinator-mcp/question-state.ts +778 -0
  203. package/src/coordinator-mcp/server.ts +1702 -238
  204. package/src/debug/index.ts +1 -1
  205. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  206. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  207. package/src/discovery/helpers.ts +1 -50
  208. package/src/edit/index.ts +64 -0
  209. package/src/eval/py/env.ts +26 -0
  210. package/src/eval/py/kernel.ts +5 -3
  211. package/src/extensibility/extensions/loader.ts +9 -1
  212. package/src/extensibility/extensions/runner.ts +12 -3
  213. package/src/extensibility/extensions/types.ts +39 -1
  214. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  215. package/src/extensibility/hooks/runner.ts +2 -2
  216. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  217. package/src/extensibility/shared-events.ts +2 -0
  218. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  219. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  220. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  221. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  222. package/src/gjc-runtime/launch-tmux.ts +2 -5
  223. package/src/gjc-runtime/launch-worktree.ts +5 -5
  224. package/src/gjc-runtime/linux-proc.ts +71 -0
  225. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  226. package/src/gjc-runtime/session-resolution.ts +18 -3
  227. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  228. package/src/gjc-runtime/state-migrations.ts +1 -1
  229. package/src/gjc-runtime/state-runtime.ts +320 -249
  230. package/src/gjc-runtime/state-writer.ts +36 -40
  231. package/src/gjc-runtime/team-launch.ts +319 -0
  232. package/src/gjc-runtime/team-notify.ts +326 -0
  233. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  234. package/src/gjc-runtime/team-store.ts +828 -0
  235. package/src/gjc-runtime/team-workers.ts +962 -0
  236. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  237. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  238. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  239. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  240. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  241. package/src/gjc-runtime/ultragoal-guard.ts +83 -12
  242. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  243. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +102 -1
  244. package/src/gjc-runtime/ultragoal-runtime.ts +156 -1319
  245. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  246. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  247. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  248. package/src/goals/continuation-timeout-guard.ts +79 -0
  249. package/src/goals/runtime.ts +7 -5
  250. package/src/goals/state.ts +18 -0
  251. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  252. package/src/harness-control-plane/phase-rollup.ts +4 -7
  253. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  254. package/src/harness-control-plane/receipt-spool.ts +51 -7
  255. package/src/harness-control-plane/session-lease.ts +12 -130
  256. package/src/hindsight/backend.ts +26 -27
  257. package/src/hindsight/state.ts +101 -32
  258. package/src/hooks/skill-state.ts +17 -11
  259. package/src/internal-urls/docs-index.generated.ts +21 -18
  260. package/src/lsp/client.ts +1 -1
  261. package/src/lsp/config.ts +145 -26
  262. package/src/lsp/index.ts +1 -1
  263. package/src/lsp/lspmux.ts +39 -10
  264. package/src/lsp/path-trust.ts +81 -0
  265. package/src/main.ts +28 -8
  266. package/src/memories/index.ts +1 -0
  267. package/src/memories/storage.ts +6 -2
  268. package/src/modes/action-registry.ts +165 -0
  269. package/src/modes/components/chrome.ts +40 -0
  270. package/src/modes/components/command-palette.ts +168 -0
  271. package/src/modes/components/custom-editor.ts +1 -1
  272. package/src/modes/components/custom-provider-wizard.ts +51 -14
  273. package/src/modes/components/index.ts +2 -1
  274. package/src/modes/components/pet-selector.ts +11 -17
  275. package/src/modes/components/plan-preview-overlay.ts +295 -0
  276. package/src/modes/components/queue-mode-selector.ts +10 -27
  277. package/src/modes/components/queue-pane.ts +79 -0
  278. package/src/modes/components/read-tool-group.ts +13 -0
  279. package/src/modes/components/session-observer-overlay.ts +207 -772
  280. package/src/modes/components/sessions-dashboard.ts +181 -0
  281. package/src/modes/components/settings-selector.ts +1 -1
  282. package/src/modes/components/show-images-selector.ts +10 -24
  283. package/src/modes/components/status-line/types.ts +1 -1
  284. package/src/modes/components/status-line.ts +1 -969
  285. package/src/modes/components/tasks-pane.ts +73 -0
  286. package/src/modes/components/theme-selector.ts +11 -31
  287. package/src/modes/components/tool-execution.ts +22 -0
  288. package/src/modes/components/tool-status-header.ts +1056 -0
  289. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  290. package/src/modes/controllers/btw-controller.ts +1 -0
  291. package/src/modes/controllers/command-controller.ts +9 -15
  292. package/src/modes/controllers/event-controller.ts +3 -2
  293. package/src/modes/controllers/extension-ui-controller.ts +72 -25
  294. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  295. package/src/modes/controllers/input-controller.ts +764 -229
  296. package/src/modes/controllers/mode-gate.ts +24 -0
  297. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  298. package/src/modes/controllers/selector-controller.ts +225 -10
  299. package/src/modes/controllers/stt-controller.ts +125 -0
  300. package/src/modes/interactive-mode.ts +302 -1151
  301. package/src/modes/jobs-observer.ts +3 -0
  302. package/src/modes/runtime-init.ts +8 -0
  303. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  304. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +78 -19
  305. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  306. package/src/modes/tasks-aggregator.ts +199 -0
  307. package/src/modes/transcript-item-registry.ts +229 -0
  308. package/src/modes/types.ts +47 -20
  309. package/src/modes/utils/ui-helpers.ts +17 -9
  310. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  311. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  312. package/src/prompts/agents/architect.md +2 -8
  313. package/src/prompts/agents/critic.md +2 -8
  314. package/src/prompts/agents/executor.md +3 -0
  315. package/src/prompts/agents/planner.md +4 -12
  316. package/src/prompts/system/custom-system-prompt.md +1 -23
  317. package/src/prompts/system/plan-mode-approved.md +8 -0
  318. package/src/prompts/system/system-prompt.md +17 -96
  319. package/src/prompts/tools/bash.md +4 -4
  320. package/src/prompts/tools/browser.md +15 -52
  321. package/src/prompts/tools/find.md +2 -2
  322. package/src/prompts/tools/hashline.md +13 -55
  323. package/src/prompts/tools/read.md +3 -6
  324. package/src/prompts/tools/search-tool-bm25.md +0 -2
  325. package/src/prompts/tools/search.md +2 -4
  326. package/src/prompts/tools/skill-discovery.md +1 -2
  327. package/src/prompts/tools/task.md +1 -1
  328. package/src/runtime-mcp/manager.ts +1 -1
  329. package/src/sdk/broker/broker.ts +206 -78
  330. package/src/sdk/broker/discovery.ts +21 -2
  331. package/src/sdk/broker/ensure.ts +85 -25
  332. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  333. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  334. package/src/sdk/broker/lifecycle.ts +1399 -142
  335. package/src/sdk/broker/process-incarnation.ts +5 -13
  336. package/src/sdk/broker/session-index.ts +145 -56
  337. package/src/sdk/broker/state-version.ts +21 -0
  338. package/src/sdk/broker/transport.ts +1 -1
  339. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  340. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  341. package/src/sdk/bus/chat-effect-journal.ts +14 -4
  342. package/src/sdk/bus/config-commands.ts +5 -4
  343. package/src/sdk/bus/config.ts +98 -0
  344. package/src/sdk/bus/conversation-store.ts +5 -29
  345. package/src/sdk/bus/daemon-paths.ts +2 -0
  346. package/src/sdk/bus/discord-daemon.ts +25 -8
  347. package/src/sdk/bus/html-format.ts +20 -3
  348. package/src/sdk/bus/index.ts +834 -113
  349. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  350. package/src/sdk/bus/native-runtime-compatibility.ts +1 -1
  351. package/src/sdk/bus/natives-augment.d.ts +26 -1
  352. package/src/sdk/bus/notification-service.ts +168 -20
  353. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  354. package/src/sdk/bus/recent-activity.ts +188 -74
  355. package/src/sdk/bus/rich-document.ts +258 -0
  356. package/src/sdk/bus/rich-render.ts +62 -70
  357. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  358. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  359. package/src/sdk/bus/telegram-daemon.ts +1491 -247
  360. package/src/sdk/bus/threaded-inbound.ts +8 -3
  361. package/src/sdk/bus/threaded-render.ts +57 -1
  362. package/src/sdk/bus/topic-registry.ts +120 -13
  363. package/src/sdk/host/control/dispatch.ts +14 -2
  364. package/src/sdk/host/control/operations.ts +6 -1
  365. package/src/sdk/host/host.ts +31 -1
  366. package/src/sdk/host/query/handlers.ts +20 -22
  367. package/src/sdk/host/query/revision-store.ts +80 -57
  368. package/src/sdk/index.ts +1 -0
  369. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  370. package/src/sdk/session-directory.ts +141 -0
  371. package/src/sdk/session.ts +458 -479
  372. package/src/session/agent-session.ts +1901 -637
  373. package/src/session/agent-storage.ts +18 -0
  374. package/src/session/cache-economics.ts +25 -8
  375. package/src/session/fallback-chain-controller.ts +53 -3
  376. package/src/session/internal/managed-session-scope.ts +2226 -0
  377. package/src/session/internal/managed-session-storage.ts +453 -0
  378. package/src/session/messages.ts +91 -2
  379. package/src/session/session-manager-internal.ts +33 -0
  380. package/src/session/session-manager.ts +1112 -333
  381. package/src/session/session-storage.ts +444 -39
  382. package/src/session/volatile-context-pruning.ts +78 -0
  383. package/src/setup/credential-auto-import.ts +2 -1
  384. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  385. package/src/setup/provider-onboarding.ts +36 -61
  386. package/src/setup/provider-presets.json +5 -0
  387. package/src/skill-state/active-state.ts +3 -13
  388. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  389. package/src/skill-state/workflow-state-contract.ts +6 -41
  390. package/src/slash-commands/builtin-registry.ts +46 -11
  391. package/src/system-prompt.ts +37 -44
  392. package/src/task/agents.ts +19 -1
  393. package/src/task/executor.ts +90 -6
  394. package/src/task/index.ts +62 -37
  395. package/src/thinking.ts +21 -6
  396. package/src/tool-discovery/tool-index.ts +2 -134
  397. package/src/tools/ask.ts +157 -21
  398. package/src/tools/ast-edit.ts +1 -1
  399. package/src/tools/bash.ts +38 -0
  400. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  401. package/src/tools/browser/tab-supervisor.ts +282 -36
  402. package/src/tools/computer.ts +10 -3
  403. package/src/tools/cron.ts +30 -16
  404. package/src/tools/eval.ts +1 -2
  405. package/src/tools/fetch.ts +11 -4
  406. package/src/tools/index.ts +76 -24
  407. package/src/tools/output-meta.ts +13 -5
  408. package/src/tools/read.ts +37 -1
  409. package/src/tools/resource-gc.ts +2 -2
  410. package/src/tools/search-tool-bm25.ts +11 -74
  411. package/src/tools/subagent-render.ts +19 -8
  412. package/src/tools/subagent.ts +84 -32
  413. package/src/utils/event-bus.ts +9 -4
  414. package/src/web/search/index.ts +3 -3
  415. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  416. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  417. package/src/discovery/plugin-dir-roots.ts +0 -28
  418. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -1,838 +1,273 @@
1
- /**
2
- * Session observer overlay component.
3
- *
4
- * Picker mode: lists main + active subagent sessions with live status.
5
- * Viewer mode: renders a scrollable, interactive transcript of the selected subagent's session
6
- * by reading its JSONL session file — shows thinking, text, tool calls, results
7
- * with expand/collapse per entry and breadcrumb navigation for nested sub-agents.
8
- *
9
- * Lifecycle:
10
- * - shortcut opens picker
11
- * - Enter on a subagent -> viewer
12
- * - shortcut while in viewer -> back to picker (or pop breadcrumb)
13
- * - Esc from viewer -> back to picker (or pop breadcrumb)
14
- * - Esc from picker -> close overlay
15
- * - Enter on main session -> close overlay (jump back)
16
- */
1
+ import * as fs from "node:fs";
17
2
  import type { ToolResultMessage } from "@gajae-code/ai";
18
- import { Container, Markdown, type MarkdownTheme, matchesKey, resolveTerminalColumns } from "@gajae-code/tui";
19
- import { formatDuration, formatNumber, logger } from "@gajae-code/utils";
3
+ import { matchesKey } from "@gajae-code/tui";
4
+ import { formatDuration, formatNumber } from "@gajae-code/utils";
20
5
  import type { KeyId } from "../../config/keybindings";
21
6
  import { isSilentAbort } from "../../session/messages";
22
7
  import type { SessionMessageEntry } from "../../session/session-manager";
23
8
  import { parseSessionEntries } from "../../session/session-manager";
24
- import { PREVIEW_LIMITS, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
25
9
  import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
26
- import { getMarkdownTheme, theme } from "../theme/theme";
27
- import { DynamicBorder } from "./dynamic-border";
10
+ import { theme } from "../theme/theme";
11
+ import { type TranscriptViewerEntry, TranscriptViewerOverlay } from "./transcript-viewer-overlay";
28
12
 
29
- /** Max thinking characters in collapsed state */
30
- const MAX_THINKING_CHARS_COLLAPSED = 200;
31
- /** Max thinking characters in expanded state */
32
- const MAX_THINKING_CHARS_EXPANDED = 4000;
33
- /** Max tool args characters to display */
34
13
  const MAX_TOOL_ARGS_CHARS = 500;
35
- /** Lines per page for PageUp/PageDown */
36
- const PAGE_SIZE = 15;
37
- /** Left indent for content under entry headers */
38
- const INDENT = " ";
39
-
40
- /** Compute the max content width for the current terminal, accounting for indent and chrome. */
41
- function contentWidth(indent = INDENT): number {
42
- return Math.max(TRUNCATE_LENGTHS.SHORT, resolveTerminalColumns() - indent.length - 2);
43
- }
44
-
45
- /** Sanitize a line for TUI display: replace tabs, then truncate to viewport width. */
46
- function sanitizeLine(text: string, maxWidth?: number): string {
47
- return truncateToWidth(replaceTabs(text), maxWidth ?? contentWidth());
48
- }
49
-
50
- /** Represents a rendered entry in the viewer for selection/expand tracking */
51
- interface ViewerEntry {
52
- lineStart: number;
53
- lineCount: number;
54
- kind: "thinking" | "text" | "toolCall" | "user";
55
- }
56
-
57
- /** Breadcrumb item for nested session navigation */
58
- interface BreadcrumbItem {
59
- sessionId: string;
60
- label: string;
61
- sessionFile: string;
62
- }
63
14
 
64
- export class SessionObserverOverlayComponent extends Container {
15
+ /** Session-observer adapter. The shared viewer owns navigation and fold state. */
16
+ export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
65
17
  #registry: SessionObserverRegistry;
66
18
  #onDone: () => void;
19
+ #observeKeys: readonly KeyId[];
67
20
  #selectedSessionId?: string;
68
- #observeKeys: KeyId[];
69
- #transcriptCache?: { path: string; bytesRead: number; entries: SessionMessageEntry[]; model?: string };
70
-
71
- // Scroll state
72
- #scrollOffset = 0;
73
- #renderedLines: string[] = [];
74
- #viewportHeight = 20;
75
- #wasAtBottom = true;
76
-
77
- // Entry selection & expand/collapse
78
- #viewerEntries: ViewerEntry[] = [];
79
- #selectedEntryIndex = 0;
80
- #expandedEntries = new Set<number>();
81
-
82
- // Breadcrumb navigation
83
- #navigationStack: BreadcrumbItem[] = [];
84
-
85
- // Cached header/footer for viewer (rebuilt on refresh)
86
- #viewerHeaderLines: string[] = [];
87
- #viewerFooterLines: string[] = [];
88
- // Markdown rendering
89
- #mdTheme: MarkdownTheme = getMarkdownTheme();
21
+ #cache?: { path: string; bytesRead: number; entries: SessionMessageEntry[]; model?: string };
90
22
 
91
23
  constructor(registry: SessionObserverRegistry, onDone: () => void, observeKeys: KeyId[]) {
92
- super();
24
+ // The option closures run during the base constructor's initial refresh,
25
+ // where `this` is still in its temporal dead zone; route through a box
26
+ // that is populated immediately after super() returns.
27
+ const box: { component?: SessionObserverOverlayComponent } = {};
28
+ super({
29
+ title: "Session Observer",
30
+ getEntries: () => (box.component ? box.component.#entries() : []),
31
+ onClose: onDone,
32
+ requestRender: () => {},
33
+ enterExpands: true,
34
+ initialSelection: "latest",
35
+ followTail: true,
36
+ maxExpandedLines: 100,
37
+ getHeaderLines: () => (box.component ? box.component.#headerLines() : []),
38
+ getFooterLines: () => (box.component ? box.component.#footerLines() : []),
39
+ footerControls:
40
+ "j/k:select Enter:expand PgUp/PgDn:page [/]/←→:cycle agents Esc/Ctrl+S:close g/G:top/bottom",
41
+ });
42
+ box.component = this;
93
43
  this.#registry = registry;
94
44
  this.#onDone = onDone;
95
45
  this.#observeKeys = observeKeys;
96
-
97
- // Jump directly to the most recently active sub-agent
98
- const mostRecent = this.#getMostRecentSubagent();
99
- if (mostRecent) {
100
- this.#selectedSessionId = mostRecent.id;
101
- this.#setupViewer();
102
- } else {
103
- // No sub-agents — close immediately
104
- queueMicrotask(() => this.#onDone());
105
- }
106
- }
107
-
108
- /** Find the most recently updated sub-agent session (prefer active ones) */
109
- #getMostRecentSubagent(): ObservableSession | undefined {
110
- const sessions = this.#registry.getSessions().filter(s => s.kind === "subagent");
111
- if (sessions.length === 0) return undefined;
112
- // Prefer active sessions, then sort by lastUpdate descending
113
- const active = sessions.filter(s => s.status === "active");
114
- const pool = active.length > 0 ? active : sessions;
115
- return pool.sort((a, b) => b.lastUpdate - a.lastUpdate)[0];
116
- }
117
-
118
- override render(width: number): string[] {
119
- return this.#renderViewer(width);
120
- }
121
-
122
- #setupViewer(): void {
123
- this.children = [];
124
- this.#scrollOffset = 0;
125
- this.#selectedEntryIndex = 0;
126
- this.#expandedEntries.clear();
127
- this.#wasAtBottom = true;
128
- this.#rebuildViewerContent();
129
- // Auto-scroll to bottom and select last entry on init
130
- if (this.#viewerEntries.length > 0) {
131
- this.#selectedEntryIndex = this.#viewerEntries.length - 1;
132
- this.#wasAtBottom = true;
133
- this.#rebuildViewerContent();
134
- }
46
+ this.#selectedSessionId = this.#mostRecent()?.id;
47
+ if (!this.#selectedSessionId) queueMicrotask(onDone);
48
+ this.refresh();
135
49
  }
136
50
 
137
- /** Rebuild content from live registry data */
138
51
  refreshFromRegistry(): void {
139
- if (this.#selectedSessionId) {
140
- // Keep auto-scrolling to bottom unless the user navigated away from the last entry
141
- this.#wasAtBottom = this.#selectedEntryIndex >= this.#viewerEntries.length - 1;
142
- this.#rebuildViewerContent();
143
- }
52
+ this.refresh();
144
53
  }
145
-
146
- /** Rebuild the transcript content lines (called on setup and refresh) */
147
- #rebuildViewerContent(): void {
148
- const sessions = this.#registry.getSessions();
149
- const session = sessions.find(s => s.id === this.#selectedSessionId);
150
-
151
- // Load transcript first so model info is available for header
152
- let messageEntries: SessionMessageEntry[] | null = null;
153
- if (session?.sessionFile) {
154
- messageEntries = this.#loadTranscript(session.sessionFile);
54
+ override handleInput(keyData: string): void {
55
+ if (this.#observeKeys.some(key => matchesKey(keyData, key))) {
56
+ this.#onDone();
57
+ return;
155
58
  }
156
-
157
- // Header
158
- this.#viewerHeaderLines = [];
159
- const breadcrumb = this.#buildBreadcrumb(session);
160
- this.#viewerHeaderLines.push(theme.fg("accent", breadcrumb));
161
- if (session) {
162
- const statusColor = session.status === "active" ? "success" : session.status === "failed" ? "error" : "dim";
163
- const statusText = theme.fg(statusColor, `[${session.status}]`);
164
- const agentTag = session.agent ? theme.fg("dim", ` ${session.agent}`) : "";
165
- const subagentIds = this.#getSubagentSessionIds();
166
- const posIdx = subagentIds.indexOf(this.#selectedSessionId ?? "");
167
- const posLabel =
168
- subagentIds.length > 1 && posIdx >= 0 ? theme.fg("dim", ` (${posIdx + 1}/${subagentIds.length})`) : "";
169
- const modelName = this.#transcriptCache?.model;
170
- const modelLabel = modelName ? theme.fg("muted", ` · ${modelName}`) : "";
171
- this.#viewerHeaderLines.push(`${theme.bold(session.label)} ${statusText}${agentTag}${posLabel}${modelLabel}`);
59
+ if (keyData === "]" || matchesKey(keyData, "right") || matchesKey(keyData, "tab")) {
60
+ this.#cycle(1);
61
+ return;
172
62
  }
173
-
174
- // Content
175
- const contentLines: string[] = [];
176
- this.#viewerEntries = [];
177
-
178
- if (!session) {
179
- contentLines.push(theme.fg("dim", "Session no longer available."));
180
- } else if (!session.sessionFile) {
181
- contentLines.push(theme.fg("dim", "No session file available yet."));
182
- } else if (!messageEntries) {
183
- contentLines.push(theme.fg("dim", "Unable to read session file."));
184
- } else if (messageEntries.length === 0) {
185
- contentLines.push(theme.fg("dim", "No messages yet."));
186
- } else {
187
- this.#buildTranscriptLines(messageEntries, contentLines);
63
+ if (keyData === "[" || matchesKey(keyData, "left") || matchesKey(keyData, "shift+tab")) {
64
+ this.#cycle(-1);
65
+ return;
188
66
  }
189
- this.#renderedLines = contentLines;
190
-
191
- // Footer
192
- this.#viewerFooterLines = [];
193
- const statsLine = this.#buildStatsLine(session);
194
- if (statsLine) this.#viewerFooterLines.push(statsLine);
195
- this.#viewerFooterLines.push(
196
- theme.fg("dim", "j/k:scroll Enter:expand [/]/←→:cycle agents Esc/Ctrl+S:close g/G:top/bottom"),
67
+ super.handleInput(keyData);
68
+ }
69
+ #mostRecent(): ObservableSession | undefined {
70
+ const all = this.#registry.getSessions().filter(session => session.kind === "subagent");
71
+ return (
72
+ all.filter(session => session.status === "active").sort((a, b) => b.lastUpdate - a.lastUpdate)[0] ??
73
+ all.sort((a, b) => b.lastUpdate - a.lastUpdate)[0]
197
74
  );
198
-
199
- // Auto-scroll to bottom if we were at bottom
200
- if (this.#wasAtBottom) {
201
- this.#scrollOffset = Math.max(0, contentLines.length - this.#viewportHeight);
202
- }
203
75
  }
204
-
205
- /** Produce the final viewer output for the overlay system */
206
- #renderViewer(width: number): string[] {
207
- const termHeight = process.stdout.rows || 40;
208
-
209
- // Compute viewport: total height minus header chrome and footer chrome
210
- // Header: border(1) + headerLines + border(1) = headerLines.length + 2
211
- // Footer: spacer(1) + scrollInfo(1) + footerLines + border(1) = footerLines.length + 2
212
- const headerChrome = this.#viewerHeaderLines.length + 2;
213
- const footerChrome = this.#viewerFooterLines.length + 2;
214
- this.#viewportHeight = Math.max(5, termHeight - headerChrome - footerChrome);
215
-
216
- // Clamp scroll offset
217
- const maxScroll = Math.max(0, this.#renderedLines.length - this.#viewportHeight);
218
- this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, maxScroll));
219
-
220
- const lines: string[] = [];
221
-
222
- // --- Header ---
223
- lines.push(...new DynamicBorder().render(width));
224
- for (const hl of this.#viewerHeaderLines) {
225
- lines.push(` ${hl}`);
226
- }
227
- lines.push(...new DynamicBorder().render(width));
228
-
229
- // --- Scrolled content viewport ---
230
- const visibleLines = this.#renderedLines.slice(this.#scrollOffset, this.#scrollOffset + this.#viewportHeight);
231
- for (const vl of visibleLines) {
232
- lines.push(` ${vl}`);
233
- }
234
- // Pad to fill viewport if content is shorter
235
- const pad = this.#viewportHeight - visibleLines.length;
236
- for (let i = 0; i < pad; i++) {
237
- lines.push("");
238
- }
239
-
240
- // --- Footer ---
241
- const scrollInfo =
242
- this.#renderedLines.length > this.#viewportHeight
243
- ? ` ${theme.fg("dim", `[${this.#scrollOffset + 1}-${Math.min(this.#scrollOffset + this.#viewportHeight, this.#renderedLines.length)}/${this.#renderedLines.length}]`)}`
244
- : "";
245
- lines.push("");
246
- lines.push(` ${this.#viewerFooterLines[0] ?? ""}${scrollInfo}`);
247
- for (let i = 1; i < this.#viewerFooterLines.length; i++) {
248
- lines.push(` ${this.#viewerFooterLines[i]}`);
249
- }
250
- lines.push(...new DynamicBorder().render(width));
251
-
252
- return lines;
76
+ #cycle(direction: 1 | -1): void {
77
+ const ids = this.#registry
78
+ .getSessions()
79
+ .filter(session => session.kind === "subagent")
80
+ .map(session => session.id);
81
+ if (ids.length < 2) return;
82
+ const current = ids.indexOf(this.#selectedSessionId ?? "");
83
+ this.#selectedSessionId = ids[(current + direction + ids.length) % ids.length];
84
+ this.#cache = undefined;
85
+ this.resetSourceState();
86
+ this.refresh();
253
87
  }
254
-
255
- #buildBreadcrumb(session: ObservableSession | undefined): string {
256
- const parts: string[] = ["Session Observer"];
257
- for (const item of this.#navigationStack) {
258
- parts.push(item.label);
259
- }
260
- if (session) parts.push(session.label);
261
- return parts.join(" > ");
88
+ #entries(): readonly TranscriptViewerEntry[] {
89
+ const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
90
+ if (!session?.sessionFile) return [];
91
+ return entriesFromMessages(this.#load(session.sessionFile));
262
92
  }
263
-
264
- #buildStatsLine(session: ObservableSession | undefined): string {
93
+ #headerLines(): string[] {
94
+ const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
95
+ if (!session) return [theme.fg("dim", "Session no longer available.")];
96
+ const ids = this.#registry
97
+ .getSessions()
98
+ .filter(candidate => candidate.kind === "subagent")
99
+ .map(candidate => candidate.id);
100
+ const position = ids.length > 1 ? theme.fg("dim", ` (${ids.indexOf(session.id) + 1}/${ids.length})`) : "";
101
+ const color = session.status === "active" ? "success" : session.status === "failed" ? "error" : "dim";
102
+ const model = this.#cache?.model ? theme.fg("muted", ` · ${this.#cache.model}`) : "";
103
+ return [
104
+ `${theme.bold(session.label)} ${theme.fg(color, `[${session.status}]`)}${session.agent ? theme.fg("dim", ` ${session.agent}`) : ""}${position}${model}`,
105
+ ];
106
+ }
107
+ #footerLines(): string[] {
108
+ const session = this.#registry.getSessions().find(candidate => candidate.id === this.#selectedSessionId);
265
109
  const progress = session?.progress;
266
- if (!progress) return "";
110
+ if (!progress) return [];
267
111
  const stats: string[] = [];
268
112
  if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
269
- // Current per-turn context — what the user reads as "how full is the context".
270
- // Falls back to cumulative billing volume (Σ-prefixed) when context size is unknown.
271
113
  if (progress.contextTokens && progress.contextTokens > 0) {
272
- const ctx =
114
+ stats.push(
273
115
  progress.contextWindow && progress.contextWindow > 0
274
116
  ? `${formatNumber(progress.contextTokens)}/${formatNumber(progress.contextWindow)} ctx`
275
- : `${formatNumber(progress.contextTokens)} ctx`;
276
- stats.push(ctx);
117
+ : `${formatNumber(progress.contextTokens)} ctx`,
118
+ );
277
119
  if (progress.tokens > 0) stats.push(`Σ${formatNumber(progress.tokens)}`);
278
- } else if (progress.tokens > 0) {
279
- stats.push(`Σ${formatNumber(progress.tokens)}`);
280
- }
120
+ } else if (progress.tokens > 0) stats.push(`Σ${formatNumber(progress.tokens)}`);
281
121
  if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
282
- const parts: string[] = [];
283
- if (stats.length > 0) parts.push(theme.fg("dim", stats.join(theme.sep.dot)));
284
- if (progress.cost > 0) parts.push(theme.fg("statusLineCost", `$${progress.cost.toFixed(2)}`));
285
- return parts.join(theme.sep.dot);
286
- }
287
-
288
- #buildTranscriptLines(messageEntries: SessionMessageEntry[], lines: string[]): void {
289
- // Build a tool call ID -> tool result map
290
- const toolResults = new Map<string, ToolResultMessage>();
291
- for (const entry of messageEntries) {
292
- if (entry.message.role === "toolResult") {
293
- toolResults.set(entry.message.toolCallId, entry.message);
294
- }
295
- }
296
-
297
- let entryIndex = 0;
298
- for (const entry of messageEntries) {
299
- const msg = entry.message;
300
-
301
- if (msg.role === "assistant") {
302
- // Handle error messages with empty content
303
- if (msg.content.length === 0 && msg.errorMessage && !isSilentAbort(msg.errorMessage)) {
304
- const startLine = lines.length;
305
- const isSelected = entryIndex === this.#selectedEntryIndex;
306
- const cursor = isSelected ? theme.fg("accent", "▶") : " ";
307
- lines.push("");
308
- const errorLines = msg.errorMessage.split("\n");
309
- const maxWidth = contentWidth();
310
- lines.push(`${cursor} ${theme.fg("error", `✗ Error: ${sanitizeLine(errorLines[0], maxWidth)}`)}`);
311
- for (let i = 1; i < errorLines.length; i++) {
312
- lines.push(`${INDENT}${theme.fg("error", sanitizeLine(errorLines[i], maxWidth))}`);
313
- }
314
- this.#viewerEntries.push({ lineStart: startLine, lineCount: lines.length - startLine, kind: "text" });
315
- entryIndex++;
316
- } else {
317
- for (const content of msg.content) {
318
- if (content.type === "thinking" && content.thinking.trim()) {
319
- const startLine = lines.length;
320
- const isExpanded = this.#expandedEntries.has(entryIndex);
321
- const isSelected = entryIndex === this.#selectedEntryIndex;
322
- this.#renderThinkingLines(lines, content.thinking.trim(), isExpanded, isSelected);
323
- this.#viewerEntries.push({
324
- lineStart: startLine,
325
- lineCount: lines.length - startLine,
326
- kind: "thinking",
327
- });
328
- entryIndex++;
329
- } else if (content.type === "text" && content.text.trim()) {
330
- const startLine = lines.length;
331
- const isExpanded = this.#expandedEntries.has(entryIndex);
332
- const isSelected = entryIndex === this.#selectedEntryIndex;
333
- this.#renderTextLines(lines, content.text.trim(), isExpanded, isSelected);
334
- this.#viewerEntries.push({
335
- lineStart: startLine,
336
- lineCount: lines.length - startLine,
337
- kind: "text",
338
- });
339
- entryIndex++;
340
- } else if (content.type === "toolCall") {
341
- const startLine = lines.length;
342
- const isExpanded = this.#expandedEntries.has(entryIndex);
343
- const isSelected = entryIndex === this.#selectedEntryIndex;
344
- const result = toolResults.get(content.id);
345
- this.#renderToolCallLines(lines, content, result, isExpanded, isSelected);
346
- this.#viewerEntries.push({
347
- lineStart: startLine,
348
- lineCount: lines.length - startLine,
349
- kind: "toolCall",
350
- });
351
- entryIndex++;
352
- }
353
- }
354
- }
355
- } else if (msg.role === "user" || msg.role === "developer") {
356
- const text =
357
- typeof msg.content === "string"
358
- ? msg.content
359
- : msg.content
360
- .filter((b): b is { type: "text"; text: string } => b.type === "text")
361
- .map(b => b.text)
362
- .join("\n");
363
- if (text.trim()) {
364
- const startLine = lines.length;
365
- const isSelected = entryIndex === this.#selectedEntryIndex;
366
- const isExpanded = this.#expandedEntries.has(entryIndex);
367
- const label = msg.role === "developer" ? "System" : "User";
368
- const cursor = isSelected ? theme.fg("accent", "▶") : " ";
369
- lines.push("");
370
- if (isExpanded) {
371
- lines.push(`${cursor} ${theme.fg("dim", `[${label}]`)}`);
372
- const mdLines = this.#renderMarkdownToLines(text.trim());
373
- for (const ml of mdLines) {
374
- lines.push(ml);
375
- }
376
- } else {
377
- const firstLine = text.trim().split("\n")[0];
378
- const totalLines = text.trim().split("\n").length;
379
- const hint = totalLines > 1 ? theme.fg("dim", ` (${totalLines} lines)`) : "";
380
- lines.push(
381
- `${cursor} ${theme.fg("dim", `[${label}]`)} ${theme.fg("muted", sanitizeLine(firstLine, TRUNCATE_LENGTHS.TITLE))}${hint}`,
382
- );
383
- }
384
- this.#viewerEntries.push({ lineStart: startLine, lineCount: lines.length - startLine, kind: "user" });
385
- entryIndex++;
386
- }
387
- }
388
- }
389
- }
390
-
391
- /** Render markdown text into indented lines using the theme's markdown renderer */
392
- #renderMarkdownToLines(text: string, indent: string = INDENT): string[] {
393
- const width = Math.max(40, resolveTerminalColumns() - indent.length - 4);
394
- const md = new Markdown(text, 0, 0, this.#mdTheme);
395
- const rendered = md.render(width);
396
- return rendered.map(line => `${indent}${line.trimEnd()}`);
397
- }
398
-
399
- #renderThinkingLines(lines: string[], thinking: string, expanded: boolean, selected: boolean): void {
400
- const cursor = selected ? theme.fg("accent", "▶") : " ";
401
- const maxChars = expanded ? MAX_THINKING_CHARS_EXPANDED : MAX_THINKING_CHARS_COLLAPSED;
402
- const truncated = thinking.length > maxChars;
403
- const expandLabel = !expanded && truncated ? theme.fg("dim", " ↵") : "";
404
-
405
- lines.push("");
406
- lines.push(`${cursor} ${theme.fg("dim", "💭 Thinking")}${expandLabel}`);
407
-
408
- const displayText = truncated ? `${thinking.slice(0, maxChars)}...` : thinking;
409
- if (expanded) {
410
- // Expanded thinking: render as markdown for readable formatting
411
- const mdLines = this.#renderMarkdownToLines(displayText);
412
- const maxLines = 100;
413
- for (let i = 0; i < Math.min(mdLines.length, maxLines); i++) {
414
- lines.push(mdLines[i]);
415
- }
416
- if (mdLines.length > maxLines) {
417
- lines.push(`${INDENT}${theme.fg("dim", `... ${mdLines.length - maxLines} more lines`)}`);
418
- }
419
- } else {
420
- // Collapsed thinking: brief italic preview
421
- const thinkingLines = displayText.split("\n");
422
- const maxLines = PREVIEW_LIMITS.COLLAPSED_LINES;
423
- for (let i = 0; i < Math.min(thinkingLines.length, maxLines); i++) {
424
- lines.push(`${INDENT}${theme.fg("thinkingText", sanitizeLine(thinkingLines[i]))}`);
425
- }
426
- if (thinkingLines.length > maxLines) {
427
- lines.push(`${INDENT}${theme.fg("dim", `... ${thinkingLines.length - maxLines} more lines`)}`);
428
- }
429
- }
430
- }
431
-
432
- #renderTextLines(lines: string[], text: string, expanded: boolean, selected: boolean): void {
433
- const cursor = selected ? theme.fg("accent", "▶") : " ";
434
-
435
- lines.push("");
436
- lines.push(`${cursor} ${theme.fg("muted", "Response")}`);
437
-
438
- if (expanded) {
439
- // Expanded: full markdown rendering
440
- const mdLines = this.#renderMarkdownToLines(text);
441
- for (const ml of mdLines) {
442
- lines.push(ml);
443
- }
444
- } else {
445
- // Collapsed: first few lines as plain text
446
- const textLines = text.split("\n");
447
- const maxLines = PREVIEW_LIMITS.COLLAPSED_LINES;
448
- const maxWidth = contentWidth();
449
- for (let i = 0; i < Math.min(textLines.length, maxLines); i++) {
450
- lines.push(`${INDENT}${sanitizeLine(textLines[i], maxWidth)}`);
451
- }
452
- if (textLines.length > maxLines) {
453
- lines.push(`${INDENT}${theme.fg("dim", `... ${textLines.length - maxLines} more lines`)}`);
454
- }
455
- }
456
- }
457
-
458
- #renderToolCallLines(
459
- lines: string[],
460
- call: { id: string; name: string; arguments: Record<string, unknown>; intent?: string },
461
- result: ToolResultMessage | undefined,
462
- expanded: boolean,
463
- selected: boolean,
464
- ): void {
465
- const cursor = selected ? theme.fg("accent", "▶") : " ";
466
- lines.push("");
467
-
468
- // Tool call header
469
- const intentStr = call.intent ? theme.fg("dim", ` ${sanitizeLine(call.intent, TRUNCATE_LENGTHS.SHORT)}`) : "";
470
- lines.push(`${cursor} ${theme.fg("accent", "▸")} ${theme.bold(theme.fg("muted", call.name))}${intentStr}`);
471
-
472
- // Key arguments
473
- const argSummary = this.#formatToolArgs(call.name, call.arguments);
474
- if (argSummary) {
475
- lines.push(`${INDENT}${theme.fg("dim", sanitizeLine(argSummary, contentWidth()))}`);
476
- }
477
-
478
- // Tool result
479
- if (result) {
480
- this.#renderToolResultLines(lines, result, expanded);
481
- }
482
- }
483
-
484
- #renderToolResultLines(lines: string[], result: ToolResultMessage, expanded: boolean): void {
485
- const textParts = result.content
486
- .filter((p): p is { type: "text"; text: string } => p.type === "text")
487
- .map(p => p.text);
488
- const text = textParts.join("\n").trim();
489
-
490
- if (result.isError) {
491
- const errorLines = text.split("\n");
492
- const maxErrorLines = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.COLLAPSED_LINES;
493
- const maxWidth = contentWidth();
494
- lines.push(`${INDENT}${theme.fg("error", `✗ ${sanitizeLine(errorLines[0] || "Error", maxWidth)}`)}`);
495
- for (let i = 1; i < Math.min(errorLines.length, maxErrorLines); i++) {
496
- lines.push(`${INDENT} ${theme.fg("error", sanitizeLine(errorLines[i], maxWidth))}`);
497
- }
498
- if (errorLines.length > maxErrorLines) {
499
- lines.push(`${INDENT} ${theme.fg("dim", `... ${errorLines.length - maxErrorLines} more lines`)}`);
500
- }
501
- return;
502
- }
503
-
504
- if (!text) {
505
- lines.push(`${INDENT}${theme.fg("dim", "✓ done")}`);
506
- return;
507
- }
508
-
509
- const resultLines = text.split("\n");
510
- const maxLines = expanded ? PREVIEW_LIMITS.EXPANDED_LINES : PREVIEW_LIMITS.OUTPUT_COLLAPSED;
511
-
512
- // Status line
513
- const statusPrefix = `${INDENT}${theme.fg("success", "✓")}`;
514
-
515
- if (resultLines.length === 1 && text.length < TRUNCATE_LENGTHS.LONG) {
516
- lines.push(`${statusPrefix} ${theme.fg("dim", sanitizeLine(text))}`);
517
- return;
518
- }
519
-
520
- lines.push(`${statusPrefix} ${theme.fg("dim", `${resultLines.length} lines`)}`);
521
- const displayLines = resultLines.slice(0, maxLines);
522
- for (const rl of displayLines) {
523
- lines.push(`${INDENT} ${theme.fg("dim", sanitizeLine(rl))}`);
524
- }
525
- if (resultLines.length > maxLines) {
526
- lines.push(`${INDENT} ${theme.fg("dim", `... ${resultLines.length - maxLines} more`)}`);
527
- }
528
- }
529
-
530
- #formatToolArgs(toolName: string, args: Record<string, unknown>): string {
531
- switch (toolName) {
532
- case "read":
533
- case "write":
534
- case "edit":
535
- return args.path ? `path: ${args.path}` : "";
536
- case "search":
537
- return [
538
- args.pattern ? `pattern: ${args.pattern}` : "",
539
- Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "",
540
- ]
541
- .filter(Boolean)
542
- .join(", ");
543
- case "find":
544
- return Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "";
545
- case "bash": {
546
- const cmd = args.command;
547
- return typeof cmd === "string" ? replaceTabs(cmd) : "";
548
- }
549
- case "lsp":
550
- return [args.action, args.file, args.symbol].filter(Boolean).join(" ");
551
- case "ast_grep":
552
- case "ast_edit":
553
- return args.path ? `path: ${args.path}` : "";
554
- case "task": {
555
- const tasks = args.tasks;
556
- return Array.isArray(tasks) ? `${tasks.length} task(s)` : "";
557
- }
558
- default: {
559
- const parts: string[] = [];
560
- let total = 0;
561
- for (const [key, value] of Object.entries(args)) {
562
- if (key.startsWith("_")) continue;
563
- const v = typeof value === "string" ? value : JSON.stringify(value);
564
- const entry = `${key}: ${replaceTabs(v ?? "")}`;
565
- if (total + entry.length > MAX_TOOL_ARGS_CHARS) break;
566
- parts.push(entry);
567
- total += entry.length;
568
- }
569
- return parts.join(", ");
570
- }
571
- }
122
+ if (progress.cost > 0) stats.push(`$${progress.cost.toFixed(2)}`);
123
+ return stats.length ? [theme.fg("dim", stats.join(theme.sep.dot))] : [];
572
124
  }
573
-
574
- #loadTranscript(sessionFile: string): SessionMessageEntry[] | null {
575
- if (this.#transcriptCache && this.#transcriptCache.path !== sessionFile) {
576
- this.#transcriptCache = undefined;
577
- }
578
-
579
- const fromByte = this.#transcriptCache?.bytesRead ?? 0;
580
- const result = readFileIncremental(sessionFile, fromByte);
581
- if (!result) {
582
- logger.debug("Session observer: failed to read session file", { path: sessionFile });
583
- return this.#transcriptCache?.entries ?? null;
584
- }
585
-
125
+ #load(filePath: string): SessionMessageEntry[] {
126
+ if (this.#cache?.path !== filePath) this.#cache = undefined;
127
+ const fromByte = this.#cache?.bytesRead ?? 0;
128
+ const result = readFileIncremental(filePath, fromByte);
129
+ if (!result) return this.#cache?.entries ?? [];
586
130
  if (result.newSize < fromByte) {
587
- this.#transcriptCache = undefined;
588
- return this.#loadTranscript(sessionFile);
131
+ this.#cache = undefined;
132
+ return this.#load(filePath);
589
133
  }
590
-
591
- if (!this.#transcriptCache) {
592
- this.#transcriptCache = { path: sessionFile, bytesRead: 0, entries: [] };
593
- }
594
-
595
- if (result.text.length > 0) {
134
+ if (!this.#cache) this.#cache = { path: filePath, bytesRead: 0, entries: [] };
135
+ if (result.text) {
596
136
  const lastNewline = result.text.lastIndexOf("\n");
597
137
  if (lastNewline >= 0) {
598
- const completeChunk = result.text.slice(0, lastNewline + 1);
599
- const newEntries = parseSessionEntries(completeChunk);
600
- for (const entry of newEntries) {
138
+ const complete = result.text.slice(0, lastNewline + 1);
139
+ for (const entry of parseSessionEntries(complete)) {
601
140
  if (entry.type === "message") {
602
- this.#transcriptCache.entries.push(entry);
603
- // Extract model from first assistant message
604
- const msg = entry.message;
605
- if (!this.#transcriptCache.model && msg.role === "assistant") {
606
- this.#transcriptCache.model = msg.model;
607
- }
608
- } else if (entry.type === "model_change") {
609
- this.#transcriptCache.model = entry.model;
610
- }
141
+ this.#cache.entries.push(entry);
142
+ if (!this.#cache.model && entry.message.role === "assistant") this.#cache.model = entry.message.model;
143
+ } else if (entry.type === "model_change") this.#cache.model = entry.model;
611
144
  }
612
- this.#transcriptCache.bytesRead = fromByte + Buffer.byteLength(completeChunk, "utf-8");
145
+ this.#cache.bytesRead = fromByte + Buffer.byteLength(complete, "utf-8");
613
146
  }
614
147
  }
615
- return this.#transcriptCache.entries;
148
+ return this.#cache.entries;
616
149
  }
150
+ }
617
151
 
618
- #navigateBack(): boolean {
619
- if (this.#navigationStack.length === 0) return false;
620
- const prev = this.#navigationStack.pop()!;
621
- this.#selectedSessionId = prev.sessionId;
622
- this.#transcriptCache = undefined;
623
- this.#scrollOffset = 0;
624
- this.#selectedEntryIndex = 0;
625
- this.#expandedEntries.clear();
626
- this.#rebuildViewerContent();
627
- return true;
628
- }
629
-
630
- handleInput(keyData: string): void {
631
- // Ctrl+S (observe key) always closes the overlay
632
- for (const key of this.#observeKeys) {
633
- if (matchesKey(keyData, key)) {
634
- this.#onDone();
635
- return;
636
- }
637
- }
638
-
639
- this.#handleViewerInput(keyData);
640
- }
641
-
642
- #handleViewerInput(keyData: string): void {
643
- const entryCount = this.#viewerEntries.length;
644
-
645
- // Escape — pop breadcrumb navigation or close overlay
646
- if (matchesKey(keyData, "escape")) {
647
- if (!this.#navigateBack()) {
648
- this.#onDone();
649
- }
650
- return;
651
- }
652
-
653
- // j / down — move selection down
654
- if (keyData === "j" || matchesKey(keyData, "down")) {
655
- if (entryCount > 0) {
656
- this.#selectedEntryIndex = Math.min(this.#selectedEntryIndex + 1, entryCount - 1);
657
- }
658
- this.#rebuildAndScroll();
659
- return;
660
- }
661
-
662
- // k / up — move selection up
663
- if (keyData === "k" || matchesKey(keyData, "up")) {
664
- if (entryCount > 0) {
665
- this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 1, 0);
666
- }
667
- this.#rebuildAndScroll();
668
- return;
669
- }
670
-
671
- // Page Down
672
- if (matchesKey(keyData, "pageDown")) {
673
- if (entryCount > 0) {
674
- const prevIndex = this.#selectedEntryIndex;
675
- this.#selectedEntryIndex = Math.min(this.#selectedEntryIndex + 5, entryCount - 1);
676
- // If selection didn't move (bottom of list or single oversized entry), fall back to line scroll
677
- if (this.#selectedEntryIndex === prevIndex) {
678
- this.#scrollOffset = Math.min(
679
- this.#scrollOffset + PAGE_SIZE,
680
- Math.max(0, this.#renderedLines.length - this.#viewportHeight),
681
- );
682
- }
683
- } else {
684
- this.#scrollOffset = Math.min(
685
- this.#scrollOffset + PAGE_SIZE,
686
- Math.max(0, this.#renderedLines.length - this.#viewportHeight),
687
- );
688
- }
689
- this.#rebuildAndScroll();
690
- return;
691
- }
692
-
693
- // Page Up
694
- if (matchesKey(keyData, "pageUp")) {
695
- if (entryCount > 0) {
696
- const prevIndex = this.#selectedEntryIndex;
697
- this.#selectedEntryIndex = Math.max(this.#selectedEntryIndex - 5, 0);
698
- // If selection didn't move (top of list or single oversized entry), fall back to line scroll
699
- if (this.#selectedEntryIndex === prevIndex) {
700
- this.#scrollOffset = Math.max(this.#scrollOffset - PAGE_SIZE, 0);
701
- }
702
- } else {
703
- this.#scrollOffset = Math.max(this.#scrollOffset - PAGE_SIZE, 0);
704
- }
705
- this.#rebuildAndScroll();
706
- return;
707
- }
708
-
709
- // Enter — toggle expand/collapse, or dive into nested session
710
- if (matchesKey(keyData, "enter") || keyData === "\r" || keyData === "\n") {
711
- if (entryCount > 0 && this.#selectedEntryIndex < entryCount) {
712
- // Toggle expand/collapse
713
- if (this.#expandedEntries.has(this.#selectedEntryIndex)) {
714
- this.#expandedEntries.delete(this.#selectedEntryIndex);
715
- } else {
716
- this.#expandedEntries.add(this.#selectedEntryIndex);
152
+ function entriesFromMessages(entries: readonly SessionMessageEntry[]): TranscriptViewerEntry[] {
153
+ const results = new Map<string, ToolResultMessage>();
154
+ for (const entry of entries)
155
+ if (entry.message.role === "toolResult") results.set(entry.message.toolCallId, entry.message);
156
+ const output: TranscriptViewerEntry[] = [];
157
+ for (const entry of entries) {
158
+ const message = entry.message;
159
+ if (message.role === "assistant") {
160
+ if (message.errorMessage && !isSilentAbort(message.errorMessage))
161
+ output.push({
162
+ id: `${entry.id}:error`,
163
+ kind: "text",
164
+ label: "✗ Error:",
165
+ payload: { text: message.errorMessage, metadata: {}, source: message },
166
+ foldable: true,
167
+ });
168
+ message.content.forEach((content, contentIndex) => {
169
+ if (content.type === "thinking" && content.thinking.trim())
170
+ output.push({
171
+ id: `${entry.id}:thinking:${contentIndex}`,
172
+ kind: "thinking",
173
+ label: "Thinking",
174
+ payload: { text: content.thinking, metadata: {}, source: content },
175
+ foldable: true,
176
+ getDisplayText: expanded => truncateThinking(content.thinking, expanded),
177
+ });
178
+ if (content.type === "text" && content.text.trim())
179
+ output.push({
180
+ id: `${entry.id}:text:${contentIndex}`,
181
+ kind: "text",
182
+ label: "Response",
183
+ payload: { text: content.text, metadata: {}, source: content },
184
+ foldable: true,
185
+ });
186
+ if (content.type === "toolCall") {
187
+ const result = results.get(content.id);
188
+ const resultText =
189
+ result?.content
190
+ .filter(part => part.type === "text")
191
+ .map(part => part.text)
192
+ .join("\n")
193
+ .trim() ?? "";
194
+ const summary = formatToolArgs(content.name, content.arguments);
195
+ const callText = [summary, content.intent].filter(Boolean).join("\n");
196
+ const resultDisplay = result?.isError ? `✗ ${resultText || "Error"}` : resultText || "✓ done";
197
+ output.push({
198
+ id: `tool:${content.id}`,
199
+ kind: "tool",
200
+ label: content.name,
201
+ payload: {
202
+ text: [callText, resultDisplay].filter(Boolean).join("\n"),
203
+ metadata: {
204
+ name: content.name,
205
+ arguments: content.arguments,
206
+ intent: content.intent,
207
+ isError: result?.isError ?? false,
208
+ },
209
+ source: { call: content, result },
210
+ },
211
+ foldable: true,
212
+ });
717
213
  }
718
- this.#rebuildAndScroll();
719
- }
720
- return;
721
- }
722
-
723
- // G — jump to bottom
724
- if (keyData === "G") {
725
- if (entryCount > 0) this.#selectedEntryIndex = entryCount - 1;
726
- this.#scrollOffset = Math.max(0, this.#renderedLines.length - this.#viewportHeight);
727
- this.#rebuildAndScroll();
728
- return;
729
- }
730
-
731
- // g — jump to top
732
- if (keyData === "g") {
733
- this.#selectedEntryIndex = 0;
734
- this.#scrollOffset = 0;
735
- this.#rebuildAndScroll();
736
- return;
737
- }
738
-
739
- // ] / → / Tab — next sub-agent session
740
- if (keyData === "]" || matchesKey(keyData, "tab") || matchesKey(keyData, "right")) {
741
- this.#cycleSession(1);
742
- return;
743
- }
744
-
745
- // [ / ← / Shift+Tab — previous sub-agent session
746
- if (keyData === "[" || matchesKey(keyData, "shift+tab") || matchesKey(keyData, "left")) {
747
- this.#cycleSession(-1);
748
- return;
749
- }
750
- }
751
-
752
- /** Get the ordered list of sub-agent session IDs (excludes main) */
753
- #getSubagentSessionIds(): string[] {
754
- return this.#registry
755
- .getSessions()
756
- .filter(s => s.kind === "subagent")
757
- .map(s => s.id);
758
- }
759
-
760
- /** Cycle to next (+1) or previous (-1) sub-agent session */
761
- #cycleSession(direction: 1 | -1): void {
762
- const ids = this.#getSubagentSessionIds();
763
- if (ids.length <= 1) return;
764
- const currentIdx = ids.indexOf(this.#selectedSessionId ?? "");
765
- if (currentIdx < 0) return;
766
- const nextIdx = (currentIdx + direction + ids.length) % ids.length;
767
- this.#selectedSessionId = ids[nextIdx];
768
- this.#transcriptCache = undefined;
769
- this.#scrollOffset = 0;
770
- this.#selectedEntryIndex = 0;
771
- this.#expandedEntries.clear();
772
- this.#wasAtBottom = true;
773
- this.#rebuildViewerContent();
774
- // Auto-scroll to bottom: select last entry
775
- if (this.#viewerEntries.length > 0) {
776
- this.#selectedEntryIndex = this.#viewerEntries.length - 1;
777
- this.#wasAtBottom = true;
778
- this.#rebuildViewerContent();
214
+ });
215
+ }
216
+ if (message.role === "user" || message.role === "developer") {
217
+ const text =
218
+ typeof message.content === "string"
219
+ ? message.content
220
+ : message.content
221
+ .filter(part => part.type === "text")
222
+ .map(part => part.text)
223
+ .join("\n");
224
+ if (text.trim())
225
+ output.push({
226
+ id: entry.id,
227
+ kind: "user",
228
+ label: message.role === "developer" ? "System" : "User",
229
+ payload: { text, metadata: {}, source: message },
230
+ foldable: true,
231
+ });
779
232
  }
780
233
  }
234
+ return output;
235
+ }
781
236
 
782
- /** Rebuild transcript lines (which depend on selectedEntryIndex/expandedEntries) and scroll to selection */
783
- #rebuildAndScroll(): void {
784
- // Resume auto-scrolling once selection returns to the last entry
785
- this.#wasAtBottom = this.#selectedEntryIndex >= this.#viewerEntries.length - 1;
786
- this.#rebuildViewerContent();
787
- this.#scrollToSelectedEntry();
788
- }
789
-
790
- #scrollToSelectedEntry(): void {
791
- if (this.#viewerEntries.length === 0) return;
792
- const entry = this.#viewerEntries[this.#selectedEntryIndex];
793
- if (!entry) return;
794
-
795
- const entryTop = entry.lineStart;
796
- const entryBottom = entry.lineStart + entry.lineCount;
797
-
798
- if (entry.lineCount >= this.#viewportHeight) {
799
- // Entry taller than viewport: only snap when it's completely out of view.
800
- // If the viewport overlaps the entry at all, the user may be paging within it.
801
- if (this.#scrollOffset + this.#viewportHeight <= entryTop) {
802
- // Viewport is entirely above the entry — snap to entry top
803
- this.#scrollOffset = Math.max(0, entryTop - 1);
804
- } else if (this.#scrollOffset >= entryBottom) {
805
- // Viewport is entirely below the entry — snap to show entry bottom
806
- this.#scrollOffset = Math.max(0, entryBottom - this.#viewportHeight);
807
- }
808
- // Otherwise: viewport overlaps the entry — don't override manual scroll
809
- } else {
810
- // Entry fits in viewport: ensure it's fully visible
811
- if (entryTop < this.#scrollOffset) {
812
- this.#scrollOffset = Math.max(0, entryTop - 1);
813
- }
814
- if (entryBottom > this.#scrollOffset + this.#viewportHeight) {
815
- this.#scrollOffset = Math.max(0, entryBottom - this.#viewportHeight + 1);
816
- }
817
- }
818
- }
237
+ function formatToolArgs(name: string, args: Record<string, unknown>): string {
238
+ if (name === "read" || name === "write" || name === "edit") return args.path ? `path: ${args.path}` : "";
239
+ if (name === "bash") return typeof args.command === "string" ? args.command.replaceAll("\t", " ") : "";
240
+ if (name === "search")
241
+ return [
242
+ args.pattern ? `pattern: ${args.pattern}` : "",
243
+ Array.isArray(args.paths) ? `paths: ${args.paths.join(", ")}` : "",
244
+ ]
245
+ .filter(Boolean)
246
+ .join(", ");
247
+ return Object.entries(args)
248
+ .filter(([key]) => !key.startsWith("_"))
249
+ .map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`)
250
+ .join(", ")
251
+ .slice(0, MAX_TOOL_ARGS_CHARS);
819
252
  }
820
253
 
821
- // Sync helpers for render path
822
- import * as fs from "node:fs";
254
+ function truncateThinking(text: string, expanded: boolean): string {
255
+ const limit = expanded ? 4_000 : 200;
256
+ return text.length > limit ? `${text.slice(0, limit)}...` : text;
257
+ }
823
258
 
824
259
  function readFileIncremental(filePath: string, fromByte: number): { text: string; newSize: number } | null {
825
260
  try {
826
261
  const stat = fs.statSync(filePath);
827
262
  if (stat.size <= fromByte) return { text: "", newSize: stat.size };
828
- const buf = Buffer.alloc(stat.size - fromByte);
263
+ const buffer = Buffer.alloc(stat.size - fromByte);
829
264
  const fd = fs.openSync(filePath, "r");
830
265
  try {
831
- fs.readSync(fd, buf, 0, buf.length, fromByte);
266
+ fs.readSync(fd, buffer, 0, buffer.length, fromByte);
832
267
  } finally {
833
268
  fs.closeSync(fd);
834
269
  }
835
- return { text: buf.toString("utf-8"), newSize: stat.size };
270
+ return { text: buffer.toString("utf-8"), newSize: stat.size };
836
271
  } catch {
837
272
  return null;
838
273
  }