@signalridge/pi-welcome 1.2.0 → 1.3.1
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 +43 -0
- package/README.md +40 -28
- package/package.json +1 -1
- package/src/index.ts +120 -63
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.1
|
|
4
|
+
### Patch Changes
|
|
5
|
+
|
|
6
|
+
- 2f22dea: Give the card its head back, with a mark that says which tool this is.
|
|
7
|
+
|
|
8
|
+
1.3.0 removed the whole title block. Only the wordmark was meant to go: the
|
|
9
|
+
tagline went with it because it sat on the same two lines, which left the card
|
|
10
|
+
opening straight into a key-hint row with nothing naming the session's tool or
|
|
11
|
+
version.
|
|
12
|
+
|
|
13
|
+
The two rows are back, now `pi` and its version over the tagline, with a pi
|
|
14
|
+
letterform for a mark — one beam over two legs, drawn from Block Elements so
|
|
15
|
+
every glyph is exactly one column in every terminal font. It says the same thing
|
|
16
|
+
as the title beside it rather than being a texture that could belong to any
|
|
17
|
+
tool, which the previous rounded outline and the solid bar that briefly replaced
|
|
18
|
+
it both were. The separate `Version` row is gone, since the version now rides
|
|
19
|
+
the title it belongs to.
|
|
20
|
+
|
|
21
|
+
## 1.3.0
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- 8488246: Put the inventory back, and take the wordmark off.
|
|
25
|
+
|
|
26
|
+
With `quietStartup` on, Pi's `[Context]`/`[Skills]`/`[Prompts]`/`[Themes]`/
|
|
27
|
+
`[Extensions]` sections are gone and nothing brings them back — `/reload` re-runs
|
|
28
|
+
the same suppressed listing. The card had replaced all of that with three
|
|
29
|
+
counts, which is strictly less than what the session used to show. It now names
|
|
30
|
+
them instead, eight per row with a `+N` tail: context files, skills, prompts,
|
|
31
|
+
extensions, themes, and the tool count.
|
|
32
|
+
|
|
33
|
+
Skills and prompts now come from `getCommands()` rather than `loadSkills()`. An
|
|
34
|
+
extension may contribute skill paths through `resources_discover`, which is
|
|
35
|
+
where most of them come from in practice, and the standalone loader cannot see
|
|
36
|
+
those — measured on a real session it returned zero skills while Pi was showing
|
|
37
|
+
dozens. Extensions and themes are read from the same settings files and
|
|
38
|
+
directories Pi reads.
|
|
39
|
+
|
|
40
|
+
The ridgeline mark, the `SIGNALRIDGE / PI` wordmark, and the tagline are gone.
|
|
41
|
+
The card exists to say what this session is, and a brand line says nothing a
|
|
42
|
+
returning user does not already know; the key hints lead instead, since with
|
|
43
|
+
`quietStartup` on they appear nowhere else. The Pi version returns to a row of
|
|
44
|
+
its own.
|
|
45
|
+
|
|
3
46
|
## 1.0.0
|
|
4
47
|
### Major Changes
|
|
5
48
|
|
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,9 +37,19 @@ import { type Component, truncateToWidth, visibleWidth, wrapTextWithAnsi } from
|
|
|
38
37
|
|
|
39
38
|
const ENTRY_TYPE = "welcome-card";
|
|
40
39
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
/**
|
|
41
|
+
* A pi letterform: one beam over two legs, drawn in the accent color.
|
|
42
|
+
*
|
|
43
|
+
* The mark says the same thing as the title line beside it — one for the eye,
|
|
44
|
+
* one for the reader — rather than being a texture that could belong to any
|
|
45
|
+
* tool. The legs are solid blocks rather than thin verticals so the two rows
|
|
46
|
+
* carry equal weight; hairlines wash out at small font sizes.
|
|
47
|
+
*
|
|
48
|
+
* Block Elements only. Each is exactly one column in every terminal font, while
|
|
49
|
+
* an emoji or a private-use glyph falls back to something double-width on some
|
|
50
|
+
* machines and shifts the text beside it out of alignment.
|
|
51
|
+
*/
|
|
52
|
+
const MARK = ["▄▄▄▄▄▄", "▐█ █▌"] as const;
|
|
44
53
|
const SUBTITLE = "A focused coding workspace";
|
|
45
54
|
|
|
46
55
|
/** Columns of padding inside the card. */
|
|
@@ -52,8 +61,16 @@ const MIN_WIDTH = 20;
|
|
|
52
61
|
/** Sentinel row rendered as a blank line, separating the card's groups. */
|
|
53
62
|
const GAP: [string, string] = ["", ""];
|
|
54
63
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Names to spell out in a resource row before collapsing the rest into `+N`.
|
|
66
|
+
*
|
|
67
|
+
* Pi's own listing prints every name and lets it wrap; with `quietStartup` on
|
|
68
|
+
* that listing is gone and this card is the only place the inventory appears,
|
|
69
|
+
* so it errs toward showing names rather than a bare count. Eight is what fits
|
|
70
|
+
* on one line at a normal width without turning the card into the wall of text
|
|
71
|
+
* the merge was meant to remove.
|
|
72
|
+
*/
|
|
73
|
+
const MAX_NAMES = 8;
|
|
57
74
|
|
|
58
75
|
interface WelcomeData {
|
|
59
76
|
rows: [string, string][];
|
|
@@ -128,8 +145,19 @@ function formatTokens(n: number): string {
|
|
|
128
145
|
return String(n);
|
|
129
146
|
}
|
|
130
147
|
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
/** `a, b, c +58` — the first few names, then how many were left out. */
|
|
149
|
+
function nameList(names: readonly string[], max = MAX_NAMES): string | undefined {
|
|
150
|
+
if (names.length === 0) return undefined;
|
|
151
|
+
const shown = names.slice(0, max).join(", ");
|
|
152
|
+
const rest = names.length - max;
|
|
153
|
+
return rest > 0 ? `${shown} +${rest}` : shown;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Sorted, de-duplicated, and free of empties — every row is built from this. */
|
|
157
|
+
function tidy(names: Iterable<string>): string[] {
|
|
158
|
+
return [...new Set([...names].map((name) => name.trim()).filter((name) => name.length > 0))].sort((a, b) =>
|
|
159
|
+
a.localeCompare(b),
|
|
160
|
+
);
|
|
133
161
|
}
|
|
134
162
|
|
|
135
163
|
/**
|
|
@@ -172,59 +200,94 @@ export function collectHints(): string[] {
|
|
|
172
200
|
}
|
|
173
201
|
|
|
174
202
|
/**
|
|
175
|
-
*
|
|
203
|
+
* Extension names, as Pi's own `[Extensions]` section lists them.
|
|
176
204
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
}
|
|
226
|
+
for (const directory of [join(getAgentDir(), "extensions"), join(cwd, ".pi", "extensions")]) {
|
|
227
|
+
try {
|
|
228
|
+
for (const entry of readdirSync(directory)) {
|
|
229
|
+
names.push(entry.replace(/\.[cm]?[jt]s$/u, "").replace(/^pi-/u, ""));
|
|
230
|
+
}
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
return tidy(names);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Custom theme names, as Pi's own `[Themes]` section lists them. */
|
|
237
|
+
function collectThemeNames(): string[] {
|
|
238
|
+
try {
|
|
239
|
+
return tidy(
|
|
240
|
+
readdirSync(join(getAgentDir(), "themes"))
|
|
241
|
+
.filter((entry) => entry.endsWith(".json"))
|
|
242
|
+
.map((entry) => entry.replace(/\.json$/u, "")),
|
|
243
|
+
);
|
|
244
|
+
} catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* What pi loaded for this session, by name — the inventory its own
|
|
251
|
+
* `[Context]`/`[Skills]`/`[Prompts]`/`[Themes]`/`[Extensions]` sections print.
|
|
252
|
+
*
|
|
253
|
+
* With `quietStartup` on those sections are gone and nothing brings them back
|
|
254
|
+
* (`/reload` re-runs the same suppressed listing), so this card is the only
|
|
255
|
+
* place the inventory appears and it names things rather than counting them.
|
|
256
|
+
*
|
|
257
|
+
* Skills and prompts come from `getCommands()`, not from `loadSkills()`: an
|
|
258
|
+
* extension may contribute skill paths through `resources_discover`, which is
|
|
259
|
+
* where most of them come from here, and the standalone loader cannot see those
|
|
260
|
+
* — it reports zero on a session showing dozens. `getCommands()` reports what
|
|
261
|
+
* Pi actually registered, which is the honest answer to "what loaded".
|
|
185
262
|
*/
|
|
186
263
|
export function collectResources(pi: ExtensionAPI, cwd: string): [string, string][] {
|
|
187
264
|
const rows: [string, string][] = [];
|
|
265
|
+
const push = (label: string, value: string | undefined) => {
|
|
266
|
+
if (value) rows.push([label, value]);
|
|
267
|
+
};
|
|
188
268
|
|
|
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
269
|
try {
|
|
207
|
-
const {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
includeDefaults: true,
|
|
212
|
-
});
|
|
213
|
-
if (skills.length > 0) loaded.push(plural(skills.length, "skill"));
|
|
270
|
+
const files = loadProjectContextFiles({ cwd, agentDir: getAgentDir() });
|
|
271
|
+
// Several scopes can contribute the same filename (a project AGENTS.md and
|
|
272
|
+
// a global one); the card names kinds of context, not paths.
|
|
273
|
+
push("Context", nameList(tidy(files.map((file) => basename(file.path)))));
|
|
214
274
|
} catch {}
|
|
275
|
+
|
|
215
276
|
try {
|
|
216
277
|
const commands = pi.getCommands?.() ?? [];
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
278
|
+
const named = (source: string, prefix = "") =>
|
|
279
|
+
tidy(commands.filter((command) => command.source === source).map((command) => `${prefix}${command.name}`));
|
|
280
|
+
push("Skills", nameList(named("skill")));
|
|
281
|
+
push("Prompts", nameList(named("prompt", "/")));
|
|
221
282
|
} catch {}
|
|
222
|
-
|
|
283
|
+
|
|
284
|
+
push("Extensions", nameList(collectExtensionNames(cwd)));
|
|
285
|
+
push("Themes", nameList(collectThemeNames()));
|
|
223
286
|
|
|
224
287
|
try {
|
|
225
288
|
const all = pi.getAllTools?.() ?? [];
|
|
226
289
|
const active = pi.getActiveTools?.() ?? [];
|
|
227
|
-
if (all.length)
|
|
290
|
+
if (all.length) push("Tools", `${active.length} active of ${all.length}`);
|
|
228
291
|
} catch {}
|
|
229
292
|
|
|
230
293
|
return rows;
|
|
@@ -356,7 +419,6 @@ export class WelcomeCard implements Component {
|
|
|
356
419
|
private renderCompact(width: number): string[] {
|
|
357
420
|
const t = this.theme;
|
|
358
421
|
const lines = [
|
|
359
|
-
t.bold(t.fg("accent", "sr / pi")),
|
|
360
422
|
t.fg("muted", this.hints.length > 0 ? (this.hints[0] ?? "ready") : "ready"),
|
|
361
423
|
...this.rows.map(([label, value]) =>
|
|
362
424
|
label === GAP[0] && value === GAP[1] ? "" : `${t.fg("dim", `${label}:`)} ${t.fg("text", value)}`,
|
|
@@ -376,30 +438,25 @@ export class WelcomeCard implements Component {
|
|
|
376
438
|
const blank = () => line("");
|
|
377
439
|
const pad = " ".repeat(PAD);
|
|
378
440
|
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
441
|
|
|
442
|
+
// The mark names pi, not a vendor: what a returning user needs from the top
|
|
443
|
+
// of the card is which tool and which version, and a company name is
|
|
444
|
+
// neither. The version rides the title line because it belongs to the same
|
|
445
|
+
// fact.
|
|
446
|
+
const markWidth = Math.max(...MARK.map(visibleWidth));
|
|
389
447
|
body.push(
|
|
390
|
-
line(pad + t.fg("accent",
|
|
391
|
-
line(pad + t.fg("accent",
|
|
448
|
+
line(pad + t.fg("accent", MARK[0].padEnd(markWidth)) + pad + t.bold(t.fg("text", `pi v${VERSION}`))),
|
|
449
|
+
line(pad + t.fg("accent", MARK[1].padEnd(markWidth)) + pad + t.fg("muted", SUBTITLE)),
|
|
450
|
+
blank(),
|
|
392
451
|
);
|
|
393
452
|
|
|
394
453
|
const hintLine = fitHints(this.hints, Math.max(1, inner - PAD * 2));
|
|
395
454
|
if (hintLine) {
|
|
396
455
|
// The whole line is dimmed rather than highlighting each key: this is
|
|
397
456
|
// orientation, not a control the user is meant to look at.
|
|
398
|
-
body.push(
|
|
457
|
+
body.push(line(pad + t.fg("dim", hintLine)), blank());
|
|
399
458
|
}
|
|
400
459
|
|
|
401
|
-
body.push(blank());
|
|
402
|
-
|
|
403
460
|
const labelWidth = this.rows.length ? Math.max(...this.rows.map(([label]) => visibleWidth(label))) + 2 : 0;
|
|
404
461
|
for (const [label, value] of this.rows) {
|
|
405
462
|
if (label === GAP[0] && value === GAP[1]) {
|