pi-umbra 0.1.4 → 0.1.5

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/README.md CHANGED
@@ -1,31 +1,33 @@
1
- # pi-umbra
2
-
3
- Every small thing pi does not build in, in one install.
4
-
5
- ```sh
6
- pi install npm:pi-umbra
7
- ```
8
-
9
- One tarball. The nine packages below ship bundled inside it, so the install does not
10
- depend on npm hoisting nine separate trees into the layout pi expects.
11
-
12
- ```
13
- pi-umbra
14
- ├── pi-umbra-theme seven themes and the chrome around them
15
- ├── pi-umbra-ask the model asks you a question mid-turn
16
- ├── pi-umbra-copy-chat /umb-copy-chat
17
- ├── pi-umbra-help /umb-help, /umb-doctor
18
- ├── pi-umbra-inputbar the input box
19
- ├── pi-umbra-preview /umb-preview
20
- ├── pi-umbra-rename /umb-rename
21
- ├── pi-umbra-shimmer a shimmer through the working indicator
22
- └── pi-umbra-skill-matcher skill completion mid-prompt
23
- ```
24
-
25
- Install any of them on its own if you want less. Each package has its own README saying
26
- what it solves.
27
-
28
- `pi-umbra-shimmer` and `pi-umbra-skill-matcher` also need a bundle patch, which is not
29
- published here. Without it they install and do nothing.
30
-
31
- MIT.
1
+ # pi-umbra
2
+
3
+ Every small thing pi does not build in, in one install.
4
+
5
+ ```sh
6
+ pi install npm:pi-umbra
7
+ ```
8
+
9
+ One tarball. The nine packages below ship bundled inside it, so the install does not
10
+ depend on npm hoisting nine separate trees into the layout pi expects.
11
+
12
+ ```
13
+ pi-umbra
14
+ ├── pi-umbra-theme seven themes and the chrome around them
15
+ ├── pi-umbra-ask the model asks you a question mid-turn
16
+ ├── pi-umbra-copy-chat /umb-copy-chat
17
+ ├── pi-umbra-help /umb-help, /umb-doctor
18
+ ├── pi-umbra-inputbar the input box
19
+ ├── pi-umbra-preview /umb-preview
20
+ ├── pi-umbra-rename /umb-rename
21
+ ├── pi-umbra-shimmer a shimmer through the working indicator
22
+ └── pi-umbra-skill-matcher skill completion mid-prompt
23
+ ```
24
+
25
+ Install any of them on its own if you want less. Each package has its own README saying
26
+ what it solves.
27
+
28
+ `pi-umbra-shimmer` and `pi-umbra-skill-matcher` reach past what the extension API
29
+ exposes, so each ships a `patch.mjs` that has to be run once after installing and again
30
+ after every pi update. Their own READMEs carry the command. Until it is run, those two
31
+ load and do nothing; the rest of the family is unaffected.
32
+
33
+ MIT.
@@ -7,26 +7,44 @@ The highlight is not one colour. It travels from white down to the theme's own m
7
7
  and back, blending the whole way rather than stepping, so the band changes tone while it
8
8
  moves.
9
9
 
10
+ ## Install
11
+
12
+ ```sh
13
+ pi install npm:pi-umbra-shimmer
14
+ node ~/.pi/agent/npm/node_modules/pi-umbra-shimmer/patch.mjs
15
+ ```
16
+
17
+ Then restart pi.
18
+
19
+ ## The second line is not optional
20
+
10
21
  pi builds the working indicator with a colour function already in hand, and paints custom
11
- spinner frames verbatim without running them through that step at all. Both points are
12
- past what the extension API exposes, so reaching them needs a bundle patch, which is not
13
- published in this repo.
22
+ spinner frames verbatim without running them through that step at all. Neither point is
23
+ reachable from the extension API, so this package ships three patches against pi's built
24
+ bundle. Without them the extension loads and does nothing.
14
25
 
15
- Installed alone this package does nothing. The patches are inert without it. With neither
16
- present pi keeps its own behaviour, so nothing here can half-apply.
26
+ ```sh
27
+ node ~/.pi/agent/npm/node_modules/pi-umbra-shimmer/patch.mjs --check
28
+ ```
29
+
30
+ `--check` writes nothing and exits 1 when a patch is missing. **Run it after every pi
31
+ update.** An update replaces the bundle, so all three patches disappear silently and the
32
+ shimmer stops without an error.
33
+
34
+ Nothing here is destructive. Each patch is additive, a patch whose text no longer matches
35
+ is reported rather than forced, and reinstalling pi returns the bundle to stock. The
36
+ patched behaviour is gated on the extension being loaded, so the patches alone change
37
+ nothing.
38
+
39
+ Set `PI_UMBRA_PI` if pi lives somewhere this script cannot find it.
17
40
 
18
41
  ```
19
42
  pi-umbra-shimmer/
20
43
  ├── checks
21
44
  │ └── umbra-shimmer.check.ts
22
- └── extensions
23
- └── umbra-shimmer.ts
24
- ```
25
-
26
- ## Install
27
-
28
- ```sh
29
- pi install npm:pi-umbra-shimmer
45
+ ├── extensions
46
+ └── umbra-shimmer.ts
47
+ └── patch.mjs
30
48
  ```
31
49
 
32
50
  MIT.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-umbra-shimmer",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A wave of colour running through pi's working indicator, over both the spinner and the text. Needs the umbra bundle patches; without them it does nothing.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -23,7 +23,8 @@
23
23
  "extensions",
24
24
  "checks",
25
25
  "README.md",
26
- "LICENSE"
26
+ "LICENSE",
27
+ "patch.mjs"
27
28
  ],
28
29
  "pi": {
29
30
  "extensions": [
@@ -0,0 +1,101 @@
1
+ // pi-umbra-shimmer needs 3 seams that pi's extension API does not expose, so it edits
2
+ // pi's built bundle. Run it after installing, and again after every pi update:
3
+ //
4
+ // node <this file> apply
5
+ // node <this file> --check report only, write nothing, exit 1 if anything is missing
6
+ //
7
+ // pi runs dist/bundle/cli.js, a self-contained bundle, and the chunk names are content-hashed,
8
+ // so every patch finds its own place by matching its own text rather than by line number.
9
+ //
10
+ // Nothing here is destructive. Each patch is additive, a patch whose text no longer matches is
11
+ // reported instead of forced, and reinstalling pi returns the bundle to stock. A pi upgrade
12
+ // replaces the bundle and drops every patch silently, which is what --check is for.
13
+ import { createRequire } from "node:module";
14
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { dirname, join } from "node:path";
17
+
18
+ // PI_UMBRA_PI points this at another pi tree. Otherwise pi is resolved from wherever this
19
+ // package was installed, since it is a peer dependency, and the bun global path is the last resort.
20
+ const resolvePi = () => {
21
+ if (process.env.PI_UMBRA_PI) return process.env.PI_UMBRA_PI;
22
+ try {
23
+ return dirname(createRequire(import.meta.url).resolve("@earendil-works/pi-coding-agent/package.json"));
24
+ } catch {
25
+ return join(homedir(), ".bun/install/global/node_modules/@earendil-works/pi-coding-agent");
26
+ }
27
+ };
28
+
29
+ const BUNDLE = join(resolvePi(), "dist/bundle");
30
+ if (!existsSync(BUNDLE)) {
31
+ console.error(`no pi bundle at ${BUNDLE} - set PI_UMBRA_PI to your pi install`);
32
+ process.exit(1);
33
+ }
34
+
35
+ const PATCHES = [
36
+ {
37
+ // The indicator is built with a colour function already in hand - the editor embeds the
38
+ // working status, so pi hands it the editor border - and the fallback below never runs.
39
+ // Standing that function down when a shimmer is installed is what lets the fallback through.
40
+ reason: "a shimmer outranks the editor border",
41
+ from: `let colorFn=isWorkingStatusEditor(this.editor)?text=>(this.editor.borderColor??theme.getThinkingBorderColor(this.session.thinkingLevel||"off"))(text):void 0;`,
42
+ to: `let colorFn=globalThis.__umbraShimmer?void 0:isWorkingStatusEditor(this.editor)?text=>(this.editor.borderColor??theme.getThinkingBorderColor(this.session.thinkingLevel||"off"))(text):void 0;`,
43
+ },
44
+ {
45
+ // Custom frames are rendered verbatim, which is what kept the braille glyph out of the
46
+ // wave: the spinner colour function was skipped for them while the message kept being
47
+ // repainted every frame. Verbatim still holds when no shimmer is installed.
48
+ reason: "and paints the custom frames too",
49
+ from: `return this.renderIndicatorVerbatim?frame2:this.spinnerColorFn(frame2)`,
50
+ to: `return this.renderIndicatorVerbatim&&!globalThis.__umbraShimmer?frame2:this.spinnerColorFn(frame2)`,
51
+ },
52
+ {
53
+ // The working indicator paints its spinner with `accent` and its message with `muted`, both
54
+ // fixed at construction. A shimmer has to repaint every character on every frame, so the
55
+ // two colour functions become a lookup the shimmer extension can fill in. Nothing
56
+ // installed means pi's own two colours, unchanged.
57
+ reason: "the working indicator's colours can be taken over",
58
+ from: `super("working",ui,colorFn??(text=>theme.fg("accent",text)),colorFn??(text=>theme.fg("muted",text)),message,indicator)`,
59
+ to: `super("working",ui,colorFn??(text=>(globalThis.__umbraShimmer?.spinner??(t=>theme.fg("accent",t)))(text)),colorFn??(text=>(globalThis.__umbraShimmer?.message??(t=>theme.fg("muted",t)))(text)),message,indicator)`,
60
+ },
61
+ ];
62
+
63
+ const files = [join(BUNDLE, "cli.js"), ...readdirSync(join(BUNDLE, "chunks")).map((name) => join(BUNDLE, "chunks", name))];
64
+ const CHECK = process.argv.includes("--check");
65
+ const unapplied = [];
66
+ const gone = [];
67
+
68
+ for (const patch of PATCHES) {
69
+ let done = false;
70
+ for (const path of files) {
71
+ const source = readFileSync(path, "utf8");
72
+ // A multi-line patch is written with newline escapes; match whatever line ending the file uses.
73
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
74
+ const from = patch.from.split("\n").join(eol);
75
+ const to = patch.to.split("\n").join(eol);
76
+ if (source.includes(to)) {
77
+ if (!CHECK) console.log(`already patched: ${patch.reason}`);
78
+ done = true;
79
+ break;
80
+ }
81
+ if (!source.includes(from)) continue;
82
+ if (CHECK) {
83
+ unapplied.push(patch.reason);
84
+ done = true;
85
+ break;
86
+ }
87
+ writeFileSync(path, source.replace(from, to));
88
+ console.log(`patched: ${patch.reason}`);
89
+ done = true;
90
+ break;
91
+ }
92
+ // Neither the original text nor the patched text is there, so pi changed the code this
93
+ // patch names. Report it rather than force anything.
94
+ if (!done) gone.push(patch.reason);
95
+ }
96
+
97
+ for (const reason of unapplied) console.log(`NOT APPLIED: ${reason}`);
98
+ for (const reason of gone) console.log(`SEAM GONE: ${reason}`);
99
+ const bad = unapplied.length + gone.length;
100
+ console.log(bad ? `${bad} of ${PATCHES.length} not in place` : `all ${PATCHES.length} patches applied`);
101
+ process.exit(bad ? 1 : 0);
@@ -3,34 +3,58 @@
3
3
  Type `/huh` and reach `skill:huh`. Complete a skill anywhere in the line, not only at the
4
4
  start of it.
5
5
 
6
- pi lists every skill in the completion dropdown under its bare name, but only `/skill:huh`
7
- runs one. So the dropdown offers `/huh`, you press enter, and pi answers "Unknown command".
8
- The fuzzy matching is not the problem; the name it matches against is. This wraps the
9
- built-in provider and rewrites every skill row to the form that dispatches, then re-runs
10
- the match against the prefixed name so `/skill:hu` still finds it after the built-in pass
11
- has given up.
6
+ ## Install
7
+
8
+ ```sh
9
+ pi install npm:pi-umbra-skill-matcher
10
+ node ~/.pi/agent/npm/node_modules/pi-umbra-skill-matcher/patch.mjs
11
+ ```
12
+
13
+ Then restart pi.
14
+
15
+ ## What it fixes
16
+
17
+ pi lists every skill in the completion dropdown under its bare name, but only
18
+ `/skill:huh` runs one. So the dropdown offers `/huh`, you press enter, and pi answers
19
+ "Unknown command". The fuzzy matching is not the problem; the name it matches against is.
20
+ This wraps the built-in provider and rewrites every skill row to the form that dispatches,
21
+ then re-runs the match against the prefixed name so `/skill:hu` still finds it after the
22
+ built-in pass has given up.
12
23
 
13
24
  It also fills the mid-sentence slash menu. pi gates that menu on the line starting with
14
- `/`, so a `/name` written inside a sentence never reaches a provider at all. Lifting the
15
- gate needs a bundle patch, which is not published in this repo. What appears behind the
16
- gate is decided here.
25
+ `/`, so a `/name` written inside a sentence never reaches a provider at all.
17
26
 
18
27
  The mid-sentence match stays deliberately narrow, so a path keeps behaving like a path:
19
28
  the token must be the last one on the line, hold no space, and follow real text ending in
20
29
  whitespace. `src/foo` and an indented `/foo` both fail those tests and are left to pi.
21
30
 
31
+ ## The second install line is not optional
32
+
33
+ Two of the four seams are past what the extension API exposes: the dispatch map that only
34
+ knows the prefixed name, and the gate that keeps the slash menu at the start of the line.
35
+ So this package ships four patches against pi's built bundle. Without them the extension
36
+ loads and does nothing.
37
+
38
+ ```sh
39
+ node ~/.pi/agent/npm/node_modules/pi-umbra-skill-matcher/patch.mjs --check
40
+ ```
41
+
42
+ `--check` writes nothing and exits 1 when a patch is missing. **Run it after every pi
43
+ update.** An update replaces the bundle, so all four patches disappear silently and `/huh`
44
+ goes back to "Unknown command".
45
+
46
+ Nothing here is destructive. Each patch is additive, a patch whose text no longer matches
47
+ is reported rather than forced, and reinstalling pi returns the bundle to stock.
48
+
49
+ Set `PI_UMBRA_PI` if pi lives somewhere this script cannot find it.
50
+
22
51
  ```
23
52
  pi-umbra-skill-matcher/
24
53
  ├── checks
25
54
  │ └── umbra-skill-matcher.check.ts
26
- └── extensions
27
- └── umbra-skill-matcher.ts
28
- ```
29
-
30
- ## Install
31
-
32
- ```sh
33
- pi install npm:pi-umbra-skill-matcher
55
+ ├── extensions
56
+ └── umbra-skill-matcher.ts
57
+ └── patch.mjs
34
58
  ```
35
59
 
36
60
  MIT.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-umbra-skill-matcher",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Type /huh and reach skill:huh. pi lists every skill under its bare name, which is not the name that dispatches.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -21,7 +21,9 @@
21
21
  "files": [
22
22
  "extensions",
23
23
  "checks",
24
- "README.md"
24
+ "README.md",
25
+ "patch.mjs",
26
+ "LICENSE"
25
27
  ],
26
28
  "pi": {
27
29
  "extensions": [
@@ -0,0 +1,115 @@
1
+ // pi-umbra-skill-matcher needs 4 seams that pi's extension API does not expose, so it edits
2
+ // pi's built bundle. Run it after installing, and again after every pi update:
3
+ //
4
+ // node <this file> apply
5
+ // node <this file> --check report only, write nothing, exit 1 if anything is missing
6
+ //
7
+ // pi runs dist/bundle/cli.js, a self-contained bundle, and the chunk names are content-hashed,
8
+ // so every patch finds its own place by matching its own text rather than by line number.
9
+ //
10
+ // Nothing here is destructive. Each patch is additive, a patch whose text no longer matches is
11
+ // reported instead of forced, and reinstalling pi returns the bundle to stock. A pi upgrade
12
+ // replaces the bundle and drops every patch silently, which is what --check is for.
13
+ import { createRequire } from "node:module";
14
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { dirname, join } from "node:path";
17
+
18
+ // PI_UMBRA_PI points this at another pi tree. Otherwise pi is resolved from wherever this
19
+ // package was installed, since it is a peer dependency, and the bun global path is the last resort.
20
+ const resolvePi = () => {
21
+ if (process.env.PI_UMBRA_PI) return process.env.PI_UMBRA_PI;
22
+ try {
23
+ return dirname(createRequire(import.meta.url).resolve("@earendil-works/pi-coding-agent/package.json"));
24
+ } catch {
25
+ return join(homedir(), ".bun/install/global/node_modules/@earendil-works/pi-coding-agent");
26
+ }
27
+ };
28
+
29
+ const BUNDLE = join(resolvePi(), "dist/bundle");
30
+ if (!existsSync(BUNDLE)) {
31
+ console.error(`no pi bundle at ${BUNDLE} - set PI_UMBRA_PI to your pi install`);
32
+ process.exit(1);
33
+ }
34
+
35
+ const PATCHES = [
36
+ {
37
+ // A skill is reachable as /skill:huh only. The prefix is hardcoded at both sites below,
38
+ // with no setting for it. The dispatch map now holds each skill under both names, so
39
+ // /huh works and any /skill:huh already in muscle memory or in a doc keeps working.
40
+ // Only the bare name is offered in autocomplete, to keep the list one line per skill.
41
+ reason: "/huh instead of /skill:huh",
42
+ from: "let commandName=`skill:${skill.name}`;this.skillCommands.set(commandName,skill.filePath),skillCommandList.push({name:commandName,",
43
+ to: "let commandName=skill.name;this.skillCommands.set(`skill:${skill.name}`,skill.filePath),this.skillCommands.set(commandName,skill.filePath),skillCommandList.push({name:commandName,",
44
+ },
45
+ {
46
+ reason: "the command list names skills without the prefix too",
47
+ from: "skills=this._resourceLoader.getSkills().skills.map(skill=>({name:`skill:${skill.name}`,",
48
+ to: "skills=this._resourceLoader.getSkills().skills.map(skill=>({name:skill.name,",
49
+ },
50
+ {
51
+ // The slash menu only ever opens on a line that starts with "/": `isAtStartOfMessage`
52
+ // gates the keystroke and `isInSlashCommandContext` gates every letter after it. A
53
+ // "/name" written in the middle of a sentence therefore reaches no provider at all, and
54
+ // an extension cannot fix that - it is never asked. These two open the door;
55
+ // umbra-skill-matcher decides what stands behind it.
56
+ //
57
+ // `umbraMidSlash` is deliberately narrow, so a path keeps behaving like a path: the
58
+ // token must be the last one on the line, hold no space, and sit after real text that
59
+ // ends in whitespace. "src/foo" fails on the last clause, " /foo" on the one before it.
60
+ // `head!==head.trimEnd()` is how "ends in a space or a tab" is written without putting an
61
+ // escape inside a patch string.
62
+ reason: "a mid-sentence /name opens the slash menu",
63
+ from: `isInSlashCommandContext(textBeforeCursor){return this.isSlashMenuAllowed()&&textBeforeCursor.trimStart().startsWith("/")}`,
64
+ to: `isInSlashCommandContext(textBeforeCursor){return this.isSlashMenuAllowed()&&textBeforeCursor.trimStart().startsWith("/")||this.umbraMidSlash(textBeforeCursor)}umbraMidSlash(t){let i=t.lastIndexOf("/");if(i<1)return!1;if(t.slice(i+1).indexOf(" ")>=0)return!1;let head=t.slice(0,i);return head.trim()!==""&&head!==head.trimEnd()}`,
65
+ },
66
+ {
67
+ // The companion to the patch above: without it the menu opens on the second character
68
+ // rather than on the slash, because typing "/" is handled by its own branch that ends at
69
+ // `isAtStartOfMessage`. At that moment the token after the slash is still empty, which is
70
+ // exactly the shape `umbraMidSlash` accepts.
71
+ reason: "typing a mid-sentence slash opens the menu on the slash",
72
+ from: `char==="/"&&this.isAtStartOfMessage()`,
73
+ to: `char==="/"&&(this.isAtStartOfMessage()||this.umbraMidSlash((this.state.lines[this.state.cursorLine]||"").slice(0,this.state.cursorCol)))`,
74
+ },
75
+ ];
76
+
77
+ const files = [join(BUNDLE, "cli.js"), ...readdirSync(join(BUNDLE, "chunks")).map((name) => join(BUNDLE, "chunks", name))];
78
+ const CHECK = process.argv.includes("--check");
79
+ const unapplied = [];
80
+ const gone = [];
81
+
82
+ for (const patch of PATCHES) {
83
+ let done = false;
84
+ for (const path of files) {
85
+ const source = readFileSync(path, "utf8");
86
+ // A multi-line patch is written with newline escapes; match whatever line ending the file uses.
87
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
88
+ const from = patch.from.split("\n").join(eol);
89
+ const to = patch.to.split("\n").join(eol);
90
+ if (source.includes(to)) {
91
+ if (!CHECK) console.log(`already patched: ${patch.reason}`);
92
+ done = true;
93
+ break;
94
+ }
95
+ if (!source.includes(from)) continue;
96
+ if (CHECK) {
97
+ unapplied.push(patch.reason);
98
+ done = true;
99
+ break;
100
+ }
101
+ writeFileSync(path, source.replace(from, to));
102
+ console.log(`patched: ${patch.reason}`);
103
+ done = true;
104
+ break;
105
+ }
106
+ // Neither the original text nor the patched text is there, so pi changed the code this
107
+ // patch names. Report it rather than force anything.
108
+ if (!done) gone.push(patch.reason);
109
+ }
110
+
111
+ for (const reason of unapplied) console.log(`NOT APPLIED: ${reason}`);
112
+ for (const reason of gone) console.log(`SEAM GONE: ${reason}`);
113
+ const bad = unapplied.length + gone.length;
114
+ console.log(bad ? `${bad} of ${PATCHES.length} not in place` : `all ${PATCHES.length} patches applied`);
115
+ process.exit(bad ? 1 : 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-umbra",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Every small thing pi does not build in, in one install: seven themes and the chrome around them, the input bar, session rename, a shimmer through the working indicator, mid-prompt skill completion, ask, copy-chat, preview and help.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -46,8 +46,8 @@
46
46
  "pi-umbra-inputbar": "0.1.2",
47
47
  "pi-umbra-preview": "0.1.2",
48
48
  "pi-umbra-rename": "0.1.2",
49
- "pi-umbra-shimmer": "0.1.2",
50
- "pi-umbra-skill-matcher": "0.1.1"
49
+ "pi-umbra-shimmer": "0.1.3",
50
+ "pi-umbra-skill-matcher": "0.1.2"
51
51
  },
52
52
  "bundledDependencies": [
53
53
  "pi-umbra-theme",