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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// umbra-image-viewer: a full-screen viewer for an image under a tool card. The wheel zooms toward
|
|
2
|
+
// the pointer and a drag pans, the way a map does.
|
|
3
|
+
//
|
|
4
|
+
// Kitty only. Its graphics protocol crops (x,y,w,h) and scales (c,r) on the terminal side, so a
|
|
5
|
+
// zoom step never decodes or re-encodes the image. The image goes out once through pi's own kitty
|
|
6
|
+
// cache, keyed by its id; every later frame is a placement with new crop controls.
|
|
7
|
+
//
|
|
8
|
+
// The click that opens it belongs to umbra-toolbox, which already owns pi's fullscreen mouse input
|
|
9
|
+
// and knows which card a row belongs to. The two meet on globalThis.__umbraImageViewer rather than
|
|
10
|
+
// an import, so either one loads, updates or goes away without the other.
|
|
11
|
+
import {
|
|
12
|
+
allocateImageId,
|
|
13
|
+
deleteKittyImage,
|
|
14
|
+
getCapabilities,
|
|
15
|
+
getCellDimensions,
|
|
16
|
+
matchesKey,
|
|
17
|
+
renderImage,
|
|
18
|
+
truncateToWidth,
|
|
19
|
+
} from "@earendil-works/pi-tui";
|
|
20
|
+
|
|
21
|
+
/** Highest zoom, in screen pixels per image pixel. Past this a pixel is a block. */
|
|
22
|
+
const MAX_PIXEL_SCALE = 8;
|
|
23
|
+
const WHEEL_STEP = 1.2;
|
|
24
|
+
const KEY_STEP = 1.5;
|
|
25
|
+
|
|
26
|
+
/** One axis of the view: the source pixels shown, and where on screen they land. */
|
|
27
|
+
export type Axis = { srcStart: number; srcSpan: number; dispStart: number; dispSpan: number; center: number };
|
|
28
|
+
|
|
29
|
+
/** Centred when the image fits the view on this axis, clamped to its edges when it does not. */
|
|
30
|
+
export const axis = (imagePx: number, viewPx: number, scale: number, center: number): Axis => {
|
|
31
|
+
const span = viewPx / scale;
|
|
32
|
+
if (span >= imagePx) {
|
|
33
|
+
const dispSpan = imagePx * scale;
|
|
34
|
+
return { srcStart: 0, srcSpan: imagePx, dispStart: (viewPx - dispSpan) / 2, dispSpan, center: imagePx / 2 };
|
|
35
|
+
}
|
|
36
|
+
const clamped = Math.min(Math.max(center, span / 2), imagePx - span / 2);
|
|
37
|
+
return { srcStart: clamped - span / 2, srcSpan: span, dispStart: 0, dispSpan: viewPx, center: clamped };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The centre that keeps the image point under `at` in place while the scale goes `from` → `to`. */
|
|
41
|
+
export const zoomCenter = (current: Axis, viewPx: number, at: number, from: number, to: number): number => {
|
|
42
|
+
const point = current.srcStart + (at - current.dispStart) / from;
|
|
43
|
+
return point - (at - viewPx / 2) / to;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const imageIdOf = (line: string): number | undefined => {
|
|
47
|
+
const found = /\x1b_G(?:[^;]*,)?i=(\d+)[,;]/.exec(line);
|
|
48
|
+
return found ? Number(found[1]) : undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
class ImageViewer {
|
|
52
|
+
private zoom = 1;
|
|
53
|
+
private cx: number;
|
|
54
|
+
private cy: number;
|
|
55
|
+
private drag?: { x: number; y: number; cx: number; cy: number };
|
|
56
|
+
private head: string[];
|
|
57
|
+
private tail: string;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly tui: any,
|
|
61
|
+
private readonly widthPx: number,
|
|
62
|
+
private readonly heightPx: number,
|
|
63
|
+
sequence: string,
|
|
64
|
+
private readonly close: () => void,
|
|
65
|
+
) {
|
|
66
|
+
this.cx = widthPx / 2;
|
|
67
|
+
this.cy = heightPx / 2;
|
|
68
|
+
// The transmission renderImage built, minus its size: every frame puts its own crop and
|
|
69
|
+
// cell size in front of the same payload.
|
|
70
|
+
const first = /^\x1b_G([^;]*);/.exec(sequence)!;
|
|
71
|
+
this.head = first[1]!.split(",").filter((control) => !/^[cr]=/.test(control));
|
|
72
|
+
this.tail = sequence.slice(first[0].length);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private geometry() {
|
|
76
|
+
const cell = getCellDimensions();
|
|
77
|
+
const cols = Math.max(1, Number(this.tui.terminal?.columns) || 80);
|
|
78
|
+
const rows = Math.max(1, (Number(this.tui.terminal?.rows) || 24) - 1);
|
|
79
|
+
const viewW = cols * cell.widthPx;
|
|
80
|
+
const viewH = rows * cell.heightPx;
|
|
81
|
+
// Fit only shrinks. Stretching a screenshot a few percent to fill the window blurs every
|
|
82
|
+
// pixel and shows nothing more; the wheel is there for a closer look.
|
|
83
|
+
const fit = Math.min(viewW / this.widthPx, viewH / this.heightPx, 1);
|
|
84
|
+
const scale = fit * this.zoom;
|
|
85
|
+
return {
|
|
86
|
+
cell,
|
|
87
|
+
cols,
|
|
88
|
+
rows,
|
|
89
|
+
viewW,
|
|
90
|
+
viewH,
|
|
91
|
+
fit,
|
|
92
|
+
scale,
|
|
93
|
+
x: axis(this.widthPx, viewW, scale, this.cx),
|
|
94
|
+
y: axis(this.heightPx, viewH, scale, this.cy),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private zoomAt(px: number, py: number, factor: number): void {
|
|
99
|
+
const g = this.geometry();
|
|
100
|
+
const maxZoom = Math.max(1, MAX_PIXEL_SCALE / g.fit);
|
|
101
|
+
this.zoom = Math.min(Math.max(this.zoom * factor, 1), maxZoom);
|
|
102
|
+
const next = g.fit * this.zoom;
|
|
103
|
+
this.cx = zoomCenter(g.x, g.viewW, px, g.scale, next);
|
|
104
|
+
this.cy = zoomCenter(g.y, g.viewH, py, g.scale, next);
|
|
105
|
+
this.tui.requestRender();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private pan(dx: number, dy: number): void {
|
|
109
|
+
const g = this.geometry();
|
|
110
|
+
this.cx = g.x.center + dx / g.scale;
|
|
111
|
+
this.cy = g.y.center + dy / g.scale;
|
|
112
|
+
this.tui.requestRender();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
render(width: number): string[] {
|
|
116
|
+
const { cell, cols, rows, scale, x, y } = this.geometry();
|
|
117
|
+
this.cx = x.center;
|
|
118
|
+
this.cy = y.center;
|
|
119
|
+
const col = Math.floor(x.dispStart / cell.widthPx);
|
|
120
|
+
const row = Math.floor(y.dispStart / cell.heightPx);
|
|
121
|
+
const srcX = Math.round(x.srcStart);
|
|
122
|
+
const srcY = Math.round(y.srcStart);
|
|
123
|
+
const controls = [
|
|
124
|
+
`x=${srcX}`,
|
|
125
|
+
`y=${srcY}`,
|
|
126
|
+
`w=${Math.max(1, Math.min(this.widthPx - srcX, Math.round(x.srcSpan)))}`,
|
|
127
|
+
`h=${Math.max(1, Math.min(this.heightPx - srcY, Math.round(y.srcSpan)))}`,
|
|
128
|
+
`c=${Math.max(1, Math.min(cols - col, Math.round(x.dispSpan / cell.widthPx)))}`,
|
|
129
|
+
`r=${Math.max(1, Math.min(rows - row, Math.round(y.dispSpan / cell.heightPx)))}`,
|
|
130
|
+
`X=${Math.floor(x.dispStart - col * cell.widthPx)}`,
|
|
131
|
+
`Y=${Math.floor(y.dispStart - row * cell.heightPx)}`,
|
|
132
|
+
];
|
|
133
|
+
const lines = Array.from({ length: rows }, () => "");
|
|
134
|
+
lines[row] = " ".repeat(col) + `\x1b_G${[...this.head, ...controls].join(",")};${this.tail}`;
|
|
135
|
+
const status = ` ${Math.round(scale * 100)}% · wheel zoom · drag move · double-click zoom in · 0 fit · esc close`;
|
|
136
|
+
lines.push(`\x1b[2m${truncateToWidth(status, width)}\x1b[22m`);
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
handleMouse(event: any): { handled?: boolean; capture?: boolean } {
|
|
141
|
+
const cell = getCellDimensions();
|
|
142
|
+
const px = event.screenX * cell.widthPx + cell.widthPx / 2;
|
|
143
|
+
const py = event.screenY * cell.heightPx + cell.heightPx / 2;
|
|
144
|
+
if (event.type === "wheel") {
|
|
145
|
+
// Only the sign: a touchpad sends many small steps, a wheel a few large ones.
|
|
146
|
+
if (event.wheelDelta) this.zoomAt(px, py, event.wheelDelta < 0 ? WHEEL_STEP : 1 / WHEEL_STEP);
|
|
147
|
+
return { handled: true };
|
|
148
|
+
}
|
|
149
|
+
if (event.type === "press" && event.button === "left") {
|
|
150
|
+
this.drag = { x: event.screenX, y: event.screenY, cx: this.cx, cy: this.cy };
|
|
151
|
+
return { capture: true };
|
|
152
|
+
}
|
|
153
|
+
if (event.type === "drag" && this.drag) {
|
|
154
|
+
const { scale } = this.geometry();
|
|
155
|
+
this.cx = this.drag.cx - ((event.screenX - this.drag.x) * cell.widthPx) / scale;
|
|
156
|
+
this.cy = this.drag.cy - ((event.screenY - this.drag.y) * cell.heightPx) / scale;
|
|
157
|
+
this.tui.requestRender();
|
|
158
|
+
return { handled: true };
|
|
159
|
+
}
|
|
160
|
+
if (event.type === "release") this.drag = undefined;
|
|
161
|
+
if (event.type === "click" && event.clickCount === 2) this.zoomAt(px, py, 2);
|
|
162
|
+
return { handled: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
handleInput(data: string): void {
|
|
166
|
+
const g = this.geometry();
|
|
167
|
+
if (matchesKey(data, "escape") || data === "q") return this.close();
|
|
168
|
+
if (data === "+" || data === "=") return this.zoomAt(g.viewW / 2, g.viewH / 2, KEY_STEP);
|
|
169
|
+
if (data === "-") return this.zoomAt(g.viewW / 2, g.viewH / 2, 1 / KEY_STEP);
|
|
170
|
+
if (data === "0") {
|
|
171
|
+
this.zoom = 1;
|
|
172
|
+
return this.tui.requestRender();
|
|
173
|
+
}
|
|
174
|
+
const step = 8;
|
|
175
|
+
if (matchesKey(data, "left")) return this.pan(-g.viewW / step, 0);
|
|
176
|
+
if (matchesKey(data, "right")) return this.pan(g.viewW / step, 0);
|
|
177
|
+
if (matchesKey(data, "up")) return this.pan(0, -g.viewH / step);
|
|
178
|
+
if (matchesKey(data, "down")) return this.pan(0, g.viewH / step);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
invalidate(): void {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Opens the viewer when `row` of `tool` (a ToolExecutionComponent rendered at `width`) falls on
|
|
186
|
+
* one of its images. False when it does not, or when the terminal is not kitty.
|
|
187
|
+
*/
|
|
188
|
+
export function openToolImageAt(tui: any, tool: any, row: number, width: number): boolean {
|
|
189
|
+
if (getCapabilities().images !== "kitty" || typeof tui?.showOverlay !== "function") return false;
|
|
190
|
+
const images: any[] = tool.imageComponents ?? [];
|
|
191
|
+
if (images.length === 0) return false;
|
|
192
|
+
const lines: string[] = tool.render(width);
|
|
193
|
+
for (const image of images) {
|
|
194
|
+
const id = image.getImageId?.();
|
|
195
|
+
if (id === undefined) continue;
|
|
196
|
+
const start = lines.findIndex((line) => imageIdOf(line) === id);
|
|
197
|
+
if (start === -1 || row < start || row >= start + image.render(width).length) continue;
|
|
198
|
+
open(tui, image.base64Data, image.dimensions);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function open(tui: any, data: string, size: { widthPx: number; heightPx: number }): void {
|
|
205
|
+
const id = allocateImageId();
|
|
206
|
+
// Registers the id with pi's kitty cache, so only the first frame carries the payload.
|
|
207
|
+
const sequence = renderImage(data, size, { maxWidthCells: 1, maxHeightCells: 1, imageId: id, moveCursor: false })!.sequence;
|
|
208
|
+
// pi hands extensions a Proxy of the TUI that forwards `set` but not `delete`, so the override
|
|
209
|
+
// is undone by assigning the old method back, never by deleting it.
|
|
210
|
+
const ownComposite = tui.compositeOverlays;
|
|
211
|
+
const close = () => {
|
|
212
|
+
handle.hide();
|
|
213
|
+
tui.compositeOverlays = ownComposite;
|
|
214
|
+
tui.terminal?.write?.(deleteKittyImage(id));
|
|
215
|
+
tui.requestRender();
|
|
216
|
+
};
|
|
217
|
+
const viewer = new ImageViewer(tui, size.widthPx, size.heightPx, sequence, close);
|
|
218
|
+
// pi keeps a base line that holds an image instead of drawing an overlay over it, and the
|
|
219
|
+
// viewer covers the whole screen. So the overlays are laid out over a blank screen, which
|
|
220
|
+
// keeps their bounds and mouse routing right, and the frame is the viewer's own lines.
|
|
221
|
+
tui.compositeOverlays = function (this: any, screen: string[], width: number, height: number) {
|
|
222
|
+
ownComposite.call(this, screen.map(() => ""), width, height);
|
|
223
|
+
return viewer.render(width);
|
|
224
|
+
};
|
|
225
|
+
const handle = tui.showOverlay(viewer, { width: "100%", maxHeight: "100%", anchor: "top-left" });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export default function () {
|
|
229
|
+
(globalThis as { __umbraImageViewer?: unknown }).__umbraImageViewer = { openToolImageAt };
|
|
230
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 底部 Agents/Tasks 面板由 pi-subagents/pi-tasks 独立 widget 负责,不经 tool 卡外置。
|
|
10
10
|
*
|
|
11
11
|
* 工具计数:read 按非空路径去重、其余按调用计数(首次出现顺序);edit/write 不进摘要。
|
|
12
|
-
* 时长 = 回合流逝挂钟;进行中 Running
|
|
12
|
+
* 时长 = 回合流逝挂钟;进行中 Running…,结束 Ran for。
|
|
13
13
|
* abort/error/length 状态行挂在摘要外层,避免被折叠吞掉。
|
|
14
14
|
* 最终 agent 回合摘要由 feature/agent-summary 独占(bash/read/edit/write/other)。
|
|
15
15
|
*
|
|
@@ -209,7 +209,7 @@ function buildMessagesSummary(
|
|
|
209
209
|
|
|
210
210
|
runningActive = runningActiveOverride ?? runningActive;
|
|
211
211
|
if (runningActive) {
|
|
212
|
-
parts.push(durationMs > 0 ? `Running
|
|
212
|
+
parts.push(durationMs > 0 ? `Running… · ${formatThoughtDuration(durationMs)}` : "Running…");
|
|
213
213
|
} else if (durationMs > 0) parts.push(`Ran for ${formatThoughtDuration(durationMs)}`);
|
|
214
214
|
for (const [name, count] of counts) parts.push(`${name}×${count}`);
|
|
215
215
|
return parts.join(", ");
|
|
@@ -823,7 +823,7 @@ function compactAssistantLineComponent(
|
|
|
823
823
|
const hintText = hint ? ` • ${showMoreHintText()}` : "";
|
|
824
824
|
const summaryWidth = Math.max(0, available - visibleWidth(hintText));
|
|
825
825
|
const resolved = typeof summary === "function" ? summary() : summary;
|
|
826
|
-
const runningActive = resolved.startsWith("Running
|
|
826
|
+
const runningActive = resolved.startsWith("Running…");
|
|
827
827
|
const hover = hoveredAssistantComponent === component;
|
|
828
828
|
const frame = runningActive ? (query?.getThinkingAnimationFrame?.() ?? 0) : 0;
|
|
829
829
|
const leadingBlank = options.leadingBlank !== false;
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from "../tool/result.ts";
|
|
10
10
|
import type { ToolGroupComponent } from "../tool/grouping.ts";
|
|
11
11
|
import { componentAtLocalRow, type ComponentRowHit } from "./layout.ts";
|
|
12
|
-
import { setScrollButtonHovered } from "./scroll.ts";
|
|
13
12
|
|
|
14
13
|
type SharedToolHoverState = { toolCallId: string | null };
|
|
15
14
|
|
|
@@ -112,7 +111,6 @@ export function cachedFullscreenComponentAtRow(
|
|
|
112
111
|
|
|
113
112
|
/** fullscreen 鼠标悬停目标。 */
|
|
114
113
|
export type FullscreenHoverTarget =
|
|
115
|
-
| { kind: "button" }
|
|
116
114
|
| { kind: "group"; component: ToolGroupComponent }
|
|
117
115
|
| { kind: "thinking"; component: ThinkingPreviewBlock }
|
|
118
116
|
| { kind: "message"; component: any }
|
|
@@ -146,7 +144,5 @@ export function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | n
|
|
|
146
144
|
const nextView = target?.kind === "tool" ? target.view : null;
|
|
147
145
|
const nextSection = target?.kind === "tool" ? target.section : null;
|
|
148
146
|
if (setHoveredToolIo(nextView, nextSection)) changed = true;
|
|
149
|
-
const nextButton = target?.kind === "button";
|
|
150
|
-
if (setScrollButtonHovered(nextButton)) changed = true;
|
|
151
147
|
if (changed) tui.requestRender?.();
|
|
152
148
|
}
|
|
@@ -48,22 +48,10 @@ import {
|
|
|
48
48
|
} from "./layout.ts";
|
|
49
49
|
import {
|
|
50
50
|
fullscreenLazyTui,
|
|
51
|
-
hideScrollButton,
|
|
52
51
|
restoreOfficialScrollToEnd,
|
|
53
|
-
syncOfficialScrollToEnd,
|
|
54
|
-
isScrollBottomInput,
|
|
55
|
-
renderScrollButton,
|
|
56
|
-
resetScrollButtonState,
|
|
57
|
-
scheduleScrollButtonSync,
|
|
58
|
-
setScrollButtonHovered,
|
|
59
|
-
setScrollButtonVisible,
|
|
60
|
-
setScrollButtonWidget,
|
|
61
52
|
setToolMouseTui,
|
|
62
|
-
getScrollButtonVisible,
|
|
63
|
-
getScrollButtonWidget,
|
|
64
53
|
getToolMouseTui,
|
|
65
54
|
toolMouseInteractionActive,
|
|
66
|
-
updateScrollButtonFromInput,
|
|
67
55
|
} from "./scroll.ts";
|
|
68
56
|
import {
|
|
69
57
|
applyFullscreenHover,
|
|
@@ -84,7 +72,7 @@ type FrameToolRender = {
|
|
|
84
72
|
};
|
|
85
73
|
|
|
86
74
|
type InteractionRegion = {
|
|
87
|
-
kind: "collapsed-hint" | "collapse-hint" | "expanded-card" | "show-more"
|
|
75
|
+
kind: "collapsed-hint" | "collapse-hint" | "expanded-card" | "show-more";
|
|
88
76
|
row: number;
|
|
89
77
|
startCol: number;
|
|
90
78
|
endCol: number;
|
|
@@ -125,7 +113,6 @@ function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
|
|
|
125
113
|
);
|
|
126
114
|
return (
|
|
127
115
|
matches.find((region) => region.kind === "show-more") ??
|
|
128
|
-
matches.find((region) => region.kind === "scroll-bottom") ??
|
|
129
116
|
matches.find((region) => region.kind === "collapsed-hint") ??
|
|
130
117
|
matches.find((region) => region.kind === "collapse-hint") ??
|
|
131
118
|
matches.find((region) => region.kind === "expanded-card") ??
|
|
@@ -151,8 +138,6 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
|
|
|
151
138
|
function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
152
139
|
if (!isSgrIdleMotion(packet)) return;
|
|
153
140
|
const region = interactionRegionAt(packet);
|
|
154
|
-
const nextScrollButtonHovered = region?.kind === "scroll-bottom";
|
|
155
|
-
const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
|
|
156
141
|
const component = region?.component;
|
|
157
142
|
const nextToolCallId = region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
|
|
158
143
|
// group 只在提示文字上高亮(展开卡整行都是 expanded-card,不能算 hint)。
|
|
@@ -166,7 +151,6 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
166
151
|
const changed = nextToolCallId !== sharedToolHoverState().toolCallId;
|
|
167
152
|
setHoveredToolCallId(nextToolCallId);
|
|
168
153
|
if (
|
|
169
|
-
scrollButtonChanged ||
|
|
170
154
|
setHoveredToolIo(nextIoView, nextIoSection) ||
|
|
171
155
|
setHoveredToolGroup(nextGroup) ||
|
|
172
156
|
changed
|
|
@@ -265,7 +249,6 @@ function resolveCollapsePress(
|
|
|
265
249
|
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
266
250
|
const region = interactionRegionAt(packet);
|
|
267
251
|
if (!region) return false;
|
|
268
|
-
if (region.kind === "scroll-bottom") return false;
|
|
269
252
|
if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
|
|
270
253
|
const component = region.component;
|
|
271
254
|
if (!component) return false;
|
|
@@ -399,10 +382,11 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
399
382
|
// 否则 skipOfficialCards 会把内部工具当成官方卡跳过。
|
|
400
383
|
const card = target.group ?? target.owner ?? component;
|
|
401
384
|
const pressComponent = target.owner ?? component;
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
385
|
+
// An image under a tool card opens full screen, collapsed card or not, when umbra-image-viewer
|
|
386
|
+
// is loaded.
|
|
387
|
+
const viewer = (globalThis as any).__umbraImageViewer;
|
|
388
|
+
if (isToolExecutionComponent(component) && viewer?.openToolImageAt(tui, component, target.row, contentWidth)) {
|
|
389
|
+
clearPendingCollapsePress();
|
|
406
390
|
return true;
|
|
407
391
|
}
|
|
408
392
|
const line = hit.box.lines?.[hit.localRow];
|
|
@@ -494,14 +478,7 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
494
478
|
const hit = fullscreenLeafAt(layout, x, y);
|
|
495
479
|
if (hit) {
|
|
496
480
|
const line = hit.box.lines?.[hit.localRow];
|
|
497
|
-
|
|
498
|
-
if (typeof line === "string" && getScrollButtonVisible() && line.includes("[ ↓")) {
|
|
499
|
-
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
500
|
-
const idx = plain.indexOf("[ ↓");
|
|
501
|
-
if (idx >= 0 && x >= idx && x <= idx + plain.length - 1) {
|
|
502
|
-
target = { kind: "button" };
|
|
503
|
-
}
|
|
504
|
-
} else if (typeof line === "string" && !/\x1b]8;/.test(line)) {
|
|
481
|
+
if (typeof line === "string" && !/\x1b]8;/.test(line)) {
|
|
505
482
|
const width = Math.max(1, Number(tui.terminal?.columns) || 80);
|
|
506
483
|
const contentWidth = fullscreenContentWidth(hit.box, width);
|
|
507
484
|
// 与点击共用同一定位算法,避免 hover 自建行段与真实组件树错位。
|
|
@@ -570,9 +547,6 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
570
547
|
tui[FULLSCREEN_VIEWPORT_PATCH] = true;
|
|
571
548
|
tui.handleViewportInput = function (this: any, data: string) {
|
|
572
549
|
if (toolMouseInteractionActive() && tui.mode === "fullscreen") {
|
|
573
|
-
// 滚动输入(wheel/pageUp/end 等)后同步回到底部按钮显隐;
|
|
574
|
-
// 官方 viewport 会消费键盘,扩展监听器无法补偿,必须在这里调度。
|
|
575
|
-
scheduleScrollButtonSync(tui, data);
|
|
576
550
|
const packets = parseSgrMousePackets(data);
|
|
577
551
|
// 官方 fullscreen 会消费全部鼠标;文本预览 overlay 活动时放行给 focused
|
|
578
552
|
// custom component,使 [esc] 点击和滚轮可用。
|
|
@@ -919,24 +893,12 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
919
893
|
if (!getToolMouseTui()) return undefined;
|
|
920
894
|
// 惰性 Proxy fullscreen:鼠标由 handleViewportInput 包装消费(官方链之前),
|
|
921
895
|
// 此处只处理键盘(鼠标事件在官方 listener 已被 consume,到不了这里)。
|
|
922
|
-
if (fullscreenLazyTui(getToolMouseTui()))
|
|
923
|
-
scheduleScrollButtonSync(getToolMouseTui(), data);
|
|
924
|
-
if (isScrollBottomInput(data)) {
|
|
925
|
-
getToolMouseTui().scrollToBottom?.();
|
|
926
|
-
hideScrollButton(getToolMouseTui());
|
|
927
|
-
return { consume: true };
|
|
928
|
-
}
|
|
929
|
-
return undefined;
|
|
930
|
-
}
|
|
931
|
-
updateScrollButtonFromInput(getToolMouseTui(), data);
|
|
896
|
+
if (fullscreenLazyTui(getToolMouseTui())) return undefined;
|
|
932
897
|
// Off mode restores native input: wheel keeps scrolling through Pi's normal
|
|
933
898
|
// dispatcher, while hover/click affordances are entirely inactive.
|
|
934
899
|
if (!toolMouseInteractionActive()) return undefined;
|
|
935
900
|
const packets = parseSgrMousePackets(data);
|
|
936
|
-
if (!packets)
|
|
937
|
-
scheduleScrollButtonSync(getToolMouseTui(), data);
|
|
938
|
-
return undefined;
|
|
939
|
-
}
|
|
901
|
+
if (!packets) return undefined;
|
|
940
902
|
|
|
941
903
|
let consumed = false;
|
|
942
904
|
for (const packet of packets) {
|
|
@@ -957,7 +919,6 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
957
919
|
|
|
958
920
|
// Let scrolling, motion, release, and clicks outside tool results reach the
|
|
959
921
|
// normal TUI input chain (including other extensions such as pi-zentui).
|
|
960
|
-
scheduleScrollButtonSync(getToolMouseTui(), data);
|
|
961
922
|
return consumed ? { consume: true } : undefined;
|
|
962
923
|
}
|
|
963
924
|
|
|
@@ -994,23 +955,18 @@ export function teardownToolMouseInteraction(
|
|
|
994
955
|
}
|
|
995
956
|
restoreToolMouseRenderPatch();
|
|
996
957
|
restoreFullscreenViewportInput(getToolMouseTui());
|
|
997
|
-
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
998
|
-
resetScrollButtonState();
|
|
999
958
|
setToolMouseTui(null);
|
|
1000
959
|
toolMouseUi = null;
|
|
1001
960
|
patchRegistry.dispose(TOOL_MOUSE_OWNER_KEY, owner);
|
|
1002
961
|
if (toolMouseInstallationOwner === owner) toolMouseInstallationOwner = null;
|
|
1003
962
|
}
|
|
1004
963
|
|
|
1005
|
-
/** off 模式清理:清空 hover
|
|
964
|
+
/** off 模式清理:清空 hover 状态(跨模块 rebind 统一经由此函数)。 */
|
|
1006
965
|
export function resetToolHoverState(): void {
|
|
1007
966
|
setHoveredToolCallId(null);
|
|
1008
967
|
setHoveredThinking(null);
|
|
1009
968
|
setHoveredMessageDisplay(null);
|
|
1010
969
|
setHoveredCompactAssistant(null);
|
|
1011
|
-
setScrollButtonVisible(false);
|
|
1012
|
-
setScrollButtonHovered(false);
|
|
1013
|
-
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
1014
970
|
releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
1015
971
|
}
|
|
1016
972
|
|
|
@@ -1087,33 +1043,29 @@ export function installToolMouseInteraction(
|
|
|
1087
1043
|
toolMouseUi = ctx.ui;
|
|
1088
1044
|
// 0.84+ 的 tui 是惰性 Proxy:regular 保留原生 scrollback;fullscreen
|
|
1089
1045
|
// 由官方 LayoutFrame 命中,并由扩展补齐 hover 所需的 all-motion 上报。
|
|
1090
|
-
ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any
|
|
1046
|
+
ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any) => {
|
|
1091
1047
|
setToolMouseTui(tui);
|
|
1092
1048
|
setToolTuiFullscreen(fullscreenLazyTui(tui));
|
|
1049
|
+
// The widget draws nothing. It is how the toolbox reaches the TUI, and in fullscreen its
|
|
1050
|
+
// render re-applies the input patches after pi swaps the TUI. pi's own jump-to-latest
|
|
1051
|
+
// indicator is left alone; an older toolbox replaced it with a button, so undo that.
|
|
1052
|
+
restoreOfficialScrollToEnd(tui);
|
|
1093
1053
|
if (isLazyProxyTui(tui)) {
|
|
1094
1054
|
patchFullscreenViewportInput(tui);
|
|
1095
1055
|
ensureFullscreenToolMouseMotion(tui);
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
render: (width: number) => {
|
|
1056
|
+
return {
|
|
1057
|
+
render: () => {
|
|
1099
1058
|
patchFullscreenViewportInput(tui);
|
|
1100
1059
|
ensureFullscreenToolMouseMotion(tui);
|
|
1101
|
-
|
|
1102
|
-
return renderScrollButton(width, theme);
|
|
1060
|
+
return [];
|
|
1103
1061
|
},
|
|
1104
1062
|
invalidate() {},
|
|
1105
|
-
}
|
|
1106
|
-
return getScrollButtonWidget();
|
|
1063
|
+
};
|
|
1107
1064
|
}
|
|
1108
1065
|
// Wrap doRender to capture the live frame for tool click/hover mapping.
|
|
1109
1066
|
patchToolMouseMotionAfterRender(tui);
|
|
1110
1067
|
if (toolMouseInteractionActive()) tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
|
|
1111
|
-
|
|
1112
|
-
render: (width: number) => renderScrollButton(width, theme),
|
|
1113
|
-
invalidate() {},
|
|
1114
|
-
};
|
|
1115
|
-
setScrollButtonWidget(widget);
|
|
1116
|
-
return widget;
|
|
1068
|
+
return { render: () => [], invalidate() {} };
|
|
1117
1069
|
});
|
|
1118
1070
|
toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
|
|
1119
1071
|
}
|
|
@@ -4,7 +4,6 @@ import { isCompactAssistantComponent } from "../compact-mode.ts";
|
|
|
4
4
|
import { ToolGroupComponent } from "../tool/grouping.ts";
|
|
5
5
|
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
6
6
|
import { isToolExecutionComponent, stripTerminalSequencesPreservingLayout } from "./packets.ts";
|
|
7
|
-
import { getScrollButtonWidget } from "./scroll.ts";
|
|
8
7
|
|
|
9
8
|
export type ComponentRowHit = {
|
|
10
9
|
component: any;
|
|
@@ -143,9 +142,6 @@ export function componentAtLocalRow(
|
|
|
143
142
|
if (isMessageDisplayComponent(component)) {
|
|
144
143
|
return { component, row: localRow };
|
|
145
144
|
}
|
|
146
|
-
if (component === getScrollButtonWidget()) {
|
|
147
|
-
return { component, row: localRow };
|
|
148
|
-
}
|
|
149
145
|
if (!Array.isArray(component.children)) return null;
|
|
150
146
|
let offset = 0;
|
|
151
147
|
for (const child of component.children) {
|