@signalridge/pi-welcome 1.2.0 → 1.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 +25 -0
- package/README.md +40 -28
- package/package.json +1 -1
- package/src/index.ts +105 -68
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
### Minor Changes
|
|
5
|
+
|
|
6
|
+
- 8488246: Put the inventory back, and take the wordmark off.
|
|
7
|
+
|
|
8
|
+
With `quietStartup` on, Pi's `[Context]`/`[Skills]`/`[Prompts]`/`[Themes]`/
|
|
9
|
+
`[Extensions]` sections are gone and nothing brings them back — `/reload` re-runs
|
|
10
|
+
the same suppressed listing. The card had replaced all of that with three
|
|
11
|
+
counts, which is strictly less than what the session used to show. It now names
|
|
12
|
+
them instead, eight per row with a `+N` tail: context files, skills, prompts,
|
|
13
|
+
extensions, themes, and the tool count.
|
|
14
|
+
|
|
15
|
+
Skills and prompts now come from `getCommands()` rather than `loadSkills()`. An
|
|
16
|
+
extension may contribute skill paths through `resources_discover`, which is
|
|
17
|
+
where most of them come from in practice, and the standalone loader cannot see
|
|
18
|
+
those — measured on a real session it returned zero skills while Pi was showing
|
|
19
|
+
dozens. Extensions and themes are read from the same settings files and
|
|
20
|
+
directories Pi reads.
|
|
21
|
+
|
|
22
|
+
The ridgeline mark, the `SIGNALRIDGE / PI` wordmark, and the tagline are gone.
|
|
23
|
+
The card exists to say what this session is, and a brand line says nothing a
|
|
24
|
+
returning user does not already know; the key hints lead instead, since with
|
|
25
|
+
`quietStartup` on they appear nowhere else. The Pi version returns to a row of
|
|
26
|
+
its own.
|
|
27
|
+
|
|
3
28
|
## 1.0.0
|
|
4
29
|
### Major Changes
|
|
5
30
|
|
package/README.md
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
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
|
|
4
|
+
repository, model, context budget, key hints, and the inventory of what Pi loaded. It
|
|
5
5
|
uses the active Pi theme, persists a bounded custom entry so a resumed session
|
|
6
6
|
redraws it, and collapses to a width-safe compact summary on narrow terminals.
|
|
7
7
|
|
|
8
8
|
```
|
|
9
|
-
|
|
10
|
-
│
|
|
11
|
-
│
|
|
12
|
-
│
|
|
13
|
-
│
|
|
14
|
-
│
|
|
15
|
-
│
|
|
16
|
-
│
|
|
17
|
-
│
|
|
18
|
-
│
|
|
19
|
-
│
|
|
20
|
-
│
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
│
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
|
|
9
|
+
╭─────────────────────────────────────────────────────────────────────────╮
|
|
10
|
+
│ │
|
|
11
|
+
│ Ctrl+C interrupt · / commands · ! bash · Ctrl+L clear │
|
|
12
|
+
│ │
|
|
13
|
+
│ Directory: ~/ghq/github.com/signalridge/pi-extensions │
|
|
14
|
+
│ Branch: main [+392 -202] │
|
|
15
|
+
│ Session: (new) │
|
|
16
|
+
│ Model: openai-codex / gpt-5.6-luna · thinking max │
|
|
17
|
+
│ Budget: 400K · compacts at 270K │
|
|
18
|
+
│ Version: 0.84.1 │
|
|
19
|
+
│ │
|
|
20
|
+
│ Context: AGENTS.md │
|
|
21
|
+
│ Skills: commit, dev-preferences, toolbelt, write-tui +12 │
|
|
22
|
+
│ Prompts: /plan, /review, /ship +4 │
|
|
23
|
+
│ Extensions: btw, code-actions, goal, gpt-fast, statusline +18 │
|
|
24
|
+
│ Themes: catppuccin-mocha, signalridge-ridgeline +2 │
|
|
25
|
+
│ Tools: 18 active of 42 │
|
|
26
|
+
│ │
|
|
27
|
+
╰─────────────────────────────────────────────────────────────────────────╯
|
|
27
28
|
```
|
|
28
29
|
|
|
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.
|
|
32
|
+
|
|
29
33
|
## Turn on `quietStartup`
|
|
30
34
|
|
|
31
35
|
The card is designed to be the whole opening screen. Pi's own startup is a
|
|
@@ -45,16 +49,24 @@ actionable is hidden.
|
|
|
45
49
|
Leaving `quietStartup: false` is supported and changes nothing about the card;
|
|
46
50
|
Pi simply prints its own block above it again.
|
|
47
51
|
|
|
48
|
-
##
|
|
52
|
+
## Where the inventory comes from
|
|
53
|
+
|
|
54
|
+
With `quietStartup` on, Pi's `[Context]`/`[Skills]`/`[Prompts]`/`[Themes]`/
|
|
55
|
+
`[Extensions]` sections are gone and nothing brings them back — `/reload` re-runs
|
|
56
|
+
the same suppressed listing — so this card is the only place they appear. It
|
|
57
|
+
names them rather than counting them, capped at eight per row with a `+N` tail.
|
|
58
|
+
|
|
59
|
+
Skills and prompts come from `getCommands()`, **not** from `loadSkills()`. An
|
|
60
|
+
extension may contribute skill paths through `resources_discover`, which is
|
|
61
|
+
where most of them come from in practice, and the standalone loader cannot see
|
|
62
|
+
those: on a session showing dozens of skills it returns zero. `getCommands()`
|
|
63
|
+
reports what Pi actually registered.
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
`loadProjectContextFiles
|
|
52
|
-
`
|
|
53
|
-
its
|
|
54
|
-
|
|
55
|
-
to construct it — whose `reload()` re-executes every extension factory and would
|
|
56
|
-
duplicate tools, listeners, timers, and child processes. So the card reports
|
|
57
|
-
counts it can derive honestly and leaves out what it cannot.
|
|
65
|
+
Extensions and themes are read from the same settings files and directories Pi
|
|
66
|
+
reads. Context files come from `loadProjectContextFiles`, and tool counts from
|
|
67
|
+
`getAllTools()` / `getActiveTools()`. Nothing here constructs a second resource
|
|
68
|
+
loader: its `reload()` re-executes every extension factory and would duplicate
|
|
69
|
+
tools, listeners, timers, and child processes.
|
|
58
70
|
|
|
59
71
|
## Untrusted text
|
|
60
72
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { execFileSync } from "node:child_process";
|
|
23
|
-
import { readFileSync } from "node:fs";
|
|
23
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
25
|
import { basename, join } from "node:path";
|
|
26
26
|
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
getAgentDir,
|
|
31
31
|
keyText,
|
|
32
32
|
loadProjectContextFiles,
|
|
33
|
-
loadSkills,
|
|
34
33
|
type Theme,
|
|
35
34
|
VERSION,
|
|
36
35
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -38,11 +37,6 @@ import { type Component, truncateToWidth, visibleWidth, wrapTextWithAnsi } from
|
|
|
38
37
|
|
|
39
38
|
const ENTRY_TYPE = "welcome-card";
|
|
40
39
|
|
|
41
|
-
/** A small original ridgeline mark; no private-use or font-specific glyphs. */
|
|
42
|
-
const LOGO = ["╭─╮", "╰─╯"] as const;
|
|
43
|
-
const BRAND = "SIGNALRIDGE / PI";
|
|
44
|
-
const SUBTITLE = "A focused coding workspace";
|
|
45
|
-
|
|
46
40
|
/** Columns of padding inside the card. */
|
|
47
41
|
const PAD = 2;
|
|
48
42
|
|
|
@@ -52,8 +46,16 @@ const MIN_WIDTH = 20;
|
|
|
52
46
|
/** Sentinel row rendered as a blank line, separating the card's groups. */
|
|
53
47
|
const GAP: [string, string] = ["", ""];
|
|
54
48
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Names to spell out in a resource row before collapsing the rest into `+N`.
|
|
51
|
+
*
|
|
52
|
+
* Pi's own listing prints every name and lets it wrap; with `quietStartup` on
|
|
53
|
+
* that listing is gone and this card is the only place the inventory appears,
|
|
54
|
+
* so it errs toward showing names rather than a bare count. Eight is what fits
|
|
55
|
+
* on one line at a normal width without turning the card into the wall of text
|
|
56
|
+
* the merge was meant to remove.
|
|
57
|
+
*/
|
|
58
|
+
const MAX_NAMES = 8;
|
|
57
59
|
|
|
58
60
|
interface WelcomeData {
|
|
59
61
|
rows: [string, string][];
|
|
@@ -128,8 +130,19 @@ function formatTokens(n: number): string {
|
|
|
128
130
|
return String(n);
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
/** `a, b, c +58` — the first few names, then how many were left out. */
|
|
134
|
+
function nameList(names: readonly string[], max = MAX_NAMES): string | undefined {
|
|
135
|
+
if (names.length === 0) return undefined;
|
|
136
|
+
const shown = names.slice(0, max).join(", ");
|
|
137
|
+
const rest = names.length - max;
|
|
138
|
+
return rest > 0 ? `${shown} +${rest}` : shown;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Sorted, de-duplicated, and free of empties — every row is built from this. */
|
|
142
|
+
function tidy(names: Iterable<string>): string[] {
|
|
143
|
+
return [...new Set([...names].map((name) => name.trim()).filter((name) => name.length > 0))].sort((a, b) =>
|
|
144
|
+
a.localeCompare(b),
|
|
145
|
+
);
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
/**
|
|
@@ -172,59 +185,94 @@ export function collectHints(): string[] {
|
|
|
172
185
|
}
|
|
173
186
|
|
|
174
187
|
/**
|
|
175
|
-
*
|
|
188
|
+
* Extension names, as Pi's own `[Extensions]` section lists them.
|
|
176
189
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
* Read from the same two settings files Pi reads, plus the extension
|
|
191
|
+
* directories it scans. Pi labels a package-sourced extension by its package
|
|
192
|
+
* name and a file-sourced one by its path, so the scope and the `pi-` prefix
|
|
193
|
+
* come off to leave the part that identifies it.
|
|
194
|
+
*/
|
|
195
|
+
function collectExtensionNames(cwd: string): string[] {
|
|
196
|
+
const names: string[] = [];
|
|
197
|
+
for (const settingsFile of [join(getAgentDir(), "settings.json"), join(cwd, ".pi", "settings.json")]) {
|
|
198
|
+
try {
|
|
199
|
+
const packages = JSON.parse(readFileSync(settingsFile, "utf8"))?.packages;
|
|
200
|
+
if (!Array.isArray(packages)) continue;
|
|
201
|
+
for (const entry of packages) {
|
|
202
|
+
const source = typeof entry === "string" ? entry : entry?.source;
|
|
203
|
+
if (typeof source !== "string") continue;
|
|
204
|
+
const spec = source.replace(/^npm:/u, "");
|
|
205
|
+
// `@scope/pi-name@1.2.3` and `./local/path` both reduce to a bare name.
|
|
206
|
+
const bare = (spec.startsWith("@") ? (spec.split("/")[1] ?? spec) : basename(spec)).replace(/@.*$/u, "");
|
|
207
|
+
names.push(bare.replace(/^pi-/u, ""));
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
for (const directory of [join(getAgentDir(), "extensions"), join(cwd, ".pi", "extensions")]) {
|
|
212
|
+
try {
|
|
213
|
+
for (const entry of readdirSync(directory)) {
|
|
214
|
+
names.push(entry.replace(/\.[cm]?[jt]s$/u, "").replace(/^pi-/u, ""));
|
|
215
|
+
}
|
|
216
|
+
} catch {}
|
|
217
|
+
}
|
|
218
|
+
return tidy(names);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Custom theme names, as Pi's own `[Themes]` section lists them. */
|
|
222
|
+
function collectThemeNames(): string[] {
|
|
223
|
+
try {
|
|
224
|
+
return tidy(
|
|
225
|
+
readdirSync(join(getAgentDir(), "themes"))
|
|
226
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
227
|
+
.map((entry) => entry.replace(/\.json$/u, "")),
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* What pi loaded for this session, by name — the inventory its own
|
|
236
|
+
* `[Context]`/`[Skills]`/`[Prompts]`/`[Themes]`/`[Extensions]` sections print.
|
|
237
|
+
*
|
|
238
|
+
* With `quietStartup` on those sections are gone and nothing brings them back
|
|
239
|
+
* (`/reload` re-runs the same suppressed listing), so this card is the only
|
|
240
|
+
* place the inventory appears and it names things rather than counting them.
|
|
241
|
+
*
|
|
242
|
+
* Skills and prompts come from `getCommands()`, not from `loadSkills()`: an
|
|
243
|
+
* extension may contribute skill paths through `resources_discover`, which is
|
|
244
|
+
* where most of them come from here, and the standalone loader cannot see those
|
|
245
|
+
* — it reports zero on a session showing dozens. `getCommands()` reports what
|
|
246
|
+
* Pi actually registered, which is the honest answer to "what loaded".
|
|
185
247
|
*/
|
|
186
248
|
export function collectResources(pi: ExtensionAPI, cwd: string): [string, string][] {
|
|
187
249
|
const rows: [string, string][] = [];
|
|
250
|
+
const push = (label: string, value: string | undefined) => {
|
|
251
|
+
if (value) rows.push([label, value]);
|
|
252
|
+
};
|
|
188
253
|
|
|
189
|
-
const contextNames = (() => {
|
|
190
|
-
try {
|
|
191
|
-
const files = loadProjectContextFiles({ cwd, agentDir: getAgentDir() });
|
|
192
|
-
// Several scopes can contribute the same filename (a project AGENTS.md
|
|
193
|
-
// and a global one); the card is naming kinds of context, not paths.
|
|
194
|
-
const names = [...new Set(files.map((file) => basename(file.path)))];
|
|
195
|
-
if (names.length === 0) return undefined;
|
|
196
|
-
const shown = names.slice(0, MAX_CONTEXT_NAMES).join(" · ");
|
|
197
|
-
const rest = names.length - MAX_CONTEXT_NAMES;
|
|
198
|
-
return rest > 0 ? `${shown} · +${rest}` : shown;
|
|
199
|
-
} catch {
|
|
200
|
-
return undefined;
|
|
201
|
-
}
|
|
202
|
-
})();
|
|
203
|
-
if (contextNames) rows.push(["Context", contextNames]);
|
|
204
|
-
|
|
205
|
-
const loaded: string[] = [];
|
|
206
254
|
try {
|
|
207
|
-
const {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
includeDefaults: true,
|
|
212
|
-
});
|
|
213
|
-
if (skills.length > 0) loaded.push(plural(skills.length, "skill"));
|
|
255
|
+
const files = loadProjectContextFiles({ cwd, agentDir: getAgentDir() });
|
|
256
|
+
// Several scopes can contribute the same filename (a project AGENTS.md and
|
|
257
|
+
// a global one); the card names kinds of context, not paths.
|
|
258
|
+
push("Context", nameList(tidy(files.map((file) => basename(file.path)))));
|
|
214
259
|
} catch {}
|
|
260
|
+
|
|
215
261
|
try {
|
|
216
262
|
const commands = pi.getCommands?.() ?? [];
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
263
|
+
const named = (source: string, prefix = "") =>
|
|
264
|
+
tidy(commands.filter((command) => command.source === source).map((command) => `${prefix}${command.name}`));
|
|
265
|
+
push("Skills", nameList(named("skill")));
|
|
266
|
+
push("Prompts", nameList(named("prompt", "/")));
|
|
221
267
|
} catch {}
|
|
222
|
-
|
|
268
|
+
|
|
269
|
+
push("Extensions", nameList(collectExtensionNames(cwd)));
|
|
270
|
+
push("Themes", nameList(collectThemeNames()));
|
|
223
271
|
|
|
224
272
|
try {
|
|
225
273
|
const all = pi.getAllTools?.() ?? [];
|
|
226
274
|
const active = pi.getActiveTools?.() ?? [];
|
|
227
|
-
if (all.length)
|
|
275
|
+
if (all.length) push("Tools", `${active.length} active of ${all.length}`);
|
|
228
276
|
} catch {}
|
|
229
277
|
|
|
230
278
|
return rows;
|
|
@@ -271,6 +319,8 @@ function collect(pi: ExtensionAPI, ctx: ExtensionContext): WelcomeData {
|
|
|
271
319
|
}
|
|
272
320
|
}
|
|
273
321
|
|
|
322
|
+
push("Version", VERSION);
|
|
323
|
+
|
|
274
324
|
// GAP is the sentinel for a blank row: session facts above, what pi loaded
|
|
275
325
|
// below, so the card reads as two groups rather than one wall.
|
|
276
326
|
const resources = collectResources(pi, ctx.cwd);
|
|
@@ -356,7 +406,6 @@ export class WelcomeCard implements Component {
|
|
|
356
406
|
private renderCompact(width: number): string[] {
|
|
357
407
|
const t = this.theme;
|
|
358
408
|
const lines = [
|
|
359
|
-
t.bold(t.fg("accent", "sr / pi")),
|
|
360
409
|
t.fg("muted", this.hints.length > 0 ? (this.hints[0] ?? "ready") : "ready"),
|
|
361
410
|
...this.rows.map(([label, value]) =>
|
|
362
411
|
label === GAP[0] && value === GAP[1] ? "" : `${t.fg("dim", `${label}:`)} ${t.fg("text", value)}`,
|
|
@@ -376,30 +425,18 @@ export class WelcomeCard implements Component {
|
|
|
376
425
|
const blank = () => line("");
|
|
377
426
|
const pad = " ".repeat(PAD);
|
|
378
427
|
const body: string[] = [blank()];
|
|
379
|
-
const logoWidth = Math.max(...LOGO.map(visibleWidth));
|
|
380
|
-
|
|
381
|
-
// The version rides the brand line rather than taking a row of its own: it
|
|
382
|
-
// is the one fact nobody scans for, and the row it used to occupy sat at
|
|
383
|
-
// the bottom where the eye lands last.
|
|
384
|
-
const version = t.fg("dim", `v${VERSION}`);
|
|
385
|
-
const brandRoom = Math.max(0, inner - PAD - logoWidth - PAD - PAD - visibleWidth(version));
|
|
386
|
-
const brand = t.bold(t.fg("text", BRAND));
|
|
387
|
-
const brandLine = brand + " ".repeat(Math.max(1, brandRoom - visibleWidth(BRAND) + 1)) + version;
|
|
388
|
-
|
|
389
|
-
body.push(
|
|
390
|
-
line(pad + t.fg("accent", LOGO[0].padEnd(logoWidth)) + pad + brandLine),
|
|
391
|
-
line(pad + t.fg("accent", LOGO[1].padEnd(logoWidth)) + pad + t.fg("muted", SUBTITLE)),
|
|
392
|
-
);
|
|
393
428
|
|
|
429
|
+
// No mark and no wordmark. The card exists to say what this session is, and
|
|
430
|
+
// a brand line says nothing a returning user does not already know. The
|
|
431
|
+
// hints lead instead: with `quietStartup` on this is the only place they
|
|
432
|
+
// appear, and they are what a new session actually needs.
|
|
394
433
|
const hintLine = fitHints(this.hints, Math.max(1, inner - PAD * 2));
|
|
395
434
|
if (hintLine) {
|
|
396
435
|
// The whole line is dimmed rather than highlighting each key: this is
|
|
397
436
|
// orientation, not a control the user is meant to look at.
|
|
398
|
-
body.push(
|
|
437
|
+
body.push(line(pad + t.fg("dim", hintLine)), blank());
|
|
399
438
|
}
|
|
400
439
|
|
|
401
|
-
body.push(blank());
|
|
402
|
-
|
|
403
440
|
const labelWidth = this.rows.length ? Math.max(...this.rows.map(([label]) => visibleWidth(label))) + 2 : 0;
|
|
404
441
|
for (const [label, value] of this.rows) {
|
|
405
442
|
if (label === GAP[0] && value === GAP[1]) {
|