@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,191 @@
1
+ /**
2
+ * @pi-unipi/utility — Automatic session naming
3
+ *
4
+ * After every confirmed round (agent_end without error/abort) whose prompt the
5
+ * user typed: gate (prefilter + jev) → isolated one-tool rename session →
6
+ * pi.setSessionName. Names the user sets (pi's /name) are respected: once the
7
+ * current name differs from the last auto name, auto-rename stops for that
8
+ * session. "Rename now" in /unipi:settings bypasses the gate.
9
+ */
10
+
11
+ import { appendFileSync, mkdirSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { join } from "node:path";
14
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
15
+ import { askJev, registerCommandRunner, resolveDecisionModel } from "@pi-unipi/core";
16
+ import { readRenameSettings } from "../settings.js";
17
+ import { detectHerdr, syncPaneTitle, type HerdrEnv } from "../herdr-sync.js";
18
+ import { decide, gateRequest, isIdleRound } from "./gate.js";
19
+ import { runRenameSession } from "./session.js";
20
+
21
+ /** Custom entry recording names auto-rename set (so resume can tell them from /name). */
22
+ export const AUTO_NAME_ENTRY = "unipi:auto-name";
23
+ /** Rounds that must pass between two automatic renames (never twice in one round). */
24
+ const MIN_ROUNDS_BETWEEN = 1;
25
+ const HERDR_POLL_MS = 3000;
26
+
27
+ /** Debug trail, gated by UNIPI_DEBUG_RENAME=1 → ~/.unipi/logs/rename.log. */
28
+ function debugLog(line: string): void {
29
+ if (process.env.UNIPI_DEBUG_RENAME !== "1") return;
30
+ try {
31
+ const dir = join(homedir(), ".unipi", "logs");
32
+ mkdirSync(dir, { recursive: true });
33
+ appendFileSync(join(dir, "rename.log"), `${new Date().toISOString()} ${line}\n`);
34
+ } catch {
35
+ // best effort
36
+ }
37
+ }
38
+
39
+ /** Text of the last assistant message of a round (what the agent concluded). */
40
+ export function lastReplyText(messages: readonly unknown[]): string {
41
+ for (let i = messages.length - 1; i >= 0; i--) {
42
+ const m = messages[i] as { role?: string; content?: unknown };
43
+ if (m.role !== "assistant") continue;
44
+ const text = typeof m.content === "string"
45
+ ? m.content
46
+ : Array.isArray(m.content) ? m.content.filter((c: { type?: string }) => c?.type === "text").map((c: { text?: string }) => c.text ?? "").join("\n") : "";
47
+ if (text.trim()) return text.replace(/<\/?summary>/g, "").trim().slice(0, 1500);
48
+ }
49
+ return "";
50
+ }
51
+
52
+ export function countToolCalls(messages: readonly unknown[]): number {
53
+ let n = 0;
54
+ for (const m of messages as Array<{ role?: string; content?: unknown }>) {
55
+ if (m.role === "assistant" && Array.isArray(m.content)) n += m.content.filter((c: { type?: string }) => c?.type === "toolCall").length;
56
+ }
57
+ return n;
58
+ }
59
+
60
+ export function registerAutoRename(pi: ExtensionAPI): void {
61
+ let requests: string[] = [];
62
+ /** The round that triggered the current rename (the name should describe it). */
63
+ let lastRound: { reply: string; toolCalls: number } = { reply: "", toolCalls: 0 };
64
+ let pending: string | null = null;
65
+ let lastAutoName: string | null = null;
66
+ let roundsSinceRename = Number.POSITIVE_INFINITY;
67
+ let inFlight = false;
68
+ let herdr: HerdrEnv = { enabled: false };
69
+ let herdrShown: string | null = null;
70
+ let herdrTimer: ReturnType<typeof setInterval> | null = null;
71
+
72
+ const currentName = (): string | null => {
73
+ try {
74
+ return pi.getSessionName() ?? null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ const syncHerdr = (name: string | null): void => {
81
+ if (!herdr.enabled || name === herdrShown || !readRenameSettings().herdrSync) return;
82
+ herdrShown = name;
83
+ void syncPaneTitle(herdr, name);
84
+ };
85
+
86
+ const apply = (name: string): void => {
87
+ if (name === currentName()) return;
88
+ pi.setSessionName(name);
89
+ lastAutoName = name;
90
+ roundsSinceRename = 0;
91
+ pi.appendEntry(AUTO_NAME_ENTRY, { name });
92
+ syncHerdr(name);
93
+ };
94
+
95
+ const rename = async (ctx: ExtensionContext, force: boolean, topicChanged = false): Promise<string | undefined> => {
96
+ if (inFlight) return undefined;
97
+ inFlight = true;
98
+ try {
99
+ const settings = readRenameSettings(ctx.cwd);
100
+ const name = await runRenameSession(ctx, { currentName: currentName(), requests, model: settings.model, topicChanged, reply: lastRound.reply }, debugLog);
101
+ debugLog(`rename session → ${name === undefined ? "(no name)" : JSON.stringify(name)}`);
102
+ if (name && (force || name !== currentName())) apply(name);
103
+ return name;
104
+ } catch (error) {
105
+ debugLog(`rename session failed: ${error instanceof Error ? error.message : String(error)}`);
106
+ return undefined;
107
+ } finally {
108
+ inFlight = false;
109
+ }
110
+ };
111
+
112
+ registerCommandRunner("unipi:rename-now", async (raw: unknown) => {
113
+ const ctx = raw as ExtensionContext;
114
+ if (requests.length === 0) {
115
+ ctx.ui?.notify?.("Nothing to name yet — send a request first.", "info");
116
+ return;
117
+ }
118
+ const name = await rename(ctx, true);
119
+ ctx.ui?.notify?.(name ? `Session named "${name}"` : "Could not generate a name.", name ? "info" : "warning");
120
+ });
121
+
122
+ pi.on("session_start", (_event, ctx) => {
123
+ try {
124
+ requests = [];
125
+ pending = null;
126
+ roundsSinceRename = Number.POSITIVE_INFINITY;
127
+ const entries = (ctx.sessionManager?.getEntries?.() ?? []) as Array<{ type?: string; customType?: string; data?: { name?: unknown } }>;
128
+ const last = entries.findLast((e) => e.type === "custom" && e.customType === AUTO_NAME_ENTRY);
129
+ lastAutoName = typeof last?.data?.name === "string" ? last.data.name : null;
130
+ herdr = detectHerdr();
131
+ herdrShown = null;
132
+ syncHerdr(currentName());
133
+ if (herdrTimer) clearInterval(herdrTimer);
134
+ herdrTimer = herdr.enabled ? setInterval(() => syncHerdr(currentName()), HERDR_POLL_MS) : null;
135
+ herdrTimer?.unref?.();
136
+ } catch {
137
+ // Naming is best-effort — never block session start.
138
+ }
139
+ });
140
+
141
+ pi.on("input", (event) => {
142
+ try {
143
+ if (event.source === "extension") return;
144
+ const text = (event.text ?? "").trim();
145
+ if (!text || text.startsWith("/")) return;
146
+ requests = [...requests, text].slice(-6);
147
+ pending = text;
148
+ } catch {
149
+ // ignore
150
+ }
151
+ });
152
+
153
+ pi.on("agent_end", (event, ctx) => {
154
+ const prompt = pending;
155
+ pending = null;
156
+ roundsSinceRename++;
157
+ try {
158
+ if (!prompt || inFlight) return;
159
+ const skip = (why: string) => debugLog(`skip ${JSON.stringify(prompt.slice(0, 60))}: ${why}`);
160
+ if (!readRenameSettings(ctx.cwd).auto) return skip("auto-rename off");
161
+ const lastAssistant = [...(event.messages ?? [])].reverse().find((m) => (m as { role?: string }).role === "assistant") as { stopReason?: string } | undefined;
162
+ if (lastAssistant?.stopReason === "error" || lastAssistant?.stopReason === "aborted") return skip(`round ${lastAssistant.stopReason}`);
163
+ const name = currentName();
164
+ if (name && name !== lastAutoName) return skip("user-set name"); // user-set name wins
165
+ if (name && roundsSinceRename < MIN_ROUNDS_BETWEEN) return skip("renamed recently");
166
+ const reply = lastReplyText(event.messages ?? []);
167
+ const toolCalls = countToolCalls(event.messages ?? []);
168
+ const input = { prompt, currentName: name, earlier: requests, reply, toolCalls };
169
+ if (isIdleRound(input)) return skip("chatter");
170
+ lastRound = { reply, toolCalls };
171
+ // Detached: the next prompt never waits on naming.
172
+ void (async () => {
173
+ try {
174
+ const answers = await askJev({ ...gateRequest(input), settings: resolveDecisionModel(ctx.cwd ?? process.cwd(), "utility"), env: process.env });
175
+ const decision = decide(input, answers);
176
+ debugLog(`gate ${JSON.stringify(prompt.slice(0, 60))} name=${JSON.stringify(name)} → ${decision.rename ? "rename" : "skip"} (${decision.reason})`);
177
+ if (decision.rename) await rename(ctx, false, Boolean(name));
178
+ } catch {
179
+ // Naming must never disturb the main session.
180
+ }
181
+ })();
182
+ } catch {
183
+ // ignore
184
+ }
185
+ });
186
+
187
+ pi.on("session_shutdown", () => {
188
+ if (herdrTimer) clearInterval(herdrTimer);
189
+ herdrTimer = null;
190
+ });
191
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @pi-unipi/utility — Rename session
3
+ *
4
+ * A throwaway in-memory pi session (the /unipi:btw shape) whose ONLY tool is
5
+ * `rename_session`. It never loads extensions, skills or context files, never
6
+ * sees the main transcript, and nothing it does reaches the main session
7
+ * except the name it sets. The main agent never has a rename tool.
8
+ */
9
+
10
+ import {
11
+ createAgentSession,
12
+ createExtensionRuntime,
13
+ defineTool,
14
+ SessionManager,
15
+ type ModelRuntime,
16
+ type ResourceLoader,
17
+ } from "@earendil-works/pi-coding-agent";
18
+ import { Type } from "typebox";
19
+ import { sanitizeName } from "./gate.js";
20
+
21
+ const SYSTEM_PROMPT = [
22
+ "You name coding-agent sessions so the user can find them later.",
23
+ "Call rename_session exactly once with a short title (2-6 words, Title Case) describing the user's current task.",
24
+ "Name the task, not the conversation: 'Kanboard Lane Ordering', not 'User Asks About Lanes'.",
25
+ "If the current title still describes the current task, call rename_session with the current title unchanged.",
26
+ "Do not write anything else.",
27
+ ].join("\n");
28
+
29
+ const RENAME_TIMEOUT_MS = 45_000;
30
+
31
+ export interface RenameCtx {
32
+ cwd?: string;
33
+ model?: unknown;
34
+ modelRegistry?: unknown;
35
+ }
36
+
37
+ export interface RenameRequest {
38
+ currentName: string | null;
39
+ requests: readonly string[];
40
+ /** "" = session model; otherwise "provider/model-id". */
41
+ model: string;
42
+ /** The gate found the latest request leaves the current title's topic. */
43
+ topicChanged?: boolean;
44
+ /** The agent's reply in the triggering round — often the clearest subject. */
45
+ reply?: string;
46
+ }
47
+
48
+ function resourceLoader(): ResourceLoader {
49
+ const extensions = { extensions: [], errors: [], runtime: createExtensionRuntime() };
50
+ return {
51
+ getExtensions: () => extensions,
52
+ getSkills: () => ({ skills: [], diagnostics: [] }),
53
+ getPrompts: () => ({ prompts: [], diagnostics: [] }),
54
+ getThemes: () => ({ themes: [], diagnostics: [] }),
55
+ getAgentsFiles: () => ({ agentsFiles: [] }),
56
+ getSystemPrompt: () => SYSTEM_PROMPT,
57
+ getSystemPromptSource: () => undefined,
58
+ getAppendSystemPrompt: () => [],
59
+ getAppendSystemPromptSources: () => [],
60
+ extendResources: () => {},
61
+ reload: async () => {},
62
+ } as unknown as ResourceLoader;
63
+ }
64
+
65
+ function resolveModel(ref: string, ctx: RenameCtx): unknown {
66
+ const registry = ctx.modelRegistry as { find?: (p: string, id: string) => unknown } | undefined;
67
+ const slash = ref.indexOf("/");
68
+ if (ref && slash > 0 && registry?.find) {
69
+ const found = registry.find(ref.slice(0, slash), ref.slice(slash + 1));
70
+ if (found) return found;
71
+ }
72
+ return ctx.model;
73
+ }
74
+
75
+ export function renamePrompt(req: RenameRequest): string {
76
+ const clip = (r: string) => r.replace(/\s+/g, " ").slice(0, 400);
77
+ if (req.topicChanged && req.currentName) {
78
+ const latest = req.requests.at(-1) ?? "";
79
+ return [
80
+ `Current title: ${req.currentName}`,
81
+ "",
82
+ "The user has moved on to a different task, so the current title no longer fits. Name the NEW task:",
83
+ `- ${clip(latest)}`,
84
+ ...(req.reply ? ["", `What the assistant did about it:\n${clip(req.reply)}`] : []),
85
+ ].join("\n");
86
+ }
87
+ const recent = req.requests.slice(-4).map((r) => `- ${clip(r)}`).join("\n");
88
+ return [
89
+ req.currentName ? `Current title: ${req.currentName}` : "The session has no title yet.",
90
+ "",
91
+ "Recent user requests (oldest first; the last one is the current task):",
92
+ recent || "- (none)",
93
+ ...(req.reply ? ["", `The assistant's latest reply (use it when the request itself is vague):\n${clip(req.reply)}`] : []),
94
+ ].join("\n");
95
+ }
96
+
97
+ /** Run the one-tool session; resolves to the sanitized name it chose, if any.
98
+ * A configured naming model that fails (removed from the catalog, no key)
99
+ * falls back once to the session model. */
100
+ export async function runRenameSession(
101
+ ctx: RenameCtx,
102
+ req: RenameRequest,
103
+ debug: (line: string) => void = () => {},
104
+ ): Promise<string | undefined> {
105
+ const runtime = (ctx.modelRegistry as { runtime?: ModelRuntime } | undefined)?.runtime;
106
+ if (!runtime) {
107
+ debug("no model runtime available");
108
+ return undefined;
109
+ }
110
+ const configured = resolveModel(req.model, ctx);
111
+ const candidates = configured && configured !== ctx.model ? [configured, ctx.model] : [ctx.model];
112
+ for (const model of candidates) {
113
+ if (!model) continue;
114
+ const name = await attempt(model, runtime, ctx, req, debug);
115
+ if (name) return name;
116
+ }
117
+ return undefined;
118
+ }
119
+
120
+ async function attempt(
121
+ model: unknown,
122
+ runtime: ModelRuntime,
123
+ ctx: RenameCtx,
124
+ req: RenameRequest,
125
+ debug: (line: string) => void,
126
+ ): Promise<string | undefined> {
127
+ let chosen: string | undefined;
128
+ const renameTool = defineTool({
129
+ name: "rename_session",
130
+ label: "Rename session",
131
+ description: "Set the session title (2-6 words, Title Case).",
132
+ parameters: Type.Object({ name: Type.String({ description: "The new session title" }) }),
133
+ async execute(_id, params) {
134
+ const name = sanitizeName(String((params as { name?: unknown }).name ?? ""));
135
+ if (!name) return { content: [{ type: "text", text: "Error: empty title." }], details: undefined };
136
+ chosen = name;
137
+ return { content: [{ type: "text", text: `Title set: ${name}` }], details: { name } };
138
+ },
139
+ });
140
+
141
+ const { session } = await createAgentSession({
142
+ sessionManager: SessionManager.inMemory(ctx.cwd ?? process.cwd()),
143
+ model: model as never,
144
+ modelRuntime: runtime,
145
+ thinkingLevel: "off" as never,
146
+ tools: ["rename_session"],
147
+ customTools: [renameTool as never],
148
+ resourceLoader: resourceLoader(),
149
+ });
150
+ const s = session as unknown as {
151
+ prompt: (m: string, o?: unknown) => Promise<void>;
152
+ abort?: () => Promise<void> | void;
153
+ dispose?: () => void;
154
+ state?: { messages?: Array<{ role?: string; stopReason?: string; errorMessage?: string; content?: unknown }> };
155
+ };
156
+ const timer = setTimeout(() => void s.abort?.(), RENAME_TIMEOUT_MS);
157
+ timer.unref?.();
158
+ try {
159
+ await s.prompt(renamePrompt(req), { source: "extension" });
160
+ if (!chosen) {
161
+ const last = (s.state?.messages ?? []).findLast((m) => m.role === "assistant");
162
+ const id = (model as { provider?: string; id?: string });
163
+ debug(`${id.provider}/${id.id}: no rename_session call; stop=${last?.stopReason ?? "?"} ${last?.errorMessage ?? JSON.stringify(last?.content ?? "").slice(0, 200)}`);
164
+ }
165
+ } finally {
166
+ clearTimeout(timer);
167
+ s.dispose?.();
168
+ }
169
+ return chosen;
170
+ }
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @pi-unipi/utility — the approved UNI-2 "delegated work" panel: one outer
3
+ * dark-cyan fill (#12363b) with a bright cyan ▏ rail (#22d3ee) on every row
4
+ * (blank interior rows included), replacing today's nested per-card
5
+ * backgrounds. Rendered content comes from the shared style helpers
6
+ * (styledToolCallLines / styledTextLines / nativeToolComponent for regular
7
+ * raw-args); every nested background SGR is stripped atomically before the
8
+ * single outer fill is applied, so foreground colours (native card fg, syntax,
9
+ * markdown semantics) survive untouched.
10
+ *
11
+ * The returned component carries spacingGroup metadata so adjacent steps join
12
+ * into one continuous panel (spacing.ts resolves it through the host's
13
+ * CustomEntryComponent.customComponent wrapper).
14
+ */
15
+ import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
16
+ import { nativeToolComponent, renderStyle, styledTextLines, styledToolCallLines, type RenderStyle, type StyledTheme } from "./styled.js";
17
+ import { paintLine, trimEdgeBlankLines } from "./reply-bg.js";
18
+ import type { SpacingGroupPosition } from "./spacing.js";
19
+
20
+ /** Minimal single-width component; the cache key (e.g. active render style)
21
+ * invalidates without host help, `invalidate()` clears unconditionally. */
22
+ function styledComponentLike(build: (width: number) => string[], cacheKey: (width: number) => string = () => ""): Component & Record<PropertyKey, unknown> {
23
+ let cache: { width: number; key: string; lines: string[] } | undefined;
24
+ return {
25
+ invalidate(): void {
26
+ cache = undefined;
27
+ },
28
+ render(width: number): string[] {
29
+ const key = cacheKey(width);
30
+ if (cache === undefined || cache.width !== width || cache.key !== key) cache = { width, key, lines: build(width) };
31
+ return cache.lines;
32
+ },
33
+ } as Component & Record<PropertyKey, unknown>;
34
+ }
35
+
36
+ /** A SidekickStep-compatible step (structural, fusion/subagent agnostic). */
37
+ export type DelegatedStep =
38
+ | { kind: "tool"; name: string; arg: string; output: string; isError: boolean; durationMs: number; args?: Record<string, unknown> }
39
+ | { kind: "text"; text: string; thinking?: string };
40
+
41
+ export interface DelegatedIdentity {
42
+ /** spacingGroup for the panel run — unique per agent/handoff. */
43
+ group: string;
44
+ /** Panel header, drawn once on the first group position (index 0). */
45
+ label?: string;
46
+ /** Render style override; defaults to the active utility render style. */
47
+ style?: RenderStyle;
48
+ }
49
+
50
+ // ── approved palette (truecolor, 256-colour fallback) ──────────────────────
51
+ export const DELEGATED_PANEL_HEX = "#12363b";
52
+ export const DELEGATED_RAIL_HEX = "#22d3ee";
53
+ const RAIL = "▏ ";
54
+
55
+ function hexToRgb(hex: string): { r: number; g: number; b: number } {
56
+ const c = hex.replace("#", "");
57
+ return { r: parseInt(c.slice(0, 2), 16), g: parseInt(c.slice(2, 4), 16), b: parseInt(c.slice(4, 6), 16) };
58
+ }
59
+
60
+ function hexTo256(hex: string): number {
61
+ const { r, g, b } = hexToRgb(hex);
62
+ // Weighted RGB distance (perceptual weights), mirroring pi's own converter:
63
+ // nearest 6×6×6-cube cell vs nearest grayscale ramp entry, closer wins.
64
+ const dist = (r2: number, g2: number, b2: number) => 0.299 * (r - r2) ** 2 + 0.587 * (g - g2) ** 2 + 0.114 * (b - b2) ** 2;
65
+ const cube = [0, 95, 135, 175, 215, 255];
66
+ const nearest = (v: number) => cube.reduce((best, x) => (Math.abs(v - x) < Math.abs(v - best) ? x : best), cube[0]!);
67
+ const ri = cube.indexOf(nearest(r)), gi = cube.indexOf(nearest(g)), bi = cube.indexOf(nearest(b));
68
+ const cubeIndex = 16 + 36 * ri + 6 * gi + bi;
69
+ const cubeDist = dist(cube[ri]!, cube[gi]!, cube[bi]!);
70
+ const gray = Array.from({ length: 24 }, (_, i) => 8 + i * 10);
71
+ let grayBest = 0;
72
+ for (let i = 1; i < gray.length; i++) if (dist(gray[i]!, gray[i]!, gray[i]!) < dist(gray[grayBest]!, gray[grayBest]!, gray[grayBest]!)) grayBest = i;
73
+ const grayIndex = 232 + grayBest;
74
+ const grayDist = dist(gray[grayBest]!, gray[grayBest]!, gray[grayBest]!);
75
+ return cubeDist <= grayDist ? cubeIndex : grayIndex;
76
+ }
77
+
78
+ type ColorMode = "truecolor" | "256color";
79
+ function modeOf(theme: { getColorMode?: () => ColorMode } | undefined): ColorMode {
80
+ try {
81
+ return theme?.getColorMode?.() ?? "truecolor";
82
+ } catch {
83
+ return "truecolor";
84
+ }
85
+ }
86
+
87
+ function fgAnsi(hex: string, mode: ColorMode): string {
88
+ if (mode === "256color") return `\x1b[38;5;${String(hexTo256(hex))}m`;
89
+ const { r, g, b } = hexToRgb(hex);
90
+ return `\x1b[38;2;${String(r)};${String(g)};${String(b)}m`;
91
+ }
92
+ function bgAnsi(hex: string, mode: ColorMode): string {
93
+ if (mode === "256color") return `\x1b[48;5;${String(hexTo256(hex))}m`;
94
+ const { r, g, b } = hexToRgb(hex);
95
+ return `\x1b[48;2;${String(r)};${String(g)};${String(b)}m`;
96
+ }
97
+
98
+ /** The panel fill (dark cyan) in the theme's colour mode. */
99
+ export function delegatedPanelBg(theme?: { getColorMode?: () => ColorMode }): string {
100
+ return bgAnsi(DELEGATED_PANEL_HEX, modeOf(theme));
101
+ }
102
+ /** The rail colour (bright cyan) in the theme's colour mode. */
103
+ export function delegatedRailFg(theme?: { getColorMode?: () => ColorMode }): string {
104
+ return fgAnsi(DELEGATED_RAIL_HEX, modeOf(theme));
105
+ }
106
+
107
+ /**
108
+ * Strip every nested background SGR from a rendered line — truecolor, 256 and
109
+ * 8-colour backgrounds plus bg resets — while preserving foreground. Extended
110
+ * sequences (38;2;r;g;b / 38;5;n) are consumed atomically so their colour
111
+ * components can't be mistaken for background params.
112
+ */
113
+ export function stripNestedBackgrounds(line: string): string {
114
+ return line.replace(/\x1b\[([0-9;]*)m/g, (seq, params: string) => {
115
+ if (params === "") return "\x1b[0m";
116
+ const parts = params.split(";");
117
+ const keep: string[] = [];
118
+ for (let i = 0; i < parts.length; i++) {
119
+ const n = Number(parts[i] || "0");
120
+ if (n === 38 || n === 48) {
121
+ const sub = Number(parts[i + 1]);
122
+ const span = sub === 2 ? 5 : sub === 5 ? 3 : 1;
123
+ const atom = parts.slice(i, i + span).join(";");
124
+ i += span - 1;
125
+ if (n === 38) keep.push(atom);
126
+ continue;
127
+ }
128
+ if (n === 49 || (n >= 40 && n <= 47) || (n >= 100 && n <= 107)) continue;
129
+ keep.push(parts[i]!);
130
+ }
131
+ return keep.length > 0 ? `\x1b[${keep.join(";")}m` : "";
132
+ });
133
+ }
134
+
135
+ /**
136
+ * One rail-painted panel row: cyan ▏ + content on the single outer fill,
137
+ * padded to `width` — blank interior rows get the same treatment, so a panel
138
+ * is continuous with no unpainted gaps. Content backgrounds are stripped.
139
+ */
140
+ export function paintDelegatedLine(line: string, width: number, theme?: { getColorMode?: () => ColorMode }): string {
141
+ const w = Math.max(1, Math.trunc(width) || 1);
142
+ const painted = paintLine(
143
+ `${delegatedRailFg(theme)}${RAIL}\x1b[39m${stripNestedBackgrounds(line)}`,
144
+ w,
145
+ delegatedPanelBg(theme),
146
+ );
147
+ return truncateToWidth(painted, w);
148
+ }
149
+
150
+ /**
151
+ * Semantic fg overrides for delegated panels: cyan accents/titles/results,
152
+ * neutral light output/text, fixed muted metadata and red errors — the user's
153
+ * global theme is never modified. Markdown semantic colours are untouched
154
+ * (they come from the global markdown theme); only these keys are remapped.
155
+ */
156
+ export function delegatedTheme(theme: StyledTheme, mode: ColorMode = "truecolor"): StyledTheme {
157
+ const overrides = new Map<string, string>([
158
+ ["accent", fgAnsi(DELEGATED_RAIL_HEX, mode)],
159
+ ["success", fgAnsi(DELEGATED_RAIL_HEX, mode)],
160
+ ["toolTitle", fgAnsi(DELEGATED_RAIL_HEX, mode)],
161
+ ["toolOutput", fgAnsi("#e8f4f4", mode)],
162
+ ["text", fgAnsi("#e8f4f4", mode)],
163
+ ["error", fgAnsi("#ff6b6b", mode)],
164
+ ["muted", fgAnsi("#86b0b5", mode)],
165
+ ["dim", fgAnsi("#5f8a8f", mode)],
166
+ ["borderMuted", fgAnsi("#5f8a8f", mode)],
167
+ ]);
168
+ return {
169
+ fg(color: string, text: string): string {
170
+ const ansi = overrides.get(color);
171
+ return ansi !== undefined ? `${ansi}${text}\x1b[39m` : theme.fg(color as never, text);
172
+ },
173
+ bold: (t: string) => theme.bold(t),
174
+ } as StyledTheme;
175
+ }
176
+
177
+ // ── the step renderer ──────────────────────────────────────────────────────
178
+
179
+ /** Test seam: how many times the native component factory actually ran. */
180
+ export const nativeFactoryStats = { calls: 0 };
181
+
182
+ /** Raw native rows for a regular-style step with raw args. The component is
183
+ * cached per (width, expanded, output, error) — the host's rebuild supplies a
184
+ * fresh inner component (and theme) anyway, so within one inner lifetime a
185
+ * repeated render at the same key reuses the native instance instead of
186
+ * re-running the factory. */
187
+ function makeNativeCache() {
188
+ let comp: Component | undefined;
189
+ let key = "";
190
+ return (step: DelegatedStep & { kind: "tool" }, expanded: boolean, inner: number): string[] | undefined => {
191
+ if (step.args === undefined) return undefined;
192
+ const k = `${String(inner)}|${String(expanded)}|${String(step.isError)}|${String(step.output.length)}`;
193
+ if (comp === undefined || key !== k) {
194
+ key = k;
195
+ try {
196
+ nativeFactoryStats.calls++;
197
+ comp = nativeToolComponent({
198
+ name: step.name,
199
+ args: step.args,
200
+ output: step.output,
201
+ isError: step.isError,
202
+ expanded,
203
+ }) ?? undefined;
204
+ } catch {
205
+ comp = undefined;
206
+ }
207
+ }
208
+ if (comp === undefined) return undefined;
209
+ try {
210
+ return trimEdgeBlankLines(comp.render(inner));
211
+ } catch {
212
+ return undefined;
213
+ }
214
+ };
215
+ }
216
+
217
+ /**
218
+ * One delegated step as a transcript component in the approved panel design.
219
+ * The component carries `spacingGroup`/`spacingKind`/`setGroupPosition` so
220
+ * spacing.ts can join adjacent members of the same group into one continuous
221
+ * panel: connectors `├` while another tool step follows in the run, `└` on the
222
+ * last; the `label` (when given) is drawn only on group position 0. A fresh
223
+ * native component is cached per inner component instance (keyed by width,
224
+ * expansion, error and output length), so repeated renders reuse it while a
225
+ * theme/expansion rebuild — which creates a fresh inner component — never sees
226
+ * stale state.
227
+ */
228
+ /** Theme colour mode, read from the passed theme when it exposes one. */
229
+ type ModeTheme = StyledTheme & { getColorMode?: () => "truecolor" | "256color" };
230
+
231
+ export function renderDelegatedStep(step: DelegatedStep, expanded: boolean, theme: StyledTheme, options: DelegatedIdentity): Component & {
232
+ spacingGroup: string;
233
+ spacingKind: string;
234
+ setGroupPosition: (pos: SpacingGroupPosition) => void;
235
+ } {
236
+ const modeTheme = theme as ModeTheme;
237
+ const mode = modeOf(modeTheme);
238
+ const dTheme = delegatedTheme(modeTheme, mode);
239
+ let pos: SpacingGroupPosition | undefined;
240
+ const nativeFor = makeNativeCache();
241
+ const comp = styledComponentLike((width: number) => {
242
+ const inner = Math.max(3, width - visibleWidth(RAIL));
243
+ const style = options.style ?? renderStyle();
244
+ const lines: string[] = [];
245
+ const first = pos === undefined || pos.index === 0;
246
+ if (first && options.label !== undefined) {
247
+ lines.push(`${dTheme.fg("accent", "◆")} ${dTheme.fg("accent", dTheme.bold(options.label))}`);
248
+ }
249
+ if (step.kind === "text") {
250
+ lines.push(...styledTextLines(style, step.text, { thinking: expanded ? step.thinking : undefined }, dTheme, inner));
251
+ } else {
252
+ const connector: "├" | "└" = pos?.nextKind === "tool" ? "├" : "└";
253
+ const native = style === "regular" ? nativeFor(step, expanded, inner) : undefined;
254
+ if (native !== undefined) {
255
+ lines.push(...native);
256
+ } else {
257
+ lines.push(...styledToolCallLines(style, {
258
+ name: step.name,
259
+ arg: step.arg,
260
+ output: step.output,
261
+ isError: step.isError,
262
+ expanded,
263
+ durationMs: step.durationMs,
264
+ connector: style === "simple" ? connector : undefined,
265
+ }, dTheme, inner));
266
+ }
267
+ }
268
+ return lines.map((l) => truncateToWidth(l, inner)).map((l) => paintDelegatedLine(l, width, modeTheme));
269
+ }, () => options.style ?? renderStyle());
270
+ comp.spacingGroup = options.group;
271
+ comp.spacingKind = step.kind === "tool" ? "tool" : "text";
272
+ comp.setGroupPosition = (p: SpacingGroupPosition) => {
273
+ // Only a real position change invalidates — the host re-renders twice per
274
+ // pass (initial + positioned), and identical positions must not churn the
275
+ // native/paint caches (and must never invalidate from inside render).
276
+ const same = pos !== undefined && pos.index === p.index && pos.count === p.count && pos.prevKind === p.prevKind && pos.nextKind === p.nextKind;
277
+ pos = p;
278
+ if (!same) comp.invalidate();
279
+ };
280
+ return comp as unknown as Component & {
281
+ spacingGroup: string;
282
+ spacingKind: string;
283
+ setGroupPosition: (pos: SpacingGroupPosition) => void;
284
+ };
285
+ }