@oh-my-pi/pi-coding-agent 15.11.6 → 15.11.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -1
- package/dist/cli.js +431 -381
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/collab/crypto.d.ts +12 -0
- package/dist/types/collab/guest.d.ts +21 -0
- package/dist/types/collab/host.d.ts +13 -0
- package/dist/types/collab/protocol.d.ts +100 -0
- package/dist/types/collab/relay-client.d.ts +22 -0
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/join.d.ts +12 -0
- package/dist/types/config/model-resolver.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +93 -1
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/slash-commands.d.ts +1 -11
- package/dist/types/modes/components/agent-hub.d.ts +13 -0
- package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
- package/dist/types/modes/components/hook-selector.d.ts +4 -6
- package/dist/types/modes/components/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/segment-track.d.ts +11 -6
- package/dist/types/modes/components/settings-selector.d.ts +8 -1
- package/dist/types/modes/components/snapcompact-shape-preview.d.ts +31 -0
- package/dist/types/modes/components/status-line/component.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +9 -0
- package/dist/types/modes/components/tool-execution.d.ts +13 -9
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +3 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +10 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +3 -0
- package/dist/types/modes/types.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/snapcompact-inline.d.ts +8 -3
- package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/renderers.d.ts +13 -0
- package/dist/types/tools/ssh.d.ts +1 -0
- package/package.json +14 -12
- package/scripts/bench-guard.ts +71 -0
- package/src/cli/args.ts +2 -0
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli-commands.ts +2 -0
- package/src/collab/crypto.ts +57 -0
- package/src/collab/guest.ts +421 -0
- package/src/collab/host.ts +494 -0
- package/src/collab/protocol.ts +191 -0
- package/src/collab/relay-client.ts +216 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/join.ts +39 -0
- package/src/config/model-registry.ts +74 -19
- package/src/config/model-resolver.ts +36 -5
- package/src/config/settings-schema.ts +119 -1
- package/src/edit/renderer.ts +5 -0
- package/src/extensibility/slash-commands.ts +1 -97
- package/src/hindsight/client.ts +26 -1
- package/src/hindsight/state.ts +6 -2
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/main.ts +11 -2
- package/src/mcp/transports/stdio.ts +81 -7
- package/src/modes/components/agent-hub.ts +119 -22
- package/src/modes/components/assistant-message.ts +126 -6
- package/src/modes/components/collab-prompt-message.ts +30 -0
- package/src/modes/components/hook-selector.ts +4 -5
- package/src/modes/components/oauth-selector.ts +67 -7
- package/src/modes/components/segment-track.ts +44 -7
- package/src/modes/components/settings-selector.ts +27 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +11 -0
- package/src/modes/components/snapcompact-shape-preview.ts +192 -0
- package/src/modes/components/status-line/component.ts +21 -1
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line/segments.ts +13 -0
- package/src/modes/components/status-line/types.ts +10 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +18 -10
- package/src/modes/controllers/input-controller.ts +80 -12
- package/src/modes/controllers/selector-controller.ts +6 -2
- package/src/modes/controllers/streaming-reveal.ts +7 -0
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/setup-wizard/index.ts +1 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +24 -6
- package/src/modes/setup-wizard/scenes/providers.ts +36 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +10 -1
- package/src/modes/setup-wizard/scenes/theme.ts +28 -1
- package/src/modes/setup-wizard/scenes/types.ts +10 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +22 -6
- package/src/modes/setup-wizard/wizard-overlay.ts +38 -1
- package/src/modes/types.ts +8 -0
- package/src/modes/utils/context-usage.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +7 -0
- package/src/prompts/bench.md +7 -0
- package/src/sdk.ts +240 -36
- package/src/session/agent-session.ts +22 -0
- package/src/session/session-manager.ts +44 -0
- package/src/session/snapcompact-inline.ts +20 -22
- package/src/slash-commands/builtin-registry.ts +210 -0
- package/src/tools/bash.ts +3 -0
- package/src/tools/eval-render.ts +4 -0
- package/src/tools/read.ts +38 -5
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +3 -0
- package/src/tools/write.ts +13 -42
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
fuzzyFilter,
|
|
7
7
|
matchesKey,
|
|
8
8
|
ScrollView,
|
|
9
|
+
type SgrMouseEvent,
|
|
9
10
|
Spacer,
|
|
10
11
|
TruncatedText,
|
|
11
12
|
} from "@oh-my-pi/pi-tui";
|
|
@@ -16,6 +17,12 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
16
17
|
|
|
17
18
|
const OAUTH_SELECTOR_MAX_VISIBLE = 10;
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Rendered lines before the provider rows: top border, spacer, title, spacer
|
|
22
|
+
* (must mirror the constructor's addChild order).
|
|
23
|
+
*/
|
|
24
|
+
const LIST_ROW_OFFSET = 4;
|
|
25
|
+
|
|
19
26
|
/** Compact, human-readable tag for each credential-origin leg. */
|
|
20
27
|
const ORIGIN_LABELS: Record<CredentialOriginKind, string> = {
|
|
21
28
|
runtime: "--api-key",
|
|
@@ -34,6 +41,10 @@ export class OAuthSelectorComponent extends Container {
|
|
|
34
41
|
#filteredProviders: OAuthProviderInfo[] = [];
|
|
35
42
|
#searchQuery = "";
|
|
36
43
|
#selectedIndex: number = 0;
|
|
44
|
+
#hoveredIndex: number | null = null;
|
|
45
|
+
/** First provider index of the visible ScrollView window (last #updateList). */
|
|
46
|
+
#scrollStart = 0;
|
|
47
|
+
#visibleCount = 0;
|
|
37
48
|
#mode: "login" | "logout";
|
|
38
49
|
#authStorage: AuthStorage;
|
|
39
50
|
#onSelectCallback: (providerId: string) => void;
|
|
@@ -252,6 +263,8 @@ export class OAuthSelectorComponent extends Container {
|
|
|
252
263
|
? 0
|
|
253
264
|
: Math.max(0, Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
|
|
254
265
|
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
266
|
+
this.#scrollStart = startIndex;
|
|
267
|
+
this.#visibleCount = endIndex - startIndex;
|
|
255
268
|
|
|
256
269
|
const rows: string[] = [];
|
|
257
270
|
for (let i = startIndex; i < endIndex; i++) {
|
|
@@ -270,6 +283,9 @@ export class OAuthSelectorComponent extends Container {
|
|
|
270
283
|
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
|
271
284
|
line = text + statusIndicator;
|
|
272
285
|
}
|
|
286
|
+
if (!isSelected && i === this.#hoveredIndex) {
|
|
287
|
+
line = theme.bg("selectedBg", line);
|
|
288
|
+
}
|
|
273
289
|
rows.push(line);
|
|
274
290
|
}
|
|
275
291
|
|
|
@@ -354,15 +370,59 @@ export class OAuthSelectorComponent extends Container {
|
|
|
354
370
|
}
|
|
355
371
|
// Enter
|
|
356
372
|
else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
this.#confirmSelection();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Confirm the selected provider (Enter or mouse click). */
|
|
378
|
+
#confirmSelection(): void {
|
|
379
|
+
const selectedProvider = this.#filteredProviders[this.#selectedIndex];
|
|
380
|
+
if (selectedProvider?.available) {
|
|
381
|
+
this.#statusMessage = undefined;
|
|
382
|
+
this.stopValidation();
|
|
383
|
+
this.#onSelectCallback(selectedProvider.id);
|
|
384
|
+
} else if (selectedProvider) {
|
|
385
|
+
this.#statusMessage = "Provider unavailable in this environment.";
|
|
386
|
+
this.#updateList();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Move the selection one step for a wheel notch (clamped, no wrap). */
|
|
391
|
+
handleWheel(delta: -1 | 1): void {
|
|
392
|
+
if (this.#filteredProviders.length === 0) return;
|
|
393
|
+
const next = Math.max(0, Math.min(this.#selectedIndex + delta, this.#filteredProviders.length - 1));
|
|
394
|
+
if (next === this.#selectedIndex) return;
|
|
395
|
+
this.#selectedIndex = next;
|
|
396
|
+
this.#statusMessage = undefined;
|
|
397
|
+
this.#updateList();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Route an SGR mouse report at component-local coordinates. Provider rows
|
|
402
|
+
* start LIST_ROW_OFFSET lines into the render; the ScrollView window shows
|
|
403
|
+
* #visibleCount rows from #scrollStart. Wheel moves the selection, motion
|
|
404
|
+
* drives the hover band, and a left click selects and confirms like Enter.
|
|
405
|
+
*/
|
|
406
|
+
routeMouse(event: SgrMouseEvent, line: number, _col: number): void {
|
|
407
|
+
if (event.wheel !== null) {
|
|
408
|
+
this.handleWheel(event.wheel);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const localRow = line - LIST_ROW_OFFSET;
|
|
412
|
+
const index = localRow >= 0 && localRow < this.#visibleCount ? this.#scrollStart + localRow : undefined;
|
|
413
|
+
const target = index !== undefined && index < this.#filteredProviders.length ? index : null;
|
|
414
|
+
if (event.motion) {
|
|
415
|
+
if (target !== this.#hoveredIndex) {
|
|
416
|
+
this.#hoveredIndex = target;
|
|
364
417
|
this.#updateList();
|
|
365
418
|
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!event.leftClick || target === null) return;
|
|
422
|
+
if (target !== this.#selectedIndex) {
|
|
423
|
+
this.#selectedIndex = target;
|
|
424
|
+
this.#statusMessage = undefined;
|
|
366
425
|
}
|
|
426
|
+
this.#confirmSelection();
|
|
367
427
|
}
|
|
368
428
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared renderer for a horizontal row of colored "segments" styled after the
|
|
3
|
-
* status line: each segment
|
|
4
|
-
* as a powerline chip (its
|
|
5
|
-
* flanked by triangle caps) and the
|
|
6
|
-
* thin separator.
|
|
3
|
+
* status line: each segment is colored by its track position from the theme's
|
|
4
|
+
* own palette, the active one is filled as a powerline chip (its color as the
|
|
5
|
+
* background, a luminance-matched label, flanked by triangle caps) and the
|
|
6
|
+
* rest are plain colored labels joined by a thin separator.
|
|
7
7
|
*
|
|
8
8
|
* Used by the plan-mode model-tier slider ({@link HookSelectorComponent}) and
|
|
9
9
|
* the ctrl+p role-cycle status so both surfaces read identically.
|
|
@@ -12,13 +12,49 @@ import { type ThemeColor, theme } from "../theme/theme";
|
|
|
12
12
|
|
|
13
13
|
export interface TrackSegment {
|
|
14
14
|
label: string;
|
|
15
|
-
/** Theme color for the segment; defaults to `accent`. */
|
|
16
|
-
color?: ThemeColor;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
const FG_RESET = "\x1b[39m";
|
|
20
18
|
const BG_RESET = "\x1b[49m";
|
|
21
19
|
|
|
20
|
+
/** Vivid theme colors for position-based segment coloring, in preference
|
|
21
|
+
* order. Themes alias many of these to the same value (titanium maps most of
|
|
22
|
+
* the syntax set onto its accent), so {@link resolveSegmentPalette} dedupes
|
|
23
|
+
* by resolved escape and hands position i the i-th distinct color. */
|
|
24
|
+
const SEGMENT_COLOR_CANDIDATES: ThemeColor[] = [
|
|
25
|
+
"accent",
|
|
26
|
+
"success",
|
|
27
|
+
"warning",
|
|
28
|
+
"error",
|
|
29
|
+
"mdCode",
|
|
30
|
+
"mdLink",
|
|
31
|
+
"syntaxString",
|
|
32
|
+
"syntaxKeyword",
|
|
33
|
+
"syntaxFunction",
|
|
34
|
+
"syntaxNumber",
|
|
35
|
+
"syntaxOperator",
|
|
36
|
+
"syntaxVariable",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve up to `count` theme colors that render distinctly under the active
|
|
41
|
+
* theme, in candidate preference order. May return fewer than `count` when the
|
|
42
|
+
* theme has fewer distinct hues (e.g. monochrome themes) — callers wrap with
|
|
43
|
+
* modulo. Never returns an empty array: `accent` always resolves.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveSegmentPalette(count: number): ThemeColor[] {
|
|
46
|
+
const palette: ThemeColor[] = [];
|
|
47
|
+
const seen = new Set<string>();
|
|
48
|
+
for (const color of SEGMENT_COLOR_CANDIDATES) {
|
|
49
|
+
const ansi = theme.getFgAnsi(color);
|
|
50
|
+
if (seen.has(ansi)) continue;
|
|
51
|
+
seen.add(ansi);
|
|
52
|
+
palette.push(color);
|
|
53
|
+
if (palette.length >= count) break;
|
|
54
|
+
}
|
|
55
|
+
return palette;
|
|
56
|
+
}
|
|
57
|
+
|
|
22
58
|
/**
|
|
23
59
|
* Render `segments` as a colored chip track with `activeIndex` filled. Returns
|
|
24
60
|
* a single line of styled text with no surrounding caption or arrows — callers
|
|
@@ -30,6 +66,7 @@ export function renderSegmentTrack(segments: TrackSegment[], activeIndex: number
|
|
|
30
66
|
const capLeft = theme.sep.powerlineRight;
|
|
31
67
|
const capRight = theme.sep.powerlineLeft;
|
|
32
68
|
const thinSep = theme.fg("statusLineSep", theme.sep.powerlineThin);
|
|
69
|
+
const palette = resolveSegmentPalette(segments.length);
|
|
33
70
|
|
|
34
71
|
let track = "";
|
|
35
72
|
segments.forEach((segment, i) => {
|
|
@@ -38,7 +75,7 @@ export function renderSegmentTrack(segments: TrackSegment[], activeIndex: number
|
|
|
38
75
|
// caps already delimit the active segment, so pad around it instead.
|
|
39
76
|
track += i === activeIndex || i - 1 === activeIndex ? " " : ` ${thinSep} `;
|
|
40
77
|
}
|
|
41
|
-
const color =
|
|
78
|
+
const color = palette[i % palette.length];
|
|
42
79
|
const fg = theme.getFgAnsi(color);
|
|
43
80
|
if (i !== activeIndex) {
|
|
44
81
|
track += `${fg}${segment.label}${FG_RESET}`;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
fuzzyRank,
|
|
8
8
|
getKeybindings,
|
|
9
9
|
getSettingItemFilterText,
|
|
10
|
+
type ImageBudget,
|
|
10
11
|
Input,
|
|
11
12
|
matchesKey,
|
|
12
13
|
parseSgrMouse,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
truncateToWidth,
|
|
23
24
|
visibleWidth,
|
|
24
25
|
} from "@oh-my-pi/pi-tui";
|
|
26
|
+
import type { ShapeTarget } from "@oh-my-pi/snapcompact";
|
|
25
27
|
import { getDefault, type SettingPath, settings } from "../../config/settings";
|
|
26
28
|
import type {
|
|
27
29
|
SettingTab,
|
|
@@ -36,6 +38,7 @@ import { getTabBarTheme } from "../shared";
|
|
|
36
38
|
import { bottomBorder, divider, row, topBorder } from "./overlay-box";
|
|
37
39
|
import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
|
|
38
40
|
import { getSettingDef, getSettingsForTab, type SettingDef } from "./settings-defs";
|
|
41
|
+
import { SnapcompactShapePreview } from "./snapcompact-shape-preview";
|
|
39
42
|
import { getPreset } from "./status-line/presets";
|
|
40
43
|
|
|
41
44
|
/**
|
|
@@ -97,6 +100,7 @@ class SelectSubmenu extends Container {
|
|
|
97
100
|
onCancel: () => void,
|
|
98
101
|
onSelectionChange?: (value: string) => void | Promise<void>,
|
|
99
102
|
private readonly getPreview?: () => string,
|
|
103
|
+
footer?: Component,
|
|
100
104
|
) {
|
|
101
105
|
super();
|
|
102
106
|
|
|
@@ -158,6 +162,13 @@ class SelectSubmenu extends Container {
|
|
|
158
162
|
// Hint
|
|
159
163
|
this.addChild(new Spacer(1));
|
|
160
164
|
this.addChild(new Text(theme.fg("dim", " Enter to select · Esc to go back"), 0, 0));
|
|
165
|
+
|
|
166
|
+
// Footer (e.g. the snapcompact shape preview) below the interactive rows,
|
|
167
|
+
// so the list never shifts while browsing.
|
|
168
|
+
if (footer) {
|
|
169
|
+
this.addChild(new Spacer(1));
|
|
170
|
+
this.addChild(footer);
|
|
171
|
+
}
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
#updatePreview(): void {
|
|
@@ -249,6 +260,12 @@ export interface SettingsRuntimeContext {
|
|
|
249
260
|
availableThemes: string[];
|
|
250
261
|
/** Working directory for plugins tab */
|
|
251
262
|
cwd: string;
|
|
263
|
+
/** Active model (api + id); resolves what the snapcompact `auto` shape maps to. */
|
|
264
|
+
model?: ShapeTarget;
|
|
265
|
+
/** Shared TUI image budget (graphics ids + transmit-once) for image previews. */
|
|
266
|
+
imageBudget?: ImageBudget;
|
|
267
|
+
/** Schedules a re-render after async preview work completes. */
|
|
268
|
+
requestRender?: () => void;
|
|
252
269
|
}
|
|
253
270
|
|
|
254
271
|
/** Status line settings subset for preview */
|
|
@@ -758,6 +775,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
758
775
|
// Preview handlers
|
|
759
776
|
let onPreview: ((value: string) => void | Promise<void>) | undefined;
|
|
760
777
|
let onPreviewCancel: (() => void) | undefined;
|
|
778
|
+
let footer: Component | undefined;
|
|
761
779
|
|
|
762
780
|
const activeThemeBeforePreview = getCurrentThemeName() ?? currentValue;
|
|
763
781
|
if (def.path === "theme.dark" || def.path === "theme.light") {
|
|
@@ -797,6 +815,14 @@ export class SettingsSelectorComponent implements Component {
|
|
|
797
815
|
const separator = settings.get("statusLine.separator");
|
|
798
816
|
this.callbacks.onStatusLinePreview?.({ separator });
|
|
799
817
|
};
|
|
818
|
+
} else if (def.path === "snapcompact.shape") {
|
|
819
|
+
const shapePreview = new SnapcompactShapePreview(currentValue, {
|
|
820
|
+
model: this.context.model,
|
|
821
|
+
imageBudget: this.context.imageBudget,
|
|
822
|
+
requestRender: this.context.requestRender,
|
|
823
|
+
});
|
|
824
|
+
onPreview = value => shapePreview.setValue(value);
|
|
825
|
+
footer = shapePreview;
|
|
800
826
|
}
|
|
801
827
|
|
|
802
828
|
// Provide status line preview for theme selection
|
|
@@ -819,6 +845,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
819
845
|
},
|
|
820
846
|
onPreview,
|
|
821
847
|
getPreview,
|
|
848
|
+
footer,
|
|
822
849
|
);
|
|
823
850
|
}
|
|
824
851
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[User]: Fix the settings overlay crash. Wheeling past the last row throws.
|
|
2
|
+
|
|
3
|
+
[Assistant tool calls]: read(path="src/select-list.ts:140-180")
|
|
4
|
+
|
|
5
|
+
[Tool result]: 162: const index = Math.floor(line / rowHeight); index is never checked against bounds.
|
|
6
|
+
|
|
7
|
+
[Assistant]: Found it. The hit test indexes past the filtered list; clamping to the last row fixes the crash.
|
|
8
|
+
|
|
9
|
+
[User]: Does the fix survive filtering?
|
|
10
|
+
|
|
11
|
+
[Assistant]: Yes. The clamp applies after the filter pass, so a narrowed list keeps the hit map in sync. Added a regression test that wheels past the last row with a filter active and asserts no throw.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live preview for the `snapcompact.shape` setting: renders a sample session
|
|
3
|
+
* transcript through the real snapcompact rasterizer as a miniature page and
|
|
4
|
+
* shows it zoomed, so cell size, ink hues, highlight bands, and dim tool-result
|
|
5
|
+
* spans are legible at terminal scale.
|
|
6
|
+
*
|
|
7
|
+
* The mini-frame (a {@link SRC_FRAME_PX}px page) is upscaled with
|
|
8
|
+
* nearest-neighbor so the glyph pixels stay crisp when the terminal scales the
|
|
9
|
+
* placement box. Graphics display requires the Kitty unicode-placeholder path —
|
|
10
|
+
* `renderImage` returning `lines` is the gate — because the bordered settings
|
|
11
|
+
* frame re-fits every row, which direct cursor-positioned placements (iTerm2,
|
|
12
|
+
* Sixel, Kitty `a=p`) do not survive. Everything else falls back to the stats
|
|
13
|
+
* line plus a dim notice.
|
|
14
|
+
*/
|
|
15
|
+
import { type Component, type ImageBudget, renderImage, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
16
|
+
import {
|
|
17
|
+
DIM_OFF,
|
|
18
|
+
DIM_ON,
|
|
19
|
+
geometry,
|
|
20
|
+
isShapeVariantName,
|
|
21
|
+
normalize,
|
|
22
|
+
renderMany,
|
|
23
|
+
resolveShape,
|
|
24
|
+
SHAPE_VARIANT_NAMES,
|
|
25
|
+
SHAPE_VARIANTS,
|
|
26
|
+
type Shape,
|
|
27
|
+
type ShapeTarget,
|
|
28
|
+
type ShapeVariantName,
|
|
29
|
+
} from "@oh-my-pi/snapcompact";
|
|
30
|
+
import { theme } from "../theme/theme";
|
|
31
|
+
import sampleDoc from "./snapcompact-shape-preview-doc.md" with { type: "text" };
|
|
32
|
+
|
|
33
|
+
/** Mini-frame edge in px — a small page from the real rasterizer ≈ a zoomed crop. */
|
|
34
|
+
const SRC_FRAME_PX = 128;
|
|
35
|
+
/** Nearest-neighbor upscale factor; keeps glyph pixels crisp on HiDPI cell boxes. */
|
|
36
|
+
const ZOOM_SCALE = 4;
|
|
37
|
+
/** Display box in terminal cells (square-ish at the typical 1:2 cell aspect). */
|
|
38
|
+
const MAX_IMAGE_COLS = 28;
|
|
39
|
+
const MAX_IMAGE_ROWS = 14;
|
|
40
|
+
|
|
41
|
+
/** Sample transcript with `[Tool result]:` bodies wrapped in dim-ink toggles. */
|
|
42
|
+
const PREVIEW_TEXT = sampleDoc
|
|
43
|
+
.trim()
|
|
44
|
+
.replace(/\[Tool result\]: ([^[]*)/g, (_match, body: string) => `[Tool result]: ${DIM_ON}${body}${DIM_OFF}`);
|
|
45
|
+
|
|
46
|
+
type PreviewEntry =
|
|
47
|
+
| { state: "rendering" }
|
|
48
|
+
| { state: "failed" }
|
|
49
|
+
| { state: "ready"; data: string; edgePx: number; imageId: number; transmitted: boolean };
|
|
50
|
+
|
|
51
|
+
export interface SnapcompactShapePreviewOptions {
|
|
52
|
+
/** Active model (api + id); resolves what `auto` maps to for this reader. */
|
|
53
|
+
model?: ShapeTarget;
|
|
54
|
+
/** Shared TUI image budget: stable graphics ids, transmit-once, exit cleanup. */
|
|
55
|
+
imageBudget?: ImageBudget;
|
|
56
|
+
/** Schedules a re-render once an async sample render completes. */
|
|
57
|
+
requestRender?: () => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class SnapcompactShapePreview implements Component {
|
|
61
|
+
#model: ShapeTarget | undefined;
|
|
62
|
+
#budget: ImageBudget | undefined;
|
|
63
|
+
#requestRender: () => void;
|
|
64
|
+
#variant: ShapeVariantName | "auto" = "auto";
|
|
65
|
+
#entries = new Map<ShapeVariantName, PreviewEntry>();
|
|
66
|
+
|
|
67
|
+
constructor(currentValue: string, options: SnapcompactShapePreviewOptions = {}) {
|
|
68
|
+
this.#model = options.model;
|
|
69
|
+
this.#budget = options.imageBudget;
|
|
70
|
+
this.#requestRender = options.requestRender ?? (() => {});
|
|
71
|
+
this.setValue(currentValue);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Track the highlighted option; the next render reflects it. */
|
|
75
|
+
setValue(value: string): void {
|
|
76
|
+
this.#variant = isShapeVariantName(value) ? value : "auto";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render(width: number): readonly string[] {
|
|
80
|
+
const shape = resolveShape(this.#model, this.#variant);
|
|
81
|
+
const name = resolvedVariantName(shape);
|
|
82
|
+
const geo = geometry(shape);
|
|
83
|
+
const label = this.#variant === "auto" ? `auto → ${name}` : name;
|
|
84
|
+
const chars = geo.capacity >= 1000 ? `${(geo.capacity / 1000).toFixed(1)}k` : String(geo.capacity);
|
|
85
|
+
const tokens =
|
|
86
|
+
shape.frameTokenEstimate >= 1000
|
|
87
|
+
? `${(shape.frameTokenEstimate / 1000).toFixed(1)}k`
|
|
88
|
+
: String(shape.frameTokenEstimate);
|
|
89
|
+
const stats = `full frame ${geo.cols}×${geo.rows} cells ≈ ${chars} chars ≈ ${tokens} tokens`;
|
|
90
|
+
const lines: string[] = [theme.fg("muted", ` Sample (zoomed) · ${label} · ${stats}`), ""];
|
|
91
|
+
|
|
92
|
+
if (!this.#budget || !TERMINAL.imageProtocol) {
|
|
93
|
+
lines.push(theme.fg("dim", " (graphic sample needs a Kitty-graphics terminal)"));
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const entry = this.#ensureEntry(name, shape);
|
|
98
|
+
if (entry.state === "rendering") {
|
|
99
|
+
lines.push(theme.fg("dim", " rendering sample…"));
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
if (entry.state === "failed") {
|
|
103
|
+
lines.push(theme.fg("dim", " (sample render failed)"));
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = renderImage(
|
|
108
|
+
entry.data,
|
|
109
|
+
{ widthPx: entry.edgePx, heightPx: entry.edgePx },
|
|
110
|
+
{
|
|
111
|
+
maxWidthCells: Math.max(8, Math.min(MAX_IMAGE_COLS, width - 4)),
|
|
112
|
+
maxHeightCells: MAX_IMAGE_ROWS,
|
|
113
|
+
imageId: entry.imageId,
|
|
114
|
+
includeTransmit: !entry.transmitted,
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
// Only the unicode-placeholder path returns text-cell `lines`; cursor-moving
|
|
118
|
+
// placements would corrupt the bordered settings frame, so skip them.
|
|
119
|
+
if (!result?.lines) {
|
|
120
|
+
lines.push(theme.fg("dim", " (graphic sample needs Kitty unicode-placeholder graphics)"));
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
if (result.transmit) {
|
|
124
|
+
this.#budget.enqueueTransmit(entry.imageId, result.transmit);
|
|
125
|
+
entry.transmitted = true;
|
|
126
|
+
}
|
|
127
|
+
for (const line of result.lines) {
|
|
128
|
+
lines.push(` ${line}`);
|
|
129
|
+
}
|
|
130
|
+
return lines;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#ensureEntry(name: ShapeVariantName, shape: Shape): PreviewEntry {
|
|
134
|
+
let entry = this.#entries.get(name);
|
|
135
|
+
if (!entry) {
|
|
136
|
+
entry = { state: "rendering" };
|
|
137
|
+
this.#entries.set(name, entry);
|
|
138
|
+
void this.#buildEntry(name, shape);
|
|
139
|
+
}
|
|
140
|
+
return entry;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async #buildEntry(name: ShapeVariantName, shape: Shape): Promise<void> {
|
|
144
|
+
try {
|
|
145
|
+
// Fill the mini-page so every variant shows a fully inked window.
|
|
146
|
+
const capacity = geometry(shape, SRC_FRAME_PX).capacity;
|
|
147
|
+
let text = PREVIEW_TEXT;
|
|
148
|
+
while (normalize(text).length < capacity) {
|
|
149
|
+
text += ` ${PREVIEW_TEXT}`;
|
|
150
|
+
}
|
|
151
|
+
const frame = renderMany(text, { shape, frameSize: SRC_FRAME_PX, maxFrames: 1 })[0];
|
|
152
|
+
if (!frame) throw new Error("empty sample frame");
|
|
153
|
+
const edgePx = SRC_FRAME_PX * ZOOM_SCALE;
|
|
154
|
+
const zoomed = await new Bun.Image(Buffer.from(frame.data, "base64"))
|
|
155
|
+
.resize(edgePx, edgePx, { filter: "nearest" })
|
|
156
|
+
.png()
|
|
157
|
+
.bytes();
|
|
158
|
+
this.#entries.set(name, {
|
|
159
|
+
state: "ready",
|
|
160
|
+
data: zoomed.toBase64(),
|
|
161
|
+
edgePx,
|
|
162
|
+
// Keyed id: reopening settings reuses the id, so data already in the
|
|
163
|
+
// terminal store is never re-transmitted (enqueueTransmit no-ops).
|
|
164
|
+
imageId: this.#budget?.acquireId(`snapshape:${name}:${edgePx}`) ?? 0,
|
|
165
|
+
transmitted: false,
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
this.#entries.set(name, { state: "failed" });
|
|
169
|
+
}
|
|
170
|
+
this.#requestRender();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Research name of the concrete geometry `resolveShape` picked (for `auto`). */
|
|
175
|
+
function resolvedVariantName(shape: Shape): ShapeVariantName {
|
|
176
|
+
for (const name of SHAPE_VARIANT_NAMES) {
|
|
177
|
+
const candidate = SHAPE_VARIANTS[name];
|
|
178
|
+
if (
|
|
179
|
+
candidate.font === shape.font &&
|
|
180
|
+
candidate.cellWidth === shape.cellWidth &&
|
|
181
|
+
candidate.cellHeight === shape.cellHeight &&
|
|
182
|
+
candidate.variant === shape.variant &&
|
|
183
|
+
candidate.lineRepeat === shape.lineRepeat &&
|
|
184
|
+
candidate.frameSize === shape.frameSize
|
|
185
|
+
) {
|
|
186
|
+
return name;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// resolveShape only hands out table geometries; the legacy shape is the
|
|
190
|
+
// conservative label if that invariant ever changes.
|
|
191
|
+
return "5x8-sent";
|
|
192
|
+
}
|
|
@@ -18,6 +18,7 @@ import { renderSegment, type SegmentContext } from "./segments";
|
|
|
18
18
|
import { getSeparator } from "./separators";
|
|
19
19
|
import { calculateTokensPerSecond } from "./token-rate";
|
|
20
20
|
import type {
|
|
21
|
+
CollabStatus,
|
|
21
22
|
EffectiveStatusLineSettings,
|
|
22
23
|
StatusLineSegmentId,
|
|
23
24
|
StatusLineSegmentOptions,
|
|
@@ -152,6 +153,7 @@ export class StatusLineComponent implements Component {
|
|
|
152
153
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
153
154
|
#loopModeStatus: { enabled: boolean } | null = null;
|
|
154
155
|
#goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
156
|
+
#collabStatus: CollabStatus | null = null;
|
|
155
157
|
|
|
156
158
|
// Git status caching (1s TTL)
|
|
157
159
|
#cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
@@ -217,6 +219,11 @@ export class StatusLineComponent implements Component {
|
|
|
217
219
|
this.#subagentCount = count;
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
/** Active subagent count as currently displayed (collab state mirroring). */
|
|
223
|
+
get subagentCount(): number {
|
|
224
|
+
return this.#subagentCount;
|
|
225
|
+
}
|
|
226
|
+
|
|
220
227
|
setSessionStartTime(time: number): void {
|
|
221
228
|
this.#sessionStartTime = time;
|
|
222
229
|
}
|
|
@@ -233,6 +240,10 @@ export class StatusLineComponent implements Component {
|
|
|
233
240
|
this.#goalModeStatus = status ?? null;
|
|
234
241
|
}
|
|
235
242
|
|
|
243
|
+
setCollabStatus(status: CollabStatus | null): void {
|
|
244
|
+
this.#collabStatus = status;
|
|
245
|
+
}
|
|
246
|
+
|
|
236
247
|
setHookStatus(key: string, text: string | undefined): void {
|
|
237
248
|
if (text === undefined) {
|
|
238
249
|
this.#hookStatuses.delete(key);
|
|
@@ -642,7 +653,15 @@ export class StatusLineComponent implements Component {
|
|
|
642
653
|
contextTokens = breakdown.usedTokens;
|
|
643
654
|
contextWindow = breakdown.contextWindow || contextWindow;
|
|
644
655
|
}
|
|
645
|
-
|
|
656
|
+
let contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
657
|
+
|
|
658
|
+
// Collab guest: context comes from the host's state frames — the local
|
|
659
|
+
// replica does no accounting of its own.
|
|
660
|
+
const collabState = this.#collabStatus?.stateOverride;
|
|
661
|
+
if (collabState?.contextUsage) {
|
|
662
|
+
contextWindow = collabState.contextUsage.contextWindow || contextWindow;
|
|
663
|
+
contextPercent = collabState.contextUsage.percent ?? contextPercent;
|
|
664
|
+
}
|
|
646
665
|
|
|
647
666
|
return {
|
|
648
667
|
session: this.session,
|
|
@@ -651,6 +670,7 @@ export class StatusLineComponent implements Component {
|
|
|
651
670
|
planMode: this.#planModeStatus,
|
|
652
671
|
loopMode: this.#loopModeStatus,
|
|
653
672
|
goalMode: this.#goalModeStatus,
|
|
673
|
+
collab: this.#collabStatus,
|
|
654
674
|
usageStats,
|
|
655
675
|
contextPercent,
|
|
656
676
|
contextWindow,
|
|
@@ -2,7 +2,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
|
|
|
2
2
|
|
|
3
3
|
export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
4
4
|
default: {
|
|
5
|
-
leftSegments: ["pi", "model", "mode", "path", "git", "pr", "context_pct", "cost"],
|
|
5
|
+
leftSegments: ["pi", "model", "mode", "collab", "path", "git", "pr", "context_pct", "cost"],
|
|
6
6
|
rightSegments: ["session_name"],
|
|
7
7
|
separator: "powerline-thin",
|
|
8
8
|
segmentOptions: {
|
|
@@ -493,6 +493,18 @@ const sessionNameSegment: StatusLineSegment = {
|
|
|
493
493
|
},
|
|
494
494
|
};
|
|
495
495
|
|
|
496
|
+
const collabSegment: StatusLineSegment = {
|
|
497
|
+
id: "collab",
|
|
498
|
+
render(ctx) {
|
|
499
|
+
if (!ctx.collab) return { content: "", visible: false };
|
|
500
|
+
const label =
|
|
501
|
+
ctx.collab.role === "host"
|
|
502
|
+
? `⇄ collab:${ctx.collab.participantCount}`
|
|
503
|
+
: `⇄ collab guest:${ctx.collab.participantCount}`;
|
|
504
|
+
return { content: theme.fg("accent", label), visible: true };
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
|
|
496
508
|
function pickUsageColor(percent: number): "muted" | "warning" | "error" {
|
|
497
509
|
if (percent >= 80) return "error";
|
|
498
510
|
if (percent >= 50) return "warning";
|
|
@@ -573,6 +585,7 @@ export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
|
|
|
573
585
|
cache_hit: cacheHitSegment,
|
|
574
586
|
session_name: sessionNameSegment,
|
|
575
587
|
usage: usageSegment,
|
|
588
|
+
collab: collabSegment,
|
|
576
589
|
};
|
|
577
590
|
|
|
578
591
|
export function renderSegment(id: StatusLineSegmentId, ctx: SegmentContext): RenderedSegment {
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import type { CollabSessionState } from "../../../collab/protocol";
|
|
1
2
|
import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../../config/settings-schema";
|
|
2
3
|
import type { AgentSession } from "../../../session/agent-session";
|
|
3
4
|
|
|
4
5
|
export type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle };
|
|
5
6
|
|
|
7
|
+
/** Collab session indicator + (guest-only) host-state override for segments. */
|
|
8
|
+
export interface CollabStatus {
|
|
9
|
+
role: "host" | "guest";
|
|
10
|
+
participantCount: number;
|
|
11
|
+
/** Guest only: host footer snapshot that overrides locally computed values. */
|
|
12
|
+
stateOverride?: CollabSessionState | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export interface StatusLineSegmentOptions {
|
|
7
16
|
model?: { showThinkingLevel?: boolean };
|
|
8
17
|
path?: { abbreviate?: boolean; maxLength?: number; stripWorkPrefix?: boolean };
|
|
@@ -49,6 +58,7 @@ export interface SegmentContext {
|
|
|
49
58
|
enabled: boolean;
|
|
50
59
|
paused: boolean;
|
|
51
60
|
} | null;
|
|
61
|
+
collab: CollabStatus | null;
|
|
52
62
|
// Cached values for performance (computed once per render)
|
|
53
63
|
usageStats: {
|
|
54
64
|
input: number;
|
|
@@ -16,4 +16,5 @@ Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smo
|
|
|
16
16
|
Press ctrl+r to search your prompt history and reuse a past message
|
|
17
17
|
`/force read` pins the next turn to one specific tool when the model keeps reaching for the wrong one
|
|
18
18
|
`/copy code` grabs the last code block to your clipboard — `/copy cmd` grabs the last shell/python command
|
|
19
|
-
`/shake` rips heavy tool results out of context to reclaim tokens without a full /compact — `/shake images` drops just images
|
|
19
|
+
`/shake` rips heavy tool results out of context to reclaim tokens without a full /compact — `/shake images` drops just images
|
|
20
|
+
Pair up live: `/collab` shares your session through an end-to-end encrypted relay link — a teammate runs `/join <link>` to watch tool calls stream and prompt the agent from their own omp
|
|
@@ -591,20 +591,28 @@ export class ToolExecutionComponent extends Container {
|
|
|
591
591
|
|
|
592
592
|
/**
|
|
593
593
|
* Whether this still-live block's settled rows may enter native scrollback
|
|
594
|
-
* (see `FinalizableBlock.isTranscriptBlockCommitStable`).
|
|
595
|
-
*
|
|
596
|
-
* (edit
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
594
|
+
* (see `FinalizableBlock.isTranscriptBlockCommitStable`). Classification is
|
|
595
|
+
* per renderer (`ToolRenderer.provisionalPendingPreview`): tail-window
|
|
596
|
+
* streaming views (edit's streamed-diff tail, bash/ssh command caps, eval
|
|
597
|
+
* cells) are re-anchored top-first by the result render, so promoting
|
|
598
|
+
* their visually static head — e.g. an edit preview idling on its last
|
|
599
|
+
* frame while the apply + LSP pass runs — would strand a stale copy of
|
|
600
|
+
* the call box above the final block the moment the result lands. Every
|
|
601
|
+
* other pending preview streams top-anchored append-shaped rows the
|
|
602
|
+
* result render preserves (a task call's context/assignment markdown, a
|
|
603
|
+
* write's content), so it stays commit-eligible — a call taller than the
|
|
604
|
+
* viewport scrolls into native history mid-stream instead of reading as
|
|
605
|
+
* cut off until the result. Expanded blocks always stream top-anchored
|
|
606
|
+
* (the over-tall write/eval scrollback contract). Displaceable waiting
|
|
603
607
|
* polls are removed wholesale by the next poll and must never commit.
|
|
604
608
|
*/
|
|
605
609
|
isTranscriptBlockCommitStable(): boolean {
|
|
606
610
|
if (this.#displaceable) return false;
|
|
607
|
-
|
|
611
|
+
if (this.#expanded || this.isTranscriptBlockFinalized()) return true;
|
|
612
|
+
if ((this.#tool as { provisionalPendingPreview?: boolean } | undefined)?.provisionalPendingPreview) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
return !toolRenderers[this.#toolName]?.provisionalPendingPreview;
|
|
608
616
|
}
|
|
609
617
|
|
|
610
618
|
/**
|