@signalridge/pi-welcome 1.3.7 → 1.3.9

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.9
4
+ ### Patch Changes
5
+
6
+ - 4d37546: Point package documentation to the maintained compatibility guide instead of removed historical review snapshots.
7
+
8
+ ## 1.3.8
9
+ ### Patch Changes
10
+
11
+ - 3364cc0: Include credential resolution in the Codex compaction deadline and promptly stop waiting on cancellation without interrupting host-owned token persistence. Preserve configured-package inventory for default-autoload exclusion-only resource filters.
12
+ - 3364cc0: Align guidance deduplication and setup profiles with host context selection. Render live welcome inventory after resource binding, restore cards from active branch facts after compaction, and use trust-resolved package and compaction settings. Keep Herdr idle during cache warming, validate blocked notifications, and allocate polling only for active TUI sessions.
13
+ - 3364cc0: Adopt Pi 1.0.4's host-provided dependency contract: declare imported Pi and TypeBox modules as `*` peers rather than installing duplicate runtime modules, and align Pi development pins with the repository's tested 1.0.4 baseline. The wildcard identifies host module ownership; it does not promise compatibility with every Pi version.
14
+
3
15
  ## 1.3.7
4
16
  ### Patch Changes
5
17
 
package/README.md CHANGED
@@ -1,9 +1,12 @@
1
1
  # pi-welcome
2
2
 
3
3
  TUI-only Signalridge startup card: one rounded panel with the current
4
- repository, model, context budget, key hints, and the inventory of what Pi loaded. It
5
- uses the active Pi theme, persists a bounded custom entry so a resumed session
6
- redraws it, and collapses to a width-safe compact summary on narrow terminals.
4
+ repository, model, context budget, key hints, and a live resource inventory. It
5
+ uses the active Pi theme, persists workspace facts in a bounded custom entry,
6
+ and collapses to a width-safe compact summary on narrow terminals. Resume keeps
7
+ the original workspace facts but refreshes inventory when creating the renderer.
8
+ A card summarized by compaction is restored from the active branch, never from
9
+ an abandoned branch.
7
10
 
8
11
  ```
9
12
  ╭─────────────────────────────────────────────────────────────────────────╮
@@ -15,24 +18,24 @@ redraws it, and collapses to a width-safe compact summary on narrow terminals.
15
18
  │ Session: (new) │
16
19
  │ Model: openai-codex / gpt-5.6-luna · thinking max │
17
20
  │ Budget: 400K · compacts at 270K │
18
- │ Version: 0.87.1 │
21
+ │ pi v1.0.4 │
19
22
  │ │
23
+ │ Inventory: live at render │
20
24
  │ Context: AGENTS.md │
21
25
  │ Skills: commit, release, review +12 │
22
26
  │ Prompts: /plan, /ship +4 │
23
- │ Extensions: btw, code-actions, goal, gpt-fast, statusline +18 │
24
- │ Themes: catppuccin-mocha +2 │
27
+ │ Configured packages: btw, goal, statusline +18 │
28
+ │ Available themes: dark, light, system +2 │
25
29
  │ Tools: 18 active of 42 │
26
30
  │ │
27
31
  ╰─────────────────────────────────────────────────────────────────────────╯
28
32
  ```
29
33
 
30
- No logo and no wordmark: the card exists to say what this session is, and a
31
- brand line says nothing a returning user does not already know.
34
+ The card leads with a compact Pi mark and the running host version.
32
35
 
33
36
  ## Install
34
37
 
35
- The declared Pi peer range covers `0.84.x`–`0.87.x` and `0.99.1+` within the 0.99 minor; the current development host is `0.99.1`. See the [compatibility review](https://github.com/signalridge/pi-extensions/blob/main/docs/pi-0.99-compatibility.md) for verification details.
38
+ The current tested host is Pi `1.0.4`. Host-provided dependencies use `"*"` peers to identify module ownership, not to guarantee compatibility with every Pi version. See the [compatibility guide](https://github.com/signalridge/pi-extensions/blob/main/docs/compatibility.md) for verification details.
36
39
 
37
40
  ```bash
38
41
  pi install npm:@signalridge/pi-welcome
@@ -76,13 +79,26 @@ Skills and prompts come from `getCommands()`, **not** from `loadSkills()`. An
76
79
  extension may contribute skill paths through `resources_discover`, which is
77
80
  where most of them come from in practice, and the standalone loader cannot see
78
81
  those: on a session showing dozens of skills it returns zero. `getCommands()`
79
- reports what Pi actually registered.
80
-
81
- Extensions and themes are read from the same settings files and directories Pi
82
- reads. Context files come from `loadProjectContextFiles`, and tool counts from
83
- `getAllTools()` / `getActiveTools()`. Nothing here constructs a second resource
84
- loader: its `reload()` re-executes every extension factory and would duplicate
85
- tools, listeners, timers, and child processes.
82
+ reports what Pi actually registered. Collection happens at renderer component
83
+ creation, after `bindExtensions()` finishes `resources_discover`, not during
84
+ `session_start` before discovery. The inventory is labeled **live at render**;
85
+ it is not a persisted startup snapshot.
86
+
87
+ **Configured packages** comes from `pi.getSettings()`, which already resolves
88
+ project trust and merges configuration. Packages filtered to no resources are
89
+ omitted; skills-only packages may appear. This is configuration, not evidence
90
+ that a package's extensions loaded, and does not enumerate standalone extension
91
+ files. **Available themes** comes from `ctx.ui.getAllThemes()` in the TUI,
92
+ including built-in and discovered themes rather than only a user themes folder.
93
+ Context filenames come from `loadProjectContextFiles`, and tool counts from
94
+ `getAllTools()` / `getActiveTools()`. Nothing here constructs or reloads a second
95
+ resource loader: its `reload()` re-executes every extension factory and would
96
+ duplicate tools, listeners, timers, and child processes.
97
+
98
+ The persisted Budget row uses effective trusted settings, resolving the exact
99
+ `provider/modelId` compaction override, ordinary reserve, then Pi's default
100
+ reserve. Zero is valid; invalid settings do not produce a guessed threshold.
101
+ When automatic compaction is disabled, there is no **compacts at** suffix.
86
102
 
87
103
  ## Untrusted text
88
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalridge/pi-welcome",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "Startup resource and repository summary card for Pi.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,8 +9,8 @@
9
9
  "lifecycle": "stable"
10
10
  },
11
11
  "peerDependencies": {
12
- "@earendil-works/pi-coding-agent": "^0.84.0 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.1",
13
- "@earendil-works/pi-tui": "^0.84.0 || ^0.85.0 || ^0.86.0 || ^0.87.0 || ^0.99.1"
12
+ "@earendil-works/pi-coding-agent": "*",
13
+ "@earendil-works/pi-tui": "*"
14
14
  },
15
15
  "pi": {
16
16
  "extensions": [
package/src/index.ts CHANGED
@@ -9,20 +9,19 @@
9
9
  * header, a key-hint block, and one `[Section]` per resource kind with a blank
10
10
  * line between each; stacking this card underneath produced two unrelated
11
11
  * designs and about forty-five lines of them. So the card carries the pieces
12
- * worth keeping — the key hints and a count of what loaded — and `quietStartup`
12
+ * worth keeping — the key hints and a live resource inventory — and `quietStartup`
13
13
  * is turned on in settings to silence the rest. With `quietStartup: false` Pi
14
14
  * still prints its own block and the two appear together again.
15
15
  *
16
16
  * The card is a custom entry, not a widget: it lands in the transcript, scrolls
17
- * away as the conversation grows, and survives a reload. Facts are captured at
18
- * session_start and stored in the entry, so a resumed session redraws the card
19
- * as it was when that session began.
17
+ * away as the conversation grows, and survives a reload. Workspace facts are
18
+ * captured at session_start and stored in the entry; inventory is collected at
19
+ * component creation, after Pi binds resources, including on resume.
20
20
  */
21
21
 
22
22
  import { execFileSync } from "node:child_process";
23
- import { readdirSync, readFileSync } from "node:fs";
24
23
  import { homedir } from "node:os";
25
- import { basename, join } from "node:path";
24
+ import { basename } from "node:path";
26
25
 
27
26
  import {
28
27
  type ExtensionAPI,
@@ -30,6 +29,7 @@ import {
30
29
  getAgentDir,
31
30
  keyText,
32
31
  loadProjectContextFiles,
32
+ SettingsManager,
33
33
  type Theme,
34
34
  VERSION,
35
35
  } from "@earendil-works/pi-coding-agent";
@@ -124,17 +124,17 @@ function gitSummary(cwd: string): string | undefined {
124
124
  return added || removed ? `${branch} [+${added} -${removed}]` : branch;
125
125
  }
126
126
 
127
- /**
128
- * `compaction.reserveTokens` from settings.json. ExtensionContext does not carry
129
- * settings, so this reads the same file pi does — the trigger point is worth the
130
- * one stat() because it, not the raw window, is what a long session runs into.
131
- */
132
- function readReserveTokens(): number | undefined {
127
+ /** Resolve trusted, merged settings with Pi's own defaults and model validation. */
128
+ function compactionTrigger(pi: ExtensionAPI, ctx: ExtensionContext): number | undefined {
133
129
  try {
134
- const raw = readFileSync(join(getAgentDir(), "settings.json"), "utf8");
135
- const value = JSON.parse(raw)?.compaction?.reserveTokens;
136
- return typeof value === "number" ? value : undefined;
130
+ const settings = pi.getSettings?.();
131
+ if (!settings || !ctx.model) return undefined;
132
+ const compaction = SettingsManager.inMemory(settings).getCompactionSettings(ctx.model);
133
+ const trigger = ctx.model.contextWindow - compaction.reserveTokens;
134
+ return compaction.enabled && trigger > 0 ? trigger : undefined;
137
135
  } catch {
136
+ // An invalid configuration (or older host without settings) is not a known
137
+ // automatic trigger. Do not invent a budget from raw files or invalid values.
138
138
  return undefined;
139
139
  }
140
140
  }
@@ -199,52 +199,33 @@ export function collectHints(): string[] {
199
199
  }
200
200
  }
201
201
 
202
- /**
203
- * Extension names, as Pi's own `[Extensions]` section lists them.
204
- *
205
- * Read from the same two settings files Pi reads, plus the extension
206
- * directories it scans. Pi labels a package-sourced extension by its package
207
- * name and a file-sourced one by its path, so the scope and the `pi-` prefix
208
- * come off to leave the part that identifies it.
209
- */
210
- function collectExtensionNames(cwd: string): string[] {
211
- const names: string[] = [];
212
- for (const settingsFile of [join(getAgentDir(), "settings.json"), join(cwd, ".pi", "settings.json")]) {
213
- try {
214
- const packages = JSON.parse(readFileSync(settingsFile, "utf8"))?.packages;
215
- if (!Array.isArray(packages)) continue;
216
- for (const entry of packages) {
217
- const source = typeof entry === "string" ? entry : entry?.source;
218
- if (typeof source !== "string") continue;
219
- const spec = source.replace(/^npm:/u, "");
220
- // `@scope/pi-name@1.2.3` and `./local/path` both reduce to a bare name.
221
- const bare = (spec.startsWith("@") ? (spec.split("/")[1] ?? spec) : basename(spec)).replace(/@.*$/u, "");
222
- names.push(bare.replace(/^pi-/u, ""));
223
- }
224
- } catch {
225
- // Discovery is best-effort: a malformed manifest contributes no names
226
- // rather than blanking the whole card.
227
- }
228
- }
229
- for (const directory of [join(getAgentDir(), "extensions"), join(cwd, ".pi", "extensions")]) {
230
- try {
231
- for (const entry of readdirSync(directory)) {
232
- names.push(entry.replace(/\.[cm]?[jt]s$/u, "").replace(/^pi-/u, ""));
233
- }
234
- } catch {
235
- // An extensions directory that does not exist is the normal case.
236
- }
237
- }
238
- return tidy(names);
239
- }
240
-
241
- /** Custom theme names, as Pi's own `[Themes]` section lists them. */
242
- function collectThemeNames(): string[] {
202
+ /** Configured packages are not proof of loaded extensions (skills-only is valid). */
203
+ function collectPackageNames(pi: ExtensionAPI): string[] {
243
204
  try {
244
205
  return tidy(
245
- readdirSync(join(getAgentDir(), "themes"))
246
- .filter((entry) => entry.endsWith(".json"))
247
- .map((entry) => entry.replace(/\.json$/u, "")),
206
+ (pi.getSettings?.().packages ?? [])
207
+ .filter((entry) => {
208
+ if (typeof entry === "string") return true;
209
+ // Respect packages explicitly filtered to no resources. A skills-only
210
+ // package still belongs here, even with extensions: [].
211
+ return (["extensions", "skills", "prompts", "themes"] as const).some((kind) => {
212
+ const patterns = entry[kind];
213
+ if (patterns === undefined) return entry.autoload !== false;
214
+ if (patterns.length === 0) return false;
215
+ // Default autoload starts with all resources when filters contain
216
+ // only exclusions; autoload:false starts empty and needs an include.
217
+ return (
218
+ entry.autoload !== false ||
219
+ patterns.some((pattern) => !pattern.startsWith("!") && !pattern.startsWith("-"))
220
+ );
221
+ });
222
+ })
223
+ .map((entry) => {
224
+ const source = typeof entry === "string" ? entry : entry.source;
225
+ const spec = source.replace(/^npm:/u, "");
226
+ const bare = (spec.startsWith("@") ? (spec.split("/")[1] ?? spec) : basename(spec)).replace(/@.*$/u, "");
227
+ return bare.replace(/^pi-/u, "");
228
+ }),
248
229
  );
249
230
  } catch {
250
231
  return [];
@@ -252,8 +233,8 @@ function collectThemeNames(): string[] {
252
233
  }
253
234
 
254
235
  /**
255
- * What pi loaded for this session, by name — the inventory its own
256
- * `[Context]`/`[Skills]`/`[Prompts]`/`[Themes]`/`[Extensions]` sections print.
236
+ * Live registered skills/prompts/tools, available themes, selected context
237
+ * filenames, and configured packages. Not a claim that every package loaded.
257
238
  *
258
239
  * With `quietStartup` on those sections are gone and nothing brings them back
259
240
  * (`/reload` re-runs the same suppressed listing), so this card is the only
@@ -265,7 +246,7 @@ function collectThemeNames(): string[] {
265
246
  * — it reports zero on a session showing dozens. `getCommands()` reports what
266
247
  * Pi actually registered, which is the honest answer to "what loaded".
267
248
  */
268
- export function collectResources(pi: ExtensionAPI, cwd: string): [string, string][] {
249
+ export function collectResources(pi: ExtensionAPI, cwd: string, ui?: ExtensionContext["ui"]): [string, string][] {
269
250
  const rows: [string, string][] = [];
270
251
  const push = (label: string, value: string | undefined) => {
271
252
  if (value) rows.push([label, value]);
@@ -283,15 +264,23 @@ export function collectResources(pi: ExtensionAPI, cwd: string): [string, string
283
264
  try {
284
265
  const commands = pi.getCommands?.() ?? [];
285
266
  const named = (source: string, prefix = "") =>
286
- tidy(commands.filter((command) => command.source === source).map((command) => `${prefix}${command.name}`));
267
+ tidy(
268
+ commands
269
+ .filter((command) => command.source === source)
270
+ .map((command) => `${prefix}${source === "skill" ? command.name.replace(/^skill:/u, "") : command.name}`),
271
+ );
287
272
  push("Skills", nameList(named("skill")));
288
273
  push("Prompts", nameList(named("prompt", "/")));
289
274
  } catch {
290
275
  // A host without these accessors simply contributes no rows.
291
276
  }
292
277
 
293
- push("Extensions", nameList(collectExtensionNames(cwd)));
294
- push("Themes", nameList(collectThemeNames()));
278
+ push("Configured packages", nameList(collectPackageNames(pi)));
279
+ try {
280
+ push("Available themes", nameList(tidy((ui?.getAllThemes() ?? []).map((theme) => theme.name))));
281
+ } catch {
282
+ // Themes are a TUI surface; an older host may not provide it.
283
+ }
295
284
 
296
285
  try {
297
286
  const all = pi.getAllTools?.() ?? [];
@@ -336,20 +325,12 @@ function collect(pi: ExtensionAPI, ctx: ExtensionContext): WelcomeData {
336
325
  if (typeof window === "number" && window > 0) {
337
326
  // Pi compacts at contextWindow - reserveTokens; surfacing the trigger up
338
327
  // front is the one number that actually governs a long session.
339
- const reserve = readReserveTokens();
340
- const trigger =
341
- typeof reserve === "number" && reserve > 0 && reserve < window
342
- ? ` · compacts at ${formatTokens(window - reserve)}`
343
- : "";
328
+ const threshold = compactionTrigger(pi, ctx);
329
+ const trigger = threshold !== undefined ? ` · compacts at ${formatTokens(threshold)}` : "";
344
330
  push("Budget", `${formatTokens(window)}${trigger}`);
345
331
  }
346
332
  }
347
333
 
348
- // GAP is the sentinel for a blank row: session facts above, what pi loaded
349
- // below, so the card reads as two groups rather than one wall.
350
- const resources = collectResources(pi, ctx.cwd);
351
- if (resources.length > 0) rows.push(GAP, ...resources);
352
-
353
334
  const hints = collectHints();
354
335
  return hints.length > 0 ? { rows, hints } : { rows };
355
336
  }
@@ -494,31 +475,52 @@ export class WelcomeCard implements Component {
494
475
 
495
476
  // ─── Extension Entry ───────────────────────────────────────────────────────────
496
477
 
478
+ const INVENTORY_LABELS = new Set([
479
+ "Inventory",
480
+ "Context",
481
+ "Skills",
482
+ "Prompts",
483
+ "Extensions",
484
+ "Themes",
485
+ "Tools",
486
+ "Configured packages",
487
+ "Available themes",
488
+ ]);
489
+
490
+ function withLiveInventory(data: WelcomeData, pi: ExtensionAPI, ctx: ExtensionContext): WelcomeData {
491
+ // Remove legacy persisted inventory too, while preserving original workspace
492
+ // facts on resume. Do not rewrite the stored entry during rendering.
493
+ const rows = data.rows.filter(([label]) => label !== "" && !INVENTORY_LABELS.has(label));
494
+ const resources = collectResources(pi, ctx.cwd, ctx.ui);
495
+ if (resources.length > 0) rows.push(GAP, ["Inventory", "live at render"], ...resources);
496
+ return { ...data, rows };
497
+ }
498
+
497
499
  export default function (pi: ExtensionAPI) {
500
+ let currentContext: ExtensionContext | undefined;
498
501
  pi.registerEntryRenderer<WelcomeData>(ENTRY_TYPE, (entry, _options, theme) => {
499
502
  const data = entry.data;
500
503
  if (!data?.rows?.length) return undefined;
501
- return new WelcomeCard(data, theme);
504
+ return new WelcomeCard(currentContext ? withLiveInventory(data, pi, currentContext) : data, theme);
502
505
  });
503
506
 
504
507
  pi.on("session_start", async (_event, ctx) => {
505
- if (ctx.mode !== "tui") return;
506
-
507
- // A resumed session already carries its card; appending on every
508
- // session_start would stack a fresh one on top at each reload.
509
- try {
510
- for (const entry of ctx.sessionManager.getEntries()) {
511
- if (entry.type === "custom" && entry.customType === ENTRY_TYPE) {
512
- return;
513
- }
514
- }
515
- } catch {
516
- // Unreadable entries mean we cannot prove the card was already written.
517
- // Falling through re-appends it, which beats never showing it at all.
518
- }
508
+ currentContext = ctx.mode === "tui" ? ctx : undefined;
509
+ if (!currentContext) return;
519
510
 
520
511
  try {
521
- pi.appendEntry<WelcomeData>(ENTRY_TYPE, collect(pi, ctx));
512
+ // The transcript uses the active, compaction-aware projection, not every
513
+ // entry in the file. An off-branch or summarized card is not visible.
514
+ const visible = ctx.sessionManager.buildContextEntries();
515
+ if (visible.some((entry) => entry.type === "custom" && entry.customType === ENTRY_TYPE)) return;
516
+
517
+ // Restore facts from this branch if compaction summarized its card. Never
518
+ // borrow facts from an abandoned sibling branch.
519
+ const original = ctx.sessionManager
520
+ .getBranch()
521
+ .find((entry) => entry.type === "custom" && entry.customType === ENTRY_TYPE);
522
+ const data = original?.type === "custom" ? (original.data as WelcomeData | undefined) : undefined;
523
+ pi.appendEntry<WelcomeData>(ENTRY_TYPE, data?.rows?.length ? data : collect(pi, ctx));
522
524
  } catch {
523
525
  // A missing card is not worth failing a session start over.
524
526
  }