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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# pi-umbra-theme
|
|
2
2
|
|
|
3
|
-
Seven dark themes for pi, and
|
|
4
|
-
background, a gutter beside model text, the footer, the working line
|
|
3
|
+
Seven dark themes for pi, and six extensions for what a theme cannot colour: the terminal
|
|
4
|
+
background, a gutter beside model text, the footer, the working line, the tool calls and a viewer
|
|
5
|
+
for the images they return.
|
|
5
6
|
|
|
6
7
|

|
|
7
8
|
|
|
@@ -33,7 +34,12 @@ Pick one in `/settings`, or set `"theme"` in `settings.json`.
|
|
|
33
34
|
pi colours its own text but leaves the window behind it alone. umbra-background sets the
|
|
34
35
|
terminal background to the active theme's `export.pageBg` with OSC 11 when pi starts, and
|
|
35
36
|
again whenever you switch theme. On exit it sends OSC 111, which gives the terminal its own
|
|
36
|
-
colour back.
|
|
37
|
+
colour back.
|
|
38
|
+
|
|
39
|
+
Some terminals ignore OSC 11 and keep their own colour. pi asks the terminal which colour it
|
|
40
|
+
has. If the answer is not the theme's colour, pi shows a warning once, and `/umb-doctor` tells
|
|
41
|
+
you where to set the colour in that terminal (VS Code, Windows Terminal, Terminal.app,
|
|
42
|
+
JetBrains, tmux).
|
|
37
43
|
|
|
38
44
|
| `piUmbraTheme.background` | Effect |
|
|
39
45
|
|---|---|
|
|
@@ -81,7 +87,7 @@ it. Numbers are shortened: `695`, `7.0k`, `224k`, `1M`.
|
|
|
81
87
|
It replaces pi's fixed "Working…" with the tool that is running:
|
|
82
88
|
|
|
83
89
|
```
|
|
84
|
-
◴
|
|
90
|
+
◴ Running… (9s · ↓ 824 tokens)
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
The label follows the running tool, then the turn's elapsed time, then the output tokens so
|
|
@@ -91,12 +97,17 @@ While the model thinks, the line adds `thought for 12s`.
|
|
|
91
97
|
| Key | Label | Frames |
|
|
92
98
|
|---|---|---|
|
|
93
99
|
| `read` | Reading | `◜◠◝◞◡◟` |
|
|
94
|
-
| `ls
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
100
|
+
| `ls` | Listing | `⎺⎻⎼⎽⎼⎻` |
|
|
101
|
+
| `find`, `grep` | Searching | `⎺⎻⎼⎽⎼⎻` |
|
|
102
|
+
| `write` | Writing | `▖▘▝▗` |
|
|
103
|
+
| `edit` | Editing | `▖▘▝▗` |
|
|
104
|
+
| `bash`, `powershell` | Running | `◴◷◶◵` |
|
|
105
|
+
| `ask_user_question` | Asking | `◇◈◆◈` |
|
|
97
106
|
| `thinking` | Thinking | `✶✸✹✺✻✼` |
|
|
98
107
|
| `idle` | Working | `⣻⢿⡿⣟⣯⣷⣾⣽` |
|
|
99
|
-
|
|
|
108
|
+
| any other tool | the title its card shows, such as `Web Search` | `◇◈◆◈` |
|
|
109
|
+
|
|
110
|
+
Set `custom` to give every tool in the last row one label instead.
|
|
100
111
|
|
|
101
112
|
Every entry can be changed, and any tool name can get its own:
|
|
102
113
|
|
|
@@ -157,7 +168,6 @@ The mode and every setting persist in `~/.pi/agent/umbra-toolbox.json`.
|
|
|
157
168
|
| fullscreen | click the hint | expand that card |
|
|
158
169
|
| fullscreen | click an expanded card | collapse it |
|
|
159
170
|
| fullscreen | click a "show more" header | open the full text in a scrollable overlay |
|
|
160
|
-
| fullscreen | `Ctrl+End` or the `Back to bottom` button | jump to the latest output |
|
|
161
171
|
| text overlay | `↑` `↓` `PgUp` `PgDn` `Home` `End`, wheel | scroll |
|
|
162
172
|
| text overlay | `Esc` or `Ctrl+C` | close |
|
|
163
173
|
| settings panel | `Tab`, `Shift+Tab` | switch between Style, UI and Diff |
|
|
@@ -186,8 +196,8 @@ Change them in the panel, or edit `umbra-toolbox.json` while pi is closed.
|
|
|
186
196
|
| `scrollStepLines` | `3` | 1 to 50 | lines per mouse wheel step in fullscreen |
|
|
187
197
|
| `dimThinkingText` | `false` | | dim headings in compact mode |
|
|
188
198
|
|
|
189
|
-
`/umb-toolbox status` also prints keys for
|
|
190
|
-
|
|
199
|
+
`/umb-toolbox status` also prints keys for features that are not part of this package, such as
|
|
200
|
+
a footer and startup header. They have no effect here.
|
|
191
201
|
|
|
192
202
|
### The write tool
|
|
193
203
|
|
|
@@ -198,12 +208,24 @@ of the way when another extension already provides `write`.
|
|
|
198
208
|
A write card can show no diff in these cases: the session was resumed (the record lives in
|
|
199
209
|
memory), the old file was over 512 KB or not UTF-8 text, or the path was not a regular file.
|
|
200
210
|
|
|
201
|
-
|
|
211
|
+
## Image viewer
|
|
202
212
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
Click an image under a tool card, such as a screenshot the model read, and it opens full screen.
|
|
214
|
+
|
|
215
|
+
| Input | Effect |
|
|
216
|
+
|---|---|
|
|
217
|
+
| wheel, or two fingers up and down on a touchpad | zoom toward the pointer |
|
|
218
|
+
| double click | zoom in 2× at that point |
|
|
219
|
+
| drag, `←` `↑` `→` `↓` | move |
|
|
220
|
+
| `+`, `-` | zoom toward the centre |
|
|
221
|
+
| `0` | fit to the screen; an image smaller than the screen stays at 100% |
|
|
222
|
+
| `Esc`, `q` | close |
|
|
223
|
+
|
|
224
|
+
It needs kitty, and fullscreen mode (`"tuiMode": "fullscreen"`), where pi reports mouse clicks.
|
|
225
|
+
Kitty crops and scales the image itself, so the image is sent to the terminal once and each zoom
|
|
226
|
+
step sends only the new crop. Kitty on Linux does not pass pinch gestures on to programs, so a
|
|
227
|
+
pinch does nothing. The click reaches the viewer through the tool cards; without them, nothing
|
|
228
|
+
opens it.
|
|
207
229
|
|
|
208
230
|
## Files
|
|
209
231
|
|
|
@@ -214,6 +236,7 @@ pi-umbra-theme/
|
|
|
214
236
|
│ ├── umbra-background.ts
|
|
215
237
|
│ ├── umbra-footer.ts
|
|
216
238
|
│ ├── umbra-gutter.ts
|
|
239
|
+
│ ├── umbra-image-viewer.ts
|
|
217
240
|
│ ├── umbra-toolbox/
|
|
218
241
|
│ └── umbra-working.ts
|
|
219
242
|
├── lib/
|
|
@@ -29,7 +29,7 @@ process.stdout.write = ((chunk: string) => {
|
|
|
29
29
|
return realWrite(chunk as never);
|
|
30
30
|
}) as never;
|
|
31
31
|
|
|
32
|
-
const { default: umbraBackground } = await import("../extensions/umbra-background.ts");
|
|
32
|
+
const { default: umbraBackground, backgroundHowTo, sameColour } = await import("../extensions/umbra-background.ts");
|
|
33
33
|
|
|
34
34
|
type Handler = (event: unknown, ctx: unknown) => void;
|
|
35
35
|
const handlers = new Map<string, Handler>();
|
|
@@ -43,6 +43,10 @@ const pi = {
|
|
|
43
43
|
umbraBackground(pi as never);
|
|
44
44
|
|
|
45
45
|
const notices: string[] = [];
|
|
46
|
+
// The terminal as the TUI sees it: it answers the OSC 11 query with this colour, or never.
|
|
47
|
+
let answer: { r: number; g: number; b: number } | undefined;
|
|
48
|
+
const setWidget = (_key: string, factory: (tui: unknown) => unknown) =>
|
|
49
|
+
factory({ queryTerminalBackgroundColor: async () => answer });
|
|
46
50
|
// Real pi hands the extension a Theme with NO sourcePath; the file it was read from is only on
|
|
47
51
|
// the list getAllThemes() returns, under the name `path`. The check used to invent a sourcePath
|
|
48
52
|
// and then assert against its own invention, which is why it passed while nothing happened on
|
|
@@ -53,6 +57,7 @@ const ctx = (sourcePath: string | undefined, mode = "tui") => ({
|
|
|
53
57
|
theme: { name: "violet-forge", sourcePath },
|
|
54
58
|
getAllThemes: () => [],
|
|
55
59
|
notify: (m: string) => notices.push(m),
|
|
60
|
+
setWidget,
|
|
56
61
|
},
|
|
57
62
|
});
|
|
58
63
|
const piShapedCtx = (path: string | undefined, mode = "tui") => ({
|
|
@@ -61,6 +66,7 @@ const piShapedCtx = (path: string | undefined, mode = "tui") => ({
|
|
|
61
66
|
theme: { name: "violet-forge" },
|
|
62
67
|
getAllThemes: () => (path === undefined ? [] : [{ name: "violet-forge", path }]),
|
|
63
68
|
notify: (m: string) => notices.push(m),
|
|
69
|
+
setWidget,
|
|
64
70
|
},
|
|
65
71
|
});
|
|
66
72
|
|
|
@@ -140,6 +146,7 @@ const liveCtx = {
|
|
|
140
146
|
{ name: "violet-forge", path: themePath },
|
|
141
147
|
],
|
|
142
148
|
notify: (m: string) => notices.push(m),
|
|
149
|
+
setWidget,
|
|
143
150
|
},
|
|
144
151
|
};
|
|
145
152
|
start()({}, liveCtx);
|
|
@@ -153,6 +160,42 @@ live.name = "bare";
|
|
|
153
160
|
await new Promise((done) => setTimeout(done, 700));
|
|
154
161
|
assert.deepEqual(written, [], "the watcher must stop at shutdown");
|
|
155
162
|
|
|
163
|
+
// Asking the terminal back. It took the colour: nothing to say. It kept its own: one warning
|
|
164
|
+
// that names the fix, and the status /umb-doctor reads. It never answered: unknown, so silence.
|
|
165
|
+
const status = () => (globalThis as { __umbraBackground?: { ignored: boolean } }).__umbraBackground;
|
|
166
|
+
const settle = () => new Promise((done) => setTimeout(done, 10));
|
|
167
|
+
setSetting(undefined);
|
|
168
|
+
notices.length = 0;
|
|
169
|
+
answer = { r: 0x19, g: 0x18, b: 0x31 };
|
|
170
|
+
start()({}, ctx(themePath));
|
|
171
|
+
await settle();
|
|
172
|
+
assert.equal(status()?.ignored, false, "a colour off by one step still counts as taken");
|
|
173
|
+
assert.deepEqual(notices, []);
|
|
174
|
+
shutdown()({}, ctx(themePath));
|
|
175
|
+
|
|
176
|
+
answer = { r: 0xff, g: 0xff, b: 0xff };
|
|
177
|
+
start()({}, ctx(themePath));
|
|
178
|
+
await settle();
|
|
179
|
+
assert.equal(status()?.ignored, true);
|
|
180
|
+
assert.equal(notices.length, 1, "a kept background is reported");
|
|
181
|
+
start()({}, ctx(themePath));
|
|
182
|
+
await settle();
|
|
183
|
+
assert.equal(notices.length, 1, "and only once per session");
|
|
184
|
+
shutdown()({}, ctx(themePath));
|
|
185
|
+
|
|
186
|
+
(globalThis as { __umbraBackground?: unknown }).__umbraBackground = undefined;
|
|
187
|
+
answer = undefined;
|
|
188
|
+
start()({}, ctx(themePath));
|
|
189
|
+
await settle();
|
|
190
|
+
assert.equal(status(), undefined, "no reply is not a verdict");
|
|
191
|
+
shutdown()({}, ctx(themePath));
|
|
192
|
+
|
|
193
|
+
assert.equal(sameColour("#191830", { r: 0x1b, g: 0x18, b: 0x30 }), true);
|
|
194
|
+
assert.equal(sameColour("#191830", { r: 0x1d, g: 0x18, b: 0x30 }), false);
|
|
195
|
+
assert.match(backgroundHowTo({ TERM_PROGRAM: "vscode" }, "#000000"), /terminal\.background.*#000000/);
|
|
196
|
+
assert.match(backgroundHowTo({ WT_SESSION: "x" }, "#000000"), /Windows Terminal/);
|
|
197
|
+
assert.match(backgroundHowTo({}, "#000000"), /terminal's own settings/);
|
|
198
|
+
|
|
156
199
|
process.stdout.write = realWrite;
|
|
157
200
|
|
|
158
201
|
console.log("umbra-background: all checks passed");
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The image viewer behind a click on a tool card's image: the map-style zoom keeps the point under
|
|
2
|
+
// the pointer in place, a click lands on the right image rows, frames crop through kitty controls,
|
|
3
|
+
// and closing puts pi's overlay compositing back and frees the image.
|
|
4
|
+
//
|
|
5
|
+
// Run it with: bun run umbra-image-viewer.check.ts
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { Image, setCapabilities, setCellDimensions } from "@earendil-works/pi-tui";
|
|
8
|
+
import viewer, { axis, openToolImageAt, zoomCenter } from "../extensions/umbra-image-viewer.ts";
|
|
9
|
+
|
|
10
|
+
// Loading the extension is what lets umbra-toolbox find it.
|
|
11
|
+
viewer();
|
|
12
|
+
assert.equal((globalThis as any).__umbraImageViewer?.openToolImageAt, openToolImageAt, "registered for umbra-toolbox");
|
|
13
|
+
|
|
14
|
+
// The zoom keeps the image point under the pointer where it was.
|
|
15
|
+
const before = axis(4000, 800, 0.2, 2000);
|
|
16
|
+
const at = 100;
|
|
17
|
+
const point = before.srcStart + (at - before.dispStart) / 0.2;
|
|
18
|
+
const after = axis(4000, 800, 0.4, zoomCenter(before, 800, at, 0.2, 0.4));
|
|
19
|
+
assert.equal(after.srcStart + (at - after.dispStart) / 0.4, point, "point under the pointer stays put");
|
|
20
|
+
assert.deepEqual(axis(100, 800, 1, 0), { srcStart: 0, srcSpan: 100, dispStart: 350, dispSpan: 100, center: 50 }, "a small image is centred");
|
|
21
|
+
assert.equal(axis(4000, 800, 1, -50).srcStart, 0, "panning stops at the left edge");
|
|
22
|
+
assert.equal(axis(4000, 800, 1, 99999).srcStart, 3200, "and at the right edge");
|
|
23
|
+
|
|
24
|
+
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
|
|
25
|
+
setCellDimensions({ widthPx: 10, heightPx: 20 });
|
|
26
|
+
// 400x200 PNG header is enough: the viewer never decodes the pixels.
|
|
27
|
+
const png = Buffer.alloc(33);
|
|
28
|
+
Buffer.from("89504e470d0a1a0a0000000d49484452", "hex").copy(png);
|
|
29
|
+
png.writeUInt32BE(400, 16);
|
|
30
|
+
png.writeUInt32BE(200, 20);
|
|
31
|
+
const image = new Image(png.toString("base64"), "image/png", { fallbackColor: (s) => s }, { maxWidthCells: 20 });
|
|
32
|
+
const tool = {
|
|
33
|
+
imageComponents: [image],
|
|
34
|
+
render: (width: number) => ["", "title", "summary", "", ...image.render(width)],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const writes: string[] = [];
|
|
38
|
+
let overlay: any;
|
|
39
|
+
let hidden = false;
|
|
40
|
+
const ownComposite = function () {
|
|
41
|
+
return ["base"];
|
|
42
|
+
};
|
|
43
|
+
// compositeOverlays lives on the prototype in pi, so the viewer's override is an own property.
|
|
44
|
+
const tui: any = Object.assign(Object.create({ compositeOverlays: ownComposite }), {
|
|
45
|
+
terminal: { columns: 80, rows: 25, write: (s: string) => writes.push(s) },
|
|
46
|
+
requestRender() {},
|
|
47
|
+
showOverlay(component: any) {
|
|
48
|
+
overlay = component;
|
|
49
|
+
return { hide: () => (hidden = true) };
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.equal(openToolImageAt(tui, tool, 2, 80), false, "a click on the summary is not the image");
|
|
54
|
+
assert.equal(openToolImageAt(tui, tool, 4, 80), true, "a click on the image's first row opens it");
|
|
55
|
+
assert.ok(overlay, "the viewer is an overlay");
|
|
56
|
+
|
|
57
|
+
const frame = () => tui.compositeOverlays(["x", "y"], 80, 25) as string[];
|
|
58
|
+
let lines = frame();
|
|
59
|
+
assert.equal(lines.length, 25, "the frame fills the terminal");
|
|
60
|
+
const controls = () => /\x1b_G([^;]*);/.exec(lines.find((l) => l.includes("\x1b_G"))!)![1]!;
|
|
61
|
+
assert.match(controls(), /x=0,y=0,w=400,h=200,c=40,r=10,/, "fit shows the whole image at its own size, 40x10 cells of 10x20 px");
|
|
62
|
+
assert.match(lines.at(-1)!, / 100% /, "and says so: fit never stretches a smaller image");
|
|
63
|
+
assert.match(controls(), /(^|,)i=\d+/, "under its own kitty id");
|
|
64
|
+
|
|
65
|
+
const wheelUp = () => overlay.handleMouse({ type: "wheel", wheelDelta: -3, screenX: 40, screenY: 12, x: 40, y: 12 });
|
|
66
|
+
wheelUp();
|
|
67
|
+
lines = frame();
|
|
68
|
+
assert.match(controls(), /w=400,h=200,c=48,/, "wheel up first grows a small image whole");
|
|
69
|
+
for (let i = 0; i < 4; i++) wheelUp();
|
|
70
|
+
lines = frame();
|
|
71
|
+
assert.doesNotMatch(controls(), /w=400/, "until it outgrows the window: then a narrower crop");
|
|
72
|
+
|
|
73
|
+
const cropX = () => Number(/x=(\d+)/.exec(controls())![1]);
|
|
74
|
+
const x0 = cropX();
|
|
75
|
+
overlay.handleMouse({ type: "press", button: "left", screenX: 40, screenY: 12 });
|
|
76
|
+
overlay.handleMouse({ type: "drag", button: "left", screenX: 30, screenY: 12 });
|
|
77
|
+
lines = frame();
|
|
78
|
+
assert.ok(cropX() > x0, "dragging left moves the view right");
|
|
79
|
+
|
|
80
|
+
overlay.handleInput("\x1b");
|
|
81
|
+
assert.ok(hidden, "esc closes");
|
|
82
|
+
assert.equal(tui.compositeOverlays, ownComposite, "pi's compositing is back");
|
|
83
|
+
assert.ok(writes.some((w) => w.includes("a=d,d=I")), "the viewer's copy of the image is freed");
|
|
84
|
+
|
|
85
|
+
console.log("umbra-image-viewer.check.ts ok - zoom holds the pointer, hit rows, kitty crop, close restores");
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import assert from "node:assert/strict";
|
|
8
8
|
import toolbox from "../extensions/umbra-toolbox/index.ts";
|
|
9
9
|
import { CONFIG_PATH } from "../extensions/umbra-toolbox/config/config.ts";
|
|
10
|
+
import { resolveShikiTheme } from "../extensions/umbra-toolbox/renderer/tool/diff/diff-palette.ts";
|
|
10
11
|
import { ShikiHighlightCache } from "../extensions/umbra-toolbox/renderer/tool/diff/shiki-highlight.ts";
|
|
11
12
|
|
|
12
13
|
const commands: string[] = [];
|
|
@@ -30,4 +31,22 @@ await Promise.race([ready, new Promise((_, reject) => setTimeout(() => reject(ne
|
|
|
30
31
|
const lines = cache.get(fallback[0]!, "ts", "github-dark", fallback);
|
|
31
32
|
assert.ok(lines?.[0]?.includes("\x1b["), "shiki resolved and coloured the line");
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
// Code in a diff wears the pi theme's own syntax colours, not one github palette for every theme.
|
|
35
|
+
const rgb = (hex: string) => `\x1b[38;2;${[1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(";")}m`;
|
|
36
|
+
const piColours: Record<string, string> = { text: "#d5ced9", syntaxKeyword: "#c74ded", syntaxNumber: "#f39c12", syntaxOperator: "#e25822" };
|
|
37
|
+
const piTheme = {
|
|
38
|
+
fg: (_slot: string, text: string) => text,
|
|
39
|
+
getFgAnsi: (slot: string) => rgb(piColours[slot] ?? "#808080"),
|
|
40
|
+
getBgAnsi: () => "\x1b[48;2;15;17;26m",
|
|
41
|
+
};
|
|
42
|
+
const syntax = resolveShikiTheme(piTheme);
|
|
43
|
+
assert.equal(typeof syntax, "object", "a 24-bit theme gets its own shiki theme");
|
|
44
|
+
const code = ["const PORT = 3000;"];
|
|
45
|
+
await new Promise<void>((resolve) => cache.get(code[0]!, "ts", syntax, code, resolve));
|
|
46
|
+
const coloured = cache.get(code[0]!, "ts", syntax, code)![0]!;
|
|
47
|
+
assert.ok(coloured.startsWith(rgb("#c74ded") + "const"), "const takes the theme's keyword colour");
|
|
48
|
+
assert.ok(coloured.includes(rgb("#e25822") + "="), "= takes its operator colour");
|
|
49
|
+
assert.ok(coloured.includes(rgb("#f39c12") + "3000"), "3000 takes its number colour");
|
|
50
|
+
assert.equal(resolveShikiTheme({ fg: (_s: string, t: string) => t, getFgAnsi: () => "", getBgAnsi: () => "\x1b[48;2;15;17;26m" }), "github-dark", "colours pi cannot report: github-dark");
|
|
51
|
+
|
|
52
|
+
console.log("umbra-toolbox.check.ts ok - loads, /umb-toolbox only, umbra-toolbox.json, shiki highlights in the pi theme's colours");
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// bun run umbra-working.check.ts
|
|
5
5
|
import assert from "node:assert";
|
|
6
|
+
import { humanizeToolLabel } from "../extensions/umbra-toolbox/renderer/tool/names.ts";
|
|
6
7
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
7
8
|
import { compact, elapsed } from "../lib/umbra-format.ts";
|
|
8
9
|
import { type Turn, entryFor, indicatorFor, labelFor, newTurn, statusLine,
|
|
@@ -16,19 +17,25 @@ const KEYS = [...TOOLS, "thinking", "idle", "custom"];
|
|
|
16
17
|
// every name in `ToolCallEvent` is named here rather than sampled.
|
|
17
18
|
assert.equal(labelFor("read"), "Reading");
|
|
18
19
|
assert.equal(labelFor("ls"), "Listing");
|
|
19
|
-
assert.equal(labelFor("find"), "
|
|
20
|
-
assert.equal(labelFor("grep"), "
|
|
21
|
-
assert.equal(labelFor("write"), "
|
|
20
|
+
assert.equal(labelFor("find"), "Searching");
|
|
21
|
+
assert.equal(labelFor("grep"), "Searching");
|
|
22
|
+
assert.equal(labelFor("write"), "Writing");
|
|
22
23
|
assert.equal(labelFor("edit"), "Editing");
|
|
23
|
-
assert.equal(labelFor("bash"), "
|
|
24
|
-
assert.equal(labelFor("powershell"), "
|
|
24
|
+
assert.equal(labelFor("bash"), "Running");
|
|
25
|
+
assert.equal(labelFor("powershell"), "Running");
|
|
25
26
|
assert.equal(labelFor("thinking"), "Thinking");
|
|
26
27
|
assert.equal(labelFor("idle"), "Working");
|
|
27
28
|
|
|
28
|
-
// CustomToolCallEvent - anything an extension registered -
|
|
29
|
-
|
|
30
|
-
assert.equal(labelFor("
|
|
29
|
+
// CustomToolCallEvent - anything an extension registered - shares one animation and shows the
|
|
30
|
+
// title its tool card shows, so the card and the line name the tool with the same words.
|
|
31
|
+
assert.equal(labelFor("fabric_exec"), "Fabric Exec");
|
|
32
|
+
assert.equal(labelFor("ask_user_question"), "Asking");
|
|
31
33
|
assert.deepEqual(indicatorFor("fabric_exec").frames, indicatorFor("custom").frames);
|
|
34
|
+
for (const name of ["web_search", "fetch_content", "fabric_exec", "codeSearch"]) {
|
|
35
|
+
assert.equal(labelFor(name), humanizeToolLabel(name), `${name}: line and card agree`);
|
|
36
|
+
}
|
|
37
|
+
// Renaming `custom` still renames every tool umbra has no word for.
|
|
38
|
+
assert.equal(labelFor("web_search", { tools: { custom: { label: "Tool" } } }), "Tool");
|
|
32
39
|
|
|
33
40
|
// --- frames ----------------------------------------------------------------------------------
|
|
34
41
|
// Every glyph must measure one column: a two-column frame shifts the whole status line on every
|
|
@@ -90,8 +97,8 @@ const at = (turn: Partial<Turn>): Turn => ({ ...newTurn(0), ...turn });
|
|
|
90
97
|
// A turn that has produced nothing yet shows no arrow, because "↓ 0 tokens" is noise.
|
|
91
98
|
assert.equal(statusLine(at({}), 1_000, ON), "Working… (1s)");
|
|
92
99
|
assert.equal(statusLine(at({ key: "read", settled: 180 }), 5_000, ON), "Reading… (5s · ↓ 180 tokens)");
|
|
93
|
-
assert.equal(statusLine(at({ key: "bash", settled: 1_200 }), 12_000, ON), "
|
|
94
|
-
assert.equal(statusLine(at({ key: "fabric_exec" }), 1_000, ON), "
|
|
100
|
+
assert.equal(statusLine(at({ key: "bash", settled: 1_200 }), 12_000, ON), "Running… (12s · ↓ 1.2k tokens)");
|
|
101
|
+
assert.equal(statusLine(at({ key: "fabric_exec" }), 1_000, ON), "Fabric Exec… (1s)");
|
|
95
102
|
assert.equal(statusLine(at({ key: "fabric_exec" }), 1_000, ON, renamed), "Fabric… (1s)");
|
|
96
103
|
|
|
97
104
|
// Streaming output counts before the message ends, or the number stands still mid-answer.
|
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
// So this does it: on start it reads the active theme's own `export.pageBg` and sets the terminal
|
|
10
10
|
// background to it, and on shutdown it puts the background back with OSC 111. Nothing is guessed —
|
|
11
11
|
// a theme without an `export.pageBg` is left alone.
|
|
12
|
+
//
|
|
13
|
+
// Some terminals drop the OSC 11 set but still answer the query, with the colour they kept. So
|
|
14
|
+
// after each set the terminal is asked back. When the answer differs, the theme cannot show as
|
|
15
|
+
// intended: pi says so once, and /umb-doctor says how to set the colour in that terminal.
|
|
12
16
|
import { readFileSync } from "node:fs";
|
|
13
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
14
19
|
|
|
15
20
|
const SETTINGS_KEY = "piUmbraTheme";
|
|
16
21
|
// How often the active theme's name is compared. It is an in-memory string, so this costs a
|
|
@@ -25,6 +30,28 @@ const setBackground = (hex: string) => process.stdout.write(`\x1b]11;${hex}\x07`
|
|
|
25
30
|
const resetBackground = () => process.stdout.write("\x1b]111\x07");
|
|
26
31
|
|
|
27
32
|
type Settings = { background?: string };
|
|
33
|
+
type Rgb = { r: number; g: number; b: number };
|
|
34
|
+
|
|
35
|
+
// Terminals round the colour they store, so a channel may come back one or two steps off.
|
|
36
|
+
export const sameColour = (hex: string, rgb: Rgb): boolean =>
|
|
37
|
+
[rgb.r, rgb.g, rgb.b].every((channel, i) => Math.abs(channel - parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16)) <= 2);
|
|
38
|
+
|
|
39
|
+
/** Where the background lives in the settings of the terminal pi runs in, from its own variables. */
|
|
40
|
+
export const backgroundHowTo = (env: Record<string, string | undefined>, hex: string): string => {
|
|
41
|
+
if (env.TERM_PROGRAM === "vscode")
|
|
42
|
+
return `VS Code: add "workbench.colorCustomizations": { "terminal.background": "${hex}" } to settings.json.`;
|
|
43
|
+
if (env.WT_SESSION !== undefined)
|
|
44
|
+
return `Windows Terminal: Settings, your profile, Appearance, Background: ${hex}. Or "background": "${hex}" in the profile.`;
|
|
45
|
+
if (env.TERM_PROGRAM === "Apple_Terminal") return `Terminal.app: Settings, Profiles, Text, Background: ${hex}.`;
|
|
46
|
+
if (env.TERMINAL_EMULATOR === "JetBrains-JediTerm")
|
|
47
|
+
return `JetBrains: Settings, Editor, Color Scheme, Console Colors, Console, Background: ${hex}.`;
|
|
48
|
+
if (env.TMUX !== undefined) return `tmux: add set -g window-style 'bg=${hex}' to ~/.tmux.conf.`;
|
|
49
|
+
return `Set the background to ${hex} in the terminal's own settings.`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** What the last check found, for /umb-doctor in pi-umbra-help. */
|
|
53
|
+
export type BackgroundStatus = { wanted: string; ignored: boolean; howTo: string };
|
|
54
|
+
|
|
28
55
|
|
|
29
56
|
const readSetting = (): string => {
|
|
30
57
|
const path = `${process.env.PI_CODING_AGENT_DIR ?? `${process.env.USERPROFILE ?? process.env.HOME}/.pi/agent`}/settings.json`;
|
|
@@ -72,17 +99,37 @@ export default function umbraBackground(pi: ExtensionAPI) {
|
|
|
72
99
|
// The theme name as of the last look, and the timer that does the looking.
|
|
73
100
|
let watched: string | undefined;
|
|
74
101
|
let watcher: ReturnType<typeof setInterval> | undefined;
|
|
102
|
+
// The query goes through pi's TUI, which routes the reply back. A widget factory is the only
|
|
103
|
+
// place an extension is handed the TUI; it runs at once and draws nothing.
|
|
104
|
+
let tui: TUI | undefined;
|
|
105
|
+
let warned = false;
|
|
106
|
+
|
|
107
|
+
const verify = async (ctx: ExtensionContext, hex: string) => {
|
|
108
|
+
// No reply means a terminal that answers no queries: unknown, not ignored.
|
|
109
|
+
const rgb = await tui?.queryTerminalBackgroundColor({ timeoutMs: 1000 });
|
|
110
|
+
if (rgb === undefined || current !== hex) return;
|
|
111
|
+
const status: BackgroundStatus = { wanted: hex, ignored: !sameColour(hex, rgb), howTo: backgroundHowTo(process.env, hex) };
|
|
112
|
+
(globalThis as { __umbraBackground?: BackgroundStatus }).__umbraBackground = status;
|
|
113
|
+
if (!status.ignored || warned) return;
|
|
114
|
+
warned = true;
|
|
115
|
+
ctx.ui.notify(`This terminal kept its own background, so the theme is not shown in full. ${status.howTo}`, "warning");
|
|
116
|
+
};
|
|
75
117
|
|
|
76
118
|
const apply = (ctx: ExtensionContext): string | undefined => {
|
|
77
119
|
const hex = resolve(ctx);
|
|
78
120
|
if (hex === undefined) return undefined;
|
|
79
121
|
setBackground(hex);
|
|
80
122
|
current = hex;
|
|
123
|
+
void verify(ctx, hex);
|
|
81
124
|
return hex;
|
|
82
125
|
};
|
|
83
126
|
|
|
84
127
|
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
85
128
|
if (ctx.mode !== "tui") return;
|
|
129
|
+
ctx.ui.setWidget("umbra-background", (given) => {
|
|
130
|
+
tui = given;
|
|
131
|
+
return { render: () => [], invalidate: () => {} };
|
|
132
|
+
});
|
|
86
133
|
apply(ctx);
|
|
87
134
|
// pi fires no theme-change event. Its internal onThemeChange slot holds a single callback
|
|
88
135
|
// that the TUI already owns, and ui_prompt_end does not cover pi's own theme picker, so the
|
|
@@ -117,9 +164,12 @@ export default function umbraBackground(pi: ExtensionAPI) {
|
|
|
117
164
|
if (ctx.mode !== "tui") return;
|
|
118
165
|
const argument = args.trim();
|
|
119
166
|
|
|
167
|
+
// Asked for by hand, so a terminal that ignores it is reported again.
|
|
168
|
+
warned = false;
|
|
120
169
|
if (argument === "off" || argument === "reset") {
|
|
121
170
|
resetBackground();
|
|
122
171
|
current = undefined;
|
|
172
|
+
(globalThis as { __umbraBackground?: BackgroundStatus }).__umbraBackground = undefined;
|
|
123
173
|
ctx.ui.notify("Terminal background restored.", "info");
|
|
124
174
|
return;
|
|
125
175
|
}
|
|
@@ -127,6 +177,7 @@ export default function umbraBackground(pi: ExtensionAPI) {
|
|
|
127
177
|
if (HEX.test(argument)) {
|
|
128
178
|
setBackground(argument);
|
|
129
179
|
current = argument;
|
|
180
|
+
void verify(ctx, argument);
|
|
130
181
|
ctx.ui.notify(`Terminal background set to ${argument}.`, "info");
|
|
131
182
|
return;
|
|
132
183
|
}
|
|
@@ -144,7 +195,7 @@ export default function umbraBackground(pi: ExtensionAPI) {
|
|
|
144
195
|
const hex = apply(ctx);
|
|
145
196
|
ctx.ui.notify(
|
|
146
197
|
hex !== undefined
|
|
147
|
-
? `Terminal background set to ${hex} from theme "${name}"
|
|
198
|
+
? `Terminal background set to ${hex} from theme "${name}".`
|
|
148
199
|
: file === undefined
|
|
149
200
|
? `pi lists no file for theme "${name}", so its background cannot be read. Use /umb-bg #rrggbb.`
|
|
150
201
|
: `Theme "${name}" (${file}) defines no export.pageBg, so nothing was changed.`,
|