@pi-unipi/unipi 2.2.6 → 2.3.0
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.
- package/CHANGELOG.md +40 -0
- package/package.json +31 -25
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +1 -1
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/info-screen.ts +4 -4
- package/packages/core/package.json +1 -1
- package/packages/core/utils.ts +37 -0
- package/packages/footer/package.json +2 -2
- package/packages/image/package.json +2 -2
- package/packages/info-screen/README.md +4 -4
- package/packages/info-screen/config.ts +28 -8
- package/packages/info-screen/core-groups.ts +5 -39
- package/packages/info-screen/index.ts +25 -10
- package/packages/info-screen/package.json +2 -2
- package/packages/info-screen/tui/info-overlay.ts +114 -38
- package/packages/info-screen/types.ts +20 -5
- package/packages/info-screen/usage-parser.ts +318 -128
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/index.ts +60 -22
- package/packages/memory/mempalace.ts +66 -1
- package/packages/memory/package.json +3 -3
- package/packages/memory/storage.ts +75 -12
- package/packages/milestone/package.json +2 -2
- package/packages/notify/package.json +2 -2
- package/packages/ralph/package.json +3 -3
- package/packages/subagents/package.json +4 -4
- package/packages/unipi/bundled.js +37694 -0
- package/packages/updater/package.json +2 -2
- package/packages/utility/package.json +2 -2
- package/packages/utility/src/tools/env.ts +1 -22
- package/packages/web-api/package.json +2 -2
- 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
|
+
## [2.3.0] — 2026-08-07
|
|
10
|
+
|
|
11
|
+
Startup went from **23.1s to 0.75s** — 31× faster, and within ~0.7s of bare `pi` with no extensions at all. A cold start (empty cache) is now the same speed as a warm one.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **`info-screen`: `showOnBoot` is now `bootMode`, with three states.** `"on"` keeps the dashboard up until you dismiss it, `"auto-close"` (the new default) closes it after `bootTimeoutMs`, and `"off"` never builds it at all. All three are configurable from `/unipi:info-settings` — `←/→` cycles the mode, and the row below adjusts the delay in 0.5s steps (clamped 0.5–30s). Any keypress cancels the auto-close, so the dashboard stays put if you are reading it. `/unipi:info` opened by hand never auto-closes.
|
|
16
|
+
|
|
17
|
+
Existing configs migrate automatically on read: `showOnBoot: true` → `"on"`, `false` → `"off"`. Note that `true` maps to `"on"` rather than the new `"auto-close"` default, so nobody's startup behaviour changes underneath them. An explicit `bootMode` always wins over the legacy key.
|
|
18
|
+
|
|
19
|
+
- **`info-screen`: dashboard panels load lazily.** Only the visible tab fetches at boot; the rest are prefetched 1.5s later, and any tab opened before that fetches on demand. Previously all ~17 panels were built before the first prompt was ready, including when the dashboard was disabled entirely.
|
|
20
|
+
|
|
21
|
+
- **`memory`: orphaned markdown is imported on first use rather than at startup.** `syncOrphanedFiles()` spawns a Python bridge; nothing reads its result until memory is actually used. A `.md` file dropped into a project directory is now picked up by the first memory tool call instead of at boot. Still runs exactly once per session; nothing is lost.
|
|
22
|
+
|
|
23
|
+
- **`unipi`: the all-in-one entry now ships as a prebuilt bundle.** pi loads extensions through jiti with its module cache disabled, so every startup transpiled ~577 TypeScript files from scratch — the factory calls themselves take 5ms, the cost was entirely transpilation. `pi.extensions` points at `packages/unipi/bundled.js`, built by `npm run build` and regenerated automatically by `prepublishOnly`. Only `@pi-unipi` sources are bundled; every third-party dependency stays external and resolves from `node_modules` at runtime.
|
|
24
|
+
|
|
25
|
+
### Performance
|
|
26
|
+
|
|
27
|
+
- **`info-screen`: usage stats are cached per file (8,671ms → 70ms).** `parseUsageStats()` re-read and re-parsed the entire session history on every call — 2.6GB across 600 JSONL files — and ran twice per startup. Results are now cached in `~/.unipi/cache/usage-stats.json`, keyed on each file's mtime and size. Statting 600 files costs ~1ms while only ~11 change on a busy day, so ~96% of the work was recomputing an immutable result.
|
|
28
|
+
|
|
29
|
+
This single function was the dominant startup cost. Because it never yielded, every other extension's `session_start` handler queued behind it — which is why earlier profiling rounds blamed unrelated extensions for ~9s each. They were waiting on this, not doing work.
|
|
30
|
+
|
|
31
|
+
- **`info-screen`: the usage parser no longer blocks the event loop.** A new `parseUsageStatsAsync()` yields via `setImmediate` between files, so even a cold parse cannot freeze keystrokes or the first paint. The reader also streams line-by-line instead of `readFileSync` — the largest single session file here is 213MB and was being materialized in full, along with its `split("\n")` array.
|
|
32
|
+
|
|
33
|
+
- **`memory`: MemPalace bridge calls are async (1,581ms → ~0ms of blocked loop).** The status-bar counts called `listAll()` and the cross-project listing through `spawnSync`, freezing the process for the full Python round-trip just to render `mem 32p/2262all`. Added `runBridgeAsync` plus `listAllAsync()` / `listAllProjectsCachedAsync()`; the status bar paints immediately and fills in when the bridge answers. The cross-project listing is additionally cached for 60s, invalidated on any store or delete.
|
|
34
|
+
|
|
35
|
+
- **`info-screen`/`utility`: pi's version is resolved without spawning a subprocess (710ms → ~1ms).** `getPiVersion()` probed a hardcoded path for one specific Node version and, on any other, fell through to `execSync("pi --version")` — starting a whole second pi process, synchronously, twice per startup. A shared implementation in `@pi-unipi/core` now walks up from pi's own entry point, resolving the symlink first (the binary on `PATH` is typically a shim), and caches the result.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- `info-screen`: the overview panel displayed **`Pi Version: unknown`**. The `execSync` fallback matched `/v([\d.]+)/`, requiring a literal `v` prefix that pi no longer emits — so it paid 710ms per startup *and* still failed. Now shows the real version.
|
|
40
|
+
- `info-screen`: `bootTimeoutMs` had existed in `InfoScreenSettings` and in user configs since the beginning but was never connected to anything. It now drives the auto-close timer.
|
|
41
|
+
- `info-screen`: `writeSettingsFile()` called `require()` from an ESM module, which throws under Node's module-format detection whenever the settings directory does not already exist.
|
|
42
|
+
- `unipi`: `diff` is a dependency of `@pi-unipi/utility` and lives in that package's own `node_modules`. That resolves fine under jiti (each file resolves from its own location) and fine when installed (npm hoists it), but was unresolvable from a single bundle in a local checkout. Now declared at the root.
|
|
43
|
+
|
|
44
|
+
## [2.2.7] — 2026-08-04
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- `ralph`/`subagents`/`memory`: these still pinned `@pi-unipi/info-screen@2.2.0`, so npm installed a second, **pre-fix** copy of the info-screen overlay nested under each of them — meaning the Escape-key fix released in 2.2.5 did not reach the info overlay for those install paths. Republished with the pin updated to `2.2.1`. No functional change to the packages themselves; this is purely a dependency-consistency fix.
|
|
48
|
+
|
|
9
49
|
## [2.2.6] — 2026-08-04
|
|
10
50
|
|
|
11
51
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/unipi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "All-in-one extension suite for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
"packages/*"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"
|
|
26
|
+
"build": "node scripts/build-bundle.mjs",
|
|
27
|
+
"prepare": "node scripts/build-bundle.mjs --if-missing",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"publish:all": "npm publish --workspaces --access public",
|
|
27
30
|
"test": "npx tsx --test tests/*.test.js && npm test --workspaces --if-present",
|
|
28
|
-
"
|
|
31
|
+
"typecheck": "npx tsc --noEmit --skipLibCheck"
|
|
29
32
|
},
|
|
30
33
|
"files": [
|
|
31
34
|
"CHANGELOG.md",
|
|
@@ -37,11 +40,12 @@
|
|
|
37
40
|
"packages/*/tui/*",
|
|
38
41
|
"packages/*/SKILL.md",
|
|
39
42
|
"packages/*/skills/**/*",
|
|
40
|
-
"packages/*/README.md"
|
|
43
|
+
"packages/*/README.md",
|
|
44
|
+
"packages/unipi/bundled.js"
|
|
41
45
|
],
|
|
42
46
|
"pi": {
|
|
43
47
|
"extensions": [
|
|
44
|
-
"packages/unipi/
|
|
48
|
+
"packages/unipi/bundled.js"
|
|
45
49
|
],
|
|
46
50
|
"skills": [
|
|
47
51
|
"packages/workflow/skills",
|
|
@@ -67,26 +71,26 @@
|
|
|
67
71
|
"typebox": "^1.1.38"
|
|
68
72
|
},
|
|
69
73
|
"dependencies": {
|
|
70
|
-
"@pi-unipi/ask-user": "2.
|
|
71
|
-
"@pi-unipi/btw": "2.
|
|
72
|
-
"@pi-unipi/compactor": "2.
|
|
73
|
-
"@pi-unipi/notify": "2.
|
|
74
|
-
"@pi-unipi/command-enchantment": "2.
|
|
75
|
-
"@pi-unipi/core": "2.
|
|
76
|
-
"@pi-unipi/info-screen": "2.
|
|
77
|
-
"@pi-unipi/mcp": "2.
|
|
78
|
-
"@pi-unipi/memory": "2.
|
|
79
|
-
"@pi-unipi/ralph": "2.
|
|
80
|
-
"@pi-unipi/subagents": "2.
|
|
81
|
-
"@pi-unipi/utility": "2.
|
|
82
|
-
"@pi-unipi/milestone": "2.
|
|
83
|
-
"@pi-unipi/kanboard": "2.
|
|
84
|
-
"@pi-unipi/web-api": "2.
|
|
85
|
-
"@pi-unipi/workflow": "2.
|
|
86
|
-
"@pi-unipi/footer": "2.
|
|
87
|
-
"@pi-unipi/updater": "2.
|
|
88
|
-
"@pi-unipi/input-shortcuts": "2.
|
|
89
|
-
"@pi-unipi/cocoindex": "2.
|
|
74
|
+
"@pi-unipi/ask-user": "2.3.0",
|
|
75
|
+
"@pi-unipi/btw": "2.3.0",
|
|
76
|
+
"@pi-unipi/compactor": "2.3.0",
|
|
77
|
+
"@pi-unipi/notify": "2.3.0",
|
|
78
|
+
"@pi-unipi/command-enchantment": "2.3.0",
|
|
79
|
+
"@pi-unipi/core": "2.3.0",
|
|
80
|
+
"@pi-unipi/info-screen": "2.3.0",
|
|
81
|
+
"@pi-unipi/mcp": "2.3.0",
|
|
82
|
+
"@pi-unipi/memory": "2.3.0",
|
|
83
|
+
"@pi-unipi/ralph": "2.3.0",
|
|
84
|
+
"@pi-unipi/subagents": "2.3.0",
|
|
85
|
+
"@pi-unipi/utility": "2.3.0",
|
|
86
|
+
"@pi-unipi/milestone": "2.3.0",
|
|
87
|
+
"@pi-unipi/kanboard": "2.3.0",
|
|
88
|
+
"@pi-unipi/web-api": "2.3.0",
|
|
89
|
+
"@pi-unipi/workflow": "2.3.0",
|
|
90
|
+
"@pi-unipi/footer": "2.3.0",
|
|
91
|
+
"@pi-unipi/updater": "2.3.0",
|
|
92
|
+
"@pi-unipi/input-shortcuts": "2.3.0",
|
|
93
|
+
"@pi-unipi/cocoindex": "2.3.0"
|
|
90
94
|
},
|
|
91
95
|
"devDependencies": {
|
|
92
96
|
"@earendil-works/pi-agent-core": "^0.80.0",
|
|
@@ -94,6 +98,8 @@
|
|
|
94
98
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
95
99
|
"@earendil-works/pi-tui": "^0.80.0",
|
|
96
100
|
"@types/node": "^25.6.0",
|
|
101
|
+
"diff": "^7.0.0",
|
|
102
|
+
"esbuild": "^0.27.7",
|
|
97
103
|
"tsx": "^4.21.0",
|
|
98
104
|
"typescript": "^6.0.0"
|
|
99
105
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ask-user",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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": "2.
|
|
43
|
+
"@pi-unipi/core": "2.3.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/btw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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.80.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@pi-unipi/core": "2.
|
|
40
|
+
"@pi-unipi/core": "2.3.0"
|
|
41
41
|
},
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/cocoindex",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "CocoIndex integration for Pi — AST-aware content indexing, semantic vector search, and incremental pipeline management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"typebox": "^1.1.38"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@pi-unipi/core": "2.
|
|
28
|
+
"@pi-unipi/core": "2.3.0"
|
|
29
29
|
},
|
|
30
30
|
"optionalDependencies": {
|
|
31
31
|
"@lancedb/lancedb": "^0.21.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/compactor",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
38
|
-
"@pi-unipi/info-screen": "2.
|
|
37
|
+
"@pi-unipi/core": "2.3.0",
|
|
38
|
+
"@pi-unipi/info-screen": "2.3.0",
|
|
39
39
|
"@earendil-works/pi-agent-core": "^0.80.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { SessionDB } from "./session/db.js";
|
|
14
14
|
import { getLastCompactionStats } from "./compaction/hooks.js";
|
|
15
|
-
import {
|
|
15
|
+
import { parseUsageStatsAsync } from "@pi-unipi/info-screen/usage-parser.js";
|
|
16
16
|
import type { RuntimeCounters } from "./types.js";
|
|
17
17
|
|
|
18
18
|
export interface CompactorInfoData {
|
|
@@ -42,9 +42,9 @@ function formatCost(n: number): string {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Estimate cost per token for the most-used model in the current session. */
|
|
45
|
-
function estimateCostPerToken(): number | null {
|
|
45
|
+
async function estimateCostPerToken(): Promise<number | null> {
|
|
46
46
|
try {
|
|
47
|
-
const usage =
|
|
47
|
+
const usage = await parseUsageStatsAsync();
|
|
48
48
|
// Use today's most-used model if available, otherwise all-time
|
|
49
49
|
const models = usage.byModelToday;
|
|
50
50
|
const todayKeys = Object.keys(models);
|
|
@@ -154,7 +154,7 @@ export async function getInfoScreenData(
|
|
|
154
154
|
: "No tool calls yet";
|
|
155
155
|
|
|
156
156
|
// ── Cost saved estimate ──
|
|
157
|
-
const costPerToken = estimateCostPerToken();
|
|
157
|
+
const costPerToken = await estimateCostPerToken();
|
|
158
158
|
const costSaved = costPerToken !== null ? tokensSaved * costPerToken : null;
|
|
159
159
|
|
|
160
160
|
// ── Last compaction details ──
|
package/packages/core/utils.ts
CHANGED
|
@@ -187,6 +187,43 @@ export function getInstalledPackageVersion(startDir: string, packageName: string
|
|
|
187
187
|
return getPackageVersion(root);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/** Cached pi version — resolved at most once per process. */
|
|
191
|
+
let cachedPiVersion: string | null = null;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get the running Pi agent's version.
|
|
195
|
+
*
|
|
196
|
+
* Resolves by walking up from Pi's own entry point (`process.argv[1]`), which
|
|
197
|
+
* must be `realpath`'d first: the executable on PATH is typically a symlink
|
|
198
|
+
* (e.g. mise shims `~/.local/share/mise/installs/node/lts/bin/pi`), and the
|
|
199
|
+
* package.json lives next to the *real* `dist/cli.js`, not the link.
|
|
200
|
+
*
|
|
201
|
+
* Never spawns a subprocess. A previous implementation fell back to
|
|
202
|
+
* `execSync("pi --version")`, which cost ~350ms per call and still returned
|
|
203
|
+
* "unknown" because it matched against a `v` prefix that Pi no longer emits.
|
|
204
|
+
*/
|
|
205
|
+
export function getPiVersion(): string {
|
|
206
|
+
if (cachedPiVersion !== null) return cachedPiVersion;
|
|
207
|
+
|
|
208
|
+
const PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
209
|
+
const entry = process.argv[1];
|
|
210
|
+
if (entry) {
|
|
211
|
+
try {
|
|
212
|
+
const realEntry = fs.realpathSync(entry);
|
|
213
|
+
const root = findPackageRoot(path.dirname(realEntry), PI_PACKAGE);
|
|
214
|
+
if (root) {
|
|
215
|
+
cachedPiVersion = getPackageVersion(root);
|
|
216
|
+
return cachedPiVersion;
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Fall through to "unknown".
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
cachedPiVersion = "unknown";
|
|
224
|
+
return cachedPiVersion;
|
|
225
|
+
}
|
|
226
|
+
|
|
190
227
|
/**
|
|
191
228
|
* Check if a module is available in node_modules.
|
|
192
229
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/footer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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": "src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pi-unipi/core": "2.
|
|
35
|
+
"@pi-unipi/core": "2.3.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/image",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Image generation and image recognition tools for the Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pi-unipi/core": "2.
|
|
37
|
+
"@pi-unipi/core": "2.3.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@earendil-works/pi-ai": "^0.80.0",
|
|
@@ -76,8 +76,8 @@ Settings in pi `settings.json`:
|
|
|
76
76
|
{
|
|
77
77
|
"unipi": {
|
|
78
78
|
"infoScreen": {
|
|
79
|
-
"
|
|
80
|
-
"bootTimeoutMs":
|
|
79
|
+
"bootMode": "auto-close",
|
|
80
|
+
"bootTimeoutMs": 2000,
|
|
81
81
|
"groups": {
|
|
82
82
|
"modules": { "show": true },
|
|
83
83
|
"ralph": { "show": true },
|
|
@@ -91,8 +91,8 @@ Settings in pi `settings.json`:
|
|
|
91
91
|
|
|
92
92
|
| Setting | Default | What It Does |
|
|
93
93
|
|---------|---------|--------------|
|
|
94
|
-
| `
|
|
95
|
-
| `bootTimeoutMs` |
|
|
94
|
+
| `bootMode` | `"auto-close"` | `"on"` keeps the dashboard up until dismissed, `"auto-close"` closes it after `bootTimeoutMs`, `"off"` never shows it |
|
|
95
|
+
| `bootTimeoutMs` | 2000 | Auto-close delay, in ms. Any keypress cancels it. Ignored unless `bootMode` is `"auto-close"` |
|
|
96
96
|
| `groups.{id}.show` | true | Toggle group visibility |
|
|
97
97
|
| `groupOrder` | priority sort | Custom group ordering |
|
|
98
98
|
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* under the "unipi.info" key.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import type { InfoScreenSettings, GroupSettings } from "./types.js";
|
|
12
|
-
import { DEFAULT_SETTINGS } from "./types.js";
|
|
11
|
+
import type { InfoScreenSettings, GroupSettings, BootMode } from "./types.js";
|
|
12
|
+
import { DEFAULT_SETTINGS, BOOT_MODES } from "./types.js";
|
|
13
13
|
|
|
14
14
|
/** Settings path */
|
|
15
15
|
const SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
|
|
@@ -44,9 +44,11 @@ function readSettingsFile(): Record<string, unknown> {
|
|
|
44
44
|
* Write the full settings file.
|
|
45
45
|
*/
|
|
46
46
|
function writeSettingsFile(data: Record<string, unknown>): void {
|
|
47
|
-
|
|
47
|
+
// These were require() calls in an ESM module, which throws under Node's
|
|
48
|
+
// module-format detection as soon as the directory is missing.
|
|
49
|
+
const dir = dirname(SETTINGS_PATH);
|
|
48
50
|
if (!existsSync(dir)) {
|
|
49
|
-
|
|
51
|
+
mkdirSync(dir, { recursive: true });
|
|
50
52
|
}
|
|
51
53
|
writeFileSync(SETTINGS_PATH, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
52
54
|
}
|
|
@@ -68,7 +70,7 @@ export function getInfoSettings(): InfoScreenSettings {
|
|
|
68
70
|
const info = unipi.info as Record<string, unknown>;
|
|
69
71
|
|
|
70
72
|
cachedSettings = {
|
|
71
|
-
|
|
73
|
+
bootMode: parseBootMode(info),
|
|
72
74
|
bootTimeoutMs: typeof info.bootTimeoutMs === "number" ? info.bootTimeoutMs : DEFAULT_SETTINGS.bootTimeoutMs,
|
|
73
75
|
groups: isRecord(info.groups) ? parseGroupSettings(info.groups) : {},
|
|
74
76
|
};
|
|
@@ -76,6 +78,24 @@ export function getInfoSettings(): InfoScreenSettings {
|
|
|
76
78
|
return cachedSettings;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the boot mode, migrating the legacy `showOnBoot` boolean.
|
|
83
|
+
*
|
|
84
|
+
* `showOnBoot: false` maps to "off". `showOnBoot: true` maps to "on" rather
|
|
85
|
+
* than the new "auto-close" default, so an existing config keeps behaving the
|
|
86
|
+
* way its owner configured it.
|
|
87
|
+
*/
|
|
88
|
+
function parseBootMode(info: Record<string, unknown>): BootMode {
|
|
89
|
+
const raw = info.bootMode;
|
|
90
|
+
if (typeof raw === "string" && (BOOT_MODES as string[]).includes(raw)) {
|
|
91
|
+
return raw as BootMode;
|
|
92
|
+
}
|
|
93
|
+
if (typeof info.showOnBoot === "boolean") {
|
|
94
|
+
return info.showOnBoot ? "on" : "off";
|
|
95
|
+
}
|
|
96
|
+
return DEFAULT_SETTINGS.bootMode;
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
/**
|
|
80
100
|
* Parse group settings from raw object.
|
|
81
101
|
*/
|
|
@@ -118,7 +138,7 @@ export function saveInfoSettings(settings: InfoScreenSettings): void {
|
|
|
118
138
|
}
|
|
119
139
|
|
|
120
140
|
(file[SETTINGS_KEY] as Record<string, unknown>).info = {
|
|
121
|
-
|
|
141
|
+
bootMode: settings.bootMode,
|
|
122
142
|
bootTimeoutMs: settings.bootTimeoutMs,
|
|
123
143
|
groups: settings.groups,
|
|
124
144
|
};
|
|
@@ -9,8 +9,9 @@ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
|
9
9
|
import { join, basename } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { getPiVersion } from "@pi-unipi/core";
|
|
12
13
|
import { infoRegistry } from "./registry.js";
|
|
13
|
-
import {
|
|
14
|
+
import { parseUsageStatsAsync, formatTokens, formatCost } from "./usage-parser.js";
|
|
14
15
|
import type { InfoGroup } from "./types.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -27,43 +28,6 @@ function getPackageVersion(packageDir: string): string {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
-
* Get pi version from its package.json.
|
|
32
|
-
*/
|
|
33
|
-
function getPiVersion(): string {
|
|
34
|
-
// Try to find pi's package.json in various locations
|
|
35
|
-
const possiblePaths = [
|
|
36
|
-
// Global npm install
|
|
37
|
-
join(homedir(), ".local", "share", "mise", "installs", "node", "24.14.1", "lib", "node_modules", "@earendil-works", "pi-coding-agent", "package.json"),
|
|
38
|
-
// Alternative locations
|
|
39
|
-
join(homedir(), ".local", "share", "mise", "installs", "node", "lib", "node_modules", "@earendil-works", "pi-coding-agent", "package.json"),
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
for (const pkgPath of possiblePaths) {
|
|
43
|
-
try {
|
|
44
|
-
if (existsSync(pkgPath)) {
|
|
45
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
46
|
-
return pkg?.version ?? "unknown";
|
|
47
|
-
}
|
|
48
|
-
} catch {
|
|
49
|
-
// Continue to next path
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Fallback: try to run pi --version
|
|
54
|
-
try {
|
|
55
|
-
const { execSync } = require("node:child_process");
|
|
56
|
-
const version = execSync("pi --version 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
57
|
-
// Extract version number from output like "pi v0.42.4"
|
|
58
|
-
const match = version.match(/v([\d.]+)/);
|
|
59
|
-
if (match) return match[1];
|
|
60
|
-
} catch {
|
|
61
|
-
// Ignore
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return "unknown";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
31
|
/**
|
|
68
32
|
* Discover loaded extensions by scanning filesystem.
|
|
69
33
|
*/
|
|
@@ -433,7 +397,9 @@ export function registerCoreGroups(): void {
|
|
|
433
397
|
],
|
|
434
398
|
},
|
|
435
399
|
dataProvider: async () => {
|
|
436
|
-
|
|
400
|
+
// Async variant yields to the event loop between files, so a cold parse
|
|
401
|
+
// cannot block keystrokes or the initial paint.
|
|
402
|
+
const stats = await parseUsageStatsAsync();
|
|
437
403
|
|
|
438
404
|
// Find top model for each period
|
|
439
405
|
const findTopModel = (modelStats: Record<string, { tokens: number; cost: number; sessions: number }> | undefined) => {
|
|
@@ -34,6 +34,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
34
34
|
// Start load tracking
|
|
35
35
|
startLoadTracking();
|
|
36
36
|
|
|
37
|
+
// Whether an info overlay is currently on screen. Module announcements only
|
|
38
|
+
// trigger a re-fetch when something is actually displaying the result.
|
|
39
|
+
let overlayVisible = false;
|
|
40
|
+
|
|
37
41
|
// Debounced MODULE_READY handling — batch module announcements
|
|
38
42
|
// to prevent layout shift from rapid per-module cache invalidation.
|
|
39
43
|
let moduleReadyBatch: Array<{ name: string; version: string; tools?: string[]; loadTimeMs?: number }> = [];
|
|
@@ -60,14 +64,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
// Single cache invalidation for all modules
|
|
67
|
+
// Single cache invalidation for all modules.
|
|
68
|
+
//
|
|
69
|
+
// Only re-fetch while an overlay is actually on screen. Otherwise this
|
|
70
|
+
// ran every module announcement even with the dashboard disabled,
|
|
71
|
+
// doing work nobody would see.
|
|
64
72
|
infoRegistry.invalidateCache("overview");
|
|
65
|
-
infoRegistry.
|
|
73
|
+
if (hasTools) infoRegistry.invalidateCache("tools");
|
|
66
74
|
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
if (!overlayVisible) return;
|
|
76
|
+
|
|
77
|
+
infoRegistry.getGroupData("overview");
|
|
78
|
+
if (hasTools) infoRegistry.getGroupData("tools");
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
// Listen for module announcements — track and trigger reactive updates
|
|
@@ -107,16 +115,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
107
115
|
* Cache-first: opens with whatever data is cached (even empty).
|
|
108
116
|
* Background: each group fetches independently, overlay re-renders reactively.
|
|
109
117
|
*/
|
|
110
|
-
function showOverlay(ctx: ExtensionContext): void {
|
|
118
|
+
function showOverlay(ctx: ExtensionContext, autoCloseMs?: number): void {
|
|
111
119
|
ctx.ui.custom<void>(
|
|
112
120
|
(tui, theme, _keybindings, done) => {
|
|
113
121
|
const overlay = new InfoOverlay();
|
|
114
122
|
overlay.setTheme(theme);
|
|
123
|
+
overlayVisible = true;
|
|
115
124
|
overlay.onClose = () => {
|
|
125
|
+
overlayVisible = false;
|
|
116
126
|
overlay.destroy();
|
|
117
127
|
done();
|
|
118
128
|
};
|
|
119
129
|
overlay.requestRender = () => tui.requestRender();
|
|
130
|
+
// Boot dashboard dismisses itself; any keypress cancels the timer.
|
|
131
|
+
if (autoCloseMs && autoCloseMs > 0) {
|
|
132
|
+
overlay.startBootTimer(autoCloseMs);
|
|
133
|
+
}
|
|
120
134
|
return {
|
|
121
135
|
render: (w: number) => overlay.render(w),
|
|
122
136
|
invalidate: () => overlay.invalidate(),
|
|
@@ -142,9 +156,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
142
156
|
pi.on("session_start", async (event, ctx) => {
|
|
143
157
|
const settings = getInfoSettings();
|
|
144
158
|
|
|
145
|
-
if (settings.
|
|
146
|
-
// Open immediately — cache-first, no waiting
|
|
147
|
-
|
|
159
|
+
if (settings.bootMode !== "off" && event.reason === "startup") {
|
|
160
|
+
// Open immediately — cache-first, no waiting. In "auto-close" mode the
|
|
161
|
+
// overlay dismisses itself after bootTimeoutMs; any keypress cancels it.
|
|
162
|
+
showOverlay(ctx, settings.bootMode === "auto-close" ? settings.bootTimeoutMs : 0);
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
finishLoadTracking();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/info-screen",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pi-unipi/core": "2.
|
|
36
|
+
"@pi-unipi/core": "2.3.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|