@fitchmultz/pi-subagents 0.0.0-stage → 0.44.4

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 (342) hide show
  1. package/CHANGELOG.md +1645 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1529 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +194 -0
  43. package/dist/history/ingest.js +447 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +461 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/broker/broker.js +426 -0
  53. package/dist/pi-intercom/broker/client.js +492 -0
  54. package/dist/pi-intercom/broker/framing.js +65 -0
  55. package/dist/pi-intercom/broker/paths.js +42 -0
  56. package/dist/pi-intercom/broker/pid.js +66 -0
  57. package/dist/pi-intercom/broker/spawn.js +221 -0
  58. package/dist/pi-intercom/config.js +102 -0
  59. package/dist/pi-intercom/index.js +2942 -0
  60. package/dist/pi-intercom/reply-tracker.js +182 -0
  61. package/dist/pi-intercom/session-targets.js +128 -0
  62. package/dist/pi-intercom/subagent-live-events.js +132 -0
  63. package/dist/pi-intercom/topics.js +157 -0
  64. package/dist/pi-intercom/types.js +163 -0
  65. package/dist/pi-intercom/ui/compose.js +269 -0
  66. package/dist/pi-intercom/ui/inline-message.js +80 -0
  67. package/dist/pi-intercom/ui/session-list.js +124 -0
  68. package/dist/runs/background/async-control.js +95 -0
  69. package/dist/runs/background/async-execution.js +676 -0
  70. package/dist/runs/background/async-job-tracker.js +383 -0
  71. package/dist/runs/background/async-result-file.js +58 -0
  72. package/dist/runs/background/async-resume.js +305 -0
  73. package/dist/runs/background/async-status.js +442 -0
  74. package/dist/runs/background/completion-dedupe.js +69 -0
  75. package/dist/runs/background/completion-delivery.js +245 -0
  76. package/dist/runs/background/notify.js +71 -0
  77. package/dist/runs/background/parallel-groups.js +55 -0
  78. package/dist/runs/background/result-watcher.js +426 -0
  79. package/dist/runs/background/run-id-resolver.js +64 -0
  80. package/dist/runs/background/run-status.js +382 -0
  81. package/dist/runs/background/stale-run-reconciler.js +335 -0
  82. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  83. package/dist/runs/background/subagent-runner.js +1872 -0
  84. package/dist/runs/background/top-level-async.js +5 -0
  85. package/dist/runs/foreground/chain-clarify.js +1152 -0
  86. package/dist/runs/foreground/clarify-invocation.js +72 -0
  87. package/dist/runs/foreground/execution-input.js +423 -0
  88. package/dist/runs/foreground/foreground-control.js +817 -0
  89. package/dist/runs/foreground/question-control.js +77 -0
  90. package/dist/runs/foreground/run-async-path.js +205 -0
  91. package/dist/runs/foreground/subagent-executor.js +442 -0
  92. package/dist/runs/foreground/subagent-params.js +165 -0
  93. package/dist/runs/foreground/wait-run.js +139 -0
  94. package/dist/runs/shared/acceptance-contract.js +253 -0
  95. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  96. package/dist/runs/shared/acceptance-finalization.js +280 -0
  97. package/dist/runs/shared/acceptance-reports.js +147 -0
  98. package/dist/runs/shared/acceptance.js +4 -0
  99. package/dist/runs/shared/chain-outputs.js +110 -0
  100. package/dist/runs/shared/child-attempt.js +693 -0
  101. package/dist/runs/shared/child-execution-cwd.js +96 -0
  102. package/dist/runs/shared/child-observations.js +114 -0
  103. package/dist/runs/shared/claude-code.js +199 -0
  104. package/dist/runs/shared/completion-guard.js +31 -0
  105. package/dist/runs/shared/dynamic-fanout.js +274 -0
  106. package/dist/runs/shared/history-index.js +72 -0
  107. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  108. package/dist/runs/shared/model-fallback.js +185 -0
  109. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  110. package/dist/runs/shared/native-finalization.js +101 -0
  111. package/dist/runs/shared/native-usage.js +153 -0
  112. package/dist/runs/shared/nested-events.js +700 -0
  113. package/dist/runs/shared/nested-path.js +48 -0
  114. package/dist/runs/shared/nested-render.js +108 -0
  115. package/dist/runs/shared/parallel-utils.js +61 -0
  116. package/dist/runs/shared/parent-receipts.js +68 -0
  117. package/dist/runs/shared/parent-usage.js +121 -0
  118. package/dist/runs/shared/pi-args.js +310 -0
  119. package/dist/runs/shared/pi-spawn.js +52 -0
  120. package/dist/runs/shared/run-history-queries.js +45 -0
  121. package/dist/runs/shared/run-history.js +103 -0
  122. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  123. package/dist/runs/shared/run-records.js +679 -0
  124. package/dist/runs/shared/session-cwd-preload.js +33 -0
  125. package/dist/runs/shared/single-output.js +222 -0
  126. package/dist/runs/shared/streaming-text.js +15 -0
  127. package/dist/runs/shared/structured-output.js +55 -0
  128. package/dist/runs/shared/subagent-control.js +179 -0
  129. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  130. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  131. package/dist/runs/shared/supervisor-questions.js +320 -0
  132. package/dist/runs/shared/workflow-graph.js +214 -0
  133. package/dist/runs/shared/workflow-policy.js +84 -0
  134. package/dist/runs/shared/worktree.js +525 -0
  135. package/dist/shared/agent-context-policy.js +126 -0
  136. package/dist/shared/artifacts.js +96 -0
  137. package/dist/shared/atomic-json.js +13 -0
  138. package/dist/shared/cooperative.js +20 -0
  139. package/dist/shared/execution-cwd.js +14 -0
  140. package/dist/shared/file-coalescer.js +21 -0
  141. package/dist/shared/fork-context.js +49 -0
  142. package/dist/shared/formatters.js +85 -0
  143. package/dist/shared/journal-reader.js +407 -0
  144. package/dist/shared/lazy-tools.js +32 -0
  145. package/dist/shared/model-info.js +68 -0
  146. package/dist/shared/native-session.js +15 -0
  147. package/dist/shared/native-tui.js +18 -0
  148. package/dist/shared/native-typebox.js +20 -0
  149. package/dist/shared/post-exit-stdio-guard.js +197 -0
  150. package/dist/shared/prompt-sections.js +8 -0
  151. package/dist/shared/session-entries.js +52 -0
  152. package/dist/shared/session-identity.js +14 -0
  153. package/dist/shared/settings.js +254 -0
  154. package/dist/shared/status-format.js +111 -0
  155. package/dist/shared/temp-root.js +114 -0
  156. package/dist/shared/types.js +207 -0
  157. package/dist/shared/ui-mode.js +3 -0
  158. package/dist/shared/utils.js +334 -0
  159. package/dist/slash/prompt-template-bridge.js +298 -0
  160. package/dist/slash/slash-bridge.js +116 -0
  161. package/dist/slash/slash-commands.js +607 -0
  162. package/dist/slash/slash-live-state.js +246 -0
  163. package/dist/tui/action-hints.js +75 -0
  164. package/dist/tui/agent-history.js +197 -0
  165. package/dist/tui/agent-view.js +1656 -0
  166. package/dist/tui/render.js +1515 -0
  167. package/docs/intercom.md +579 -0
  168. package/docs/performance.md +121 -0
  169. package/package.json +101 -4
  170. package/prompts/gather-context-and-clarify.md +14 -0
  171. package/prompts/parallel-cleanup.md +62 -0
  172. package/prompts/parallel-context-build.md +56 -0
  173. package/prompts/parallel-handoff-plan.md +62 -0
  174. package/prompts/parallel-research.md +51 -0
  175. package/prompts/parallel-review.md +59 -0
  176. package/prompts/review-loop.md +42 -0
  177. package/scripts/build.mjs +134 -0
  178. package/scripts/check-compat.mjs +48 -0
  179. package/scripts/compat-host.mjs +19 -0
  180. package/scripts/linux-smoke.sh +51 -0
  181. package/scripts/local-install-smoke.mjs +66 -0
  182. package/scripts/native-package-smoke.mjs +82 -0
  183. package/scripts/package-smoke.mjs +205 -0
  184. package/scripts/prepare.mjs +67 -0
  185. package/scripts/real-pi-smoke.mjs +415 -0
  186. package/scripts/run-tests.mjs +149 -0
  187. package/skills/pi-intercom/SKILL.md +113 -0
  188. package/skills/pi-intercom/evals/evals.json +29 -0
  189. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  190. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  191. package/skills/pi-subagents/SKILL.md +76 -0
  192. package/skills/pi-subagents/evals/evals.json +29 -0
  193. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  194. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  195. package/src/agents/agent-management.ts +813 -0
  196. package/src/agents/agent-scope.ts +7 -0
  197. package/src/agents/agent-selection.ts +23 -0
  198. package/src/agents/agent-serializer.ts +99 -0
  199. package/src/agents/agents.ts +916 -0
  200. package/src/agents/chain-serializer.ts +329 -0
  201. package/src/agents/frontmatter.ts +21 -0
  202. package/src/agents/identity.ts +30 -0
  203. package/src/agents/skills.ts +637 -0
  204. package/src/extension/build-info.ts +2 -0
  205. package/src/extension/compact-tools.ts +114 -0
  206. package/src/extension/config.ts +23 -0
  207. package/src/extension/control-notices.ts +47 -0
  208. package/src/extension/doctor.ts +171 -0
  209. package/src/extension/fanout-child.ts +312 -0
  210. package/src/extension/index.ts +649 -0
  211. package/src/extension/schemas.ts +444 -0
  212. package/src/extension/tool-input.ts +19 -0
  213. package/src/extension/tool-result.ts +16 -0
  214. package/src/history/canonical-result.ts +62 -0
  215. package/src/history/database.ts +59 -0
  216. package/src/history/index.ts +152 -0
  217. package/src/history/ingest.ts +332 -0
  218. package/src/history/queries.ts +241 -0
  219. package/src/history/store.ts +203 -0
  220. package/src/history/types.ts +166 -0
  221. package/src/history/worker.ts +317 -0
  222. package/src/intercom/intercom-bridge.ts +123 -0
  223. package/src/intercom/live-intercom.ts +104 -0
  224. package/src/intercom/result-intercom.ts +429 -0
  225. package/src/pi-intercom/agent-dir.ts +9 -0
  226. package/src/pi-intercom/broker/broker.ts +463 -0
  227. package/src/pi-intercom/broker/client.ts +558 -0
  228. package/src/pi-intercom/broker/framing.ts +77 -0
  229. package/src/pi-intercom/broker/paths.ts +47 -0
  230. package/src/pi-intercom/broker/pid.ts +59 -0
  231. package/src/pi-intercom/broker/spawn.ts +248 -0
  232. package/src/pi-intercom/config.ts +142 -0
  233. package/src/pi-intercom/index.ts +2929 -0
  234. package/src/pi-intercom/reply-tracker.ts +211 -0
  235. package/src/pi-intercom/session-targets.ts +172 -0
  236. package/src/pi-intercom/subagent-live-events.ts +133 -0
  237. package/src/pi-intercom/topics.ts +125 -0
  238. package/src/pi-intercom/types.ts +271 -0
  239. package/src/pi-intercom/ui/compose.ts +289 -0
  240. package/src/pi-intercom/ui/inline-message.ts +92 -0
  241. package/src/pi-intercom/ui/session-list.ts +139 -0
  242. package/src/runs/background/async-control.ts +89 -0
  243. package/src/runs/background/async-execution.ts +850 -0
  244. package/src/runs/background/async-job-tracker.ts +385 -0
  245. package/src/runs/background/async-result-file.ts +62 -0
  246. package/src/runs/background/async-resume.ts +333 -0
  247. package/src/runs/background/async-status.ts +481 -0
  248. package/src/runs/background/completion-dedupe.ts +79 -0
  249. package/src/runs/background/completion-delivery.ts +195 -0
  250. package/src/runs/background/notify.ts +123 -0
  251. package/src/runs/background/parallel-groups.ts +53 -0
  252. package/src/runs/background/result-watcher.ts +423 -0
  253. package/src/runs/background/run-id-resolver.ts +69 -0
  254. package/src/runs/background/run-status.ts +384 -0
  255. package/src/runs/background/stale-run-reconciler.ts +369 -0
  256. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  257. package/src/runs/background/subagent-runner.ts +2053 -0
  258. package/src/runs/background/top-level-async.ts +13 -0
  259. package/src/runs/foreground/chain-clarify.ts +1340 -0
  260. package/src/runs/foreground/clarify-invocation.ts +77 -0
  261. package/src/runs/foreground/execution-input.ts +467 -0
  262. package/src/runs/foreground/foreground-control.ts +821 -0
  263. package/src/runs/foreground/question-control.ts +70 -0
  264. package/src/runs/foreground/run-async-path.ts +243 -0
  265. package/src/runs/foreground/subagent-executor.ts +501 -0
  266. package/src/runs/foreground/subagent-params.ts +320 -0
  267. package/src/runs/foreground/wait-run.ts +121 -0
  268. package/src/runs/shared/acceptance-contract.ts +257 -0
  269. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  270. package/src/runs/shared/acceptance-finalization.ts +353 -0
  271. package/src/runs/shared/acceptance-reports.ts +152 -0
  272. package/src/runs/shared/acceptance.ts +33 -0
  273. package/src/runs/shared/chain-outputs.ts +114 -0
  274. package/src/runs/shared/child-attempt.ts +623 -0
  275. package/src/runs/shared/child-execution-cwd.ts +91 -0
  276. package/src/runs/shared/child-observations.ts +99 -0
  277. package/src/runs/shared/claude-code.ts +243 -0
  278. package/src/runs/shared/completion-guard.ts +35 -0
  279. package/src/runs/shared/dynamic-fanout.ts +295 -0
  280. package/src/runs/shared/history-index.ts +69 -0
  281. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  282. package/src/runs/shared/model-fallback.ts +219 -0
  283. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  284. package/src/runs/shared/native-finalization.ts +128 -0
  285. package/src/runs/shared/native-usage.ts +113 -0
  286. package/src/runs/shared/nested-events.ts +752 -0
  287. package/src/runs/shared/nested-path.ts +52 -0
  288. package/src/runs/shared/nested-render.ts +119 -0
  289. package/src/runs/shared/parallel-utils.ts +153 -0
  290. package/src/runs/shared/parent-receipts.ts +59 -0
  291. package/src/runs/shared/parent-usage.ts +121 -0
  292. package/src/runs/shared/pi-args.ts +350 -0
  293. package/src/runs/shared/pi-spawn.ts +52 -0
  294. package/src/runs/shared/run-history-queries.ts +44 -0
  295. package/src/runs/shared/run-history.ts +93 -0
  296. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  297. package/src/runs/shared/run-records.ts +607 -0
  298. package/src/runs/shared/session-cwd-preload.ts +27 -0
  299. package/src/runs/shared/single-output.ts +267 -0
  300. package/src/runs/shared/streaming-text.ts +18 -0
  301. package/src/runs/shared/structured-output.ts +68 -0
  302. package/src/runs/shared/subagent-control.ts +219 -0
  303. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  304. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  305. package/src/runs/shared/supervisor-questions.ts +372 -0
  306. package/src/runs/shared/workflow-graph.ts +220 -0
  307. package/src/runs/shared/workflow-policy.ts +125 -0
  308. package/src/runs/shared/worktree.ts +635 -0
  309. package/src/shared/agent-context-policy.ts +184 -0
  310. package/src/shared/artifacts.ts +95 -0
  311. package/src/shared/atomic-json.ts +16 -0
  312. package/src/shared/cooperative.ts +21 -0
  313. package/src/shared/execution-cwd.ts +14 -0
  314. package/src/shared/file-coalescer.ts +29 -0
  315. package/src/shared/fork-context.ts +68 -0
  316. package/src/shared/formatters.ts +84 -0
  317. package/src/shared/journal-reader.ts +333 -0
  318. package/src/shared/lazy-tools.ts +36 -0
  319. package/src/shared/model-info.ts +81 -0
  320. package/src/shared/native-session.ts +8 -0
  321. package/src/shared/native-tui.ts +10 -0
  322. package/src/shared/native-typebox.ts +14 -0
  323. package/src/shared/post-exit-stdio-guard.ts +204 -0
  324. package/src/shared/prompt-sections.ts +10 -0
  325. package/src/shared/session-entries.ts +49 -0
  326. package/src/shared/session-identity.ts +18 -0
  327. package/src/shared/settings.ts +384 -0
  328. package/src/shared/status-format.ts +123 -0
  329. package/src/shared/temp-root.ts +107 -0
  330. package/src/shared/types.ts +1259 -0
  331. package/src/shared/ui-mode.ts +5 -0
  332. package/src/shared/utils.ts +342 -0
  333. package/src/slash/prompt-template-bridge.ts +408 -0
  334. package/src/slash/slash-bridge.ts +178 -0
  335. package/src/slash/slash-commands.ts +617 -0
  336. package/src/slash/slash-live-state.ts +292 -0
  337. package/src/tui/action-hints.ts +69 -0
  338. package/src/tui/agent-history.ts +210 -0
  339. package/src/tui/agent-view.ts +1323 -0
  340. package/src/tui/render.ts +1550 -0
  341. package/tsconfig.build.json +12 -0
  342. package/tsconfig.json +19 -0
@@ -0,0 +1,271 @@
1
+ export interface SessionInfo {
2
+ id: string;
3
+ name?: string;
4
+ cwd: string;
5
+ model: string;
6
+ /** Opaque same-repository/worktree identity used for ambient peer awareness. */
7
+ projectId?: string;
8
+ status?: string;
9
+ /** Last time the broker observed any activity from this session (liveness). */
10
+ lastSeen?: number;
11
+ /** Last time this session exchanged an intercom message (send/receive). */
12
+ lastIntercomActivity?: number;
13
+ /** Number of inbound asks this session still owes a reply to. */
14
+ pendingAsks?: number;
15
+ /** Whether this session is currently willing/able to answer asks. */
16
+ acceptsAsks?: boolean;
17
+ subscriptions?: TopicSubscription[];
18
+ topics?: TopicUpdate[];
19
+ }
20
+
21
+ export interface TopicSubscription { topic: string; awaitRelease?: boolean }
22
+ export interface TopicUpdate {
23
+ topic: string;
24
+ text: string;
25
+ event: "update" | "blocker" | "decision" | "release";
26
+ resource?: string;
27
+ ownership?: "held" | "released";
28
+ revision: number;
29
+ updatedAt: number;
30
+ }
31
+
32
+ export function isTopicSubscription(value: unknown): value is TopicSubscription {
33
+ if (!value || typeof value !== "object") return false;
34
+ const item = value as Record<string, unknown>;
35
+ return typeof item.topic === "string" && safeLabel(item.topic.trim()) && (item.awaitRelease === undefined || typeof item.awaitRelease === "boolean");
36
+ }
37
+ export function isTopicUpdate(value: unknown): value is TopicUpdate {
38
+ if (!value || typeof value !== "object") return false;
39
+ const item = value as Record<string, unknown>;
40
+ return typeof item.topic === "string" && safeLabel(item.topic.trim()) && typeof item.text === "string" && safeText(item.text)
41
+ && ["update", "blocker", "decision", "release"].includes(String(item.event))
42
+ && typeof item.revision === "number" && Number.isSafeInteger(item.revision) && item.revision > 0
43
+ && typeof item.updatedAt === "number" && Number.isFinite(item.updatedAt)
44
+ && (item.ownership !== "released" || item.event === "release")
45
+ && (item.resource === undefined || typeof item.resource === "string" && safeLabel(item.resource))
46
+ && (item.ownership === undefined || typeof item.resource === "string" && (item.ownership === "held" || item.ownership === "released"));
47
+ }
48
+
49
+ export type MessageDelivery = "queue" | "steer" | "passive";
50
+ export type QueueMode = "stack" | "replace";
51
+
52
+ export interface HumanMessageOrigin {
53
+ ownerSessionId: string;
54
+ runId: string;
55
+ index: number;
56
+ }
57
+
58
+ export function isHumanMessageOrigin(value: unknown): value is HumanMessageOrigin {
59
+ if (!value || typeof value !== "object") return false;
60
+ const origin = value as Record<string, unknown>;
61
+ return typeof origin.ownerSessionId === "string" && /^[a-zA-Z0-9_-]+$/.test(origin.ownerSessionId)
62
+ && typeof origin.runId === "string" && /^[a-zA-Z0-9_-]+$/.test(origin.runId)
63
+ && typeof origin.index === "number" && Number.isSafeInteger(origin.index) && origin.index >= 0;
64
+ }
65
+
66
+ export interface Message {
67
+ id: string;
68
+ timestamp: number;
69
+ replyTo?: string;
70
+ expectsReply?: boolean;
71
+ /** Only the owning parent's human UI sets this; recipients verify ownership before elevating it. */
72
+ human?: HumanMessageOrigin;
73
+ topic?: TopicUpdate;
74
+ /** Active-recipient behavior. Omitted delivery defaults to steer unless expectsReply is true. */
75
+ delivery?: MessageDelivery;
76
+ /** For delivery="queue": stack normally, or replace older undelivered messages in the same thread. */
77
+ queueMode?: QueueMode;
78
+ /** Stable topic key for queueMode="replace". */
79
+ threadId?: string;
80
+ /** If true, render without waking the recipient model. Discouraged for agent-to-agent messages. */
81
+ passive?: boolean;
82
+ content: {
83
+ text: string;
84
+ attachments?: Attachment[];
85
+ };
86
+ }
87
+
88
+ const UNSAFE_TEXT_CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/;
89
+ const UNSAFE_LABEL_CONTROLS = /[\u0000-\u001f\u007f-\u009f]/;
90
+
91
+ function safeText(value: string): boolean {
92
+ return !UNSAFE_TEXT_CONTROLS.test(value);
93
+ }
94
+
95
+ function safeLabel(value: string): boolean {
96
+ return value.length > 0 && !UNSAFE_LABEL_CONTROLS.test(value);
97
+ }
98
+
99
+ export interface Attachment {
100
+ type: "file" | "snippet" | "context";
101
+ name: string;
102
+ content: string;
103
+ language?: string;
104
+ }
105
+
106
+ export function isAttachment(value: unknown): value is Attachment {
107
+ if (typeof value !== "object" || value === null) {
108
+ return false;
109
+ }
110
+
111
+ const attachment = value as Record<string, unknown>;
112
+
113
+ if (
114
+ attachment.type !== "file"
115
+ && attachment.type !== "snippet"
116
+ && attachment.type !== "context"
117
+ ) {
118
+ return false;
119
+ }
120
+
121
+ if (typeof attachment.name !== "string" || !safeLabel(attachment.name) || typeof attachment.content !== "string" || !safeText(attachment.content)) return false;
122
+ return attachment.language === undefined || (typeof attachment.language === "string" && safeLabel(attachment.language));
123
+ }
124
+
125
+ export function normalizeSessionInfo(value: unknown): SessionInfo | null {
126
+ if (typeof value !== "object" || value === null || typeof (value as { id?: unknown }).id !== "string") return null;
127
+ if (isSessionRegistration(value)) return value as SessionInfo;
128
+ if (!("projectId" in value)) return null;
129
+ const { projectId: _projectId, ...withoutProjectId } = value as Record<string, unknown>;
130
+ return isSessionRegistration(withoutProjectId) ? withoutProjectId as unknown as SessionInfo : null;
131
+ }
132
+
133
+ export function isMessage(value: unknown): value is Message {
134
+ if (typeof value !== "object" || value === null) {
135
+ return false;
136
+ }
137
+
138
+ const message = value as Record<string, unknown>;
139
+
140
+ if (typeof message.id !== "string" || !safeLabel(message.id) || typeof message.timestamp !== "number" || !Number.isFinite(message.timestamp)) {
141
+ return false;
142
+ }
143
+
144
+ if (message.replyTo !== undefined && (typeof message.replyTo !== "string" || !safeLabel(message.replyTo))) {
145
+ return false;
146
+ }
147
+
148
+ if (message.human !== undefined && !isHumanMessageOrigin(message.human)) return false;
149
+ if (message.topic !== undefined && (!isTopicUpdate(message.topic) || message.human !== undefined || message.expectsReply || message.replyTo)) return false;
150
+
151
+ if (message.expectsReply !== undefined && typeof message.expectsReply !== "boolean") {
152
+ return false;
153
+ }
154
+
155
+ if (message.passive !== undefined && typeof message.passive !== "boolean") {
156
+ return false;
157
+ }
158
+
159
+ if (
160
+ message.delivery !== undefined
161
+ && message.delivery !== "queue"
162
+ && message.delivery !== "steer"
163
+ && message.delivery !== "passive"
164
+ ) {
165
+ return false;
166
+ }
167
+
168
+ if (
169
+ message.queueMode !== undefined
170
+ && message.queueMode !== "stack"
171
+ && message.queueMode !== "replace"
172
+ ) {
173
+ return false;
174
+ }
175
+
176
+ if (message.threadId !== undefined && (typeof message.threadId !== "string" || !safeLabel(message.threadId.trim()))) {
177
+ return false;
178
+ }
179
+
180
+ if (message.passive === true && message.delivery !== undefined && message.delivery !== "passive") {
181
+ return false;
182
+ }
183
+
184
+ if ((message.passive === true || message.delivery === "passive") && message.expectsReply === true) {
185
+ return false;
186
+ }
187
+
188
+ if (message.queueMode !== undefined && message.delivery !== "queue") {
189
+ return false;
190
+ }
191
+
192
+ if (message.queueMode === "replace" && typeof message.threadId !== "string") {
193
+ return false;
194
+ }
195
+
196
+ if (message.threadId !== undefined && message.queueMode !== "replace") {
197
+ return false;
198
+ }
199
+
200
+ if (typeof message.content !== "object" || message.content === null) {
201
+ return false;
202
+ }
203
+
204
+ const content = message.content as Record<string, unknown>;
205
+ if (typeof content.text !== "string" || !safeText(content.text)) {
206
+ return false;
207
+ }
208
+
209
+ return content.attachments === undefined
210
+ || (Array.isArray(content.attachments) && content.attachments.every(isAttachment));
211
+ }
212
+
213
+ export function isSessionRegistration(value: unknown): value is Omit<SessionInfo, "id"> {
214
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
215
+ const session = value as Record<string, unknown>;
216
+ if (typeof session.cwd !== "string" || !safeLabel(session.cwd) || typeof session.model !== "string" || !safeLabel(session.model)) return false;
217
+ if (session.name !== undefined && (typeof session.name !== "string" || !safeLabel(session.name))) return false;
218
+ if (session.projectId !== undefined && (typeof session.projectId !== "string" || !/^[a-f0-9]{64}$/.test(session.projectId))) return false;
219
+ if (session.status !== undefined && (typeof session.status !== "string" || !safeLabel(session.status))) return false;
220
+ if (session.lastSeen !== undefined && (typeof session.lastSeen !== "number" || !Number.isFinite(session.lastSeen))) return false;
221
+ if (session.lastIntercomActivity !== undefined && (typeof session.lastIntercomActivity !== "number" || !Number.isFinite(session.lastIntercomActivity))) return false;
222
+ if (session.pendingAsks !== undefined && (typeof session.pendingAsks !== "number" || !Number.isInteger(session.pendingAsks) || session.pendingAsks < 0)) return false;
223
+ if (session.subscriptions !== undefined && (!Array.isArray(session.subscriptions) || !session.subscriptions.every(isTopicSubscription))) return false;
224
+ if (session.topics !== undefined && (!Array.isArray(session.topics) || !session.topics.every(isTopicUpdate))) return false;
225
+ return session.acceptsAsks === undefined || typeof session.acceptsAsks === "boolean";
226
+ }
227
+
228
+ export interface SendResult {
229
+ id: string;
230
+ accepted: boolean;
231
+ delivered: boolean;
232
+ queued?: boolean;
233
+ reason?: string;
234
+ }
235
+
236
+ export type TopicChange =
237
+ | { action: "publish" | "restore"; topic: TopicUpdate }
238
+ | { action: "subscribe" | "restore"; subscription: TopicSubscription }
239
+ | { action: "unsubscribe"; topic: string };
240
+ export interface SessionSnapshot {
241
+ sessions: SessionInfo[];
242
+ receipts: Array<SendResult & { to: string }>;
243
+ }
244
+
245
+ /** Topic text and the standard coalescing key need only one copy on the wire. */
246
+ export function compactTopicMessage(message: Message) {
247
+ if (!message.topic) return message;
248
+ return { ...message,
249
+ topic: message.topic.text === message.content.text ? { ...message.topic, text: undefined } : message.topic,
250
+ threadId: message.queueMode === "replace" && message.threadId === `topic:${message.topic.topic}` ? undefined : message.threadId,
251
+ };
252
+ }
253
+
254
+ export function normalizeMessage(value: unknown): Message | null {
255
+ if (isMessage(value)) return value;
256
+ if (!value || typeof value !== "object") return null;
257
+ const message = value as Partial<Message>;
258
+ if (!message.topic || typeof message.topic !== "object" || !message.content) return null;
259
+ const restored = { ...message, topic: { ...message.topic, text: message.topic.text === undefined ? message.content.text : message.topic.text },
260
+ ...(message.queueMode === "replace" && message.threadId === undefined ? { threadId: `topic:${message.topic.topic}` } : {}) };
261
+ return isMessage(restored) ? restored : null;
262
+ }
263
+
264
+ export type BrokerMessage =
265
+ | { type: "registered"; sessionId: string; topicsSupported?: true; topicFrames?: true }
266
+ | { type: "sessions"; requestId: string; sessions: Array<Partial<SessionInfo> & Pick<SessionInfo, "id">>; more?: boolean; receipts?: SessionSnapshot["receipts"]; error?: string }
267
+ | { type: "message"; from: SessionInfo; message: Message | ReturnType<typeof compactTopicMessage> }
268
+ | { type: "session_left"; sessionId: string }
269
+ | { type: "delivered"; messageId: string }
270
+ | { type: "delivery_queued"; messageId: string; reason: string }
271
+ | { type: "delivery_failed"; messageId: string; reason: string };
@@ -0,0 +1,289 @@
1
+ import type { TUI, TuiMouseEvent } from "@earendil-works/pi-tui";
2
+ import { Box, Container, Text, sliceByColumn, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { actionHints } from "../../tui/action-hints.ts";
4
+ import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
5
+ import type { IntercomClient } from "../broker/client.ts";
6
+ import type { SessionInfo } from "../types.ts";
7
+
8
+ const BRACKETED_PASTE_START = "\x1b[200~";
9
+ const BRACKETED_PASTE_END = "\x1b[201~";
10
+ const MAX_PASTE_CHARS = 1_000_000;
11
+ const INCOMPLETE_PASTE_IDLE_MS = 200;
12
+ const PASTE_RENDER_TAIL_CHARS = 8192;
13
+
14
+ export interface ComposeResult {
15
+ sent: boolean;
16
+ messageId?: string;
17
+ text?: string;
18
+ expectsReply?: boolean;
19
+ }
20
+
21
+ export class ComposeOverlay extends Container {
22
+ private tui: TUI;
23
+ private theme: Theme;
24
+ private keybindings: KeybindingsManager;
25
+ private target: SessionInfo;
26
+ private targetLabel: string;
27
+ private client: IntercomClient;
28
+ private done: (result: ComposeResult) => void;
29
+ private inputBuffer: string = "";
30
+ private mode: "send" | "ask" = "send";
31
+ private completed = false;
32
+ private sending: boolean = false;
33
+ private error: string | null = null;
34
+ private pasteBuffer: string | null = null;
35
+ private pasteStartPrefix = "";
36
+ private pasteIdleTimer: NodeJS.Timeout | null = null;
37
+
38
+ constructor(
39
+ tui: TUI,
40
+ theme: Theme,
41
+ keybindings: KeybindingsManager,
42
+ target: SessionInfo,
43
+ targetLabel: string,
44
+ client: IntercomClient,
45
+ done: (result: ComposeResult) => void,
46
+ ) {
47
+ super();
48
+ this.tui = tui;
49
+ this.theme = theme;
50
+ this.keybindings = keybindings;
51
+ this.target = target;
52
+ this.targetLabel = targetLabel;
53
+ this.client = client;
54
+ this.done = done;
55
+ }
56
+
57
+ dispose(): void {
58
+ this.completed = true;
59
+ if (this.pasteIdleTimer) clearTimeout(this.pasteIdleTimer);
60
+ this.pasteIdleTimer = null;
61
+ }
62
+
63
+ handleMouse(event: TuiMouseEvent) {
64
+ const width = Math.min(event.width, 72);
65
+ if (this.sending || this.completed || this.pasteBuffer !== null || this.pasteStartPrefix || event.x >= width) return;
66
+ return super.handleMouse({ ...event, width });
67
+ }
68
+
69
+ private act(action: "close" | "mode" | "send"): void {
70
+ if (this.sending || this.completed || this.pasteBuffer !== null || this.pasteStartPrefix) return;
71
+ if (action === "close") this.finish({ sent: false });
72
+ else if (action === "mode") { this.mode = this.mode === "send" ? "ask" : "send"; this.error = null; }
73
+ else if (this.inputBuffer.trim()) void this.sendMessage();
74
+ this.tui.requestRender();
75
+ }
76
+
77
+ private finish(result: ComposeResult): void {
78
+ if (this.completed) return;
79
+ this.dispose();
80
+ this.done(result);
81
+ }
82
+
83
+ private scheduleIncompletePasteFlush(): void {
84
+ if (this.pasteIdleTimer) clearTimeout(this.pasteIdleTimer);
85
+ this.pasteIdleTimer = setTimeout(() => {
86
+ this.pasteIdleTimer = null;
87
+ if (this.pasteBuffer === null || this.completed) return;
88
+ const data = this.pasteBuffer.replace(/\r\n?/g, "\n");
89
+ this.pasteBuffer = null;
90
+ const printable = [...data].filter((character) => character >= " " || character === "\n" || character === "\t").join("");
91
+ if (printable) this.inputBuffer += printable;
92
+ this.error = null;
93
+ this.tui.requestRender();
94
+ }, INCOMPLETE_PASTE_IDLE_MS);
95
+ this.pasteIdleTimer.unref?.();
96
+ }
97
+
98
+ handleInput(data: string): void {
99
+ if (this.sending || this.completed || !data) return;
100
+
101
+ let pasted = false;
102
+ if (this.pasteBuffer !== null) {
103
+ this.pasteBuffer += data;
104
+ const end = this.pasteBuffer.indexOf(BRACKETED_PASTE_END);
105
+ if (end === -1 && this.pasteBuffer.length <= MAX_PASTE_CHARS) {
106
+ this.scheduleIncompletePasteFlush();
107
+ this.tui.requestRender();
108
+ return;
109
+ }
110
+ data = end === -1
111
+ ? this.pasteBuffer
112
+ : this.pasteBuffer.slice(0, end) + this.pasteBuffer.slice(end + BRACKETED_PASTE_END.length);
113
+ this.pasteBuffer = null;
114
+ if (this.pasteIdleTimer) clearTimeout(this.pasteIdleTimer);
115
+ this.pasteIdleTimer = null;
116
+ data = data.replace(/\r\n?/g, "\n");
117
+ pasted = true;
118
+ } else {
119
+ if (this.pasteStartPrefix) {
120
+ if (this.pasteIdleTimer) clearTimeout(this.pasteIdleTimer);
121
+ this.pasteIdleTimer = null;
122
+ data = this.pasteStartPrefix + data;
123
+ this.pasteStartPrefix = "";
124
+ }
125
+ if (data !== BRACKETED_PASTE_START && BRACKETED_PASTE_START.startsWith(data)) {
126
+ this.pasteStartPrefix = data;
127
+ this.pasteIdleTimer = setTimeout(() => {
128
+ this.pasteIdleTimer = null;
129
+ const pendingPrefix = this.pasteStartPrefix;
130
+ this.pasteStartPrefix = "";
131
+ if (pendingPrefix === "\x1b" && this.keybindings.matches(pendingPrefix, "tui.select.cancel")) this.finish({ sent: false });
132
+ }, INCOMPLETE_PASTE_IDLE_MS);
133
+ this.pasteIdleTimer.unref?.();
134
+ return;
135
+ }
136
+ }
137
+ if (!pasted && data.startsWith(BRACKETED_PASTE_START)) {
138
+ const body = data.slice(BRACKETED_PASTE_START.length);
139
+ const end = body.indexOf(BRACKETED_PASTE_END);
140
+ if (end === -1 && body.length <= MAX_PASTE_CHARS) {
141
+ this.pasteBuffer = body;
142
+ this.scheduleIncompletePasteFlush();
143
+ this.tui.requestRender();
144
+ return;
145
+ }
146
+ data = end === -1 ? body : body.slice(0, end) + body.slice(end + BRACKETED_PASTE_END.length);
147
+ data = data.replace(/\r\n?/g, "\n");
148
+ pasted = true;
149
+ }
150
+ if (!pasted && data.includes(BRACKETED_PASTE_END)) data = data.replaceAll(BRACKETED_PASTE_END, "");
151
+ if (!data) return;
152
+
153
+ if (!pasted && this.keybindings.matches(data, "tui.select.cancel")) {
154
+ this.act("close");
155
+ return;
156
+ }
157
+
158
+ if (!pasted && data === "\t") {
159
+ this.act("mode");
160
+ return;
161
+ }
162
+
163
+ if (!pasted && this.keybindings.matches(data, "tui.select.confirm")) {
164
+ this.act("send");
165
+ return;
166
+ }
167
+
168
+ if (!pasted && data.startsWith("\x1b")) {
169
+ return;
170
+ }
171
+
172
+ if (!pasted && this.keybindings.matches(data, "tui.editor.deleteCharBackward")) {
173
+ this.inputBuffer = [...this.inputBuffer].slice(0, -1).join("");
174
+ this.error = null;
175
+ this.tui.requestRender();
176
+ return;
177
+ }
178
+
179
+ const printable = [...data].filter(c => c >= " " || c === "\n" || c === "\t").join("");
180
+ if (printable) {
181
+ this.inputBuffer += printable;
182
+ this.error = null;
183
+ this.tui.requestRender();
184
+ }
185
+ }
186
+
187
+ private async sendMessage(): Promise<void> {
188
+ this.sending = true;
189
+ this.error = null;
190
+ this.tui.requestRender();
191
+
192
+ try {
193
+ const expectsReply = this.mode === "ask";
194
+ const text = this.inputBuffer;
195
+ const result = await this.client.send(this.target.id, {
196
+ text,
197
+ expectsReply,
198
+ });
199
+
200
+ if (!result.accepted) {
201
+ this.error = result.reason ?? "Message not delivered. Session may not exist or has disconnected.";
202
+ this.sending = false;
203
+ this.tui.requestRender();
204
+ return;
205
+ }
206
+
207
+ this.finish({
208
+ sent: true,
209
+ messageId: result.id,
210
+ text,
211
+ expectsReply,
212
+ });
213
+ } catch (error) {
214
+ this.error = error instanceof Error ? error.message : String(error);
215
+ this.sending = false;
216
+ this.tui.requestRender();
217
+ }
218
+ }
219
+
220
+ private renderInputLines(row: (text?: string) => string, lines: string[], contentWidth: number): void {
221
+ const pendingPaste = this.pasteBuffer === null
222
+ ? ""
223
+ : [...this.pasteBuffer.slice(-PASTE_RENDER_TAIL_CHARS)].filter((character) => character >= " " || character === "\n" || character === "\t").join("");
224
+ const rawLines = `${this.inputBuffer.slice(-PASTE_RENDER_TAIL_CHARS)}${pendingPaste}`.split("\n");
225
+ const visibleLines = rawLines.slice(-8);
226
+ visibleLines.forEach((line, index) => {
227
+ const isLast = index === visibleLines.length - 1;
228
+ const prefix = index === 0 ? " > " : " ";
229
+ if (isLast) {
230
+ const graphemes = [...line];
231
+ const budget = Math.max(1, contentWidth - prefix.length - 1);
232
+ let used = 0;
233
+ let start = graphemes.length;
234
+ while (start > 0) {
235
+ const width = visibleWidth(graphemes[start - 1]!);
236
+ if (used + width > budget) break;
237
+ used += width;
238
+ start--;
239
+ }
240
+ line = graphemes.slice(start).join("");
241
+ }
242
+ lines.push(row(`${prefix}${line}${isLast ? "█" : ""}`));
243
+ });
244
+ }
245
+
246
+ render(width: number): string[] {
247
+ this.clear();
248
+ if (width < 3) return [truncateToWidth("Intercom", width)];
249
+ const innerWidth = Math.min(width, 72);
250
+ const contentWidth = Math.max(1, innerWidth - 2);
251
+ const send = [this.keybindings.getKeys("tui.select.confirm").join("/"), this.mode === "ask" ? "Request reply" : "Send"].filter(Boolean).join(": ");
252
+ const mode = `Tab: ${this.mode === "ask" ? "Send mode" : "Request-reply mode"}`;
253
+ const close = [this.keybindings.getKeys("tui.select.cancel").join("/"), "Close"].filter(Boolean).join(": ");
254
+ const footer = this.sending ? ["Sending…"] : this.pasteBuffer !== null || this.pasteStartPrefix ? ["Pasting…"] : [
255
+ { text: send, run: () => this.act("send") }, " • ", { text: mode, run: () => this.act("mode") }, " • ", { text: close, run: () => this.act("close") },
256
+ ];
257
+ const border = (text: string) => this.theme.fg("accent", text);
258
+ const row = (text = "") => {
259
+ const clipped = truncateToWidth(text, contentWidth, "…", true);
260
+ return `${border("│")}${clipped}${" ".repeat(Math.max(0, contentWidth - visibleWidth(clipped)))}${border("│")}`;
261
+ };
262
+
263
+ const lines: string[] = [];
264
+ lines.push(border(`╭${"─".repeat(contentWidth)}╮`));
265
+ lines.push(row(this.theme.bold(` ${this.mode === "ask" ? "Request reply" : "Send"} to: ${this.targetLabel}`)));
266
+ lines.push(row(this.theme.fg("dim", ` Native session cwd: ${this.target.cwd} • ${this.target.model}`)));
267
+ lines.push(border(`├${"─".repeat(contentWidth)}┤`));
268
+ lines.push(row());
269
+
270
+ if (this.sending) {
271
+ lines.push(row(this.theme.fg("dim", " Sending...")));
272
+ } else if (this.error) {
273
+ lines.push(row(this.theme.fg("error", ` Error: ${this.error}`)));
274
+ lines.push(row());
275
+ this.renderInputLines(row, lines, contentWidth);
276
+ } else {
277
+ this.renderInputLines(row, lines, contentWidth);
278
+ }
279
+
280
+ lines.push(row());
281
+ lines.push(border(`├${"─".repeat(contentWidth)}┤`));
282
+ this.addChild(new Text(lines.join("\n"), 0, 0));
283
+ const controls = new Box(1, 0, (line) => row(sliceByColumn(line, 1, contentWidth)));
284
+ controls.addChild(actionHints([" ", ...footer], (text) => this.theme.fg("dim", text), "…"));
285
+ this.addChild(controls);
286
+ this.addChild(new Text(border(`╰${"─".repeat(contentWidth)}╯`), 0, 0));
287
+ return super.render(innerWidth);
288
+ }
289
+ }
@@ -0,0 +1,92 @@
1
+ import type { Component } from "@earendil-works/pi-tui";
2
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
+ import { keyText, type Theme } from "@earendil-works/pi-coding-agent";
4
+ import type { SessionInfo, Message } from "../types.ts";
5
+
6
+ export class InlineMessageComponent implements Component {
7
+ private from: SessionInfo;
8
+ private message: Message;
9
+ private theme: Theme;
10
+ private replyCommand?: string;
11
+ private bodyText?: string;
12
+ private expanded: boolean;
13
+ private cachedWidth?: number;
14
+ private cachedLines?: string[];
15
+
16
+ constructor(from: SessionInfo, message: Message, theme: Theme, replyCommand?: string, bodyText?: string, expanded = true) {
17
+ this.from = from;
18
+ this.message = message;
19
+ this.theme = theme;
20
+ this.replyCommand = replyCommand;
21
+ this.bodyText = bodyText;
22
+ this.expanded = expanded;
23
+ }
24
+
25
+ invalidate(): void {
26
+ this.cachedWidth = undefined;
27
+ this.cachedLines = undefined;
28
+ }
29
+
30
+ render(width: number): string[] {
31
+ if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
32
+
33
+ const lines: string[] = [];
34
+ const borderChar = "─";
35
+ if (width < 3) {
36
+ this.cachedWidth = width;
37
+ this.cachedLines = [truncateToWidth(`From ${this.from.name || this.from.id.slice(0, 8)}`, width)];
38
+ return this.cachedLines;
39
+ }
40
+ const bodyWidth = Math.max(1, width - 2);
41
+
42
+ const senderName = this.from.name || this.from.id.slice(0, 8);
43
+ const header = ` 📨 From: ${senderName} (Native session cwd: ${this.from.cwd}) `;
44
+ const headerText = truncateToWidth(header, bodyWidth, "");
45
+ const headerPadding = Math.max(0, bodyWidth - visibleWidth(headerText));
46
+ lines.push(this.theme.fg("accent", `╭${headerText}${borderChar.repeat(headerPadding)}╮`));
47
+
48
+ const body = this.bodyText || this.message.content.text;
49
+ // Grouped subagent results start with run, mode, status and child counts after the title.
50
+ const contentLines = this.expanded ? wrapTextWithAnsi(body, bodyWidth) : body.split("\n").slice(2, 6);
51
+ if (!this.expanded) contentLines.push(this.theme.fg("dim", `${keyText("app.tools.expand")} full response`));
52
+ for (const line of contentLines) {
53
+ const text = truncateToWidth(line, bodyWidth, "");
54
+ const padding = Math.max(0, bodyWidth - visibleWidth(text));
55
+ lines.push(this.theme.fg("accent", `│${text}${" ".repeat(padding)}│`));
56
+ }
57
+
58
+ if (this.replyCommand) {
59
+ lines.push(this.theme.fg("accent", `│${" ".repeat(bodyWidth)}│`));
60
+ const replyLines = wrapTextWithAnsi(this.theme.fg("dim", ` ↩ To reply: ${this.replyCommand}`), bodyWidth);
61
+ for (const line of replyLines) {
62
+ const text = truncateToWidth(line, bodyWidth, "");
63
+ const padding = Math.max(0, bodyWidth - visibleWidth(text));
64
+ lines.push(this.theme.fg("accent", `│${text}${" ".repeat(padding)}│`));
65
+ }
66
+ }
67
+
68
+ if (this.message.content.attachments?.length && !this.bodyText) {
69
+ lines.push(this.theme.fg("accent", `│${" ".repeat(bodyWidth)}│`));
70
+ for (const att of this.message.content.attachments) {
71
+ const label = this.theme.fg("dim", ` 📎 ${att.name}`);
72
+ const text = truncateToWidth(label, bodyWidth, "");
73
+ const padding = Math.max(0, bodyWidth - visibleWidth(text));
74
+ lines.push(this.theme.fg("accent", `│${text}${" ".repeat(padding)}│`));
75
+ }
76
+ }
77
+
78
+ if (this.message.replyTo && !this.message.expectsReply) {
79
+ lines.push(this.theme.fg("accent", `│${" ".repeat(bodyWidth)}│`));
80
+ const reply = this.theme.fg("dim", ` ↳ Reply to ${this.message.replyTo.slice(0, 8)}`);
81
+ const text = truncateToWidth(reply, bodyWidth, "");
82
+ const padding = Math.max(0, bodyWidth - visibleWidth(text));
83
+ lines.push(this.theme.fg("accent", `│${text}${" ".repeat(padding)}│`));
84
+ }
85
+
86
+ lines.push(this.theme.fg("accent", `╰${borderChar.repeat(bodyWidth)}╯`));
87
+
88
+ this.cachedWidth = width;
89
+ this.cachedLines = lines;
90
+ return lines;
91
+ }
92
+ }