@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
@@ -1,8 +1,10 @@
1
1
  import { Text, type Component } from "@earendil-works/pi-tui";
2
2
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
+ import { isChildProcess, registerWaitSource } from "@pi-unipi/core";
4
5
  import type { SidekickRuntime, HandoffProgress, HandoffReport } from "./sidekick-runtime.js";
5
- import { duration, frameSidekick, markdownText, renderSidekickTranscript, sidekickWorkingHeader } from "./transcript.js";
6
+ import { createCompletionDelivery, registerSubagentReader, type SubagentReader } from "@pi-unipi/core/child-agent.js";
7
+ import { duration } from "./transcript.js";
6
8
 
7
9
  const SidekickParams = Type.Object({
8
10
  message: Type.String({ description: "A concrete implementation or verification brief for the sidekick" }),
@@ -22,46 +24,6 @@ export interface FusionToolDeps {
22
24
  onDetach?: (ctx: ExtensionContext) => void;
23
25
  }
24
26
 
25
- /**
26
- * Exactly-once completion delivery for handoffs nobody is waiting on.
27
- *
28
- * `attach`/`detach` bracket every waiting period. The completion is sent only
29
- * if the report lands (or has already landed) while no waiter is attached, and
30
- * only once per handoff id.
31
- */
32
- export function createCompletionDelivery(send: (report: HandoffReport) => void): {
33
- attach(id: string): void;
34
- detach(id: string, done: Promise<HandoffReport>): void;
35
- consume(id: string): void;
36
- } {
37
- const waiting = new Set<string>();
38
- const armed = new Set<string>();
39
- const delivered = new Set<string>();
40
-
41
- return {
42
- attach(id) {
43
- waiting.add(id);
44
- },
45
- detach(id, done) {
46
- waiting.delete(id);
47
- // One continuation per handoff, however many times a waiter gives up.
48
- if (armed.has(id)) return;
49
- armed.add(id);
50
- void done
51
- .then((report) => {
52
- if (waiting.has(id) || delivered.has(id)) return;
53
- delivered.add(id);
54
- send(report);
55
- })
56
- .catch(() => undefined);
57
- },
58
- consume(id) {
59
- waiting.delete(id);
60
- delivered.add(id);
61
- },
62
- };
63
- }
64
-
65
27
  function firstLine(value: string): string {
66
28
  return value.split("\n", 1)[0] ?? "";
67
29
  }
@@ -141,95 +103,86 @@ type ThemeLike = {
141
103
  bold: (text: string) => string;
142
104
  };
143
105
 
144
- type FramedTheme = ThemeLike & { bg: (color: string, text: string) => string };
145
-
146
- function transcriptStatus(partial: boolean, report: HandoffReport | undefined): "working" | "completed" | "error" {
147
- if (partial) return "working";
148
- return report?.status === "completed" ? "completed" : "error";
149
- }
150
-
151
106
  /**
152
- * Sidekick output on the main surface: the same markdown/tool format the lead
153
- * uses, marked as sidekick-origin by the existing `▍` rail.
107
+ * In-flight NON-blocking handoffs, for the turn arbiter's wait source: a
108
+ * block:false sidekick is still working while the lead settles, so the
109
+ * arbiter must defer (its completion lands as an event turn). Blocking
110
+ * handoffs never reach settle (the lead waits inside the tool call), and
111
+ * interrupted waits detach to background the same way.
154
112
  */
155
- function frameTranscript(theme: ThemeLike, status: "working" | "completed" | "error", content: Component): Component {
156
- return frameSidekick(theme as FramedTheme, status, content);
157
- }
113
+ export class BackgroundHandoffTracker {
114
+ private readonly handoffs = new Map<string, Promise<HandoffReport>>();
158
115
 
159
- // Model-facing result text carries handoff ids and protocol instructions the
160
- // lead needs; none of it may reach the terminal. Anything rendered as plain
161
- // content goes through this first.
162
- const HANDOFF_ID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
163
- function displayText(text: string): string {
164
- return text
165
- .replace(/<\/?subagent_completion_notification[^>]*>/g, "")
166
- .replace(/use `?read_subagent`?[^.\n]*\.?/gi, "")
167
- .replace(HANDOFF_ID, "")
168
- .replace(/agent_id[ =]?"?"?/g, "")
169
- .replace(/read_subagent\([^)]*\)/g, "read_subagent")
170
- .replace(/\(\s*\)/g, "")
171
- .replace(/[ \t]{2,}/g, " ")
172
- .replace(/\bHandoff\s*(?=(is|was|failed|aborted|started)\b)/g, "The handoff ")
173
- .replace(/ for\s*\./g, ".")
174
- .replace(/ +$/gm, "")
175
- .trim();
176
- }
116
+ /** Track a detached handoff. Cleanup queues behind the completion delivery: every caller detaches the delivery BEFORE tracking, so the wait source only clears after the completion followUp is queued. */
117
+ track(id: string, done: Promise<HandoffReport>): void {
118
+ void done.catch(() => undefined).then(() => this.handoffs.delete(id));
119
+ this.handoffs.set(id, done);
120
+ }
177
121
 
178
- function contentText(content: unknown): string {
179
- if (!Array.isArray(content)) return String(content ?? "");
180
- return content.map((part) => typeof part === "object" && part !== null && typeof (part as { text?: unknown }).text === "string" ? (part as { text: string }).text : "").filter(Boolean).join("\n");
181
- }
122
+ get size(): number {
123
+ return this.handoffs.size;
124
+ }
182
125
 
183
- function backgroundComponent(theme: ThemeLike): Component {
184
- return new Text(`${theme.fg("accent", theme.bold("◆ sidekick"))} ${theme.fg("dim", "· continuing in background")}`, 0, 0);
126
+ /** Arbiter wait-source reason, or null when nothing is in flight. */
127
+ reason(): string | null {
128
+ return this.handoffs.size > 0 ? "sidekick working" : null;
129
+ }
185
130
  }
186
131
 
187
- type ToolDetails = Partial<HandoffReport> & { progress?: HandoffProgress; background?: boolean; id?: string };
188
132
 
189
- function reportHeader(theme: ThemeLike, label: string, report: HandoffReport | undefined): string {
190
- const status = report?.status ?? "done";
191
- return `${theme.fg(status === "completed" ? "success" : "error", "◆")} ${theme.fg("accent", theme.bold(`${label} ${status}`))} ${theme.fg("dim", report ? `· ${String(report.toolCalls)} tool calls · ${duration(report.durationMs)} · in ${String(report.usage.input)} / out ${String(report.usage.output)} tokens` : "")}`;
192
- }
133
+ type ToolDetails = Partial<HandoffReport> & { progress?: HandoffProgress; background?: boolean; id?: string };
193
134
 
194
- function renderToolTranscript(result: { content?: unknown; details?: unknown; isError?: boolean }, options: { expanded?: boolean }, theme: ThemeLike, label: "sidekick" | "read_subagent"): Component {
135
+ /**
136
+ * Compact one-line tool status — the steps themselves stream as
137
+ * `sidekick-step` entries, so the tool card is just the status line:
138
+ * `◆ sidekick working · N steps · 12s` → `◆ sidekick done · N steps · 34s ·
139
+ * in X / out Y tokens`.
140
+ */
141
+ function renderToolStatus(result: { content?: unknown; details?: unknown; isError?: boolean }, theme: ThemeLike): Component {
195
142
  const details = result.details as ToolDetails | undefined;
196
- if (details?.background === true) return backgroundComponent(theme);
197
- const hasProgress = details !== undefined && "progress" in details;
143
+ if (details?.background === true) {
144
+ return new Text(`${theme.fg("accent", "◆")} ${theme.fg("toolTitle", theme.bold("sidekick"))} ${theme.fg("dim", "· continuing in background")}`, 0, 0);
145
+ }
198
146
  const progress = details?.progress;
199
- if (hasProgress && progress === undefined) return backgroundComponent(theme);
200
- const report = progress ? undefined : details as HandoffReport | undefined;
201
- const events = progress?.events ?? report?.events;
202
- const partial = progress !== undefined;
203
- if (!events) return new Text(displayText(contentText(result.content)), 0, 0);
204
- const header = partial ? sidekickWorkingHeader(theme, progress, label) : reportHeader(theme, label, report);
205
- return frameTranscript(theme, transcriptStatus(partial, report), renderSidekickTranscript(theme, {
206
- events,
207
- droppedEvents: progress?.droppedEvents,
208
- header,
209
- expanded: options.expanded === true,
210
- isPartial: partial,
211
- report,
212
- renderText: markdownText,
213
- }));
147
+ if (progress !== undefined) {
148
+ const steps = progress.toolCalls;
149
+ const elapsed = duration(Date.now() - progress.startedAt);
150
+ return new Text(`${theme.fg("accent", "◆")} ${theme.fg("toolTitle", theme.bold("sidekick"))} ${theme.fg("dim", `working · ${String(steps)} steps · ${elapsed}`)}`, 0, 0);
151
+ }
152
+ const report = details as HandoffReport | undefined;
153
+ const failed = result.isError === true || (report !== undefined && report.status !== "completed" && report.status !== undefined);
154
+ const glyph = failed ? theme.fg("error", "✗") : theme.fg("accent", "◆");
155
+ const label = failed ? "sidekick failed" : "sidekick done";
156
+ const meta = report?.durationMs !== undefined
157
+ ? ` · ${String(report.toolCalls)} steps · ${duration(report.durationMs)} · in ${String(report.usage?.input ?? 0)} / out ${String(report.usage?.output ?? 0)} tokens`
158
+ : "";
159
+ return new Text(`${glyph} ${theme.fg("toolTitle", theme.bold(label))}${theme.fg("dim", meta)}`, 0, 0);
214
160
  }
215
161
 
216
- function renderCompletionCard(theme: ThemeLike, report: HandoffReport | undefined): Component {
217
- if (!report) return new Text(`${theme.fg("accent", "◆")} ${theme.fg("accent", theme.bold("sidekick done"))}`, 0, 0);
218
- return frameTranscript(theme, transcriptStatus(false, report), renderSidekickTranscript(theme, {
219
- events: report.events ?? [],
220
- header: reportHeader(theme, "sidekick", report),
221
- expanded: false,
222
- isPartial: false,
223
- report,
224
- renderText: markdownText,
225
- }));
162
+ function renderCompletionLine(theme: ThemeLike, report: HandoffReport | undefined): Component {
163
+ const failed = report !== undefined && report.status !== "completed";
164
+ const glyph = failed ? theme.fg("error", "✗") : theme.fg("accent", "◆");
165
+ const label = failed ? "sidekick failed" : "sidekick finished";
166
+ const meta = report?.durationMs !== undefined ? ` · ${String(report.toolCalls)} steps · ${duration(report.durationMs)}` : "";
167
+ return new Text(`${glyph} ${theme.fg("dim", `${label}${meta}`)}`, 0, 0);
226
168
  }
227
169
 
228
170
  export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): void {
229
- pi.registerMessageRenderer("sidekick-completion", (message: { details?: HandoffReport }, _options, theme) => renderCompletionCard(theme as unknown as ThemeLike, message.details));
171
+ // Turn-arbiter wait source (lead only).
172
+ const backgroundHandoffs = new BackgroundHandoffTracker();
173
+ const trackBackgroundHandoff = (id: string, done: Promise<HandoffReport>): void => backgroundHandoffs.track(id, done);
174
+ if (!isChildProcess()) {
175
+ try {
176
+ registerWaitSource("fusion", () => backgroundHandoffs.reason());
177
+ } catch {
178
+ // Registration must never block tool registration.
179
+ }
180
+ }
181
+
182
+ pi.registerMessageRenderer("sidekick-completion", (message: { details?: HandoffReport }, _options, theme) => renderCompletionLine(theme as unknown as ThemeLike, message.details));
230
183
 
231
184
  // One delivery mechanism for every handoff nobody is waiting on.
232
- const completion = createCompletionDelivery((report) => {
185
+ const completion = createCompletionDelivery<HandoffReport>((report) => {
233
186
  pi.sendMessage(completionMessage(report) as never, { deliverAs: "followUp", triggerTurn: true } as never);
234
187
  });
235
188
 
@@ -239,7 +192,7 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
239
192
  description: "Hand off work to your persistent sidekick subagent (one per session; context and shells persist across handoffs; runs on the same machine). block:true (default) waits and returns the report. block:false returns immediately and the report arrives later as a <subagent_completion_notification>. Calling again while a handoff is running injects the message as an interrupt rather than starting a second sidekick.",
240
193
  parameters: SidekickParams,
241
194
  renderCall: (args, theme) => new Text(`${theme.fg("toolTitle", theme.bold("◆ sidekick"))} ${theme.fg("dim", firstLine(String(args.message)).slice(0, 100))}`, 0, 0),
242
- renderResult: (result, options, theme) => renderToolTranscript(result, options, theme as unknown as ThemeLike, "sidekick"),
195
+ renderResult: (result, _options, theme) => renderToolStatus(result, theme as unknown as ThemeLike),
243
196
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
244
197
  const runtime = deps.getRuntime(ctx);
245
198
  if (!runtime) return result("Fusion is not active — pick a Fusion pair with /unipi:model.", undefined, true);
@@ -249,18 +202,22 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
249
202
  if (params.block === false) {
250
203
  void handoff.done.then((report) => deps.onReport?.(ctx, report)).catch(() => undefined);
251
204
  completion.detach(handoff.id, handoff.done);
205
+ trackBackgroundHandoff(handoff.id, handoff.done);
252
206
  deps.onDetach?.(ctx);
253
207
  return result(`Handoff ${handoff.id} started in the background. You will receive a <subagent_completion_notification agent_id="${handoff.id}"> when it finishes; use read_subagent to wait.`, { background: true, id: handoff.id });
254
208
  }
255
209
  deps.onAttach?.(ctx);
210
+ runtime.attachUi?.(ctx.ui as never);
256
211
  completion.attach(handoff.id);
257
212
  const waited = await waitForReport(runtime, handoff.id, handoff.done, signal, ctx, onUpdate ? (update) => onUpdate(update as never) : undefined);
213
+ runtime.detachUi?.();
258
214
  if (waited.report) {
259
215
  completion.consume(handoff.id);
260
216
  deps.onReport?.(ctx, waited.report);
261
217
  return result(reportText(waited.report), waited.report, waited.report.status !== "completed");
262
218
  }
263
219
  completion.detach(handoff.id, handoff.done);
220
+ trackBackgroundHandoff(handoff.id, handoff.done);
264
221
  deps.onDetach?.(ctx);
265
222
  if (waited.error) return result(`Handoff ${handoff.id} failed: ${waited.error}`, undefined, true);
266
223
  if (waited.aborted) return result(`${progressText(runtime, handoff.id)}\nHandoff ${handoff.id} aborted.`, undefined, true);
@@ -269,14 +226,19 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
269
226
  },
270
227
  });
271
228
 
272
- pi.registerTool({
273
- name: "read_subagent",
274
- label: "Read Sidekick",
275
- description: "Read a sidekick handoff report by agent_id (omit for the latest). block:true waits for completion (default timeout 2700s when omitted); block:false returns the current progress snapshot immediately.",
276
- parameters: ReadSubagentParams,
277
- renderCall: (args, theme) => new Text(`${theme.fg("toolTitle", theme.bold("◆ read_subagent"))} ${theme.fg("dim", args.block === false ? "· snapshot" : "· waiting")}`, 0, 0),
278
- renderResult: (result, options, theme) => renderToolTranscript(result, options, theme as unknown as ThemeLike, "read_subagent"),
279
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
229
+ // Fusion registers a READER for the shared `read_subagent` tool (core).
230
+ // Same semantics as before: consume completions, block:true waits with the
231
+ // shared timeout cap, interrupted waits detach to background.
232
+ const reader: SubagentReader = {
233
+ owns: (id, ctx) => runtime_reports_owns(deps, id, ctx ?? lastCtx),
234
+ latest: (ctx) => {
235
+ const ctx0 = ctx ?? lastCtx;
236
+ const rt = ctx0 === undefined ? undefined : deps.getRuntime(ctx0);
237
+ const latest = rt?.latest();
238
+ if (!latest || !rt) return undefined;
239
+ return { id: latest.id, startedAt: rt.progress(latest.id)?.startedAt ?? 0 };
240
+ },
241
+ read: async (params, signal, onUpdate, ctx) => {
280
242
  const runtime = deps.getRuntime(ctx);
281
243
  if (!runtime) return result("Fusion is not active — pick a Fusion pair with /unipi:model.", undefined, true);
282
244
  const latest = runtime.latest();
@@ -285,28 +247,45 @@ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): voi
285
247
  const selected = runtime.reports.get(id);
286
248
  if (selected) {
287
249
  completion.consume(id);
288
- return result(reportText(selected), selected, selected.status !== "completed");
250
+ return result(reportText(selected), { ...selected, owner: "fusion" }, selected.status !== "completed");
289
251
  }
290
252
  if (id !== latest.id) return result(`No sidekick handoff found for ${id}.`, undefined, true);
291
- if (params.block !== true) return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`, { progress: runtime.progress(id), id });
253
+ if (params.block !== true) return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`, { progress: runtime.progress(id), id, owner: "fusion" });
292
254
  deps.onAttach?.(ctx);
255
+ runtime.attachUi?.(ctx.ui as never);
293
256
  completion.attach(id);
294
- const timeoutMs = (params.timeout ?? 2700) * 1000;
295
- const waited = await waitForReport(runtime, id, latest.done, signal, ctx, onUpdate ? (update) => onUpdate(update as never) : undefined, timeoutMs);
257
+ const timeoutMs = Math.min(600, params.timeout ?? 30) * 1000;
258
+ const waited = await waitForReport(runtime, id, latest.done, signal, ctx, onUpdate, timeoutMs);
259
+ runtime.detachUi?.();
296
260
  if (waited.report) {
297
261
  completion.consume(id);
298
262
  deps.onReport?.(ctx, waited.report);
299
- return result(reportText(waited.report), waited.report, waited.report.status !== "completed");
263
+ return result(reportText(waited.report), { ...waited.report, owner: "fusion" }, waited.report.status !== "completed");
300
264
  }
301
265
  completion.detach(id, latest.done);
266
+ trackBackgroundHandoff(id, latest.done);
302
267
  deps.onDetach?.(ctx);
303
268
  if (waited.error) return result(`Handoff ${id} failed: ${waited.error}`, undefined, true);
304
269
  if (waited.aborted) return result(`Handoff ${id} aborted.`, undefined, true);
305
- if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${id}) was working.\n${waited.interrupted}`, { progress: runtime.progress(id), id });
306
- return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`, { progress: runtime.progress(id), id });
270
+ if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${id}) was working.\n${waited.interrupted}`, { progress: runtime.progress(id), id, owner: "fusion" });
271
+ return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`, { progress: runtime.progress(id), id, owner: "fusion" });
272
+ },
273
+ };
274
+ let lastCtx: ExtensionContext | undefined;
275
+ registerSubagentReader("fusion", {
276
+ owns: (id, ctx) => reader.owns(id, ctx),
277
+ latest: (ctx) => reader.latest(ctx),
278
+ read: (params, signal, onUpdate, ctx) => {
279
+ lastCtx = ctx;
280
+ return reader.read(params, signal, onUpdate, ctx);
307
281
  },
308
282
  });
309
283
 
284
+ function runtime_reports_owns(d: FusionToolDeps, id: string, ctx: ExtensionContext | undefined): boolean {
285
+ const rt = ctx === undefined ? undefined : d.getRuntime(ctx);
286
+ return rt !== undefined && (rt.reports.has(id) || rt.latest()?.id === id);
287
+ }
288
+
310
289
  }
311
290
 
312
291
  export { reportText, progressText };
@@ -1,10 +1,15 @@
1
- import { Box, Container, Markdown, Text, type Component } from "@earendil-works/pi-tui";
1
+ import { Markdown, type Component } from "@earendil-works/pi-tui";
2
2
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
3
- import type { HandoffProgress, SidekickEvent } from "./sidekick-runtime.js";
3
+ import { renderDelegatedStep, type DelegatedStep } from "@pi-unipi/utility/src/render/delegated.js";
4
+ import type { SidekickStep } from "./sidekick-runtime.js";
4
5
 
5
6
  export interface ThemeLike {
6
7
  fg: (color: string, text: string) => string;
7
8
  bold: (text: string) => string;
9
+ getBgAnsi?: (key: string) => string;
10
+ /** Colour mode when the caller knows it — the delegated panel uses it for
11
+ * its truecolor/256 rail+fill+fg fallbacks. */
12
+ getColorMode?: () => "truecolor" | "256color";
8
13
  }
9
14
 
10
15
  export function duration(ms: number): string {
@@ -15,51 +20,6 @@ export function markdownText(markdown: string): Component {
15
20
  return new Markdown(markdown, 0, 0, getMarkdownTheme());
16
21
  }
17
22
 
18
- export function sidekickWorkingHeader(theme: ThemeLike, progress: Pick<HandoffProgress, "toolCalls" | "startedAt">, label = "sidekick"): string {
19
- return `${theme.fg("accent", theme.bold(`◆ ${label} working`))} ${theme.fg("dim", `· ${String(progress.toolCalls)} tool calls · ${duration(Date.now() - progress.startedAt)}`)}`;
20
- }
21
-
22
- export class RailComponent implements Component {
23
- constructor(private readonly inner: Component, private readonly rail: string) {}
24
-
25
- render(width: number): string[] {
26
- return this.inner.render(Math.max(1, width - 2)).map((line) => `${this.rail} ${line}`);
27
- }
28
-
29
- invalidate(): void {
30
- this.inner.invalidate?.();
31
- }
32
-
33
- handleInput(data: string): void {
34
- this.inner.handleInput?.(data);
35
- }
36
- }
37
-
38
- export function frameSidekick(theme: ThemeLike & { bg: (color: string, text: string) => string }, status: "working" | "completed" | "error", content: Component): Component {
39
- const railColor = status === "working" ? "accent" : status === "completed" ? "success" : "error";
40
- const rail = theme.fg(railColor, "▍");
41
- const boxed = new Box(1, 0, (text) => theme.bg("customMessageBg", text));
42
- boxed.addChild(new RailComponent(content, rail));
43
- return boxed;
44
- }
45
-
46
- export interface TranscriptOptions {
47
- events: readonly SidekickEvent[];
48
- droppedEvents?: number;
49
- header: string;
50
- expanded: boolean;
51
- isPartial: boolean;
52
- report?: { text: string };
53
- renderText: (markdown: string) => Component;
54
- }
55
-
56
- /**
57
- * Non-expanded transcripts show this many most recent events. Windowing is the
58
- * only cap: a completed handoff renders its events, it is never collapsed into
59
- * a "N steps" stub.
60
- */
61
- export const TRANSCRIPT_WINDOW = 8;
62
-
63
23
  function firstLine(value: string): string {
64
24
  return value.split("\n", 1)[0] ?? "";
65
25
  }
@@ -80,44 +40,26 @@ export function primaryArg(name: string, args: Record<string, unknown> | undefin
80
40
  return typeof value === "string" ? truncate(firstLine(value), 100) : "";
81
41
  }
82
42
 
83
- function toolComponent(theme: ThemeLike, event: Extract<SidekickEvent, { kind: "tool" }>, expanded: boolean): Text {
84
- const title = `${event.isError ? theme.fg("error", "✗ ") : ""}${theme.fg("toolTitle", theme.bold(event.name))}`;
85
- const argument = primaryArg(event.name, event.args);
86
- const lines = [`${title}${argument.length > 0 ? ` ${theme.fg("accent", argument)}` : ""}`];
87
- if (!event.done) {
88
- lines[0] += theme.fg("warning", " ⋯ running");
89
- } else if (event.output.length > 0) {
90
- const output = event.output.split("\n");
91
- const visible = expanded ? output.slice(-40) : output.slice(-3);
92
- lines.push(...visible.map((line) => theme.fg("toolOutput", truncate(line, 160))));
93
- }
94
- return new Text(lines.join("\n"), 0, 0);
95
- }
96
-
97
- export function renderSidekickTranscript(theme: ThemeLike, opts: TranscriptOptions): Component {
98
- const box = new Container();
99
- box.addChild(new Text(opts.header, 0, 0));
100
-
101
- const dropped = opts.droppedEvents ?? 0;
102
- const start = opts.expanded ? 0 : Math.max(0, opts.events.length - TRANSCRIPT_WINDOW);
103
- if (opts.expanded && dropped > 0) {
104
- box.addChild(new Text(theme.fg("dim", `… ${String(dropped)} earliest steps dropped`), 0, 0));
105
- } else if (start > 0) {
106
- box.addChild(new Text(theme.fg("dim", `… ${String(start + dropped)} earlier steps`), 0, 0));
107
- }
43
+ // ─── sidekick-step entry renderer (Devin-style merged steps) ────────────────
108
44
 
109
- for (const event of opts.events.slice(start)) {
110
- if (event.kind === "tool") box.addChild(toolComponent(theme, event, opts.expanded));
111
- else if (event.text.trim()) box.addChild(opts.renderText(event.text.trim()));
112
- }
45
+ export interface StepIdentity {
46
+ /** spacingGroup run — unique per handoff so resumed handoffs don't merge. */
47
+ group?: string;
48
+ /** Panel header drawn once on the group's first position (e.g. "Sidekick"). */
49
+ label?: string;
50
+ }
113
51
 
114
- if (!opts.isPartial && opts.report?.text) {
115
- // The final assistant message usually IS the report; don't print it twice.
116
- const last = opts.events.at(-1);
117
- if (!(last?.kind === "text" && last.text.trim() === opts.report.text.trim())) {
118
- box.addChild(new Text(theme.fg("dim", "── report ──"), 0, 0));
119
- box.addChild(opts.renderText(opts.report.text));
120
- }
121
- }
122
- return box;
52
+ /**
53
+ * One sidekick step in the lead chat, drawn by the shared delegated-panel
54
+ * helper: the active render style behind the approved cyan `▏` rail on a
55
+ * single dark-cyan fill. Steps carry `spacingGroup` so adjacent steps of the
56
+ * same handoff join into one continuous panel (`├…└` tree, no blank
57
+ * separators); the optional identity comes from the persisted entry (older
58
+ * entries without one fall back to the shared "sidekick" group).
59
+ */
60
+ export function renderSidekickStep(step: SidekickStep, expanded: boolean, theme: ThemeLike, identity?: StepIdentity): Component {
61
+ return renderDelegatedStep(step as DelegatedStep, expanded, theme as never, {
62
+ group: identity?.group ?? "sidekick",
63
+ label: identity?.label,
64
+ });
123
65
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "3.0.0-alpha.2",
3
+ "version": "3.0.0-alpha.21",
4
4
  "description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.2"
35
+ "@pi-unipi/core": "3.0.0-alpha.21"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,21 +1,19 @@
1
1
  # @pi-unipi/input-shortcuts
2
2
 
3
- Keyboard shortcuts for Pi's input box. Stash and restore text, undo/redo, clipboard operations, thinking level toggle, and tab insertion — all via a vim-style chord overlay triggered by `ALT+S`.
3
+ Keyboard shortcuts for Pi's input box. Stash and restore text, undo/redo, copy the last response, push the editor text to the kanboard backlog, and tab insertion — all via a vim-style chord overlay triggered by `ALT+S`.
4
4
 
5
- Press `ALT+S`, a small overlay appears with key hints. Press a key, the action runs. The overlay closes automatically after 300ms or on `ESC`.
5
+ Press `ALT+S`, a small overlay appears with key hints. Press a key, the action runs. The overlay closes automatically after selecting an action, on `ESC`, or on an unknown key.
6
6
 
7
7
  ## Shortcuts
8
8
 
9
9
  | Chord | Action | Description |
10
10
  |-------|--------|-------------|
11
- | `ALT+S → S` | Stash/Restore | Save input to stash register, or restore it |
12
- | `ALT+S → U` | Undo | Pop from undo buffer |
13
- | `ALT+S → R` | Redo | Push current text forward, restore previous |
14
- | `ALT+S → Y` | Copy | Copy input to system clipboard |
15
- | `ALT+S → D` | Cut | Copy to clipboard, then clear input |
16
- | `ALT+S → T` | Toggle Thinking | Cycle: off → low → medium → high → xhigh → off |
17
- | `ALT+S → A → [0-9]` | Append Register | Append from numbered register 0-9 |
18
- | `ALT+S → A → S` | Append Stash | Append from stash register |
11
+ | `ALT+S → S` | Stash/Restore | Save input to the stash register, or restore it |
12
+ | `ALT+S → U` | Undo | Step back through editor checkpoints |
13
+ | `ALT+S → R` | Redo | Step forward again (an edit invalidates the redo) |
14
+ | `ALT+S → A` | Append Stash | Append the stash text to the input (stash kept) |
15
+ | `ALT+S → Y` | Copy Last Response | Copy the last assistant response to the system clipboard |
16
+ | `ALT+S → K` | Add to Kanboard Backlog | Add the editor text to the project's kanboard backlog as a body-only task (attached file paths in the text are uploaded) |
19
17
  | `ALT+I` | Tab Insert | Insert literal tab character |
20
18
 
21
19
  ## Commands
@@ -32,36 +30,26 @@ Every action shows a brief success or error message in the status bar via `ctx.u
32
30
 
33
31
  ## How It Works
34
32
 
35
- ### Registers
33
+ ### Stash
36
34
 
37
35
  - **Stash register**: 1 register for quick save/restore
38
- - **Numbered registers**: 10 registers (0-9) for stored text snippets
39
- - **Persistence**: Saved to `.unipi/config/input-shortcuts.json` (per-project, atomic writes)
36
+ - **Persistence**: Saved to `.unipi/config/input-shortcuts.json` (per-project, atomic writes). Files written by older versions (with ten numbered registers 0-9) still load; the register keys are ignored and dropped on the next save.
40
37
 
41
38
  ### Undo/Redo
42
39
 
43
- - In-memory ring buffer, max 50 snapshots per session
44
- - 500ms debounce on snapshot creation (prevents noise from rapid typing)
45
- - 1s throttle on undo (prevents rapid-fire undo)
46
- - Redo buffer cleared on new snapshot
40
+ - Linear checkpoint history, O(1) amortized per record
41
+ - A typing burst records the editor text exactly twice — before the first keystroke and after the last (burst closes after 600ms of quiet, and long bursts split every ~2s) — so undo works from the very first keystroke without reading the editor on every keypress
42
+ - At most 100 checkpoints / 2M characters in memory (oldest evicted first)
43
+ - An edit after an undo invalidates the redo; undoing captures un-flushed typing first
47
44
  - Not persisted across sessions
48
45
 
49
- ### Clipboard
46
+ ### Copy Last Response
50
47
 
51
- Cross-platform clipboard detection with automatic fallback:
48
+ Uses pi's own cross-platform `copyToClipboard` (wl-copy / xclip / pbcopy / Windows / WSL / OSC 52 fallbacks) on the text of the last assistant response — the same text pi's `/copy` copies.
52
49
 
53
- | Platform | Read | Write |
54
- |----------|------|-------|
55
- | Linux (X11) | `xclip -selection clipboard -o` | `xclip -selection clipboard` |
56
- | Linux (alt) | `xsel --clipboard --output` | `xsel --clipboard --input` |
57
- | macOS | `pbpaste` | `pbcopy` |
58
- | Windows | `powershell Get-Clipboard` | `clip` / `powershell Set-Clipboard` |
50
+ ### Kanboard Backlog
59
51
 
60
- Detected tool is cached after first use. Returns graceful error if no clipboard tool is available.
61
-
62
- ### Thinking Toggle
63
-
64
- Cycles through Pi's thinking levels: off → low → medium → high → xhigh → off.
52
+ The K chord hands the editor text to `globalThis.__unipi_kanboard_api.captureToBacklog()` (registered by `@pi-unipi/kanboard`): the text becomes a body-only Backlog task, and file paths in the text (pasted/dragged files and pasted images, which pi inserts as paths) are attached. Refuses with a status message when kanboard is not loaded or the folder is not onboarded.
65
53
 
66
54
  ## Configurables
67
55
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/input-shortcuts",
3
- "version": "3.0.0-alpha.2",
3
+ "version": "3.0.0-alpha.21",
4
4
  "description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.2"
36
+ "@pi-unipi/core": "3.0.0-alpha.21"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",