@pi-unipi/unipi 3.0.0-alpha.24 → 3.0.0-alpha.26

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 (68) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +71 -1
  3. package/package.json +22 -21
  4. package/packages/ask-user/handoff.ts +2 -1
  5. package/packages/ask-user/package.json +2 -2
  6. package/packages/autocomplete/package.json +1 -1
  7. package/packages/autocomplete/src/constants.ts +1 -1
  8. package/packages/background-tasks/package.json +2 -2
  9. package/packages/btw/package.json +2 -2
  10. package/packages/compactor/package.json +3 -3
  11. package/packages/compactor/src/compaction/hooks.ts +2 -2
  12. package/packages/compactor/src/info-screen.ts +32 -2
  13. package/packages/core/README.md +7 -1
  14. package/packages/core/harness-messages.ts +28 -4
  15. package/packages/core/index.ts +3 -0
  16. package/packages/core/package.json +1 -1
  17. package/packages/core/src/compaction-savings.ts +122 -0
  18. package/packages/core/src/hints/index.ts +42 -0
  19. package/packages/core/src/hints/lines.ts +268 -6
  20. package/packages/core/src/tui/viz.ts +511 -0
  21. package/packages/footer/package.json +3 -3
  22. package/packages/fusion/package.json +4 -4
  23. package/packages/fusion/src/index.ts +9 -0
  24. package/packages/info-screen/README.md +113 -34
  25. package/packages/info-screen/config.ts +16 -14
  26. package/packages/info-screen/core-groups.ts +281 -483
  27. package/packages/info-screen/index.ts +197 -165
  28. package/packages/info-screen/package.json +7 -3
  29. package/packages/info-screen/pages/context.ts +211 -0
  30. package/packages/info-screen/pages/inventory.ts +156 -0
  31. package/packages/info-screen/pages/modules.ts +358 -0
  32. package/packages/info-screen/pages/session.ts +356 -0
  33. package/packages/info-screen/pages/usage.ts +170 -0
  34. package/packages/info-screen/palette.ts +80 -0
  35. package/packages/info-screen/registry.ts +183 -105
  36. package/packages/info-screen/tui/info-overlay.ts +287 -499
  37. package/packages/info-screen/tui/page-kit.ts +222 -0
  38. package/packages/info-screen/tui/self-dismiss.ts +55 -0
  39. package/packages/info-screen/tui/splash.ts +204 -0
  40. package/packages/info-screen/types.ts +39 -10
  41. package/packages/info-screen/usage-parser.ts +147 -11
  42. package/packages/input-shortcuts/package.json +2 -2
  43. package/packages/input-shortcuts/src/index.ts +1 -0
  44. package/packages/kanboard/package.json +7 -7
  45. package/packages/long-horizon/index.ts +8 -0
  46. package/packages/long-horizon/package.json +2 -2
  47. package/packages/long-horizon/src/commands.ts +7 -1
  48. package/packages/long-horizon/src/gate.ts +70 -1
  49. package/packages/mcp/package.json +2 -2
  50. package/packages/mcp/src/index.ts +13 -0
  51. package/packages/memory/index.ts +19 -0
  52. package/packages/memory/package.json +3 -3
  53. package/packages/notify/package.json +2 -2
  54. package/packages/skill-registry/index.ts +6 -4
  55. package/packages/skill-registry/package.json +2 -2
  56. package/packages/subagents/package.json +3 -3
  57. package/packages/unipi/bundled.js +4188 -1921
  58. package/packages/unipi/index.ts +27 -1
  59. package/packages/updater/package.json +2 -2
  60. package/packages/updater/src/index.ts +22 -9
  61. package/packages/utility/package.json +2 -2
  62. package/packages/utility/src/answer/index.ts +7 -2
  63. package/packages/utility/src/render/harness.ts +12 -2
  64. package/packages/utility/src/summarize/index.ts +8 -2
  65. package/packages/watchdog/package.json +3 -3
  66. package/packages/web-api/package.json +2 -2
  67. package/packages/web-api/src/index.ts +21 -0
  68. package/packages/workflow/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [3.0.0-alpha.26] — 2026-10-04
10
+
11
+ ### Fixed
12
+ - `fusion`: the "Non-trivial shell work since last handoff" nudge fires at most once per agent run instead of repeating on every 4th non-trivial shell command; a sidekick handoff still resets the streak but no longer re-arms the nudge, and a new prompt arms it again. (UNI-87)
13
+ - `long-horizon`: mode-control tools are now truly OFF in pi's active tool set outside their mode (previously registered/active and only hidden from the provider payload), so pi's tool snippets and other modules no longer see them. Fresh and resumed sessions start with them off; resolving into a mode (command, judge, or active owner) turns that mode's tools on. (UNI-90)
14
+ - `info-screen`: scope order now reads biggest first — footer legend `g global p project s session`, Compactor savings tiles global → project → session, Memory tiles global → project → share.
15
+ - `info-screen`: Skills page tags each source with its real scope (`g` for user/agents/unipi/package skills, `p` for project skills) instead of implying session scope; the Tools page's tool list is tagged `s` (the active set is per session).
16
+ - `info-screen`: Usage page leaves one blank line between the model share bar and the ranked model rows.
17
+
18
+ ## [3.0.0-alpha.25] — 2026-10-04
19
+
20
+ ### Added
21
+ - `info-screen`: **Unicrab splash** at startup — the existing Unicrab half-block art, a shimmering block wordmark with version chip, greeting, how fast Pi got ready, and live facts (resumed session totals or cwd/branch, today's spend from the cached snapshot, update available), with a draining countdown edge. Non-capturing and stack-safe in `auto-close` mode. Replaces the boot-time dashboard; the hints start-screen header is unchanged. (UNI-78)
22
+ - `info-screen`: redesigned **`/unipi:info`** dashboard — opaque panel coloured per page, pill tab strip, fixed body height, page dots, `1`–`9` jump, `/unipi:info <page>`. Pages draw themselves with a new Unicode toolkit, no emoji. (UNI-79)
23
+ - **This session** (first page): cost / tokens / replies / time in 3-row gradient digits, context meter, token mix + cache hit, tokens-per-reply braille chart, ranked tools with failures, directory/branch/files/compactions. (UNI-80)
24
+ - **Usage**: today / week / month spend, 30-day column chart with Monday ticks, model share bar + ranked models. (UNI-81)
25
+ - **Tools / Skills / Modules**: live from pi (`getAllTools`, `getCommands`), source share bars, column grids, active vs. off; modules with versions and measured load time. (UNI-82)
26
+ - **MCP, Compactor, Updates, Web, Memory, Keys**: status dots, compaction before→after bars, version track, provider × capability matrix, memory types, keycaps. (UNI-83)
27
+ - `core`: `viz` kit (`Paint`, `bigText`, `gradient`, `spark`, `columns`, `brailleArea`, `gauge`, `shareBar`, `chip`, `grid`, …) — every piece returns exact-width lines in truecolor or 256-colour. (UNI-77)
28
+ - `scripts/info-preview`: print the splash and every page at any width without Pi.
29
+ - `info-screen`: **scope tags** `s` session / `p` project / `g` global on every scoped number (legend in the footer); project pages (memory, mcp, compactor) are cached per workspace, so one project's numbers never show in another. MCP servers show whether they come from global or project config. (UNI-91)
30
+ - `info-screen` / `core`: **compaction savings** — tokens not re-sent (saved × replies after each compaction) and ≈ money not spent at the context rate actually paid, for the session, the project and all projects (`core/src/compaction-savings.ts`; the usage parser collects it per session file). Compactor page: savings tiles, without-vs-with bars, per-compaction timeline. (UNI-92)
31
+ - `core`: 37 new Unicrab hints (127 in total). Catch-up tips for what shipped since the hints release — `/unipi:summarize`, `/unipi:info`, `/unipi:regular`, `Ctrl+O` in simple mode, **E**/**M** in `/unipi:skills`, the splash setting, the `s`/`p`/`g` tags — and 8 "what's new" tips tied to alpha.22–alpha.25 (shown once after you update). Eight new lore lines.
32
+ - `core`: **moment hints** — Unicrab reacts once a session to a late-night turn, 50 clean tool calls in a row, an `rm -rf`, a force push, a 40k+ character write, a ≥95% cache-hit reply, a finished goal, a deleted memory, new MCP tools and a ralph lap. The hint-count test is gone; only the "every named command exists" check stays.
33
+ - `long-horizon`: the owner-changed event now carries `reason` when an owner finishes.
34
+ - `info-screen`: **context breakdown** on This session — a bucket split into system prompt, tool schemas, your prompts, replies, tool results and summaries (from what pi will send next), window gauge on its edge, heaviest tool outputs. (UNI-93)
35
+
36
+ ### Changed
37
+ - `info-screen`: Usage chart labels its peak and today over the bars with a real date axis; one model → one line; model bars are true shares. (UNI-94)
38
+ - `info-screen`: Modules page shows what each module adds (tools, commands, settings, keys — recorded exactly by `packages/unipi` during load) instead of millisecond bars; load time is flagged only past 20 ms. (UNI-95)
39
+ - harness: prompts that UniPi sends for you (`/unipi:goal`/`ralph`/`swarm`/`graph` kickoffs, `/unipi:summarize`, `/unipi:answer`, the compactor follow-up, the ask-user handoff) render as a UniPi harness panel instead of a plain user message. The skills reveal names the skills in simple mode and lists them in advanced mode. (UNI-88, UNI-89)
40
+ - `info-screen`: `bootMode` / `bootTimeoutMs` now control the splash (default 2.5 s). `groupOrder` and per-page show/hide are honoured and re-read when the dashboard opens. Duplicate tab priorities fixed by one page-style table.
41
+
42
+ ### Docs
43
+ - README: new **See your session** section with screenshots of the splash and the dashboard (demo data), a **What's new** table for the recent releases, and two new rows in "Why UniPi". The Info Screen README explains the context bucket and how compaction savings are counted.
44
+ - `scripts/info-preview/demo.ts`: demo session and history for screenshots — no real paths or spend.
45
+
46
+ ### Performance
47
+ - `info-screen`: page data is saved to `~/.unipi/cache/info-screen.json`, so the dashboard opens filled; page bodies are memoised; the session page walks the transcript once per new entry. Usage refresh on a 1,500-session history went from ~190 ms to ~35 ms warm (in-memory parser cache, debounced cache writes, no per-record string keys). (UNI-84)
48
+
9
49
  ## [3.0.0-alpha.24] — 2026-10-04
10
50
 
11
51
  ### Added
package/README.md CHANGED
@@ -64,6 +64,8 @@ runs commands. UniPi adds the parts that a long session needs.
64
64
  | **A board for deferred work** | Kanboard keeps tasks in Markdown files, with a web UI and a terminal UI. |
65
65
  | **Hang detection** | Watchdog asks a small judge model about long tool calls. |
66
66
  | **A short transcript** | Simple mode shows one line for each tool call. Ctrl+O expands it. |
67
+ | **A view of the session** | `/unipi:info` shows cost, context use, spend history and what compaction saved. |
68
+ | **A recap at the end** | The `summarize` skill gives the answer first, then open items, in plain words. |
67
69
 
68
70
  ## Proof
69
71
 
@@ -118,6 +120,61 @@ review or answer a blocked question in one click.
118
120
 
119
121
  The screenshots use demo data. Read the [Kanboard README](packages/kanboard/README.md).
120
122
 
123
+ ## See your session: `/unipi:info`
124
+
125
+ Pi starts. Unicrab says hello and gives you three facts: how fast Pi got
126
+ ready, the session you came back to, and what you spent today. Then it goes
127
+ away. It does not take your keys, so you can type your first prompt at once.
128
+
129
+ <p align="center">
130
+ <img src="docs/assets/screenshots/info-splash.png" width="760" alt="The Unicrab startup splash: mascot, UNIPI wordmark, version, ready time, resumed session and today's spend">
131
+ </p>
132
+
133
+ Run `/unipi:info` when you want the full picture. The first page is about
134
+ **this session**. It answers the questions you ask during long work:
135
+
136
+ - **What did this cost?** Cost, tokens, replies and time, in large digits.
137
+ - **What fills my context?** A bucket shows each part in its own colour:
138
+ system prompt, tool schemas, summaries, your prompts, replies and tool
139
+ results. The edge of the bucket shows how full the window is. It turns
140
+ amber at 70% and red at 90%.
141
+ - **Which tool output is the heaviest?** The page names the three largest.
142
+ - **Which tools failed?** Each tool gets a bar, and failures show in red.
143
+
144
+ <p align="center">
145
+ <img src="docs/assets/screenshots/info-session.png" alt="The This session page: cost, tokens, replies, time, the context bucket, billed tokens and tools">
146
+ </p>
147
+
148
+ The other pages answer the questions that come later:
149
+
150
+ | Page | It shows |
151
+ |---|---|
152
+ | Usage | Spend today, this week and this month, a 30-day chart, and the share of each model. |
153
+ | Compactor | Tokens that compaction kept out of your requests, and the money that saved. |
154
+ | Tools · Skills · Modules | What Pi loaded, where it came from, and what each UniPi module adds. |
155
+ | MCP · Memory · Web · Updates · Keys | The state of each module. |
156
+
157
+ <table>
158
+ <tr>
159
+ <td><img src="docs/assets/screenshots/info-usage.png" alt="The Usage page: spend tiles, a 30-day chart with the peak labelled, and models this month"></td>
160
+ <td><img src="docs/assets/screenshots/info-compactor.png" alt="The Compactor page: money saved for this session, this project and all projects, and a without-versus-with bar"></td>
161
+ </tr>
162
+ </table>
163
+
164
+ A small letter before a number tells you its scope: **`s`** is this session,
165
+ **`p`** is this project and **`g`** is every project on this machine. Numbers
166
+ from one project never show in another.
167
+
168
+ How the Compactor page counts: each compaction makes the context smaller.
169
+ Every reply after it sends that smaller context. The page multiplies the
170
+ tokens removed by the number of replies that followed. Then it prices them at
171
+ the rate you paid for context. A free model saves tokens but no money.
172
+
173
+ Keys: `←`/`→` or `1`–`9` change the page, `r` refreshes it, `q` closes it.
174
+ `/unipi:info usage` opens one page. The dashboard opens at once on cached
175
+ numbers, then updates them in the background. The screenshots use demo data.
176
+ Read the [Info Screen README](packages/info-screen/README.md).
177
+
121
178
  ## Harness architecture
122
179
 
123
180
  UniPi adds these mechanisms to the Pi harness. Each page gives the problem, a
@@ -167,7 +224,9 @@ each package.
167
224
 
168
225
  Unicrab is the UniPi mascot. It started as the author's pet crab. Now it says
169
226
  hello when Pi starts and leaves hints above the input box. Press `Alt+H` for the
170
- next hint.
227
+ next hint, or run `/unipi:hint` to read all 127 of them.
228
+
229
+ After an update, the first hint tells you what is new in that release.
171
230
 
172
231
  Read [The making of Unicrab](docs/story/unicrab.md): from a real crab, to pixel
173
232
  art, to a terminal character that is 7 columns wide.
@@ -176,6 +235,17 @@ art, to a terminal character that is 7 columns wide.
176
235
  <img src="docs/assets/screenshots/unicrab-start.png" alt="The UniPi start screen with Unicrab in half-block pixel art">
177
236
  </p>
178
237
 
238
+ ## What's new
239
+
240
+ The recent releases, in short. The [changelog](CHANGELOG.md) has the details.
241
+
242
+ | Release | What changed for you |
243
+ |---|---|
244
+ | alpha.25 | The Unicrab splash at startup. A new `/unipi:info`: this session, the context bucket, compaction savings and `s`/`p`/`g` scope tags. Prompts that UniPi sends for you (goal, summarize, answer) show as a UniPi panel, not as your own message. |
245
+ | alpha.24 | The `summarize` skill and `/unipi:summarize [focus]`. Long work ends with a short recap: the answer first, then findings, open items and questions. |
246
+ | alpha.23 | `/unipi:settings` explains each setting and each option in a panel below the list. Web tools with no working provider hide from the agent. |
247
+ | alpha.22 | Simple mode is the default. Each tool call is one line, and `Ctrl+O` opens it. Images no longer overlap the text below them. |
248
+
179
249
  ## Docs
180
250
 
181
251
  - [Getting started](docs/guide/getting-started.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "3.0.0-alpha.24",
3
+ "version": "3.0.0-alpha.26",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,6 +44,7 @@
44
44
  "packages/*/bridge/**/*.py",
45
45
  "packages/*/platforms/*",
46
46
  "packages/*/tui/*",
47
+ "packages/*/pages/*.ts",
47
48
  "packages/*/SKILL.md",
48
49
  "packages/*/skills/**/*",
49
50
  "packages/*/README.md",
@@ -82,26 +83,26 @@
82
83
  "typebox": "^1.1.38"
83
84
  },
84
85
  "dependencies": {
85
- "@pi-unipi/ask-user": "3.0.0-alpha.24",
86
- "@pi-unipi/background-tasks": "3.0.0-alpha.24",
87
- "@pi-unipi/btw": "3.0.0-alpha.24",
88
- "@pi-unipi/command-enchantment": "3.0.0-alpha.24",
89
- "@pi-unipi/compactor": "3.0.0-alpha.24",
90
- "@pi-unipi/core": "3.0.0-alpha.24",
91
- "@pi-unipi/footer": "3.0.0-alpha.24",
92
- "@pi-unipi/fusion": "3.0.0-alpha.24",
93
- "@pi-unipi/info-screen": "3.0.0-alpha.24",
94
- "@pi-unipi/input-shortcuts": "3.0.0-alpha.24",
95
- "@pi-unipi/kanboard": "3.0.0-alpha.24",
96
- "@pi-unipi/mcp": "3.0.0-alpha.24",
97
- "@pi-unipi/memory": "3.0.0-alpha.24",
98
- "@pi-unipi/notify": "3.0.0-alpha.24",
99
- "@pi-unipi/skill-registry": "3.0.0-alpha.24",
100
- "@pi-unipi/subagents": "3.0.0-alpha.24",
101
- "@pi-unipi/updater": "3.0.0-alpha.24",
102
- "@pi-unipi/utility": "3.0.0-alpha.24",
103
- "@pi-unipi/web-api": "3.0.0-alpha.24",
104
- "@pi-unipi/workflow": "3.0.0-alpha.24"
86
+ "@pi-unipi/ask-user": "3.0.0-alpha.26",
87
+ "@pi-unipi/background-tasks": "3.0.0-alpha.26",
88
+ "@pi-unipi/btw": "3.0.0-alpha.26",
89
+ "@pi-unipi/command-enchantment": "3.0.0-alpha.26",
90
+ "@pi-unipi/compactor": "3.0.0-alpha.26",
91
+ "@pi-unipi/core": "3.0.0-alpha.26",
92
+ "@pi-unipi/footer": "3.0.0-alpha.26",
93
+ "@pi-unipi/fusion": "3.0.0-alpha.26",
94
+ "@pi-unipi/info-screen": "3.0.0-alpha.26",
95
+ "@pi-unipi/input-shortcuts": "3.0.0-alpha.26",
96
+ "@pi-unipi/kanboard": "3.0.0-alpha.26",
97
+ "@pi-unipi/mcp": "3.0.0-alpha.26",
98
+ "@pi-unipi/memory": "3.0.0-alpha.26",
99
+ "@pi-unipi/notify": "3.0.0-alpha.26",
100
+ "@pi-unipi/skill-registry": "3.0.0-alpha.26",
101
+ "@pi-unipi/subagents": "3.0.0-alpha.26",
102
+ "@pi-unipi/updater": "3.0.0-alpha.26",
103
+ "@pi-unipi/utility": "3.0.0-alpha.26",
104
+ "@pi-unipi/web-api": "3.0.0-alpha.26",
105
+ "@pi-unipi/workflow": "3.0.0-alpha.26"
105
106
  },
106
107
  "devDependencies": {
107
108
  "@earendil-works/pi-agent-core": "^0.87.1",
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+ import { sendHarnessUserMessage } from "@pi-unipi/core";
8
9
  import type { SessionLaunchReason, SessionLaunchStatus } from "./types.js";
9
10
 
10
11
  /** Compact handoff fallback timer. Keeps the launcher from stalling if callbacks wait for the tool turn to finish. */
@@ -80,7 +81,7 @@ function deliverFollowUpMessage(
80
81
  reason: SessionLaunchReason,
81
82
  ): HandoffResult {
82
83
  try {
83
- pi.sendUserMessage(prefill, { deliverAs: "followUp" });
84
+ sendHarnessUserMessage(pi, prefill, { source: "Ask user", title: "Handoff", synopsis: "Prefill from the launcher" }, { deliverAs: "followUp" });
84
85
  return { status: "queued", reason, prefill };
85
86
  } catch (error) {
86
87
  return fallbackToEditor(ctx, prefill, reason, error);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "3.0.0-alpha.24",
3
+ "version": "3.0.0-alpha.26",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@pi-unipi/core": "3.0.0-alpha.24"
43
+ "@pi-unipi/core": "3.0.0-alpha.26"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.87.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "3.0.0-alpha.24",
3
+ "version": "3.0.0-alpha.26",
4
4
  "description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -171,7 +171,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
171
171
 
172
172
  "unipi:settings": "Configure all unipi modules in one panel",
173
173
 
174
- "unipi:info": "Show system information",
174
+ "unipi:info": "Dashboard — this session, context, usage, compaction savings, modules",
175
175
  "unipi:hint": "Browse and search Unicrab hints, or cycle/reset",
176
176
 
177
177
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "3.0.0-alpha.24",
3
+ "version": "3.0.0-alpha.26",
4
4
  "description": "Background tasks for UniPi — durable shell jobs and delegated background agents with a footer dock",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@earendil-works/pi-ai": "^0.87.1",
13
- "@pi-unipi/core": "3.0.0-alpha.24",
13
+ "@pi-unipi/core": "3.0.0-alpha.26",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "3.0.0-alpha.24",
3
+ "version": "3.0.0-alpha.26",
4
4
  "description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/btw.ts",
@@ -37,7 +37,7 @@
37
37
  "@earendil-works/pi-tui": "^0.87.1"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "3.0.0-alpha.24"
40
+ "@pi-unipi/core": "3.0.0-alpha.26"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "3.0.0-alpha.24",
3
+ "version": "3.0.0-alpha.26",
4
4
  "description": "Compaction for Pi — lossless zero-LLM or model summaries, rebuilt from full history, with session recall",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,8 +33,8 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "3.0.0-alpha.24",
37
- "@pi-unipi/info-screen": "3.0.0-alpha.24",
36
+ "@pi-unipi/core": "3.0.0-alpha.26",
37
+ "@pi-unipi/info-screen": "3.0.0-alpha.26",
38
38
  "@earendil-works/pi-agent-core": "^0.87.1"
39
39
  },
40
40
  "peerDependencies": {
@@ -14,7 +14,7 @@
14
14
 
15
15
  import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent, SessionCompactEvent } from "@earendil-works/pi-coding-agent";
16
16
  import { compact as piCompact, generateSummaryWithUsage, DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
17
- import { collectCompactionContext, emitEvent, formatTokens, UNIPI_EVENTS } from "@pi-unipi/core";
17
+ import { collectCompactionContext, emitEvent, formatTokens, sendHarnessUserMessage, UNIPI_EVENTS } from "@pi-unipi/core";
18
18
  import { loadConfig } from "../config/manager.js";
19
19
  import { autoCompactionOf } from "../config/schema.js";
20
20
  import { buildOwnCut, resolveSmartKeepUserTurns, applyTailBudget, MAX_SMART_TAIL_TOKENS } from "./cut.js";
@@ -444,7 +444,7 @@ export function registerCompactionHooks(pi: ExtensionAPI, deps: CompactionHookDe
444
444
  if (followUp) {
445
445
  setTimeout(() => {
446
446
  try {
447
- pi.sendUserMessage(followUp, { deliverAs: "followUp" });
447
+ sendHarnessUserMessage(pi, followUp, { source: "Compactor", title: "Follow-up", synopsis: "Prompt queued after compaction" }, { deliverAs: "followUp" });
448
448
  } catch {}
449
449
  }, 0);
450
450
  }
@@ -3,17 +3,33 @@
3
3
  * compactions, read from the session branch.
4
4
  */
5
5
 
6
- import { formatTokens } from "@pi-unipi/core";
6
+ import { compactionSavings, formatTokens, type CompactionSaving, type SavingsTotals } from "@pi-unipi/core";
7
7
  import { loadConfig } from "./config/manager.js";
8
8
  import { methodBreakdown, sessionCompactionStats } from "./stats.js";
9
9
 
10
- type Stat = { value: string; detail: string };
10
+ type Stat = { value: string; detail: string; raw?: unknown };
11
+
12
+ /** Structured payload for the info-screen Compactor page renderer. */
13
+ export interface CompactorPageRaw {
14
+ method: string;
15
+ trigger: string;
16
+ piCompact: string;
17
+ history: Array<{ before: number; after: number; method: string; at?: number }>;
18
+ before: number;
19
+ after: number;
20
+ saved: number;
21
+ /** This session's savings (tokens not re-sent + money). */
22
+ session: SavingsTotals;
23
+ /** Per-compaction savings for the timeline. */
24
+ items: CompactionSaving[];
25
+ }
11
26
 
12
27
  export interface CompactorInfoData {
13
28
  settings: Stat;
14
29
  compactions: Stat;
15
30
  tokens: Stat;
16
31
  last: Stat;
32
+ raw: Stat;
17
33
  }
18
34
 
19
35
  const METHOD_NAME = { vcc: "lossless", llm: "model summary" } as const;
@@ -32,7 +48,21 @@ export function getInfoScreenData(branch: readonly any[], cwd: string = process.
32
48
  const count = stats.compactions.length;
33
49
  const last = stats.compactions[count - 1];
34
50
  const pct = stats.tokensBefore > 0 ? Math.round((stats.tokensSaved / stats.tokensBefore) * 100) : 0;
51
+ const raw: CompactorPageRaw = {
52
+ method: METHOD_NAME[config.method],
53
+ trigger: config.trigger === "pi" ? "at Pi's context limit" : `at ${config.thresholdPercent}% of context`,
54
+ piCompact: config.piCompact === "follow" ? "same as method" : METHOD_NAME[config.piCompact],
55
+ history: stats.compactions.slice(-24).map((c) => ({ before: c.tokensBefore, after: c.tokensAfter, method: METHOD_NAME[c.method] ?? c.method, at: c.at })),
56
+ before: stats.tokensBefore,
57
+ after: stats.tokensAfter,
58
+ saved: stats.tokensSaved,
59
+ ...(() => {
60
+ const s = compactionSavings(branch);
61
+ return { session: s.totals, items: s.items.slice(-24) };
62
+ })(),
63
+ };
35
64
  return {
65
+ raw: { value: "", detail: "", raw },
36
66
  settings: {
37
67
  value: METHOD_NAME[config.method],
38
68
  detail: `when: ${config.trigger === "pi" ? "Pi's context limit" : `${config.thresholdPercent}% of context`} · Pi's /compact: ${config.piCompact === "follow" ? "same as method" : METHOD_NAME[config.piCompact]}`,
@@ -70,7 +70,13 @@ Unicrab is the UniPi mascot. The hints engine shows one short tip above the
70
70
  editor.
71
71
 
72
72
  - At startup, the engine shows the tip with the lowest show count. After a
73
- version upgrade, it shows a "what's new" tip first.
73
+ version upgrade, it shows a "what's new" tip first. Each release adds its
74
+ own "what's new" tips, so you learn about a feature when it arrives.
75
+ - The engine has 127 tips in 9 categories: commands, shortcuts, settings,
76
+ capabilities, explanations, troubleshooting, what's new, workflows and lore.
77
+ - Some tips react to a moment, one time per session. Examples are a late-night
78
+ turn, 50 tool calls with no error, an `rm -rf`, a force push, a hot prompt
79
+ cache and a finished goal.
74
80
  - The start screen shows the Unicrab pixel art, the version and a random lore
75
81
  line. It needs 40 columns or more. At 72 columns or more, it shows the large crab.
76
82
  - Some events show a tip. Examples are context use of 70% or more, a `bash`
@@ -31,7 +31,7 @@
31
31
  * boundaries purge everything outstanding — the failure mode is always native
32
32
  * fallback, never a human message mislabelled.
33
33
  */
34
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
34
+ import { parseSkillBlock, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
35
35
 
36
36
  export type HarnessDelivery =
37
37
  | "direct"
@@ -47,6 +47,8 @@ export interface HarnessMessageMeta {
47
47
  source: string;
48
48
  title: string;
49
49
  synopsis?: string;
50
+ /** Optional pre-rendered summary rows (advanced style renders one row each). */
51
+ lines?: string[];
50
52
  delivery: HarnessDelivery;
51
53
  severity?: "warning";
52
54
  }
@@ -172,6 +174,10 @@ export function readHarnessMeta(source: unknown): HarnessMessageMeta | undefined
172
174
  ) {
173
175
  return undefined;
174
176
  }
177
+ // Optional summary lines: malformed ones are dropped, the meta stays valid.
178
+ if (m.lines !== undefined && !(Array.isArray(m.lines) && m.lines.every((l) => typeof l === "string"))) {
179
+ return { ...m, lines: undefined } as HarnessMessageMeta;
180
+ }
175
181
  return m as HarnessMessageMeta;
176
182
  }
177
183
 
@@ -185,6 +191,24 @@ export function userMessageText(content: unknown): string {
185
191
  .join("");
186
192
  }
187
193
 
194
+ /** Match an admission record's text against a dispatched text. Exact by
195
+ * default; a `/skill:<name> <rest>` record additionally matches its
196
+ * POST-EXPANSION form — pi turns the command into a `<skill …>` block AFTER
197
+ * the input handlers ran (AgentSession._expandSkillCommand), so the
198
+ * before_agent_start prompt and the message user text no longer equal the
199
+ * armed text. Everything else stays exact; no match = native (fail closed). */
200
+ function recordMatchesText(recordText: string, text: string): boolean {
201
+ if (recordText === text) return true;
202
+ if (!recordText.startsWith("/skill:")) return false;
203
+ const skill = parseSkillBlock(text);
204
+ if (!skill) return false;
205
+ // Mirror _expandSkillCommand's split: name up to the first space, rest trimmed.
206
+ const spaceIndex = recordText.indexOf(" ");
207
+ const name = spaceIndex === -1 ? recordText.slice(7) : recordText.slice(7, spaceIndex);
208
+ const rest = spaceIndex === -1 ? "" : recordText.slice(spaceIndex + 1).trim();
209
+ return skill.name === name && (skill.userMessage ?? "") === rest;
210
+ }
211
+
188
212
  function userTextFromMessage(message: unknown): string {
189
213
  if (!message || typeof message !== "object") return "";
190
214
  const m = message as { role?: unknown; content?: unknown };
@@ -203,7 +227,7 @@ export function sendHarnessUserMessage(
203
227
  pi: ExtensionAPI,
204
228
  content: string,
205
229
  meta: HarnessMessageMetaInput,
206
- options?: { deliverAs?: "steer" | "followUp" },
230
+ options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
207
231
  ): ReturnType<ExtensionAPI["sendUserMessage"]> {
208
232
  let arm: Arm | undefined;
209
233
  try {
@@ -253,7 +277,7 @@ function prune(state: HarnessRegistryState): void {
253
277
  * matching group. */
254
278
  function selectRecord(state: HarnessRegistryState, text: string): HarnessMessageMeta | undefined {
255
279
  const candidates = state.records.filter(
256
- (r) => r.state === "admitted" && r.text === text && (r.kind !== "harness" || r.eligible),
280
+ (r) => r.state === "admitted" && recordMatchesText(r.text, text) && (r.kind !== "harness" || r.eligible),
257
281
  );
258
282
  if (candidates.length === 0) return undefined;
259
283
  const origins = new Set(candidates.map((r) => r.origin));
@@ -336,7 +360,7 @@ export function installHarnessProvenance(pi: ExtensionAPI): void {
336
360
  // unconfirmed direct records are stale and dropped. Queued untouched.
337
361
  for (const r of state.records) {
338
362
  if (r.kind === "harness" && r.delivery === "direct" && r.state === "admitted") {
339
- if (r.text === prompt) r.eligible = true;
363
+ if (recordMatchesText(r.text, prompt)) r.eligible = true;
340
364
  }
341
365
  }
342
366
  state.records = state.records.filter(
@@ -28,6 +28,8 @@ export * from "./src/package-colors.js";
28
28
  export * from "./src/tui/hub-kit.js";
29
29
  export * from "./src/tui/kit.js";
30
30
  export * from "./src/tui/progress.js";
31
+ export * from "./src/tui/viz.js";
32
+ export * from "./src/compaction-savings.js";
31
33
  export * from "./src/jev/client.js";
32
34
  export * from "./src/jev/settings.js";
33
35
  export * from "./src/jev/chatter.js";
@@ -45,4 +47,5 @@ export * from "./compaction-context.js";
45
47
  export * from "./src/settings/migrations.js";
46
48
  export * from "./src/hints/index.js";
47
49
  export * from "./src/hints/store.js";
50
+ export { CRAB_14_LINES_256, CRAB_14_LINES_TRUECOLOR, CRAB_22_LINES_256, CRAB_22_LINES_TRUECOLOR } from "./src/hints/crab-data.js";
48
51
  export * from "./harness-messages.js";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "3.0.0-alpha.24",
3
+ "version": "3.0.0-alpha.26",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @pi-unipi/core — what compaction saved, from session entries.
3
+ *
4
+ * For every compaction:
5
+ * saved = context before − context after
6
+ * replies = assistant replies sent after it (until the next compaction)
7
+ * avoided = saved × replies tokens that were NOT re-sent
8
+ * rate = Σ paid for context / Σ context tokens of those replies
9
+ * (input + cache read + cache write — the blended price the
10
+ * user actually paid per context token, so cache discounts
11
+ * are already in it)
12
+ * dollars = avoided × rate ≈ money not spent
13
+ *
14
+ * "after" is the compactor's own `details.tokensAfter` when present, else the
15
+ * context size the first reply after the compaction actually reported — the
16
+ * real number, not an estimate.
17
+ *
18
+ * Feed entries in file/branch order; works streaming (usage parser) or over
19
+ * a branch array (info screen).
20
+ */
21
+
22
+ export interface CompactionSaving {
23
+ at: number;
24
+ before: number;
25
+ after: number;
26
+ replies: number;
27
+ /** Σ cost paid for context tokens by the replies after this compaction. */
28
+ paid: number;
29
+ /** Σ context tokens of those replies. */
30
+ context: number;
31
+ }
32
+
33
+ export interface SavingsTotals {
34
+ count: number;
35
+ saved: number;
36
+ avoided: number;
37
+ dollars: number;
38
+ /** Σ context actually sent after compactions (the "with" bar). */
39
+ sent: number;
40
+ }
41
+
42
+ export class SavingsAccumulator {
43
+ readonly items: CompactionSaving[] = [];
44
+ private seen = new Set<string>();
45
+ private open: CompactionSaving | null = null;
46
+ private afterKnown = false;
47
+
48
+ feed(entry: unknown): void {
49
+ const e = entry as { type?: string; timestamp?: string; tokensBefore?: number; details?: { tokensAfter?: number }; message?: any };
50
+ if (!e || typeof e !== "object") return;
51
+ if (e.type === "compaction") {
52
+ const at = Date.parse(e.timestamp ?? "") || 0;
53
+ const before = Number(e.tokensBefore ?? 0);
54
+ const key = `${at}:${before}`;
55
+ // Forked session files copy history; count each compaction once.
56
+ if (this.seen.has(key)) {
57
+ this.open = null;
58
+ return;
59
+ }
60
+ this.seen.add(key);
61
+ const after = Number(e.details?.tokensAfter ?? 0);
62
+ this.open = { at, before, after, replies: 0, paid: 0, context: 0 };
63
+ this.afterKnown = after > 0;
64
+ this.items.push(this.open);
65
+ return;
66
+ }
67
+ if (e.type !== "message" || !this.open) return;
68
+ const m = e.message;
69
+ if (m?.role !== "assistant" || m.stopReason === "error" || m.stopReason === "aborted") return;
70
+ const u = m.usage ?? {};
71
+ const ctx = (u.input ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0);
72
+ if (ctx <= 0) return;
73
+ if (!this.afterKnown) {
74
+ this.open.after = ctx;
75
+ this.afterKnown = true;
76
+ }
77
+ this.open.replies++;
78
+ this.open.context += ctx;
79
+ const c = u.cost ?? {};
80
+ this.open.paid += (c.input ?? 0) + (c.cacheRead ?? 0) + (c.cacheWrite ?? 0);
81
+ }
82
+
83
+ totals(): SavingsTotals {
84
+ return totalsOf(this.items);
85
+ }
86
+ }
87
+
88
+ export function savedOf(c: CompactionSaving): number {
89
+ return Math.max(0, c.before - (c.after || c.before));
90
+ }
91
+
92
+ export function avoidedOf(c: CompactionSaving): number {
93
+ return savedOf(c) * c.replies;
94
+ }
95
+
96
+ export function dollarsOf(c: CompactionSaving): number {
97
+ return c.context > 0 ? avoidedOf(c) * (c.paid / c.context) : 0;
98
+ }
99
+
100
+ export function totalsOf(items: readonly CompactionSaving[]): SavingsTotals {
101
+ let saved = 0, avoided = 0, dollars = 0, sent = 0;
102
+ for (const c of items) {
103
+ saved += savedOf(c);
104
+ avoided += avoidedOf(c);
105
+ dollars += dollarsOf(c);
106
+ sent += c.context;
107
+ }
108
+ return { count: items.length, saved, avoided, dollars, sent };
109
+ }
110
+
111
+ export function addTotals(a: SavingsTotals, b: SavingsTotals): SavingsTotals {
112
+ return { count: a.count + b.count, saved: a.saved + b.saved, avoided: a.avoided + b.avoided, dollars: a.dollars + b.dollars, sent: a.sent + b.sent };
113
+ }
114
+
115
+ export const EMPTY_SAVINGS: SavingsTotals = { count: 0, saved: 0, avoided: 0, dollars: 0, sent: 0 };
116
+
117
+ /** Convenience: savings for a whole branch. */
118
+ export function compactionSavings(branch: readonly unknown[]): { items: CompactionSaving[]; totals: SavingsTotals } {
119
+ const acc = new SavingsAccumulator();
120
+ for (const e of branch) acc.feed(e);
121
+ return { items: acc.items, totals: acc.totals() };
122
+ }