@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,73 @@
1
+ /**
2
+ * @pi-unipi/core — progress entries: `◎ Goal ██████▒░░░░░ ~55% turn 4/30`.
3
+ *
4
+ * Written with `pi.appendEntry`, so they persist in the transcript for the user
5
+ * but never reach the model (custom entries are outside the LLM context).
6
+ */
7
+
8
+ import type { ExtensionAPI, ThemeColor } from "@earendil-works/pi-coding-agent";
9
+ import { truncateToWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
10
+ import { type KitTheme, progressBar } from "./kit.js";
11
+
12
+ export const PROGRESS_ENTRY = "unipi-progress";
13
+
14
+ export interface ProgressData {
15
+ /** Leading glyph (◎ goal, ↻ ralph, ▣ kanboard). */
16
+ icon?: string;
17
+ label: string;
18
+ done: number;
19
+ /** Units in progress right now — rendered as the shaded part. */
20
+ active: number;
21
+ total: number;
22
+ /** Count suffix, e.g. "items", "tasks". Omitted → percent. */
23
+ unit?: string;
24
+ /** Estimated by a model rather than counted (prefixes `~`). */
25
+ estimated?: boolean;
26
+ detail?: string;
27
+ summary?: string;
28
+ color?: ThemeColor;
29
+ }
30
+
31
+ const BAR_WIDTH = 20;
32
+
33
+ export function progressLines(t: KitTheme, d: ProgressData, width: number): string[] {
34
+ const color = d.color ?? "accent";
35
+ const pct = d.total > 0 ? Math.round((d.done / d.total) * 100) : 0;
36
+ const count = d.unit ? `${String(d.done)}/${String(d.total)} ${d.unit}` : `${d.estimated ? "~" : ""}${String(pct)}%`;
37
+ const head = [
38
+ `${t.fg(color, d.icon ?? "◎")} ${t.bold(d.label)}`,
39
+ progressBar(t, d.done, d.active, d.total, BAR_WIDTH, color),
40
+ t.fg("text", count),
41
+ d.detail ? t.fg("dim", d.detail) : "",
42
+ ].filter(Boolean).join(" ");
43
+ const out = [truncateToWidth(head, width)];
44
+ if (d.summary) {
45
+ for (const l of wrapTextWithAnsi(t.fg("muted", d.summary.trim()), Math.max(10, width - 2))) out.push(` ${l}`);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ const registered = new WeakSet<object>();
51
+
52
+ /** Register the entry renderer once per extension API instance. */
53
+ export function registerProgressRenderer(pi: ExtensionAPI): void {
54
+ if (registered.has(pi)) return;
55
+ registered.add(pi);
56
+ try {
57
+ pi.registerEntryRenderer<ProgressData>(PROGRESS_ENTRY, (entry, _opts, theme): Component | undefined => {
58
+ const d = entry.data;
59
+ if (!d) return undefined;
60
+ return { invalidate() {}, render: (w: number) => progressLines(theme as KitTheme, d, w) } satisfies Component;
61
+ });
62
+ } catch {
63
+ /* UI-dependent */
64
+ }
65
+ }
66
+
67
+ export function appendProgress(pi: ExtensionAPI, data: ProgressData): void {
68
+ try {
69
+ pi.appendEntry(PROGRESS_ENTRY, data);
70
+ } catch {
71
+ /* session replaced */
72
+ }
73
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Turn arbiter — the single `agent_before_settle` decision point.
3
+ *
4
+ * Nudge providers (long-horizon owner, kanboard monitor, …) propose at most
5
+ * one continuation each; wait sources (bg wake, busy sidekick, background
6
+ * subagent) veto; the handler delivers at most ONE custom_message nudge per
7
+ * settle and continues the run exactly once.
8
+ *
9
+ * Composition semantics (pi 0.99, verified live, docs/plans/2026-09-30-01a0f095.md
10
+ * Step 0): boundary handlers run in registration order, each awaited; a handler
11
+ * result's `entries` REPLACES the accumulated array (so the handler must concat
12
+ * `event.entries` itself) and `continue` is OR-ed. Events queued as followUps
13
+ * are drained before the boundary, so `pendingMessages` is a defence-only
14
+ * check; a run aborted with Esc never reaches the boundary at all.
15
+ *
16
+ * State lives in a `Symbol.for` global so every package that imports core
17
+ * shares one arbiter, and the installer is idempotent across reloads.
18
+ */
19
+
20
+ import type { AgentBeforeSettleEventResult, CustomMessageEntryDraft, ExtensionAPI, SessionBoundaryDraft } from "@earendil-works/pi-coding-agent";
21
+
22
+ export interface SettleInfo {
23
+ outcome: "completed" | "aborted" | "error";
24
+ pendingMessages: number;
25
+ /** Text parts of the last assistant message, joined. */
26
+ lastAssistantText: string;
27
+ /** Whether the last assistant message carried tool-call parts. */
28
+ lastAssistantHadToolCalls: boolean;
29
+ /** Tool calls since the last `agent_start` (counted by the installer). */
30
+ toolCallsThisRun: number;
31
+ }
32
+
33
+ export interface Nudge {
34
+ source: string;
35
+ priority: number;
36
+ customType: string;
37
+ content: string;
38
+ display: boolean;
39
+ details?: unknown;
40
+ /** Called once, only when THIS nudge is the one delivered. */
41
+ onDelivered?: () => void;
42
+ }
43
+
44
+ export type SettleDecision =
45
+ | { kind: "nudge"; source: string; priority: number }
46
+ | { kind: "none"; reason: "outcome" | "pending" | "continuing" | "wait" | "no-proposal"; detail?: string };
47
+
48
+ export type NudgeProvider = {
49
+ source: string;
50
+ priority: number;
51
+ propose: (s: SettleInfo) => Nudge | null | Promise<Nudge | null>;
52
+ };
53
+
54
+ export type WaitSource = { source: string; waiting: () => string | null };
55
+
56
+ export type SettleDecisionListener = (decision: SettleDecision, info: SettleInfo) => void;
57
+
58
+ interface ArbiterHolder {
59
+ /** Keyed by source: re-registering a source replaces it (reload-safe) and keeps its original order. */
60
+ providers: Map<string, NudgeProvider>;
61
+ waitSources: Map<string, WaitSource>;
62
+ listeners: Set<SettleDecisionListener>;
63
+ installed: boolean;
64
+ toolCallsThisRun: number;
65
+ providerTimeoutMs: number;
66
+ }
67
+
68
+ const KEY = Symbol.for("unipi.turn.arbiter");
69
+ const DEFAULT_PROVIDER_TIMEOUT_MS = 2_000;
70
+
71
+ function holder(): ArbiterHolder {
72
+ const g = globalThis as { [KEY]?: ArbiterHolder };
73
+ g[KEY] ??= {
74
+ providers: new Map(),
75
+ waitSources: new Map(),
76
+ listeners: new Set(),
77
+ installed: false,
78
+ toolCallsThisRun: 0,
79
+ providerTimeoutMs: DEFAULT_PROVIDER_TIMEOUT_MS,
80
+ };
81
+ return g[KEY] as ArbiterHolder;
82
+ }
83
+
84
+ /** Children are the hands, the lead is the voice: no arbiter, no nudges. */
85
+ export function isChildProcess(env: NodeJS.ProcessEnv = process.env): boolean {
86
+ return env.UNIPI_FUSION_CHILD === "1" || env.UNIPI_SUBAGENT_CHILD === "1" || Boolean(env.UNIPI_KANBOARD_CHILD);
87
+ }
88
+
89
+ export function registerNudgeProvider(
90
+ source: string,
91
+ priority: number,
92
+ propose: (s: SettleInfo) => Nudge | null | Promise<Nudge | null>,
93
+ ): () => void {
94
+ const h = holder();
95
+ h.providers.set(source, { source, priority, propose });
96
+ return () => {
97
+ const current = h.providers.get(source);
98
+ if (current?.propose === propose) h.providers.delete(source);
99
+ };
100
+ }
101
+
102
+ export function registerWaitSource(source: string, waiting: () => string | null): () => void {
103
+ const h = holder();
104
+ h.waitSources.set(source, { source, waiting });
105
+ return () => {
106
+ const current = h.waitSources.get(source);
107
+ if (current?.waiting === waiting) h.waitSources.delete(source);
108
+ };
109
+ }
110
+
111
+ export function onSettleDecision(listener: SettleDecisionListener): () => void {
112
+ const h = holder();
113
+ h.listeners.add(listener);
114
+ return () => {
115
+ h.listeners.delete(listener);
116
+ };
117
+ }
118
+
119
+ async function raceTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | null> {
120
+ let timer: ReturnType<typeof setTimeout> | undefined;
121
+ const timeout = new Promise<null>((resolve) => {
122
+ timer = setTimeout(() => resolve(null), timeoutMs);
123
+ });
124
+ try {
125
+ return await Promise.race([promise, timeout]);
126
+ } finally {
127
+ if (timer !== undefined) clearTimeout(timer);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Pure decision core of the settle handler (no pi types), exported for tests.
133
+ * Order matters: outcome → pending (defence only, see Step 0) → already
134
+ * continuing → wait sources → highest-priority proposal (tie: earliest
135
+ * registered). Provider throw/timeout counts as no proposal.
136
+ */
137
+ export async function decideSettle(
138
+ info: SettleInfo,
139
+ alreadyContinuing: boolean,
140
+ ): Promise<{ decision: SettleDecision; nudge?: Nudge }> {
141
+ const h = holder();
142
+ if (info.outcome !== "completed") {
143
+ return { decision: { kind: "none", reason: "outcome", detail: info.outcome } };
144
+ }
145
+ if (info.pendingMessages > 0) {
146
+ return { decision: { kind: "none", reason: "pending" } };
147
+ }
148
+ if (alreadyContinuing) {
149
+ return { decision: { kind: "none", reason: "continuing" } };
150
+ }
151
+ for (const waitSource of h.waitSources.values()) {
152
+ let reason: string | null = null;
153
+ try {
154
+ reason = waitSource.waiting();
155
+ } catch {
156
+ reason = null; // a broken wait source never blocks settlement
157
+ }
158
+ if (reason !== null) {
159
+ return { decision: { kind: "none", reason: "wait", detail: `${waitSource.source}: ${reason}` } };
160
+ }
161
+ }
162
+ let best: { nudge: Nudge } | undefined;
163
+ for (const provider of h.providers.values()) {
164
+ let nudge: Nudge | null | undefined;
165
+ try {
166
+ nudge = await raceTimeout(Promise.resolve(provider.propose(info)), h.providerTimeoutMs);
167
+ } catch {
168
+ nudge = null;
169
+ }
170
+ if (nudge === null || nudge === undefined) continue;
171
+ if (best === undefined || nudge.priority > best.nudge.priority) best = { nudge };
172
+ }
173
+ if (best === undefined) return { decision: { kind: "none", reason: "no-proposal" } };
174
+ return {
175
+ decision: { kind: "nudge", source: best.nudge.source, priority: best.nudge.priority },
176
+ nudge: best.nudge,
177
+ };
178
+ }
179
+
180
+ const TOOL_CALL_PART_TYPES = new Set(["toolCall", "tool_call", "toolUse", "tool_use"]);
181
+
182
+ interface AssistantSummary {
183
+ text: string;
184
+ hadToolCalls: boolean;
185
+ }
186
+
187
+ function summarizeAssistant(messages: readonly unknown[]): AssistantSummary {
188
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
189
+ const message = messages[index];
190
+ if (typeof message !== "object" || message === null) continue;
191
+ const record = message as Record<string, unknown>;
192
+ if (record.role !== "assistant") continue;
193
+ const content = record.content;
194
+ if (typeof content === "string") return { text: content, hadToolCalls: false };
195
+ if (!Array.isArray(content)) return { text: "", hadToolCalls: false };
196
+ let text = "";
197
+ let hadToolCalls = false;
198
+ for (const part of content) {
199
+ if (typeof part !== "object" || part === null) continue;
200
+ const block = part as Record<string, unknown>;
201
+ if (block.type === "text" && typeof block.text === "string") text += block.text;
202
+ else if (block.type !== undefined && TOOL_CALL_PART_TYPES.has(String(block.type))) hadToolCalls = true;
203
+ }
204
+ return { text, hadToolCalls };
205
+ }
206
+ return { text: "", hadToolCalls: false };
207
+ }
208
+
209
+ /**
210
+ * Install the single `agent_before_settle` handler. Idempotent: the first
211
+ * call wins (holder flag) — later calls, including from long-horizon's
212
+ * standalone factory, are no-ops. In child processes it registers nothing.
213
+ */
214
+ export function installArbiter(pi: ExtensionAPI): void {
215
+ const h = holder();
216
+ if (h.installed) return;
217
+ h.installed = true;
218
+ if (isChildProcess()) return;
219
+
220
+ pi.on("agent_start", () => {
221
+ try {
222
+ h.toolCallsThisRun = 0;
223
+ } catch {
224
+ // Accounting must never abort a turn.
225
+ }
226
+ });
227
+ pi.on("tool_call", () => {
228
+ try {
229
+ h.toolCallsThisRun += 1;
230
+ } catch {
231
+ // Accounting must never abort a turn.
232
+ }
233
+ });
234
+ pi.on("agent_before_settle", async (event): Promise<AgentBeforeSettleEventResult | undefined> => {
235
+ try {
236
+ const settleEvent = event as {
237
+ outcome?: SettleInfo["outcome"];
238
+ continue?: boolean;
239
+ entries?: SessionBoundaryDraft[];
240
+ context?: { pendingMessages?: unknown[]; contextMessages?: unknown[] };
241
+ };
242
+ const assistant = summarizeAssistant(settleEvent.context?.contextMessages ?? []);
243
+ const info: SettleInfo = {
244
+ outcome: settleEvent.outcome ?? "completed",
245
+ pendingMessages: settleEvent.context?.pendingMessages?.length ?? 0,
246
+ lastAssistantText: assistant.text,
247
+ lastAssistantHadToolCalls: assistant.hadToolCalls,
248
+ toolCallsThisRun: h.toolCallsThisRun,
249
+ };
250
+ const { decision, nudge } = await decideSettle(info, settleEvent.continue === true);
251
+ for (const listener of h.listeners) {
252
+ try {
253
+ listener(decision, info);
254
+ } catch {
255
+ // A broken listener must never break settlement.
256
+ }
257
+ }
258
+ if (decision.kind !== "nudge" || nudge === undefined) return undefined;
259
+ try {
260
+ nudge.onDelivered?.();
261
+ } catch {
262
+ // Delivery bookkeeping must never abort a turn.
263
+ }
264
+ const entry: CustomMessageEntryDraft = {
265
+ type: "custom_message",
266
+ customType: nudge.customType,
267
+ content: nudge.content,
268
+ display: nudge.display,
269
+ ...(nudge.details !== undefined ? { details: nudge.details } : {}),
270
+ };
271
+ // Boundary `entries` are REPLACED by the last handler that returns
272
+ // them — always carry the accumulated array forward.
273
+ return { entries: [...(settleEvent.entries ?? []), entry], continue: true };
274
+ } catch {
275
+ // The arbiter must never abort a turn.
276
+ return undefined;
277
+ }
278
+ });
279
+ }
280
+
281
+ /** Test hook: clear every registration and the installed flag. */
282
+ export function resetArbiterForTests(opts?: { providerTimeoutMs?: number }): void {
283
+ const h = holder();
284
+ h.providers.clear();
285
+ h.waitSources.clear();
286
+ h.listeners.clear();
287
+ h.installed = false;
288
+ h.toolCallsThisRun = 0;
289
+ h.providerTimeoutMs = opts?.providerTimeoutMs ?? DEFAULT_PROVIDER_TIMEOUT_MS;
290
+ }
@@ -37,7 +37,7 @@ Footer subscribes to events from every Unipi package:
37
37
  | Group | Events | Segments |
38
38
  |-------|--------|----------|
39
39
  | core | Pi SDK | model, thinking, path, git, context_pct, cost, tokens, session |
40
- | compactor | Pi session data + `COMPACTOR_COMPACTED` | session_events, compactions, tokens_saved, compression_ratio |
40
+ | compactor | Pi session data | compactions — `cmp 4× 39k→13k · 3m` (count, tokens before → after, time since the last; hidden until the first) |
41
41
  | memory | `MEMORY_STORED`/`DELETED`/`CONSOLIDATED` | project_count, total_count, consolidations |
42
42
  | mcp | `MCP_SERVER_STARTED`/`STOPPED`/`ERROR` | servers_total, servers_active, tools_total |
43
43
  | ralph | `RALPH_LOOP_START`/`END`/`ITERATION_DONE` | active_loops, total_iterations, loop_status |
@@ -90,9 +90,7 @@ Settings in `~/.pi/agent/settings.json` under `unipi.footer`:
90
90
  "compactor": {
91
91
  "show": true,
92
92
  "segments": {
93
- "session_events": true,
94
- "compactions": true,
95
- "tokens_saved": true
93
+ "compactions": true
96
94
  }
97
95
  }
98
96
  }
@@ -137,11 +135,11 @@ When `iconStyle` is not set, footer auto-detects Nerd Font support and defaults
137
135
 
138
136
  ```
139
137
  Wide terminal (>120 cols):
140
- model | thinking | path | git | context | cost | compactions | tokens_saved | project_count
138
+ model | thinking | path | git | context | compactions | cost | project_count
141
139
 
142
140
  Narrow terminal (<120 cols):
143
141
  Row 1: model | thinking | path | git | context | cost
144
- Row 2: compactions | tokens_saved | project_count | ralph | workflow
142
+ Row 2: compactions | project_count | ralph | workflow
145
143
  ```
146
144
 
147
145
  ## License
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "3.0.0-alpha.2",
3
+ "version": "3.0.0-alpha.21",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "3.0.0-alpha.2",
36
- "@pi-unipi/background-tasks": "3.0.0-alpha.2"
35
+ "@pi-unipi/core": "3.0.0-alpha.21",
36
+ "@pi-unipi/background-tasks": "3.0.0-alpha.21"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -5,12 +5,14 @@
5
5
  * subclass CustomEditor, use super.render() ONLY to obtain editor content
6
6
  * lines, then compose our own rounded frame. Layout per the v3 design:
7
7
  *
8
- * ╭─UNIPI │ feat/footer-default-v2 │ ─────────────────────╮
9
- * │ Type your prompt here... │
8
+ * ╭─UNIPI │ feat/footer-default-v2 ─────────────────────╮
9
+ * Type your prompt here...
10
10
  * ╰─ unipi ────────── 42%/1.0M │ Claude Opus 4.5 │ thinking:high ╯
11
11
  *
12
12
  * Top border carries the UNIPI brand + git branch. Bottom border right side
13
13
  * carries workspace · context%/window · model · thinking level; left filler.
14
+ * The body is open-sided: no vertical rails — the rounded corners are the
15
+ * only frame marks (content keeps its column via a one-space indent).
14
16
  * All keybindings/autocomplete/history/paste behavior inherited.
15
17
  */
16
18
 
@@ -19,6 +21,7 @@ import type { EditorOptions, EditorTheme, TUI } from "@earendil-works/pi-tui";
19
21
  import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
20
22
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
21
23
  import { getIcon, getResolvedIconStyle } from "./rendering/icons.js";
24
+ import { kanboardGlanceLabel } from "@pi-unipi/core";
22
25
  import { lolcatRainbow, paintLolcatGradient } from "./rendering/lolcat.js";
23
26
 
24
27
  /** Live status injected by the footer extension. */
@@ -41,12 +44,13 @@ export interface GlanceStatus {
41
44
  modelName: string;
42
45
  /** Current thinking level (already "off"-filtered by the provider). */
43
46
  thinkingLevel: string | null;
44
- /**
45
- * Active Fusion pair, when selected. When set, the model slot renders
46
- * `Fusion · <lead> <effort> ◆ <sidekick>` — lead lit, sidekick muted —
47
- * and the plain thinking slot is suppressed (efforts are inline).
47
+ /** Active Fusion pair, when selected. When set, the model slot renders
48
+ * `Fusion · <lead> <effort> ◆ <sidekick>` — lead lit, sidekick muted —
49
+ * and the plain thinking slot is suppressed (efforts are inline).
48
50
  */
49
51
  fusion: { leadName: string; leadEffort: string; sidekickName: string; sidekickEffort: string; savedUsd?: number; busy?: boolean; leadToolCalls?: number; sidekickToolCalls?: number } | null;
52
+ /** Kanboard claims/autowork snapshot (null → no `▣` segment). */
53
+ kanboard: { claims: string[]; autowork: boolean } | null;
50
54
  }
51
55
 
52
56
  const BORDER = {
@@ -196,17 +200,21 @@ export function composeGlanceTitles(
196
200
  branch: string | null,
197
201
  workspace: string,
198
202
  lhMode: string | null = null,
203
+ kanboard: { claims: string[]; autowork: boolean } | null = null,
199
204
  ): { titleParts: string[]; leftTitle: string } {
205
+ const kanboardLabel = kanboardGlanceLabel(kanboard);
200
206
  const titleParts: string[] = [];
201
207
  if (getResolvedIconStyle() === "text") {
202
208
  titleParts.push(brand);
203
209
  if (lhMode) titleParts.push(`mode:${lhMode}`);
210
+ if (kanboardLabel) titleParts.push(kanboardLabel.replace(/^▣ /, "kanboard:"));
204
211
  if (branch) titleParts.push(`branch:${branch}`);
205
212
  return { titleParts, leftTitle: ` workspace:${workspace} ` };
206
213
  }
207
214
  const brandIcon = getIcon("model");
208
215
  titleParts.push(`${brandIcon ? brandIcon + " " : ""}${brand}`);
209
216
  if (lhMode) titleParts.push(lhMode);
217
+ if (kanboardLabel) titleParts.push(kanboardLabel);
210
218
  if (branch) {
211
219
  const gitIcon = getIcon("git");
212
220
  titleParts.push(`${gitIcon ? gitIcon + " " : ""}${branch}`);
@@ -276,7 +284,7 @@ export class GlanceEditor extends CustomEditor {
276
284
  // Brand rendered as an animated lolcat gradient (phase from wall time;
277
285
  // the footer's 1s refresh timer re-renders, so it shimmers each tick).
278
286
  const brand = lolcatRainbow("UNIPI", Date.now() / 1000);
279
- const { titleParts, leftTitle } = composeGlanceTitles(brand, st.branch, st.workspace, st.lhMode);
287
+ const { titleParts, leftTitle } = composeGlanceTitles(brand, st.branch, st.workspace, st.lhMode, st.kanboard);
280
288
  const title = titleParts.join(SEP);
281
289
  const leadRule = `${BORDER.horizontal} `;
282
290
  const titleText = ` ${title}${SEP}`;
@@ -296,10 +304,10 @@ export class GlanceEditor extends CustomEditor {
296
304
  topCluster +
297
305
  border(BORDER.topRight);
298
306
 
299
- // ── Body rows: │ content │ ──
300
- const bodyRows = contentLines.map(row =>
301
- border(BORDER.vertical) + padLine(row, inner) + border(BORDER.vertical),
302
- );
307
+ // ── Body rows: open sides — the corner curves are the only frame marks.
308
+ // One-space indent keeps content in the column it had between the old
309
+ // rails; no trailing padding (rows may end short of the frame width).
310
+ const bodyRows = contentLines.map(row => " " + truncateToWidth(row, inner, ""));
303
311
 
304
312
  // ── Bottom frame: ╰─ unipi ─────── [RIGHT CLUSTER] ─╯ ──
305
313
  // Right cluster: workspace · pct%/window │ model │ thinking:level
@@ -75,7 +75,7 @@ function buildHelpLines(
75
75
  export function getGroupForSegment(segId: string): string {
76
76
  const coreIds = ["model", "api_state", "tool_count", "git", "context_pct", "cost", "tokens_total", "tokens_in", "tokens_out", "session", "hostname", "time", "tps", "clock", "duration", "thinking_level", "uni", "directory"];
77
77
  if (coreIds.includes(segId)) return "core";
78
- const compactorIds = ["session_events", "compactions", "tokens_saved", "compression_ratio", "sandbox_runs", "search_queries"];
78
+ const compactorIds = ["compactions"];
79
79
  if (compactorIds.includes(segId)) return "compactor";
80
80
  if (["project_count", "total_count", "consolidations"].includes(segId)) return "memory";
81
81
  if (["servers_total", "servers_active", "tools_total", "servers_failed"].includes(segId)) return "mcp";
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ExtensionAPI, Theme, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
- import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS, getSharedFusionStatus, getSharedLongHorizonMode, getSharedPlanPermissionStatus } from "@pi-unipi/core";
10
+ import { UNIPI_EVENTS, emitEvent, UNIPI_PREFIX, FOOTER_COMMANDS, getSharedFusionStatus, getSharedKanboardStatus, getSharedLongHorizonMode, getSharedPlanPermissionStatus } from "@pi-unipi/core";
11
11
  import { FooterRegistry, getFooterRegistry } from "./registry/index.js";
12
12
  import { FooterRenderer } from "./rendering/renderer.js";
13
13
  import { subscribeToEvents } from "./events.js";
@@ -18,7 +18,7 @@ import { GlanceEditor } from "./glance-editor.js";
18
18
 
19
19
  // Import segment groups
20
20
  import { CORE_SEGMENTS } from "./segments/core.js";
21
- import { COMPACTOR_SEGMENTS } from "./segments/compactor.js";
21
+ import { COMPACTOR_SEGMENTS, compactionSummary } from "./segments/compactor.js";
22
22
  import { MEMORY_SEGMENTS } from "./segments/memory.js";
23
23
  import { MCP_SEGMENTS } from "./segments/mcp.js";
24
24
  import { RALPH_SEGMENTS } from "./segments/ralph.js";
@@ -448,6 +448,7 @@ function installGlanceEditor(
448
448
  const lhMode = typeof lhModeRaw === "string" && lhModeRaw.length > 0
449
449
  ? MODE_LABELS[lhModeRaw] ?? lhModeRaw
450
450
  : null;
451
+ const kanboard = getSharedKanboardStatus() ?? null;
451
452
  // Plan/permission state rides the same CORE registry data, written by
452
453
  // the workflow module's events (also re-emitted on session_start).
453
454
  const coreData = st.registry.getGroupData("core") as
@@ -470,6 +471,7 @@ function installGlanceEditor(
470
471
  modelName,
471
472
  thinkingLevel: typeof p?.thinkingLevel === "string" ? p.thinkingLevel : null,
472
473
  fusion,
474
+ kanboard,
473
475
  };
474
476
  }),
475
477
  );
@@ -546,14 +548,30 @@ const STRIP_COLOR = {
546
548
  psMid: "\x1b[97m", // white — in between
547
549
  cacheHit: "\x1b[92m", // green — high hit
548
550
  cacheWarn: "\x1b[93m", // amber — lowish hit
551
+ compact: "\x1b[94m", // blue — compactions
549
552
  reset: "\x1b[39m",
550
553
  } as const;
551
554
 
552
555
  const c = (code: string, text: string) => `${code}${text}${STRIP_COLOR.reset}`;
553
556
 
557
+ function fmtTokensShort(n: number): string {
558
+ if (n < 1000) return String(n);
559
+ if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
560
+ if (n < 1000000) return `${Math.round(n / 1000)}k`;
561
+ return `${(n / 1000000).toFixed(1)}M`;
562
+ }
563
+
564
+ function agoShort(ms: number): string {
565
+ const sec = Math.max(0, Math.round(ms / 1000));
566
+ if (sec < 60) return "just now";
567
+ if (sec < 3600) return `${Math.round(sec / 60)}m ago`;
568
+ if (sec < 86400) return `${Math.round(sec / 3600)}h ago`;
569
+ return `${Math.round(sec / 86400)}d ago`;
570
+ }
571
+
554
572
  /**
555
573
  * Glance-style centered stats strip under the input:
556
- * n Turn · n Steps | wall · tool wall | avg TTFT · n tok/s | cache n%
574
+ * n Turn · n Steps | wall · tool wall | avg TTFT · n tok/s | n compactions · 39k→13k · 3m ago | cache n%
557
575
  */
558
576
  function renderSessionStrip(piContext: unknown): string | null {
559
577
  const parts: string[] = [];
@@ -588,6 +606,21 @@ function renderSessionStrip(piContext: unknown): string | null {
588
606
  if (seg) parts.push(seg);
589
607
  }
590
608
 
609
+ // Compactions: hidden until the first one.
610
+ const branch = (() => {
611
+ try {
612
+ return ((piContext as any)?.sessionManager?.getBranch?.() ?? []) as unknown[];
613
+ } catch {
614
+ return [];
615
+ }
616
+ })();
617
+ const cmp = compactionSummary(branch);
618
+ if (cmp.count > 0) {
619
+ const sizes = cmp.before > 0 ? ` \u00b7 ${c(STRIP_COLOR.compact, `${fmtTokensShort(cmp.before)}\u2192${fmtTokensShort(cmp.after)}`)}` : "";
620
+ const age = cmp.lastAt != null ? ` \u00b7 ${agoShort(Date.now() - cmp.lastAt)}` : "";
621
+ parts.push(`${c(STRIP_COLOR.compact, String(cmp.count))} compaction${cmp.count === 1 ? "" : "s"}${sizes}${age}`);
622
+ }
623
+
591
624
  const hit = cacheHitPct(piContext);
592
625
  if (hit !== null) {
593
626
  const hitColor = hit >= 70 ? STRIP_COLOR.cacheHit : STRIP_COLOR.cacheWarn;
@@ -18,7 +18,7 @@ const DEFAULT_PRESET: PresetDef = {
18
18
  "uni", "lh_mode", "model", "thinking_level", "directory", "git",
19
19
  ],
20
20
  rightSegments: [
21
- "context_pct", "tokens_total",
21
+ "context_pct", "compactions", "tokens_total",
22
22
  "tps", "cost",
23
23
  "clock", "duration",
24
24
  ],
@@ -33,7 +33,7 @@ const CLASSIC_PRESET: PresetDef = {
33
33
  ],
34
34
  rightSegments: [
35
35
  "tps", "context_pct", "cost",
36
- "compactions", "tokens_saved", "project_count",
36
+ "compactions", "project_count",
37
37
  "current_command", "loop_status", "extension_statuses",
38
38
  "clock", "duration",
39
39
  ],
@@ -76,7 +76,7 @@ const FULL_PRESET: PresetDef = {
76
76
  ],
77
77
  rightSegments: [
78
78
  "tps", "context_pct", "cost", "tokens_total",
79
- "session_events", "compactions", "tokens_saved",
79
+ "compactions",
80
80
  "project_count", "total_count",
81
81
  "servers_total", "servers_active", "tools_total",
82
82
  "active_loops", "loop_status",
@@ -87,7 +87,6 @@ const FULL_PRESET: PresetDef = {
87
87
  secondarySegments: [
88
88
  "hostname",
89
89
  "tokens_in", "tokens_out",
90
- "compression_ratio",
91
90
  "platforms_enabled", "last_sent",
92
91
  "thinking_level",
93
92
  ],