pi-umbra 0.2.0 → 0.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/README.md +7 -5
- package/node_modules/pi-umbra-help/README.md +1 -0
- package/node_modules/pi-umbra-help/checks/umbra-help.check.ts +7 -0
- package/node_modules/pi-umbra-help/extensions/umbra-help.ts +12 -1
- package/node_modules/pi-umbra-help/package.json +1 -1
- package/node_modules/pi-umbra-inputbar/extensions/umbra-inputbar.ts +2 -1
- package/node_modules/pi-umbra-inputbar/package.json +1 -1
- package/node_modules/pi-umbra-shimmer/package.json +1 -1
- package/node_modules/pi-umbra-shimmer/patch.mjs +21 -7
- package/node_modules/pi-umbra-skill-matcher/package.json +1 -1
- package/node_modules/pi-umbra-skill-matcher/patch.mjs +21 -7
- package/node_modules/pi-umbra-subagents/LICENSE +21 -0
- package/node_modules/pi-umbra-subagents/README.md +101 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-loop.ts +102 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/bar/bar-line.ts +262 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/bar.check.ts +198 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/bar.ts +229 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/index.ts +141 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/models.ts +137 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/spec.ts +88 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/store.check.ts +140 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/fan/store.ts +490 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/panel.check.ts +237 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/panel.ts +378 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/SKILL.md +95 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/beacon.ts +210 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/delegate.env +12 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/report.md +15 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/run.check.sh +120 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/run.sh +170 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/state.check.ts +137 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/delegate/state.ts +295 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents/skills/fan/SKILL.md +52 -0
- package/node_modules/pi-umbra-subagents/extensions/umbra-subagents.ts +4 -0
- package/node_modules/pi-umbra-subagents/package.json +43 -0
- package/node_modules/pi-umbra-subagents/patch.mjs +97 -0
- package/node_modules/pi-umbra-theme/README.md +39 -16
- package/node_modules/pi-umbra-theme/checks/umbra-background.check.ts +44 -1
- package/node_modules/pi-umbra-theme/checks/umbra-image-viewer.check.ts +85 -0
- package/node_modules/pi-umbra-theme/checks/umbra-toolbox.check.ts +20 -1
- package/node_modules/pi-umbra-theme/checks/umbra-working.check.ts +17 -10
- package/node_modules/pi-umbra-theme/extensions/umbra-background.ts +52 -1
- package/node_modules/pi-umbra-theme/extensions/umbra-image-viewer.ts +230 -0
- package/node_modules/pi-umbra-theme/extensions/umbra-toolbox/renderer/compact-mode.ts +3 -3
- package/node_modules/pi-umbra-theme/extensions/umbra-toolbox/renderer/mouse/hover.ts +0 -4
- package/node_modules/pi-umbra-theme/extensions/umbra-toolbox/renderer/mouse/interaction.ts +20 -68
- package/node_modules/pi-umbra-theme/extensions/umbra-toolbox/renderer/mouse/layout.ts +0 -4
- package/node_modules/pi-umbra-theme/extensions/umbra-toolbox/renderer/mouse/scroll.ts +6 -190
- package/node_modules/pi-umbra-theme/extensions/umbra-toolbox/renderer/tool/diff/diff-palette.ts +44 -3
- package/node_modules/pi-umbra-theme/extensions/umbra-toolbox/renderer/tool/diff/shiki-highlight.ts +5 -3
- package/node_modules/pi-umbra-theme/extensions/umbra-toolbox/renderer/tool/names.ts +1 -0
- package/node_modules/pi-umbra-theme/extensions/umbra-working.ts +16 -6
- package/node_modules/pi-umbra-theme/package.json +1 -1
- package/node_modules/pi-umbra-theme/themes/umbra-astral-veil.json +1 -1
- package/node_modules/pi-umbra-theme/themes/umbra-deep-current.json +4 -4
- package/node_modules/pi-umbra-theme/themes/umbra-ember-ash.json +10 -10
- package/node_modules/pi-umbra-theme/themes/umbra-onyx-slate.json +5 -5
- package/node_modules/pi-umbra-theme/themes/umbra-tidal-drift.json +20 -20
- package/node_modules/pi-umbra-theme/themes/umbra-venom-dusk.json +4 -4
- package/package.json +16 -9
package/README.md
CHANGED
|
@@ -2,25 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
Every pi-umbra package in one install: seven dark themes, tool calls drawn as cards, a
|
|
4
4
|
status footer, an input bar with the session name, a working line that names the running
|
|
5
|
-
tool, and the `/umb-*` commands.
|
|
5
|
+
tool, parallel read-only branches with a live panel, and the `/umb-*` commands.
|
|
6
6
|
|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
pi install npm:pi-umbra
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The
|
|
14
|
-
run
|
|
13
|
+
The ten packages ship bundled inside this one tarball. Three of them patch pi's bundle, so
|
|
14
|
+
run the three scripts once now and again after every pi update, then restart pi:
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
17
|
node ~/.pi/agent/npm/node_modules/pi-umbra/node_modules/pi-umbra-shimmer/patch.mjs
|
|
18
18
|
node ~/.pi/agent/npm/node_modules/pi-umbra/node_modules/pi-umbra-skill-matcher/patch.mjs
|
|
19
|
+
node ~/.pi/agent/npm/node_modules/pi-umbra/node_modules/pi-umbra-subagents/patch.mjs
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
| Package | What it adds |
|
|
22
23
|
|---|---|
|
|
23
|
-
| [pi-umbra-theme](https://www.npmjs.com/package/pi-umbra-theme) | seven themes, terminal background, footer, gutter, tool cards, working line |
|
|
24
|
+
| [pi-umbra-theme](https://www.npmjs.com/package/pi-umbra-theme) | seven themes, terminal background, footer, gutter, tool cards, image viewer, working line |
|
|
24
25
|
| [pi-umbra-inputbar](https://www.npmjs.com/package/pi-umbra-inputbar) | `❯` prompt, session name on the input border, `/umb-color` |
|
|
25
26
|
| [pi-umbra-ask](https://www.npmjs.com/package/pi-umbra-ask) | a tool the model calls to ask you a question and wait |
|
|
26
27
|
| [pi-umbra-shimmer](https://www.npmjs.com/package/pi-umbra-shimmer) | a wave of colour through the working line, `/umb-shimmer` |
|
|
@@ -29,6 +30,7 @@ node ~/.pi/agent/npm/node_modules/pi-umbra/node_modules/pi-umbra-skill-matcher/p
|
|
|
29
30
|
| [pi-umbra-copy-chat](https://www.npmjs.com/package/pi-umbra-copy-chat) | `/umb-copy-chat`, the session on the clipboard |
|
|
30
31
|
| [pi-umbra-preview](https://www.npmjs.com/package/pi-umbra-preview) | `/umb-preview`, render a document in the browser |
|
|
31
32
|
| [pi-umbra-help](https://www.npmjs.com/package/pi-umbra-help) | `/umb-help` and `/umb-doctor` |
|
|
33
|
+
| [pi-umbra-subagents](https://www.npmjs.com/package/pi-umbra-subagents) | parallel read-only pi branches with a live panel, `/umb-fan`, `/umb-loop` |
|
|
32
34
|
|
|
33
35
|
Full documentation: [github.com/grknbyk/pi-umbra](https://github.com/grknbyk/pi-umbra).
|
|
34
36
|
|
|
@@ -22,6 +22,7 @@ Reports the faults that give no error of their own:
|
|
|
22
22
|
|---|---|
|
|
23
23
|
| a bundle patch is missing | after a pi update, the shimmer and the mid-sentence skill menu stop without a word |
|
|
24
24
|
| two skills share a name | `~/.pi/agent/skills`, `<project>/.pi/skills` and `~/.agents/skills` are read in that order and the first copy wins, even when a later one is newer |
|
|
25
|
+
| the terminal kept its own background | the theme is not shown in full; it says where to set the colour in your terminal |
|
|
25
26
|
| no truecolor announced | `COLORTERM` is not set and the terminal is not a known truecolor one, so colours may band |
|
|
26
27
|
| the theme is not an umbra one | only a note; everything still works |
|
|
27
28
|
|
|
@@ -87,6 +87,13 @@ const offTheme = diagnose(at({ theme: "dark" }));
|
|
|
87
87
|
assert.equal(offTheme[0]!.level, "note", "another theme is a remark, not a fault");
|
|
88
88
|
assert.equal(diagnose(at({ theme: "umbra-ember-ash" })).length, 0);
|
|
89
89
|
|
|
90
|
+
// A background the terminal kept is reported with the terminal's own fix; one it took, or one it
|
|
91
|
+
// never answered about, says nothing.
|
|
92
|
+
const kept = diagnose(at({ background: { wanted: "#0b0e14", ignored: true, howTo: "VS Code: add it." } }));
|
|
93
|
+
assert.equal(kept.length, 1);
|
|
94
|
+
assert.match(kept[0]!.detail, /#0b0e14.*VS Code: add it\./);
|
|
95
|
+
assert.equal(diagnose(at({ background: { wanted: "#0b0e14", ignored: false, howTo: "" } })).length, 0);
|
|
96
|
+
|
|
90
97
|
// --- help ----------------------------------------------------------------------------------
|
|
91
98
|
// Only umb- rows, sorted, with pi's own descriptions - so the list cannot drift from the commands.
|
|
92
99
|
const help = renderHelp(
|
|
@@ -38,7 +38,7 @@ export const PROBES: Probe[] = [
|
|
|
38
38
|
id: "loop",
|
|
39
39
|
marker: "globalThis.__piSubmit",
|
|
40
40
|
feature: "the /umb-loop resubmit",
|
|
41
|
-
without: "
|
|
41
|
+
without: "/umb-loop refuses to start",
|
|
42
42
|
command: "umb-loop",
|
|
43
43
|
},
|
|
44
44
|
{
|
|
@@ -58,6 +58,8 @@ export type Facts = {
|
|
|
58
58
|
skillRoots: { root: string; names: string[] }[];
|
|
59
59
|
truecolor: boolean;
|
|
60
60
|
theme: string;
|
|
61
|
+
/** Set by umbra-background once the terminal has answered; absent when it never did. */
|
|
62
|
+
background?: { wanted: string; ignored: boolean; howTo: string };
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
export type Finding = { level: "warn" | "note"; title: string; detail: string };
|
|
@@ -106,6 +108,14 @@ export const diagnose = (facts: Facts): Finding[] => {
|
|
|
106
108
|
});
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
if (facts.background?.ignored) {
|
|
112
|
+
findings.push({
|
|
113
|
+
level: "warn",
|
|
114
|
+
title: "the terminal kept its own background",
|
|
115
|
+
detail: `It did not take ${facts.background.wanted}, so the theme is not shown in full. ${facts.background.howTo}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
109
119
|
// A note rather than a warning: plenty of terminals handle 24-bit colour without announcing
|
|
110
120
|
// it, so this is a guess that has to earn its line rather than cry wolf on every run.
|
|
111
121
|
if (!facts.truecolor) {
|
|
@@ -208,6 +218,7 @@ export const gather = (cwd: string, theme: string, commands: Command[]): Facts =
|
|
|
208
218
|
skillRoots: roots.map((root) => ({ root, names: namesIn(root) })).filter((entry) => entry.names.length > 0),
|
|
209
219
|
truecolor: announcesTruecolor(process.env),
|
|
210
220
|
theme,
|
|
221
|
+
background: (globalThis as { __umbraBackground?: Facts["background"] }).__umbraBackground,
|
|
211
222
|
};
|
|
212
223
|
};
|
|
213
224
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-umbra-help",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Two commands over the pi-umbra family: /umb-help lists what is installed and how to configure it, /umb-doctor reports the faults that would otherwise stay silent.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -130,7 +130,8 @@ export class InputBar extends CustomEditor {
|
|
|
130
130
|
const blank = " ".repeat(gutter);
|
|
131
131
|
return lines.map((line, i) => {
|
|
132
132
|
if (i === 0 || i === shown + 1) return edge + line;
|
|
133
|
-
|
|
133
|
+
// The chevron is part of what you type into, so it takes the text colour, not the border's.
|
|
134
|
+
if (i === 1) return CHEVRON + blank.slice(1) + line;
|
|
134
135
|
return blank + line;
|
|
135
136
|
});
|
|
136
137
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-umbra-shimmer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -10,20 +10,34 @@
|
|
|
10
10
|
// Nothing here is destructive. Each patch is additive, a patch whose text no longer matches is
|
|
11
11
|
// reported instead of forced, and reinstalling pi returns the bundle to stock. A pi upgrade
|
|
12
12
|
// replaces the bundle and drops every patch silently, which is what --check is for.
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
13
14
|
import { createRequire } from "node:module";
|
|
14
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { existsSync, realpathSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
import { dirname, join } from "node:path";
|
|
17
18
|
|
|
18
|
-
// PI_UMBRA_PI points this at another pi tree. Otherwise pi is
|
|
19
|
-
//
|
|
19
|
+
// PI_UMBRA_PI points this at another pi tree. Otherwise the pi that runs is the one to patch:
|
|
20
|
+
// the `pi` on PATH, followed through its symlink. Windows puts a .cmd shim there instead, so
|
|
21
|
+
// npm's global root (%APPDATA%\npm\node_modules) comes next, then bun's. The copy npm installs
|
|
22
|
+
// beside this package as a peer dependency is last: pi never runs it. The first that has a built
|
|
23
|
+
// bundle wins.
|
|
20
24
|
const resolvePi = () => {
|
|
21
25
|
if (process.env.PI_UMBRA_PI) return process.env.PI_UMBRA_PI;
|
|
26
|
+
const quiet = { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 20_000 };
|
|
27
|
+
const candidates = [];
|
|
22
28
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
const bin = execSync(process.platform === "win32" ? "where pi" : "command -v pi", quiet).split(/\r?\n/)[0].trim();
|
|
30
|
+
for (let dir = dirname(realpathSync(bin)); dir !== dirname(dir); dir = dirname(dir)) candidates.push(dir);
|
|
31
|
+
} catch {}
|
|
32
|
+
try {
|
|
33
|
+
const root = execSync("npm root -g", quiet).trim();
|
|
34
|
+
if (root) candidates.push(join(root, "@earendil-works", "pi-coding-agent"));
|
|
35
|
+
} catch {}
|
|
36
|
+
candidates.push(join(homedir(), ".bun", "install", "global", "node_modules", "@earendil-works", "pi-coding-agent"));
|
|
37
|
+
try {
|
|
38
|
+
candidates.push(dirname(createRequire(import.meta.url).resolve("@earendil-works/pi-coding-agent/package.json")));
|
|
39
|
+
} catch {}
|
|
40
|
+
return candidates.find((dir) => existsSync(join(dir, "dist", "bundle"))) ?? candidates[0];
|
|
27
41
|
};
|
|
28
42
|
|
|
29
43
|
const BUNDLE = join(resolvePi(), "dist/bundle");
|
|
@@ -10,20 +10,34 @@
|
|
|
10
10
|
// Nothing here is destructive. Each patch is additive, a patch whose text no longer matches is
|
|
11
11
|
// reported instead of forced, and reinstalling pi returns the bundle to stock. A pi upgrade
|
|
12
12
|
// replaces the bundle and drops every patch silently, which is what --check is for.
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
13
14
|
import { createRequire } from "node:module";
|
|
14
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { existsSync, realpathSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
16
|
import { homedir } from "node:os";
|
|
16
17
|
import { dirname, join } from "node:path";
|
|
17
18
|
|
|
18
|
-
// PI_UMBRA_PI points this at another pi tree. Otherwise pi is
|
|
19
|
-
//
|
|
19
|
+
// PI_UMBRA_PI points this at another pi tree. Otherwise the pi that runs is the one to patch:
|
|
20
|
+
// the `pi` on PATH, followed through its symlink. Windows puts a .cmd shim there instead, so
|
|
21
|
+
// npm's global root (%APPDATA%\npm\node_modules) comes next, then bun's. The copy npm installs
|
|
22
|
+
// beside this package as a peer dependency is last: pi never runs it. The first that has a built
|
|
23
|
+
// bundle wins.
|
|
20
24
|
const resolvePi = () => {
|
|
21
25
|
if (process.env.PI_UMBRA_PI) return process.env.PI_UMBRA_PI;
|
|
26
|
+
const quiet = { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 20_000 };
|
|
27
|
+
const candidates = [];
|
|
22
28
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
const bin = execSync(process.platform === "win32" ? "where pi" : "command -v pi", quiet).split(/\r?\n/)[0].trim();
|
|
30
|
+
for (let dir = dirname(realpathSync(bin)); dir !== dirname(dir); dir = dirname(dir)) candidates.push(dir);
|
|
31
|
+
} catch {}
|
|
32
|
+
try {
|
|
33
|
+
const root = execSync("npm root -g", quiet).trim();
|
|
34
|
+
if (root) candidates.push(join(root, "@earendil-works", "pi-coding-agent"));
|
|
35
|
+
} catch {}
|
|
36
|
+
candidates.push(join(homedir(), ".bun", "install", "global", "node_modules", "@earendil-works", "pi-coding-agent"));
|
|
37
|
+
try {
|
|
38
|
+
candidates.push(dirname(createRequire(import.meta.url).resolve("@earendil-works/pi-coding-agent/package.json")));
|
|
39
|
+
} catch {}
|
|
40
|
+
return candidates.find((dir) => existsSync(join(dir, "dist", "bundle"))) ?? candidates[0];
|
|
27
41
|
};
|
|
28
42
|
|
|
29
43
|
const BUNDLE = join(resolvePi(), "dist/bundle");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 grkn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# pi-umbra-subagents
|
|
2
|
+
|
|
3
|
+
Parallel, read-only pi branches you can watch while they work. A branch is a separate `pi -p`
|
|
4
|
+
process with an empty context; a live list above the input box shows what each one is doing,
|
|
5
|
+
and their answers come back to the session when the last one ends. Also `/umb-loop`, which
|
|
6
|
+
sends a prompt again after every reply, on a count or a timer.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pi install npm:pi-umbra-subagents
|
|
12
|
+
node ~/.pi/agent/npm/node_modules/pi-umbra-subagents/patch.mjs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Restart pi after the patch. Only `/umb-loop` needs it; the branches work without it.
|
|
16
|
+
|
|
17
|
+
Nothing is added to the model's prompt and no tool is registered. The model starts a run by
|
|
18
|
+
ending its answer with a fenced `fan` block, which the bundled `fan` skill teaches it:
|
|
19
|
+
|
|
20
|
+
````
|
|
21
|
+
```fan
|
|
22
|
+
name: weather-map
|
|
23
|
+
desc: Map the weather API
|
|
24
|
+
# Map
|
|
25
|
+
routes: List every route in src/server.ts with file:line.
|
|
26
|
+
upstream: List what src/forecast.ts fetches, with file:line.
|
|
27
|
+
# Plan
|
|
28
|
+
design: Given the Map results, propose the change.
|
|
29
|
+
```
|
|
30
|
+
````
|
|
31
|
+
|
|
32
|
+
`# Title` starts a phase. Phases run in order; the branches inside one run at the same time.
|
|
33
|
+
`label@provider/model: task` picks a model for one branch, otherwise it runs on the session's
|
|
34
|
+
model. The results arrive as a follow-up message at the start of the next turn.
|
|
35
|
+
|
|
36
|
+
## Commands and keys
|
|
37
|
+
|
|
38
|
+
| Command or key | Effect |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `/umb-fan [spec]` | start a run yourself; with no argument an editor opens with a template |
|
|
41
|
+
| `/umb-agents`, `alt+a` | open the agent panel |
|
|
42
|
+
| `↓` from the last input line | move the `❯` from `main` into the agent list under the input box |
|
|
43
|
+
| `↑` `↓`, then `enter` in that list | pick an agent and open the panel on it; `↑` past the first agent or `esc` goes back |
|
|
44
|
+
| `↑` `↓` in the panel | move between phases, or between the agents of one phase |
|
|
45
|
+
| `→` `←` in the panel | go into a phase's agents, and back out to the phases |
|
|
46
|
+
| `x` in the panel | stop the selected phase, or the selected agent |
|
|
47
|
+
| `esc` in the panel | back to the input box, unsent text kept |
|
|
48
|
+
| `/umb-loop [count\|duration] [prompt]` | send the prompt again after each reply; run it again to stop |
|
|
49
|
+
|
|
50
|
+
`/umb-loop 5 fix the next failing test` runs five times, `/umb-loop 10m continue` for ten
|
|
51
|
+
minutes, `/umb-loop 0 …` until stopped. Without a prompt it sends `Continue.`. Escape cancels
|
|
52
|
+
one round and keeps the loop.
|
|
53
|
+
|
|
54
|
+
## Skills
|
|
55
|
+
|
|
56
|
+
| Skill | Use |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `fan` | the fenced block above; the extension owns the branches |
|
|
59
|
+
| `delegate` | the same branches started from a bash call (`dstart`, `branch`, `dwait`), for runs the model wants to read back itself, or continue with `dresume` after a branch asks a question |
|
|
60
|
+
|
|
61
|
+
Both write every branch's answer to `.pi-out/<run>/<phase>-<name>.md` and its errors to the
|
|
62
|
+
matching `.err`. Add `.pi-out/` to `.gitignore`. Quitting pi stops the branches; what they
|
|
63
|
+
wrote stays on disk.
|
|
64
|
+
|
|
65
|
+
## Settings
|
|
66
|
+
|
|
67
|
+
| Variable | Default | Effect |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `FAN_MODEL` | the session's model | model for `fan` branches that do not name one |
|
|
70
|
+
| `FAN_TOOLS` | `read,grep,find,ls` | tools a branch may use |
|
|
71
|
+
| `FAN_LOAD` | `$LOAD` from `delegate.env` | extra `-e <path>` flags; branches start with `--no-extensions`, so a provider that comes from an extension has to be listed here |
|
|
72
|
+
| `FAN_TIMEOUT_MS` | `$DELEGATE_TIMEOUT` × 1000, else `300000` | a branch still running after this long is cut off |
|
|
73
|
+
|
|
74
|
+
Both ways in share one settings file: the packaged `delegate.env`, then
|
|
75
|
+
`~/.pi/agent/delegate.env`, which wins.
|
|
76
|
+
|
|
77
|
+
A branch starts without extensions, so a model that only an extension provides is not there
|
|
78
|
+
for it. When no branch can start on its model, you get a warning and the model is told to ask
|
|
79
|
+
you which one to use. The suggestions are your scoped models (`enabledModels`) that a branch
|
|
80
|
+
can reach; if none can, free models; if there are none, the first few a branch can reach.
|
|
81
|
+
Name the choice per branch with `label@provider/model`, set `FAN_MODEL`, or load the extension
|
|
82
|
+
through `LOAD`.
|
|
83
|
+
|
|
84
|
+
A model id may carry a colon, as in `label@openrouter/some-model:free: task`; the key ends at
|
|
85
|
+
the first colon followed by a space.
|
|
86
|
+
|
|
87
|
+
## The patch
|
|
88
|
+
|
|
89
|
+
`/umb-loop` sends its prompt the way the input box does, and the extension API has no way to
|
|
90
|
+
do that. `patch.mjs` makes one small additive edit to pi's installed bundle that exposes it. A
|
|
91
|
+
pi update removes the patch without any error, so run the script again after every update.
|
|
92
|
+
|
|
93
|
+
| Command | Effect |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `node .../patch.mjs` | apply the patch; a part already applied is skipped |
|
|
96
|
+
| `node .../patch.mjs --check` | change nothing, exit 1 if the patch is missing |
|
|
97
|
+
|
|
98
|
+
The script patches the `pi` on your PATH. Set `PI_UMBRA_PI` to the `pi-coding-agent`
|
|
99
|
+
directory to patch another one.
|
|
100
|
+
|
|
101
|
+
MIT.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
// The editor's submit path is not on the extension API, so repatch.mjs publishes it.
|
|
4
|
+
const submit = (text: string) => (globalThis as any).__piSubmit?.(text);
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PROMPT = "Continue.";
|
|
7
|
+
const UNIT_MS: Record<string, number> = { h: 3_600_000, m: 60_000, s: 1000 };
|
|
8
|
+
// "90s", "10 min", "1h30m" — a run of number+unit pairs at the very start of the args. A unit
|
|
9
|
+
// ends at any non-letter, not at \b, which never falls between "h" and "30".
|
|
10
|
+
const DURATION = /^(?:\d+\s*(?:hours?|hrs?|h|minutes?|mins?|m|seconds?|secs?|s)(?![a-z])\s*)+/i;
|
|
11
|
+
const COUNT = /^(\d+)\s*/;
|
|
12
|
+
const RESUBMIT_DELAY_MS = 50;
|
|
13
|
+
|
|
14
|
+
type Loop = { prompt: string; iterations?: number; deadline?: number; done: number };
|
|
15
|
+
|
|
16
|
+
const durationMs = (text: string) =>
|
|
17
|
+
[...text.matchAll(/(\d+)\s*([a-z]+)/gi)].reduce(
|
|
18
|
+
(sum, [, amount, unit]) => sum + Number(amount) * UNIT_MS[unit[0].toLowerCase()],
|
|
19
|
+
0,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const parse = (args: string): Loop => {
|
|
23
|
+
const trimmed = args.trim();
|
|
24
|
+
const rest = (from: string) => trimmed.slice(from.length).trim() || DEFAULT_PROMPT;
|
|
25
|
+
|
|
26
|
+
const duration = trimmed.match(DURATION);
|
|
27
|
+
if (duration) return { prompt: rest(duration[0]), deadline: Date.now() + durationMs(duration[0]), done: 0 };
|
|
28
|
+
|
|
29
|
+
const count = trimmed.match(COUNT);
|
|
30
|
+
if (count) {
|
|
31
|
+
// 0 means "no limit". Either branch strips the number from the prompt, so
|
|
32
|
+
// "/umb-loop 0 fix the tests" submits "fix the tests", not "0 fix the tests".
|
|
33
|
+
const iterations = Number(count[1]);
|
|
34
|
+
return iterations > 0
|
|
35
|
+
? { prompt: rest(count[0]), iterations, done: 0 }
|
|
36
|
+
: { prompt: rest(count[0]), done: 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { prompt: trimmed || DEFAULT_PROMPT, done: 0 };
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default function (pi: ExtensionAPI) {
|
|
43
|
+
let loop: Loop | undefined;
|
|
44
|
+
let cancelled = false;
|
|
45
|
+
|
|
46
|
+
const describe = () => {
|
|
47
|
+
if (!loop) return undefined;
|
|
48
|
+
if (loop.iterations) return `loop ${loop.done}/${loop.iterations}`;
|
|
49
|
+
if (loop.deadline) return `loop ${Math.max(0, Math.round((loop.deadline - Date.now()) / 1000))}s`;
|
|
50
|
+
return `loop ${loop.done}`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const stop = (ctx: ExtensionContext, reason: string) => {
|
|
54
|
+
loop = undefined;
|
|
55
|
+
ctx.ui.setStatus("loop", undefined);
|
|
56
|
+
ctx.ui.notify(reason);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
pi.registerCommand("umb-loop", {
|
|
60
|
+
description: "Toggle automatic resubmission after each yield: /umb-loop [count|duration] [prompt]",
|
|
61
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
62
|
+
if (loop) return stop(ctx, "loop off");
|
|
63
|
+
// Without the patch nothing would ever be sent, while the status claimed a running loop.
|
|
64
|
+
if (!(globalThis as any).__piSubmit) {
|
|
65
|
+
return ctx.ui.notify("umb-loop needs its patch: run pi-umbra-subagents/patch.mjs, then restart pi", "warning");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
loop = parse(args);
|
|
69
|
+
cancelled = false;
|
|
70
|
+
ctx.ui.setStatus("loop", describe());
|
|
71
|
+
ctx.ui.notify(`loop on — "${loop.prompt}"`);
|
|
72
|
+
if (ctx.isIdle()) {
|
|
73
|
+
loop.done++;
|
|
74
|
+
submit(loop.prompt);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// An aborted run (Escape) keeps the loop armed but skips this yield, so the turn comes back to
|
|
80
|
+
// the user. Read from how the run ended, not from the Escape key: an Escape that only closed a
|
|
81
|
+
// panel or an autocomplete list aborts nothing and must not stall the loop.
|
|
82
|
+
pi.on("agent_end", (event) => {
|
|
83
|
+
const last = [...event.messages].reverse().find((message) => message.role === "assistant");
|
|
84
|
+
if (loop && last && "stopReason" in last && last.stopReason === "aborted") cancelled = true;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
88
|
+
if (!loop) return;
|
|
89
|
+
if (cancelled) {
|
|
90
|
+
cancelled = false;
|
|
91
|
+
ctx.ui.notify("loop: iteration cancelled");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (loop.deadline && Date.now() >= loop.deadline) return stop(ctx, "loop done (time up)");
|
|
95
|
+
if (loop.iterations && loop.done >= loop.iterations) return stop(ctx, "loop done");
|
|
96
|
+
|
|
97
|
+
loop.done++;
|
|
98
|
+
ctx.ui.setStatus("loop", describe());
|
|
99
|
+
const prompt = loop.prompt;
|
|
100
|
+
setTimeout(() => loop && submit(prompt), RESUBMIT_DELAY_MS);
|
|
101
|
+
});
|
|
102
|
+
}
|