@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +8 -11
  3. package/docs/prefix-cache-architecture.md +14 -0
  4. package/package.json +22 -24
  5. package/packages/ask-user/README.md +55 -107
  6. package/packages/ask-user/ask-ui.ts +306 -732
  7. package/packages/ask-user/clipboard.ts +50 -0
  8. package/packages/ask-user/config.ts +74 -78
  9. package/packages/ask-user/package.json +2 -2
  10. package/packages/ask-user/questions.ts +151 -0
  11. package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
  12. package/packages/ask-user/tools.ts +198 -432
  13. package/packages/autocomplete/package.json +1 -1
  14. package/packages/autocomplete/src/constants.ts +38 -43
  15. package/packages/autocomplete/src/sorting.ts +8 -0
  16. package/packages/background-tasks/package.json +2 -2
  17. package/packages/background-tasks/src/config.ts +0 -73
  18. package/packages/background-tasks/src/index.ts +20 -16
  19. package/packages/background-tasks/src/registry.ts +1 -171
  20. package/packages/background-tasks/src/types.ts +0 -32
  21. package/packages/btw/README.md +13 -56
  22. package/packages/btw/extensions/btw.ts +403 -1796
  23. package/packages/btw/package.json +5 -2
  24. package/packages/btw/skills/btw/SKILL.md +7 -143
  25. package/packages/compactor/README.md +56 -287
  26. package/packages/compactor/package.json +5 -9
  27. package/packages/compactor/skills/compactor/SKILL.md +13 -20
  28. package/packages/compactor/src/card.ts +90 -0
  29. package/packages/compactor/src/commands/index.ts +73 -130
  30. package/packages/compactor/src/compaction/compact-args.ts +5 -3
  31. package/packages/compactor/src/compaction/filter-noise.ts +1 -1
  32. package/packages/compactor/src/compaction/hooks.ts +487 -311
  33. package/packages/compactor/src/compaction/source.ts +178 -0
  34. package/packages/compactor/src/compaction/summarize.ts +586 -52
  35. package/packages/compactor/src/config/manager.ts +130 -127
  36. package/packages/compactor/src/config/schema.ts +32 -48
  37. package/packages/compactor/src/index.ts +40 -431
  38. package/packages/compactor/src/info-screen.ts +44 -190
  39. package/packages/compactor/src/session/recall-blocks.ts +1 -1
  40. package/packages/compactor/src/stats.ts +76 -0
  41. package/packages/compactor/src/tools/context-budget.ts +17 -34
  42. package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
  43. package/packages/compactor/src/tools/register.ts +29 -269
  44. package/packages/compactor/src/types.ts +47 -186
  45. package/packages/core/README.md +25 -0
  46. package/packages/core/child-agent.ts +1 -0
  47. package/packages/core/command-echo.ts +63 -0
  48. package/packages/core/compaction-context.ts +41 -0
  49. package/packages/core/constants.ts +8 -38
  50. package/packages/core/events.ts +0 -53
  51. package/packages/core/fusion-status.ts +4 -0
  52. package/packages/core/global.d.ts +10 -0
  53. package/packages/core/harness-messages.ts +392 -0
  54. package/packages/core/index.ts +14 -0
  55. package/packages/core/kanboard-status.ts +49 -0
  56. package/packages/core/long-horizon-owner-status.ts +46 -0
  57. package/packages/core/model-cache.ts +81 -7
  58. package/packages/core/package.json +6 -3
  59. package/packages/core/src/attach/detect.ts +163 -0
  60. package/packages/core/src/child-agent/child-args.ts +31 -0
  61. package/packages/core/src/child-agent/delivery.ts +41 -0
  62. package/packages/core/src/child-agent/index.ts +5 -0
  63. package/packages/core/src/child-agent/readers.ts +203 -0
  64. package/packages/core/src/child-agent/runtime.ts +584 -0
  65. package/packages/core/src/evidence.ts +78 -0
  66. package/packages/core/src/hints/browser.ts +278 -0
  67. package/packages/core/src/hints/crab-data.ts +19 -0
  68. package/packages/core/src/hints/index.ts +865 -0
  69. package/packages/core/src/hints/lines.ts +607 -0
  70. package/packages/core/src/hints/store.ts +120 -0
  71. package/packages/core/src/jev/chatter.ts +24 -0
  72. package/packages/core/src/jev/settings.ts +177 -14
  73. package/packages/core/src/package-colors.ts +2 -0
  74. package/packages/core/src/settings/catalog.ts +39 -15
  75. package/packages/core/src/settings/engine.ts +48 -2
  76. package/packages/core/src/settings/hub.ts +83 -20
  77. package/packages/core/src/settings/open.ts +49 -0
  78. package/packages/core/src/settings/schema.ts +27 -7
  79. package/packages/core/src/tui/hub-kit.ts +18 -5
  80. package/packages/core/src/tui/kit.ts +198 -0
  81. package/packages/core/src/tui/progress.ts +73 -0
  82. package/packages/core/src/turn/arbiter.ts +290 -0
  83. package/packages/footer/README.md +4 -6
  84. package/packages/footer/package.json +3 -3
  85. package/packages/footer/src/glance-editor.ts +19 -11
  86. package/packages/footer/src/help.ts +1 -1
  87. package/packages/footer/src/index.ts +36 -3
  88. package/packages/footer/src/presets.ts +3 -4
  89. package/packages/footer/src/rendering/icons.ts +0 -20
  90. package/packages/footer/src/segments/compactor.ts +52 -142
  91. package/packages/footer/src/segments/status-ext.ts +0 -1
  92. package/packages/fusion/README.md +14 -4
  93. package/packages/fusion/package.json +4 -3
  94. package/packages/fusion/src/child-args.ts +1 -0
  95. package/packages/fusion/src/index.ts +161 -8
  96. package/packages/fusion/src/pi-settings.ts +57 -0
  97. package/packages/fusion/src/picker.ts +186 -51
  98. package/packages/fusion/src/preset.ts +56 -2
  99. package/packages/fusion/src/prompts.ts +1 -1
  100. package/packages/fusion/src/sidekick-runtime.ts +1 -420
  101. package/packages/fusion/src/tools.ts +107 -128
  102. package/packages/fusion/src/transcript.ts +27 -85
  103. package/packages/info-screen/package.json +2 -2
  104. package/packages/input-shortcuts/README.md +18 -30
  105. package/packages/input-shortcuts/package.json +2 -2
  106. package/packages/input-shortcuts/src/burst.ts +88 -0
  107. package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
  108. package/packages/input-shortcuts/src/cleared-input.ts +97 -0
  109. package/packages/input-shortcuts/src/index.ts +99 -193
  110. package/packages/input-shortcuts/src/last-response.ts +40 -0
  111. package/packages/input-shortcuts/src/registers.ts +8 -23
  112. package/packages/input-shortcuts/src/types.ts +4 -16
  113. package/packages/input-shortcuts/src/undo-redo.ts +59 -47
  114. package/packages/kanboard/README.md +51 -47
  115. package/packages/kanboard/index.ts +293 -55
  116. package/packages/kanboard/package.json +8 -8
  117. package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
  118. package/packages/kanboard/src/badges.ts +93 -0
  119. package/packages/kanboard/src/bin.ts +4 -2
  120. package/packages/kanboard/src/commands.ts +218 -84
  121. package/packages/kanboard/src/debug.ts +20 -0
  122. package/packages/kanboard/src/guard.ts +267 -109
  123. package/packages/kanboard/src/monitor.ts +330 -0
  124. package/packages/kanboard/src/notice-buffer.ts +74 -0
  125. package/packages/kanboard/src/progress.ts +27 -0
  126. package/packages/kanboard/src/reminders.ts +234 -0
  127. package/packages/kanboard/src/settings.ts +69 -15
  128. package/packages/kanboard/src/shapes.ts +5 -0
  129. package/packages/long-horizon/index.ts +190 -13
  130. package/packages/long-horizon/package.json +3 -3
  131. package/packages/long-horizon/src/commands.ts +103 -7
  132. package/packages/long-horizon/src/compaction-brief.ts +35 -0
  133. package/packages/long-horizon/src/engine/continuation.ts +84 -15
  134. package/packages/long-horizon/src/engine/goal-state.ts +64 -13
  135. package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
  136. package/packages/long-horizon/src/engine/ralph.ts +3 -2
  137. package/packages/long-horizon/src/engine/runaway.ts +68 -11
  138. package/packages/long-horizon/src/engine/verifier.ts +19 -0
  139. package/packages/long-horizon/src/gate.ts +37 -12
  140. package/packages/long-horizon/src/judge/resolve.ts +1 -0
  141. package/packages/long-horizon/src/owner.ts +35 -2
  142. package/packages/long-horizon/src/progress.ts +87 -0
  143. package/packages/long-horizon/src/prompts/goal.ts +1 -1
  144. package/packages/long-horizon/src/runtime.ts +172 -18
  145. package/packages/long-horizon/src/settings.ts +54 -39
  146. package/packages/long-horizon/src/tools/goal.ts +6 -0
  147. package/packages/long-horizon/src/tools/graph.ts +1 -1
  148. package/packages/long-horizon/src/tools/swarm.ts +3 -3
  149. package/packages/mcp/package.json +2 -2
  150. package/packages/memory/commands.ts +7 -2
  151. package/packages/memory/index.ts +210 -32
  152. package/packages/memory/package.json +3 -3
  153. package/packages/memory/save-session.ts +264 -0
  154. package/packages/memory/settings.ts +17 -0
  155. package/packages/memory/tools.ts +253 -203
  156. package/packages/notify/package.json +2 -2
  157. package/packages/skill-registry/README.md +53 -16
  158. package/packages/skill-registry/index.ts +230 -0
  159. package/packages/skill-registry/package.json +19 -8
  160. package/packages/skill-registry/src/editor.ts +206 -0
  161. package/packages/skill-registry/src/judge.ts +289 -0
  162. package/packages/skill-registry/src/registry.ts +87 -0
  163. package/packages/skill-registry/src/settings.ts +259 -0
  164. package/packages/skill-registry/src/vault.ts +80 -0
  165. package/packages/subagents/README.md +39 -119
  166. package/packages/subagents/package.json +6 -13
  167. package/packages/subagents/skills/subagents/SKILL.md +18 -42
  168. package/packages/subagents/src/agents.ts +213 -0
  169. package/packages/subagents/src/cards.ts +182 -0
  170. package/packages/subagents/src/index.ts +609 -1084
  171. package/packages/subagents/src/manager.ts +429 -0
  172. package/packages/subagents/src/profiles.ts +130 -0
  173. package/packages/subagents/src/transcript.ts +154 -0
  174. package/packages/subagents/src/ui.ts +389 -0
  175. package/packages/unipi/bundled.js +39756 -45569
  176. package/packages/unipi/index.ts +55 -10
  177. package/packages/updater/package.json +2 -2
  178. package/packages/utility/README.md +76 -69
  179. package/packages/utility/package.json +2 -2
  180. package/packages/utility/skills/image/SKILL.md +37 -0
  181. package/packages/utility/skills/utility/SKILL.md +17 -60
  182. package/packages/utility/src/answer/extract.ts +128 -0
  183. package/packages/utility/src/answer/index.ts +213 -0
  184. package/packages/utility/src/answer/reply.ts +133 -0
  185. package/packages/utility/src/answer/web.ts +167 -0
  186. package/packages/utility/src/attach/index.ts +205 -0
  187. package/packages/utility/src/commands.ts +38 -142
  188. package/packages/utility/src/diagnostics/engine.ts +50 -5
  189. package/packages/utility/src/image/index.ts +33 -0
  190. package/packages/utility/src/image/models.ts +221 -0
  191. package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
  192. package/packages/utility/src/image/settings.ts +178 -0
  193. package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
  194. package/packages/utility/src/image/tools.ts +168 -0
  195. package/packages/utility/src/image/transports.ts +186 -0
  196. package/packages/utility/src/index.ts +68 -389
  197. package/packages/utility/src/lifecycle/cleanup.ts +116 -311
  198. package/packages/utility/src/rename/gate.ts +102 -0
  199. package/packages/utility/src/rename/index.ts +191 -0
  200. package/packages/utility/src/rename/session.ts +170 -0
  201. package/packages/utility/src/render/delegated.ts +285 -0
  202. package/packages/utility/src/render/harness.ts +683 -0
  203. package/packages/utility/src/render/parse.ts +156 -0
  204. package/packages/utility/src/render/pi-settings.ts +38 -0
  205. package/packages/utility/src/render/reply-bg.ts +307 -0
  206. package/packages/utility/src/render/simple.ts +724 -0
  207. package/packages/utility/src/render/spacing.ts +235 -0
  208. package/packages/utility/src/render/styled.ts +251 -0
  209. package/packages/utility/src/render/tools.ts +353 -0
  210. package/packages/utility/src/settings.ts +166 -233
  211. package/packages/utility/src/types.ts +1 -222
  212. package/packages/watchdog/index.ts +17 -6
  213. package/packages/watchdog/package.json +3 -3
  214. package/packages/watchdog/src/config.ts +3 -2
  215. package/packages/web-api/package.json +2 -2
  216. package/packages/workflow/README.md +3 -0
  217. package/packages/workflow/index.ts +0 -2
  218. package/packages/workflow/package.json +2 -2
  219. package/packages/workflow/src/permission/decide.ts +6 -4
  220. package/packages/workflow/src/permission/index.ts +17 -7
  221. package/packages/workflow/src/permission/settings.ts +39 -4
  222. package/packages/workflow/src/plan/index.ts +19 -4
  223. package/packages/background-tasks/extensions/delegate-child.ts +0 -1
  224. package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
  225. package/packages/background-tasks/src/context-token-budget.ts +0 -903
  226. package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
  227. package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
  228. package/packages/background-tasks/src/delegate/budget.ts +0 -415
  229. package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
  230. package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
  231. package/packages/background-tasks/src/delegate/launch.ts +0 -484
  232. package/packages/background-tasks/src/delegate/result-package.ts +0 -459
  233. package/packages/background-tasks/src/delegate/runner.ts +0 -430
  234. package/packages/background-tasks/src/delegate/seed.ts +0 -423
  235. package/packages/background-tasks/src/delegate/types.ts +0 -323
  236. package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
  237. package/packages/background-tasks/src/delegate-extension.ts +0 -654
  238. package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
  239. package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
  240. package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
  241. package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
  242. package/packages/compactor/src/compaction/build-sections.ts +0 -76
  243. package/packages/compactor/src/compaction/extract/goals.ts +0 -36
  244. package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
  245. package/packages/compactor/src/compaction/format.ts +0 -53
  246. package/packages/compactor/src/compaction/merge.ts +0 -143
  247. package/packages/compactor/src/config/presets.ts +0 -160
  248. package/packages/compactor/src/executor/executor.ts +0 -233
  249. package/packages/compactor/src/executor/runtime.ts +0 -115
  250. package/packages/compactor/src/security/evaluator.ts +0 -89
  251. package/packages/compactor/src/security/policy.ts +0 -96
  252. package/packages/compactor/src/security/scanner.ts +0 -65
  253. package/packages/compactor/src/session/auto-inject.ts +0 -60
  254. package/packages/compactor/src/session/db.ts +0 -325
  255. package/packages/compactor/src/session/extract.ts +0 -144
  256. package/packages/compactor/src/session/resume-inject.ts +0 -64
  257. package/packages/compactor/src/session/snapshot.ts +0 -327
  258. package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
  259. package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
  260. package/packages/compactor/src/tools/ctx-execute.ts +0 -23
  261. package/packages/compactor/src/tools/ctx-stats.ts +0 -96
  262. package/packages/image/README.md +0 -99
  263. package/packages/image/package.json +0 -61
  264. package/packages/image/skills/image/SKILL.md +0 -76
  265. package/packages/image/src/generate.ts +0 -256
  266. package/packages/image/src/index.ts +0 -135
  267. package/packages/image/src/models.ts +0 -519
  268. package/packages/image/src/openai-images-api.ts +0 -282
  269. package/packages/image/src/register-providers.ts +0 -209
  270. package/packages/image/src/settings.ts +0 -173
  271. package/packages/image/src/tools.ts +0 -326
  272. package/packages/input-shortcuts/src/clipboard.ts +0 -119
  273. package/packages/kanboard/src/runner.ts +0 -817
  274. package/packages/skill-registry/skills/auto/SKILL.md +0 -465
  275. package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
  276. package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
  277. package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
  278. package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
  279. package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
  280. package/packages/skill-registry/skills/debug/SKILL.md +0 -228
  281. package/packages/skill-registry/skills/document/SKILL.md +0 -120
  282. package/packages/skill-registry/skills/fix/SKILL.md +0 -212
  283. package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
  284. package/packages/skill-registry/skills/plan/SKILL.md +0 -231
  285. package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
  286. package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
  287. package/packages/skill-registry/skills/research/SKILL.md +0 -251
  288. package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
  289. package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
  290. package/packages/skill-registry/skills/work/SKILL.md +0 -218
  291. package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
  292. package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
  293. package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
  294. package/packages/subagents/agents/delegate.md +0 -14
  295. package/packages/subagents/agents/oracle.md +0 -78
  296. package/packages/subagents/agents/researcher.md +0 -52
  297. package/packages/subagents/agents/reviewer.md +0 -80
  298. package/packages/subagents/agents/scout.md +0 -50
  299. package/packages/subagents/agents/worker.md +0 -59
  300. package/packages/subagents/prompts/council.md +0 -48
  301. package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
  302. package/packages/subagents/prompts/parallel-cleanup.md +0 -59
  303. package/packages/subagents/prompts/parallel-research.md +0 -50
  304. package/packages/subagents/prompts/parallel-review.md +0 -54
  305. package/packages/subagents/prompts/review-loop.md +0 -43
  306. package/packages/subagents/skills/council-mode/SKILL.md +0 -230
  307. package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
  308. package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
  309. package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
  310. package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
  311. package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
  312. package/packages/subagents/src/acceptance.ts +0 -331
  313. package/packages/subagents/src/agent-manager.ts +0 -405
  314. package/packages/subagents/src/agent-memory.ts +0 -300
  315. package/packages/subagents/src/agent-overrides.ts +0 -183
  316. package/packages/subagents/src/agent-runner.ts +0 -271
  317. package/packages/subagents/src/async-runner.ts +0 -422
  318. package/packages/subagents/src/authority-policy.ts +0 -47
  319. package/packages/subagents/src/budgets.ts +0 -285
  320. package/packages/subagents/src/child-safety.ts +0 -140
  321. package/packages/subagents/src/config.ts +0 -249
  322. package/packages/subagents/src/conversation-viewer.ts +0 -281
  323. package/packages/subagents/src/core-compat.ts +0 -94
  324. package/packages/subagents/src/custom-agents.ts +0 -343
  325. package/packages/subagents/src/file-system-retry.ts +0 -50
  326. package/packages/subagents/src/fleet-data.ts +0 -105
  327. package/packages/subagents/src/fleet-view.ts +0 -322
  328. package/packages/subagents/src/foreground-detach.ts +0 -59
  329. package/packages/subagents/src/fork-context.ts +0 -285
  330. package/packages/subagents/src/global.d.ts +0 -7
  331. package/packages/subagents/src/guide.ts +0 -129
  332. package/packages/subagents/src/mission-state.ts +0 -133
  333. package/packages/subagents/src/mission-store.ts +0 -436
  334. package/packages/subagents/src/output-limits.ts +0 -142
  335. package/packages/subagents/src/parity-types.ts +0 -315
  336. package/packages/subagents/src/pi-args.ts +0 -167
  337. package/packages/subagents/src/result-files.ts +0 -232
  338. package/packages/subagents/src/result-watcher.ts +0 -221
  339. package/packages/subagents/src/retained-children.ts +0 -176
  340. package/packages/subagents/src/run-fanout-budget.ts +0 -356
  341. package/packages/subagents/src/scheduled-runs.ts +0 -349
  342. package/packages/subagents/src/schemas.ts +0 -263
  343. package/packages/subagents/src/slash-commands.ts +0 -80
  344. package/packages/subagents/src/supervisor-channel.ts +0 -249
  345. package/packages/subagents/src/tool-handler.ts +0 -1144
  346. package/packages/subagents/src/types.ts +0 -238
  347. package/packages/subagents/src/widget.ts +0 -457
  348. package/packages/subagents/src/workflow-script.ts +0 -737
  349. package/packages/subagents/src/workflow-worker.ts +0 -384
  350. package/packages/subagents/src/worktree.ts +0 -614
  351. package/packages/utility/src/analytics/collector.ts +0 -293
  352. package/packages/utility/src/info-screen.ts +0 -80
  353. package/packages/utility/src/lifecycle/process.ts +0 -162
  354. package/packages/utility/src/prefix-cache.ts +0 -263
  355. package/packages/utility/src/skill-discovery.ts +0 -542
  356. package/packages/utility/src/tools/env.ts +0 -113
  357. package/packages/utility/src/tui/name-badge-state.ts +0 -315
  358. package/packages/utility/src/tui/name-badge.ts +0 -109
  359. /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
@@ -0,0 +1,683 @@
1
+ /**
2
+ * @pi-unipi/utility — harness message provenance rendering (UNI-53).
3
+ *
4
+ * Production renderer for harness-generated content: ONE violet ▏ rail
5
+ * (#a78bfa) on a dark-slate fill (#20222d) per row, body markdown rendered at
6
+ * width-2 so nothing is cut, light text forced on the dark fill, blank rows
7
+ * painted. Three densities (simple / regular / advanced) mirroring the approved
8
+ * UNI-49 preview; the expand hint is Ctrl+O (pi\'s real toggle), not the
9
+ * preview\'s "e".
10
+ *
11
+ * THEME: the panel is theme-PARAMETERIZED — the caller passes the host theme it
12
+ * already received (renderer callbacks, entry renderers, the setWidget probe).
13
+ * This module never imports or overrides the global theme; body markdown uses
14
+ * the SDK\'s public getMarkdownTheme() (current global theme) with a forced
15
+ * readable light fg on the dark fill.
16
+ *
17
+ * Also installs:
18
+ * - registerMessageRenderer for the KNOWN harness custom types (details
19
+ * `unipiHarness` meta when present, generic type fallback otherwise — never
20
+ * text detection);
21
+ * - the native USER card patch: wraps UserMessageComponent.render only, mapped
22
+ * by strict ordinal alignment against ctx.sessionManager.buildContextEntries()
23
+ * (compaction-aware), skill blocks excluded via the SDK\'s own parseSkillBlock;
24
+ * - `withHarnessToolAnnotations(def)` for tool renderers: a provenance header
25
+ * line for results carrying `unipiHarnessAnnotations` details (text stays
26
+ * exactly what the handler produced).
27
+ */
28
+ import { getMarkdownTheme, parseSkillBlock } from "@earendil-works/pi-coding-agent";
29
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
30
+ import { Markdown, truncateToWidth, wrapTextWithAnsi, visibleWidth, type Component } from "@earendil-works/pi-tui";
31
+ import { paintLine } from "./reply-bg.js";
32
+ import { findTranscriptContainer } from "./spacing.js";
33
+ import { renderStyle, type RenderStyle } from "./styled.js";
34
+ import {
35
+ harnessToolResultDetails,
36
+ readHarnessMeta,
37
+ type HarnessDelivery,
38
+ type HarnessMessageMeta,
39
+ type HarnessMessageMetaInput,
40
+ type HarnessToolAnnotation,
41
+ } from "@pi-unipi/core";
42
+
43
+ export type { HarnessToolAnnotation } from "@pi-unipi/core";
44
+ export { harnessToolResultDetails, readHarnessMeta } from "@pi-unipi/core";
45
+ export type { HarnessMessageMeta, HarnessDelivery } from "@pi-unipi/core";
46
+ export type { RenderStyle } from "./styled.js";
47
+
48
+ // ── palette (fixed dark fill; light text; mode-aware SGR) ───────────────────
49
+ interface Palette {
50
+ violet: string;
51
+ warn: string;
52
+ fill: string;
53
+ light: string;
54
+ lightDim: string;
55
+ }
56
+
57
+ const TRUECOLOR: Palette = {
58
+ violet: "\x1b[38;2;167;139;250m", // #a78bfa
59
+ warn: "\x1b[38;2;251;191;36m", // #fbbf24
60
+ fill: "\x1b[48;2;32;34;45m", // #20222d
61
+ light: "\x1b[38;2;232;234;242m", // #e8eaf2
62
+ lightDim: "\x1b[38;2;154;160;181m", // #9aa0b5
63
+ };
64
+
65
+ const COLOR256: Palette = {
66
+ violet: "\x1b[38;5;139m",
67
+ warn: "\x1b[38;5;215m",
68
+ fill: "\x1b[48;5;235m",
69
+ light: "\x1b[38;5;253m",
70
+ lightDim: "\x1b[38;5;245m",
71
+ };
72
+
73
+ type ColorMode = "truecolor" | "256color";
74
+
75
+ /** Theme surface the panel reads (pi\'s Theme satisfies it). May also be
76
+ * supplied as a FUNCTION resolved per render for dynamic native state. */
77
+ export type HarnessThemeCompat = { getColorMode?: () => ColorMode; appearance?: string } | (() => HarnessThemeCompat);
78
+
79
+ function resolveTheme(theme: HarnessThemeCompat | undefined): { getColorMode?: () => ColorMode; appearance?: string } | undefined {
80
+ return (typeof theme === "function" ? theme() : theme) as { getColorMode?: () => ColorMode; appearance?: string } | undefined;
81
+ }
82
+
83
+ function paletteFor(theme: HarnessThemeCompat | undefined): Palette {
84
+ const mode = resolveTheme(theme)?.getColorMode?.() ?? (process.env.COLORTERM === "truecolor" ? "truecolor" : "256color");
85
+ return mode === "truecolor" ? TRUECOLOR : COLOR256;
86
+ }
87
+
88
+ function modeOf(theme: HarnessThemeCompat | undefined): ColorMode {
89
+ return resolveTheme(theme)?.getColorMode?.() ?? (process.env.COLORTERM === "truecolor" ? "truecolor" : "256color");
90
+ }
91
+
92
+ const FG_RESET = "\x1b[39m";
93
+
94
+ function stripAnsi(line: string): string {
95
+ return line.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
96
+ }
97
+
98
+ function mdLines(text: string, width: number, light: string): string[] {
99
+ if (text.length === 0) return [];
100
+ let markdownTheme: unknown;
101
+ try {
102
+ markdownTheme = getMarkdownTheme(); // SDK public getter — current global theme
103
+ } catch {
104
+ markdownTheme = undefined; // test mocks / uninitialized hosts: plain fallback
105
+ }
106
+ if (markdownTheme === undefined || markdownTheme === null) {
107
+ // Forced readable light fg for the raw fallback (invisible otherwise on
108
+ // light terminals).
109
+ return text.split("\n").flatMap((segment) => {
110
+ if (segment.length === 0) return [""];
111
+ return wrapTextWithAnsi(`${light}${segment}${FG_RESET}`, width);
112
+ });
113
+ }
114
+ const comp = new Markdown(text, 0, 0, markdownTheme as never, { color: (c: string) => `${light}${c}${FG_RESET}` });
115
+ return comp.render(width);
116
+ }
117
+
118
+ export interface HarnessRenderOptions {
119
+ expanded: boolean | (() => boolean);
120
+ style?: RenderStyle | (() => RenderStyle);
121
+ }
122
+
123
+ interface ThemeTagger {
124
+ tag(theme: unknown): string;
125
+ mode(theme: HarnessThemeCompat | undefined): ColorMode;
126
+ }
127
+
128
+ function makeThemeTagger(): ThemeTagger {
129
+ const tags = new WeakMap<object, number>();
130
+ let counter = 0;
131
+ return {
132
+ tag(theme: unknown): string {
133
+ const resolved = resolveTheme(theme as HarnessThemeCompat | undefined);
134
+ if (!resolved || typeof resolved !== "object") return "fn";
135
+ let t = tags.get(resolved);
136
+ if (t === undefined) {
137
+ counter += 1;
138
+ t = counter;
139
+ tags.set(resolved, t);
140
+ }
141
+ return `th${t}`;
142
+ },
143
+ mode(theme: HarnessThemeCompat | undefined): ColorMode {
144
+ return modeOf(theme);
145
+ },
146
+ };
147
+ }
148
+
149
+ const themeTagger = makeThemeTagger();
150
+
151
+ /** Shared production panel for harness content — full text preserved expanded.
152
+ * `theme`/`options.style` may be functions (dynamic native state); resolved per
153
+ * render and included in the cache key. */
154
+ export class HarnessPanel implements Component {
155
+ private cacheKey = "";
156
+ private cache: string[] = [];
157
+ private readonly expandedOf: () => boolean;
158
+ private readonly theme: HarnessThemeCompat | (() => HarnessThemeCompat);
159
+
160
+ constructor(
161
+ private readonly content: string,
162
+ private readonly meta: HarnessMessageMeta,
163
+ private readonly options: HarnessRenderOptions,
164
+ theme: HarnessThemeCompat | (() => HarnessThemeCompat),
165
+ ) {
166
+ this.expandedOf = typeof options.expanded === "function" ? options.expanded : () => options.expanded === true;
167
+ this.theme = theme;
168
+ }
169
+
170
+ invalidate(): void {
171
+ this.cacheKey = ""; // next render rebuilds (refreshes markdown)
172
+ }
173
+
174
+ render(width: number): string[] {
175
+ const style: RenderStyle = typeof this.options.style === "function" ? this.options.style() : this.options.style ?? "simple";
176
+ const theme = resolveTheme(this.theme);
177
+ const expanded = this.expandedOf();
178
+ const key = `${width}|${style}|${expanded}|${this.content.length}|${this.meta.id}|${themeTagger.tag(this.theme)}|${themeTagger.mode(this.theme)}`;
179
+ if (key !== this.cacheKey) {
180
+ this.cache = this.build(width, style, expanded, theme, paletteFor(theme));
181
+ this.cacheKey = key;
182
+ }
183
+ return this.cache;
184
+ }
185
+
186
+ private build(width: number, style: RenderStyle, expanded: boolean, theme: HarnessThemeCompat | undefined, pal: Palette): string[] {
187
+ const railGlyph = `${pal.violet}▏${FG_RESET}`;
188
+ const inner = Math.max(8, width - 2);
189
+ const V = (s: string) => `${pal.violet}${s}${FG_RESET}`;
190
+ const W = (s: string) => `${pal.warn}${s}${FG_RESET}`;
191
+ const DIM = (s: string) => `${pal.lightDim}${s}${FG_RESET}`;
192
+ const paint = (line: string): string => paintLine(`${railGlyph} ${line}`, width, pal.fill);
193
+ // Rows are built as PLAIN text, wrapped at the inner width, then colorized
194
+ // wholesale — no ANSI inside the wrap input, so wrapping is width-exact.
195
+ const plainRows = (text: string, colorize: (l: string) => string): string[] =>
196
+ wrapTextWithAnsi(text, inner).map(colorize);
197
+ const warning = this.meta.severity === "warning";
198
+ const label = `${warning ? `${pal.warn}⚠${FG_RESET} ` : ""}UniPi · ${this.meta.source} · ${this.meta.title}`;
199
+ const labelColorize = this.meta.severity === "warning" ? W : V;
200
+
201
+ const body = mdLines(this.content, inner, pal.light);
202
+ const out: string[] = [];
203
+ out.push(...plainRows(label, labelColorize));
204
+ if (style === "simple") {
205
+ const synopsis = this.meta.synopsis ?? this.meta.title;
206
+ out.push(...plainRows(synopsis, (l) => (warning ? W(l) : DIM(l))));
207
+ if (expanded) out.push(...body);
208
+ else out.push(...plainRows(`Ctrl+O: full message (${body.length} rendered rows)`, DIM));
209
+ } else if (style === "regular") {
210
+ if (expanded) out.push(...plainRows(detailText(this.meta), DIM));
211
+ for (const l of body) out.push(l);
212
+ } else {
213
+ if (expanded) {
214
+ out.push(...body);
215
+ } else {
216
+ for (const l of body.slice(0, 4)) out.push(l);
217
+ if (body.length > 4) out.push(...plainRows(`Ctrl+O: expand full (${body.length - 4} more rendered rows)`, DIM));
218
+ }
219
+ out.push(...plainRows(`origin: harness | model role: user | delivery: ${this.meta.delivery}`, DIM));
220
+ }
221
+ return out.map((l) => truncateToWidth(l, inner)).map(paint);
222
+ }
223
+ }
224
+
225
+ function detailText(meta: HarnessMessageMeta): string {
226
+ return [
227
+ `id: ${meta.id}`,
228
+ `origin: harness · transport: ${meta.delivery}`,
229
+ meta.synopsis ? `synopsis: ${meta.synopsis}` : "",
230
+ ].filter((r) => r.length > 0).join("\n");
231
+ }
232
+
233
+ /** Public pure entry: build the panel component for a harness message.
234
+ * `theme` is REQUIRED — pass the host theme the caller already received. */
235
+ export function renderHarnessMessage(
236
+ content: string,
237
+ meta: HarnessMessageMeta,
238
+ options: HarnessRenderOptions,
239
+ theme: HarnessThemeCompat | (() => HarnessThemeCompat),
240
+ ): Component {
241
+ return new HarnessPanel(content, meta, options, theme);
242
+ }
243
+
244
+ // ── known custom types ──────────────────────────────────────────────────────
245
+ interface KnownType {
246
+ source: string;
247
+ title: string;
248
+ severity?: "warning";
249
+ delivery: HarnessDelivery;
250
+ }
251
+
252
+ /** Faithful per-type fallback delivery (actual transport), stable fallback id. */
253
+ export const KNOWN_CUSTOM_TYPES: Record<string, KnownType> = {
254
+ "unipi:lh-continue": { source: "Long-horizon", title: "Continuation", delivery: "boundary" },
255
+ "unipi:kanboard-continue": { source: "Kanboard", title: "Unfinished task", delivery: "boundary" },
256
+ "unipi:kanboard-next": { source: "Kanboard", title: "Next task", delivery: "boundary" },
257
+ "unipi-memory-recall-reminder": { source: "Memory", title: "Memory recall", delivery: "before_agent_start" },
258
+ "unipi-memory-retro-reminder": { source: "Memory", title: "Memory save?", delivery: "nextTurn" },
259
+ "unipi-skills-revealed": { source: "Skills", title: "Skill reveal", delivery: "direct" },
260
+ "unipi:plan-mode-message": { source: "Plan mode", title: "Plan mode", delivery: "direct" },
261
+ "unipi-watchdog": { source: "Watchdog", title: "Warning", severity: "warning", delivery: "before_agent_start" },
262
+ "compactor-recall": { source: "Compactor", title: "Session recall", delivery: "direct" },
263
+ "unipi-response": { source: "Utility", title: "Response", delivery: "followUp" },
264
+ };
265
+
266
+ /** Stable fallback meta (no fresh ids per paint). */
267
+ export function fallbackHarnessMeta(customType: string): HarnessMessageMeta {
268
+ const known = KNOWN_CUSTOM_TYPES[customType] ?? { source: customType, title: customType, delivery: "direct" as HarnessDelivery };
269
+ return {
270
+ version: 1,
271
+ id: `hm-fallback-${customType}`,
272
+ delivery: known.delivery,
273
+ source: known.source,
274
+ title: known.title,
275
+ ...(known.severity ? { severity: known.severity } : {}),
276
+ };
277
+ }
278
+
279
+ export function contentText(content: unknown): string {
280
+ if (typeof content === "string") return content;
281
+ if (!Array.isArray(content)) return "";
282
+ return content
283
+ .filter((c): c is { type: "text"; text: string } => (c as { type?: string }).type === "text")
284
+ .map((c) => (c as { text: string }).text)
285
+ .join("");
286
+ }
287
+
288
+ /** Renderer for the KNOWN harness custom types. Body text unchanged; meta from
289
+ * details.unipiHarness when present, generic type fallback otherwise. */
290
+ export function installHarnessRenderers(pi: ExtensionAPI): void {
291
+ for (const customType of Object.keys(KNOWN_CUSTOM_TYPES)) {
292
+ try {
293
+ pi.registerMessageRenderer(customType, ((message: { content?: unknown; details?: unknown }, options: { expanded?: boolean }, theme: HarnessThemeCompat) => {
294
+ const meta = readHarnessMeta(message.details) ?? fallbackHarnessMeta(customType);
295
+ const text = contentText(message.content);
296
+ return renderHarnessMessage(text, meta, {
297
+ expanded: options?.expanded === true,
298
+ style: renderStyle(),
299
+ }, theme);
300
+ }) as never);
301
+ } catch {
302
+ // Renderer registration is UI-dependent; never block load.
303
+ }
304
+ }
305
+ }
306
+
307
+ // ── native USER card patch ──────────────────────────────────────────────────
308
+ interface UserComponentLike {
309
+ text: string;
310
+ rebuild: () => void;
311
+ setOutputPad: (pad: number) => void;
312
+ render: (width: number) => string[];
313
+ invalidate?: () => void;
314
+ }
315
+
316
+ interface ExpectedCard {
317
+ entryId: string;
318
+ text: string;
319
+ meta?: HarnessMessageMeta;
320
+ }
321
+
322
+ interface HarnessUiContext {
323
+ cwd?: string;
324
+ sessionManager?: { buildContextEntries: () => Array<{ type?: string; id?: string; message?: unknown }>; getLeafId?: () => string | undefined };
325
+ ui?: { getToolsExpanded?: () => boolean; requestRender?: (force?: boolean) => void };
326
+ }
327
+
328
+ interface Wrapped {
329
+ original: (width: number) => string[];
330
+ originalInvalidate: (() => void) | undefined;
331
+ panel: HarnessPanel;
332
+ metaId: string;
333
+ }
334
+
335
+ const OSC133_ZONE_START = "\x1b]133;A\x07";
336
+ const OSC133_ZONE_END = "\x1b]133;B\x07";
337
+ const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
338
+ const MAX_PERSIST_RETRIES = 3;
339
+
340
+ export function installHarnessUserRendering(pi: ExtensionAPI): void {
341
+ let scheduled = false;
342
+ const timers = new Set<ReturnType<typeof setTimeout>>();
343
+ const wrapped = new Map<UserComponentLike, Wrapped>();
344
+ let lastContainer: { children?: unknown[] } | undefined;
345
+ let lastChildrenRef: unknown[] | undefined;
346
+ let lastFirst: unknown;
347
+ let lastLast: unknown;
348
+ let lastLength = -1;
349
+ let lastLeaf: string | undefined;
350
+ let lastTheme: HarnessThemeCompat | undefined;
351
+ let lastCtx: HarnessUiContext;
352
+ let tui: unknown;
353
+ let theme: HarnessThemeCompat | undefined;
354
+ let probeInstalled = false;
355
+ let persistRetries = 0;
356
+ let shuttingDown = false;
357
+
358
+ const requestRender = (): void => {
359
+ try {
360
+ (tui as { requestRender?: (force?: boolean) => void } | undefined)?.requestRender?.(true);
361
+ } catch {}
362
+ };
363
+
364
+ const schedule = (): void => {
365
+ if (shuttingDown || scheduled) return;
366
+ scheduled = true;
367
+ const t = setTimeout(() => {
368
+ scheduled = false;
369
+ timers.delete(t);
370
+ try {
371
+ reconcile();
372
+ } catch (err) {
373
+ }
374
+ }, 0);
375
+ timers.add(t);
376
+ };
377
+
378
+ const captureCtx = (ctx: unknown): void => {
379
+ if (!ctx || typeof ctx !== "object") return;
380
+ lastCtx = ctx as HarnessUiContext;
381
+ };
382
+
383
+ const leafId = (): string | undefined => {
384
+ try {
385
+ return lastCtx?.sessionManager?.getLeafId?.();
386
+ } catch {
387
+ return undefined;
388
+ }
389
+ };
390
+
391
+ const expectedCards = (): ExpectedCard[] => {
392
+ const sm = lastCtx?.sessionManager;
393
+ if (!sm || typeof sm.buildContextEntries !== "function") return [];
394
+ const out: ExpectedCard[] = [];
395
+ for (const entry of sm.buildContextEntries()) {
396
+ if (entry?.type !== "message") continue;
397
+ const message = entry.message as { role?: unknown; content?: unknown; unipiHarness?: unknown } | undefined;
398
+ if (!message || message.role !== "user") continue;
399
+ const text = contentText(message.content);
400
+ const skill = parseSkillBlock(text);
401
+ if (skill) {
402
+ // Skill blocks render as their own component — never harness-labelled.
403
+ if (skill.userMessage) out.push({ entryId: String(entry.id), text: skill.userMessage });
404
+ continue;
405
+ }
406
+ if (text.length === 0) continue; // images-only: no native card
407
+ const meta = readHarnessMeta(message);
408
+ out.push({ entryId: String(entry.id), text, meta });
409
+ }
410
+ return out;
411
+ };
412
+
413
+ const isUserComponent = (c: unknown): c is UserComponentLike =>
414
+ !!c && typeof c === "object" &&
415
+ typeof (c as { text?: unknown }).text === "string" &&
416
+ typeof (c as { rebuild?: unknown }).rebuild === "function" &&
417
+ typeof (c as { setOutputPad?: unknown }).setOutputPad === "function" &&
418
+ (c as { message?: unknown }).message === undefined;
419
+
420
+ function unpatch(comp: UserComponentLike, w: Wrapped): void {
421
+ comp.render = w.original;
422
+ if (w.originalInvalidate) comp.invalidate = w.originalInvalidate;
423
+ else delete (comp as { invalidate?: unknown }).invalidate;
424
+ }
425
+
426
+ function unpatchAll(): boolean {
427
+ if (wrapped.size === 0) return false;
428
+ for (const [comp, w] of wrapped) unpatch(comp, w);
429
+ wrapped.clear();
430
+ return true;
431
+ }
432
+
433
+ const reconcile = (): void => {
434
+ const container = findTranscriptContainer(tui) as { children?: unknown[] } | undefined;
435
+ if (!container || !Array.isArray(container.children)) return;
436
+ const children = container.children;
437
+ const comps = children.filter((c): c is UserComponentLike => isUserComponent(c));
438
+ const expected = expectedCards();
439
+
440
+ let changed = false;
441
+ let ok = comps.length === expected.length;
442
+ if (ok) {
443
+ for (let i = 0; i < comps.length; i++) {
444
+ if (comps[i]!.text !== expected[i]!.text) {
445
+ ok = false;
446
+ break;
447
+ }
448
+ }
449
+ }
450
+ if (!ok) {
451
+ // Mismatch: fall back to native. Bounded persistence retries after a
452
+ // message_end (the entry may not be visible yet); then stop rescheduling.
453
+ changed = unpatchAll();
454
+ if (persistRetries > 0) {
455
+ persistRetries -= 1;
456
+ schedule();
457
+ }
458
+ } else {
459
+ persistRetries = 0;
460
+ // Prune wrapped entries whose component is absent.
461
+ for (const comp of [...wrapped.keys()]) {
462
+ if (!comps.includes(comp)) {
463
+ unpatch(comp, wrapped.get(comp)!);
464
+ wrapped.delete(comp);
465
+ changed = true;
466
+ }
467
+ }
468
+ for (let i = 0; i < comps.length; i++) {
469
+ const comp = comps[i]!;
470
+ const meta = expected[i]!.meta;
471
+ const existing = wrapped.get(comp);
472
+ if (!meta) {
473
+ if (existing) {
474
+ unpatch(comp, existing);
475
+ wrapped.delete(comp);
476
+ changed = true;
477
+ }
478
+ continue;
479
+ }
480
+ if (existing && existing.metaId === meta.id) continue;
481
+ // New wrap OR metadata changed (same component) → (re)create the panel.
482
+ if (existing) unpatch(comp, existing);
483
+ const panel = new HarnessPanel(comp.text, meta, {
484
+ expanded: () => lastCtx?.ui?.getToolsExpanded?.() === true,
485
+ style: () => renderStyle(lastCtx?.cwd) as RenderStyle,
486
+ }, () => theme ?? { getColorMode: () => (process.env.COLORTERM === "truecolor" ? "truecolor" : "256color") });
487
+ const original = comp.render.bind(comp);
488
+ const originalInvalidate = comp.invalidate?.bind(comp);
489
+ const osc = (rows: string[]): string[] => {
490
+ if (rows.length === 0) return rows;
491
+ const o = [...rows];
492
+ o[0] = OSC133_ZONE_START + o[0]!;
493
+ o[o.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + o[o.length - 1]!;
494
+ return o;
495
+ };
496
+ comp.render = (w: number) => osc(panel.render(w));
497
+ comp.invalidate = () => {
498
+ panel.invalidate();
499
+ originalInvalidate?.();
500
+ };
501
+ wrapped.set(comp, { original, originalInvalidate, panel, metaId: meta.id });
502
+ changed = true;
503
+ }
504
+ }
505
+
506
+ // Store the O(1) probe fields AFTER this pass — both paths.
507
+ lastContainer = container;
508
+ lastChildrenRef = children;
509
+ lastLength = children.length;
510
+ lastFirst = children[0];
511
+ lastLast = children[children.length - 1];
512
+ lastLeaf = leafId();
513
+ lastTheme = theme;
514
+
515
+ if (changed) requestRender();
516
+ };
517
+
518
+ /** O(1) probe against the stored identity fields. */
519
+ const probe = (): void => {
520
+ if (shuttingDown) return;
521
+ const container = findTranscriptContainer(tui) as { children?: unknown[] } | undefined;
522
+ if (!container) return;
523
+ const children = container.children;
524
+ if (!Array.isArray(children)) return;
525
+ const leaf = leafId();
526
+ if (
527
+ container !== lastContainer ||
528
+ children !== lastChildrenRef ||
529
+ children.length !== lastLength ||
530
+ children[0] !== lastFirst ||
531
+ children[children.length - 1] !== lastLast ||
532
+ leaf !== lastLeaf ||
533
+ theme !== lastTheme
534
+ ) {
535
+ schedule();
536
+ }
537
+ };
538
+
539
+ try {
540
+ pi.on("session_start", ((_event: unknown, ctx: unknown) => {
541
+ captureCtx(ctx);
542
+ // AFTER event on a FRESH context: re-register the probe widget.
543
+ probeInstalled = false;
544
+ const c = lastCtx as { hasUI?: boolean; ui?: { setWidget?: (name: string, render: (t: unknown, th: unknown) => unknown, opts?: unknown) => void } } | undefined;
545
+ if (!c?.hasUI || probeInstalled || typeof c.ui?.setWidget !== "function") return;
546
+ probeInstalled = true;
547
+ try {
548
+ // 0-height probe widget: the factory captures root+theme once; the
549
+ // render callback runs per frame and O(1)-probes for changes.
550
+ c.ui.setWidget(
551
+ "unipi.harnessUserProbe",
552
+ (t: unknown, th: unknown) => {
553
+ tui = t;
554
+ theme = th as HarnessThemeCompat;
555
+ probe();
556
+ return {
557
+ invalidate(): void {
558
+ schedule();
559
+ },
560
+ render(): string[] {
561
+ probe();
562
+ return [];
563
+ },
564
+ };
565
+ },
566
+ { placement: "belowEditor" },
567
+ );
568
+ } catch {}
569
+ }) as never);
570
+ } catch {
571
+ // optional
572
+ }
573
+ const onAny = pi.on as unknown as (event: string, handler: (event?: unknown, ctx?: unknown) => unknown) => void;
574
+ for (const evt of ["message_start", "message_end", "agent_start"]) {
575
+ onAny(evt, (event, ctx) => {
576
+ if (ctx) captureCtx(ctx);
577
+ if (evt === "message_end") persistRetries = MAX_PERSIST_RETRIES;
578
+ schedule();
579
+ });
580
+ }
581
+ for (const evt of ["session_before_switch", "session_before_fork", "session_before_tree"]) {
582
+ // BEFORE events: cancel stale pending work — do not apply the old tree.
583
+ onAny(evt, () => {
584
+ for (const t of timers) clearTimeout(t);
585
+ timers.clear();
586
+ scheduled = false;
587
+ });
588
+ }
589
+ for (const evt of ["session_tree", "session_compact"]) {
590
+ // AFTER events: full rebind against the new tree.
591
+ onAny(evt, (_event, ctx) => {
592
+ if (ctx) captureCtx(ctx);
593
+ persistRetries = MAX_PERSIST_RETRIES;
594
+ schedule();
595
+ });
596
+ }
597
+ onAny("session_shutdown", () => {
598
+ shuttingDown = true;
599
+ for (const t of timers) clearTimeout(t);
600
+ timers.clear();
601
+ unpatchAll();
602
+ });
603
+ }
604
+
605
+
606
+ // ── tool annotations ────────────────────────────────────────────────────────
607
+ const ANNOTATION_WRAPPED = Symbol.for("unipi.harnessToolAnnotations");
608
+
609
+ function annotationsOf(result: unknown): HarnessToolAnnotation[] {
610
+ const details = (result as { details?: { unipiHarnessAnnotations?: unknown } } | null | undefined)?.details;
611
+ const list = details?.unipiHarnessAnnotations;
612
+ if (!Array.isArray(list)) return [];
613
+ return list.filter((a): a is HarnessToolAnnotation => {
614
+ const meta = (a as { meta?: unknown })?.meta;
615
+ const text = (a as { text?: unknown })?.text;
616
+ return !!meta && typeof text === "string" && readHarnessMeta({ unipiHarness: meta }) !== undefined;
617
+ });
618
+ }
619
+
620
+ /** Width-aware lazy composite: renders base + annotation rows at call width.
621
+ * `base` may be a Component (live width) or pre-rendered rows (string[]). */
622
+ function composeWithRows(base: unknown, rows: (width: number) => string[]): Component {
623
+ const inner = base as Component | undefined;
624
+ if (inner && typeof inner.render === "function") {
625
+ return {
626
+ render(width: number): string[] {
627
+ return [...inner.render(width), ...rows(width)];
628
+ },
629
+ invalidate(): void {
630
+ inner?.invalidate?.();
631
+ },
632
+ };
633
+ }
634
+ const fixed = Array.isArray(base) ? (base as string[]) : base !== undefined && base !== null ? [String(base)] : [];
635
+ return {
636
+ render(width: number): string[] {
637
+ return [...fixed, ...rows(width)];
638
+ },
639
+ invalidate(): void {},
640
+ };
641
+ }
642
+
643
+ /** Wrap a tool renderer definition so results carrying `unipiHarnessAnnotations`
644
+ * get a provenance header. Original output (including the annotation text
645
+ * itself, shown exactly once) is untouched; defs without annotations render
646
+ * EXACTLY as before; defs without a renderResult keep the native default.
647
+ * Idempotent per definition (mutates + marks). */
648
+ export function withHarnessToolAnnotations<T extends object>(def: T & { name?: string }): T {
649
+ const anyDef = def as { [ANNOTATION_WRAPPED]?: boolean; renderResult?: unknown; simpleResult?: unknown; name?: string };
650
+ if (anyDef[ANNOTATION_WRAPPED]) return def;
651
+ const toolName = typeof anyDef.name === "string" ? anyDef.name : undefined;
652
+ const headerFor = (result: unknown): string[] => {
653
+ const pal = paletteFor(undefined);
654
+ const rows: string[] = [];
655
+ for (const a of annotationsOf(result)) {
656
+ const glyph = a.meta.severity === "warning" ? `${pal.warn}⚠${FG_RESET}` : `${pal.violet}◇${FG_RESET}`;
657
+ rows.push(`${glyph} UniPi · ${pal.violet}${a.meta.source}${FG_RESET} · ${a.meta.title}${toolName ? ` · ${toolName} guidance` : " · tool guidance"}`);
658
+ if (a.meta.synopsis) rows.push(` ${a.meta.synopsis}`);
659
+ }
660
+ return rows;
661
+ };
662
+ if (typeof anyDef.renderResult === "function") {
663
+ const original = anyDef.renderResult as unknown as (r: unknown, o: unknown, t: unknown, c?: unknown) => unknown;
664
+ anyDef.renderResult = ((result: unknown, options: unknown, theme: unknown, ctx: unknown) => {
665
+ const out: unknown = original(result, options, theme, ctx);
666
+ if (annotationsOf(result).length === 0) return out;
667
+ return composeWithRows(out, (width) => headerFor(result).map((l) => truncateToWidth(l, width)));
668
+ }) as unknown as (result: never, options: never, theme: never, ctx: never) => unknown;
669
+ }
670
+ const originalSimple = anyDef.simpleResult as unknown as ((r: unknown, t: unknown, c?: unknown) => unknown) | undefined;
671
+ anyDef.simpleResult = ((result: unknown, theme: unknown, ctx?: unknown) => {
672
+ const base: unknown = originalSimple ? originalSimple(result, theme, ctx) : undefined;
673
+ const anns = annotationsOf(result);
674
+ if (anns.length === 0) return base;
675
+ return composeWithRows(base, (width) =>
676
+ anns.flatMap((a) =>
677
+ wrapTextWithAnsi(`⚠ UniPi · ${a.meta.source} · ${a.meta.synopsis ?? a.meta.title}`, width),
678
+ ),
679
+ );
680
+ }) as unknown as (result: never, theme: never, ctx?: never) => unknown;
681
+ Object.defineProperty(def, ANNOTATION_WRAPPED, { value: true, enumerable: false });
682
+ return def;
683
+ }