@oh-my-pi/pi-coding-agent 15.11.4 → 15.11.7
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 +82 -1
- package/dist/cli.js +520 -451
- package/dist/types/cli/bench-cli.d.ts +78 -0
- package/dist/types/cli/usage-cli.d.ts +10 -1
- package/dist/types/commands/bench.d.ts +29 -0
- package/dist/types/commands/usage.d.ts +9 -0
- package/dist/types/config/model-resolver.d.ts +3 -2
- package/dist/types/config/settings-schema.d.ts +125 -3
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -1
- package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- 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/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -0
- package/dist/types/modes/session-observer-registry.d.ts +2 -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 +2 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -1
- package/dist/types/session/agent-session.d.ts +14 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/codex-auto-reset.d.ts +107 -0
- package/dist/types/session/snapcompact-inline.d.ts +107 -4
- package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
- package/dist/types/task/render.d.ts +1 -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/dist/types/tools/todo.d.ts +0 -11
- package/package.json +11 -11
- package/src/cli/bench-cli.ts +437 -0
- package/src/cli/usage-cli.ts +187 -16
- package/src/cli-commands.ts +1 -0
- package/src/commands/bench.ts +42 -0
- package/src/commands/usage.ts +8 -0
- package/src/config/model-registry.ts +52 -5
- package/src/config/model-resolver.ts +36 -5
- package/src/config/settings-schema.ts +148 -3
- package/src/config/settings.ts +9 -0
- package/src/edit/renderer.ts +5 -0
- package/src/hindsight/client.ts +26 -1
- package/src/hindsight/state.ts +6 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/mcp/transports/stdio.ts +81 -7
- package/src/modes/components/oauth-selector.ts +67 -7
- package/src/modes/components/reset-usage-selector.ts +161 -0
- package/src/modes/components/session-selector.ts +8 -2
- package/src/modes/components/settings-selector.ts +89 -47
- 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/tool-execution.ts +26 -0
- package/src/modes/components/transcript-container.ts +23 -1
- package/src/modes/controllers/command-controller.ts +24 -1
- package/src/modes/controllers/input-controller.ts +8 -6
- package/src/modes/controllers/selector-controller.ts +72 -2
- package/src/modes/interactive-mode.ts +83 -0
- package/src/modes/session-observer-registry.ts +61 -3
- 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/theme/theme.ts +2 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/context-usage.ts +75 -1
- package/src/prompts/bench.md +7 -0
- package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
- package/src/prompts/system/snapcompact-context-stub.md +1 -0
- package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
- package/src/prompts/tools/browser.md +33 -43
- package/src/prompts/tools/eval.md +27 -50
- package/src/prompts/tools/irc.md +29 -31
- package/src/prompts/tools/read.md +31 -37
- package/src/prompts/tools/todo.md +1 -2
- package/src/sdk.ts +4 -2
- package/src/session/agent-session.ts +136 -6
- package/src/session/auth-storage.ts +3 -0
- package/src/session/codex-auto-reset.ts +190 -0
- package/src/session/snapcompact-inline.ts +404 -75
- package/src/slash-commands/builtin-registry.ts +145 -8
- package/src/slash-commands/helpers/context-report.ts +28 -1
- package/src/slash-commands/helpers/reset-usage.ts +66 -0
- package/src/slash-commands/helpers/usage-report.ts +12 -0
- package/src/task/index.ts +30 -7
- package/src/task/render.ts +34 -19
- package/src/tools/bash.ts +3 -0
- package/src/tools/eval-render.ts +4 -0
- package/src/tools/renderers.ts +13 -0
- package/src/tools/ssh.ts +3 -0
- package/src/tools/todo.ts +8 -128
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
type Component,
|
|
5
5
|
Container,
|
|
6
6
|
extractPrintableText,
|
|
7
|
-
|
|
7
|
+
fuzzyRank,
|
|
8
8
|
getKeybindings,
|
|
9
9
|
getSettingItemFilterText,
|
|
10
|
+
type ImageBudget,
|
|
10
11
|
Input,
|
|
11
12
|
matchesKey,
|
|
12
|
-
padding,
|
|
13
13
|
parseSgrMouse,
|
|
14
14
|
type SelectItem,
|
|
15
15
|
SelectList,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
truncateToWidth,
|
|
24
24
|
visibleWidth,
|
|
25
25
|
} from "@oh-my-pi/pi-tui";
|
|
26
|
+
import type { ShapeTarget } from "@oh-my-pi/snapcompact";
|
|
26
27
|
import { getDefault, type SettingPath, settings } from "../../config/settings";
|
|
27
28
|
import type {
|
|
28
29
|
SettingTab,
|
|
@@ -37,6 +38,7 @@ import { getTabBarTheme } from "../shared";
|
|
|
37
38
|
import { bottomBorder, divider, row, topBorder } from "./overlay-box";
|
|
38
39
|
import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
|
|
39
40
|
import { getSettingDef, getSettingsForTab, type SettingDef } from "./settings-defs";
|
|
41
|
+
import { SnapcompactShapePreview } from "./snapcompact-shape-preview";
|
|
40
42
|
import { getPreset } from "./status-line/presets";
|
|
41
43
|
|
|
42
44
|
/**
|
|
@@ -68,8 +70,6 @@ class TextInputSubmenu extends Container {
|
|
|
68
70
|
this.#input = new Input();
|
|
69
71
|
if (currentValue) {
|
|
70
72
|
this.#input.setValue(currentValue);
|
|
71
|
-
// Move cursor to end of pre-filled value (ctrl+e = cursorLineEnd).
|
|
72
|
-
this.#input.handleInput("\x05");
|
|
73
73
|
}
|
|
74
74
|
this.#input.onSubmit = value => {
|
|
75
75
|
this.onSubmit(value); // empty string clears the setting
|
|
@@ -100,6 +100,7 @@ class SelectSubmenu extends Container {
|
|
|
100
100
|
onCancel: () => void,
|
|
101
101
|
onSelectionChange?: (value: string) => void | Promise<void>,
|
|
102
102
|
private readonly getPreview?: () => string,
|
|
103
|
+
footer?: Component,
|
|
103
104
|
) {
|
|
104
105
|
super();
|
|
105
106
|
|
|
@@ -161,6 +162,13 @@ class SelectSubmenu extends Container {
|
|
|
161
162
|
// Hint
|
|
162
163
|
this.addChild(new Spacer(1));
|
|
163
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
|
+
}
|
|
164
172
|
}
|
|
165
173
|
|
|
166
174
|
#updatePreview(): void {
|
|
@@ -252,6 +260,12 @@ export interface SettingsRuntimeContext {
|
|
|
252
260
|
availableThemes: string[];
|
|
253
261
|
/** Working directory for plugins tab */
|
|
254
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;
|
|
255
269
|
}
|
|
256
270
|
|
|
257
271
|
/** Status line settings subset for preview */
|
|
@@ -291,6 +305,8 @@ export class SettingsSelectorComponent implements Component {
|
|
|
291
305
|
#currentTabId: SettingTab | "plugins" = "appearance";
|
|
292
306
|
#preSearchTabId: SettingTab | "plugins" = "appearance";
|
|
293
307
|
#searchQuery = "";
|
|
308
|
+
/** Single-line editor backing the search banner (cursor, word ops, paste). */
|
|
309
|
+
#searchInput = new Input();
|
|
294
310
|
#searchMatchCount = 0;
|
|
295
311
|
/** First matching item id per tab id, for Tab-key jumps while searching. */
|
|
296
312
|
#searchFirstMatch = new Map<string, string>();
|
|
@@ -354,7 +370,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
354
370
|
|
|
355
371
|
#footerHintText(): string {
|
|
356
372
|
if (this.#searchList) {
|
|
357
|
-
return "Enter
|
|
373
|
+
return "Enter to change · Tab to jump tabs · Esc to exit search";
|
|
358
374
|
}
|
|
359
375
|
if (this.#currentTabId === "plugins") {
|
|
360
376
|
return "Tab to switch tabs · Esc to close";
|
|
@@ -366,28 +382,17 @@ export class SettingsSelectorComponent implements Component {
|
|
|
366
382
|
return `Enter/Space to change · ${nav} · Type to search · Esc to close`;
|
|
367
383
|
}
|
|
368
384
|
|
|
369
|
-
/** Single-line search banner: accent icon,
|
|
385
|
+
/** Single-line search banner: accent icon, editable query with live cursor, right-aligned match count. */
|
|
370
386
|
#renderSearchBanner(width: number): string {
|
|
371
387
|
const icon = theme.symbol("icon.search");
|
|
372
388
|
const countText = this.#searchMatchCount === 1 ? "1 match" : `${this.#searchMatchCount} matches`;
|
|
373
389
|
const rightWidth = visibleWidth(countText) + 1; // trailing margin
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
let display = this.#searchQuery;
|
|
379
|
-
if (visibleWidth(display) > queryBudget) {
|
|
380
|
-
const chars = [...display];
|
|
381
|
-
while (chars.length > 1 && visibleWidth(chars.join("")) > queryBudget - 1) {
|
|
382
|
-
chars.shift();
|
|
383
|
-
}
|
|
384
|
-
display = `…${chars.join("")}`;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const left = ` ${theme.fg("accent", icon)} ${theme.bold(display)}${theme.fg("accent", "▌")}`;
|
|
390
|
+
const prefix = ` ${theme.fg("accent", icon)} `;
|
|
391
|
+
// The input pads itself to exactly this width and keeps the cursor in view.
|
|
392
|
+
const inputWidth = Math.max(4, width - visibleWidth(prefix) - rightWidth - 1);
|
|
393
|
+
const inputLine = this.#searchInput.render(inputWidth)[0] ?? "";
|
|
388
394
|
const count = theme.fg(this.#searchMatchCount > 0 ? "dim" : "warning", countText);
|
|
389
|
-
|
|
390
|
-
return truncateToWidth(`${left}${padding(gap)}${count} `, width);
|
|
395
|
+
return truncateToWidth(`${prefix}${theme.bold(inputLine)} ${count} `, width);
|
|
391
396
|
}
|
|
392
397
|
|
|
393
398
|
/**
|
|
@@ -511,6 +516,9 @@ export class SettingsSelectorComponent implements Component {
|
|
|
511
516
|
/** Swap the tab content for the global search result list. */
|
|
512
517
|
#startSearch(initialQuery: string): void {
|
|
513
518
|
this.#preSearchTabId = this.#currentTabId;
|
|
519
|
+
this.#searchInput = new Input();
|
|
520
|
+
this.#searchInput.prompt = "";
|
|
521
|
+
this.#searchInput.setValue(initialQuery);
|
|
514
522
|
const list = new SettingsList(
|
|
515
523
|
[],
|
|
516
524
|
10,
|
|
@@ -547,6 +555,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
547
555
|
|
|
548
556
|
const counts = new Map<SettingTab, number>();
|
|
549
557
|
const items: SettingItem[] = [];
|
|
558
|
+
const tabResults: { tab: SettingTab; matched: SettingItem[]; bestScore: number; order: number }[] = [];
|
|
550
559
|
this.#searchFirstMatch.clear();
|
|
551
560
|
let total = 0;
|
|
552
561
|
for (const tab of SETTING_TABS) {
|
|
@@ -555,24 +564,40 @@ export class SettingsSelectorComponent implements Component {
|
|
|
555
564
|
const item = this.#defToItem(def);
|
|
556
565
|
if (item) candidates.push(item);
|
|
557
566
|
}
|
|
558
|
-
const
|
|
567
|
+
const ranked = fuzzyRank(candidates, query, getSettingItemFilterText);
|
|
568
|
+
const matched = ranked.map(result => result.item);
|
|
559
569
|
counts.set(tab, matched.length);
|
|
560
570
|
if (matched.length === 0) continue;
|
|
561
571
|
total += matched.length;
|
|
562
|
-
|
|
572
|
+
tabResults.push({
|
|
573
|
+
tab,
|
|
574
|
+
matched,
|
|
575
|
+
bestScore: ranked[0]?.score ?? 0,
|
|
576
|
+
order: SETTING_TABS.indexOf(tab),
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
tabResults.sort((a, b) => a.bestScore - b.bestScore || a.order - b.order);
|
|
581
|
+
for (const result of tabResults) {
|
|
582
|
+
const meta = TAB_METADATA[result.tab];
|
|
563
583
|
items.push({
|
|
564
|
-
id: `__tab:${tab}`,
|
|
584
|
+
id: `__tab:${result.tab}`,
|
|
565
585
|
label: `${theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0])} ${meta.label}`,
|
|
566
586
|
currentValue: "",
|
|
567
587
|
heading: true,
|
|
568
588
|
});
|
|
569
|
-
this.#searchFirstMatch.set(tab, matched[0]
|
|
570
|
-
items.push(...matched);
|
|
589
|
+
this.#searchFirstMatch.set(result.tab, result.matched[0]?.id ?? "");
|
|
590
|
+
items.push(...result.matched);
|
|
571
591
|
}
|
|
572
592
|
|
|
573
593
|
this.#searchList.setItems(items);
|
|
574
594
|
this.#searchMatchCount = total;
|
|
575
|
-
this.#tabBar.setTabs(
|
|
595
|
+
this.#tabBar.setTabs(
|
|
596
|
+
this.#buildSearchTabs(
|
|
597
|
+
counts,
|
|
598
|
+
tabResults.map(result => result.tab),
|
|
599
|
+
),
|
|
600
|
+
);
|
|
576
601
|
this.#syncTabBarToSelection(this.#searchList.getSelectedItem());
|
|
577
602
|
}
|
|
578
603
|
|
|
@@ -597,20 +622,25 @@ export class SettingsSelectorComponent implements Component {
|
|
|
597
622
|
}
|
|
598
623
|
}
|
|
599
624
|
|
|
600
|
-
/** Matching tabs first (counts attached), the rest muted at the end. */
|
|
601
|
-
#buildSearchTabs(counts: Map<SettingTab, number
|
|
625
|
+
/** Matching tabs first (counts attached), ordered by best result score; the rest stay muted at the end. */
|
|
626
|
+
#buildSearchTabs(counts: Map<SettingTab, number>, matchedTabOrder: readonly SettingTab[]): Tab[] {
|
|
602
627
|
const matched: Tab[] = [];
|
|
603
628
|
const empty: Tab[] = [];
|
|
604
|
-
|
|
629
|
+
const matchedIds = new Set<SettingTab>(matchedTabOrder);
|
|
630
|
+
for (const id of matchedTabOrder) {
|
|
605
631
|
const meta = TAB_METADATA[id];
|
|
606
632
|
const icon = theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0]);
|
|
607
633
|
const count = counts.get(id) ?? 0;
|
|
608
634
|
if (count > 0) {
|
|
609
635
|
matched.push({ id, label: `${icon} ${meta.label} (${count})`, short: `${icon} ${count}` });
|
|
610
|
-
} else {
|
|
611
|
-
empty.push({ id, label: `${icon} ${meta.label}`, short: icon, muted: true });
|
|
612
636
|
}
|
|
613
637
|
}
|
|
638
|
+
for (const id of SETTING_TABS) {
|
|
639
|
+
if (matchedIds.has(id)) continue;
|
|
640
|
+
const meta = TAB_METADATA[id];
|
|
641
|
+
const icon = theme.symbol(meta.icon as Parameters<typeof theme.symbol>[0]);
|
|
642
|
+
empty.push({ id, label: `${icon} ${meta.label}`, short: icon, muted: true });
|
|
643
|
+
}
|
|
614
644
|
// Plugins hosts its own UI; it is not part of the schema-backed search.
|
|
615
645
|
empty.push({ id: "plugins", label: `${theme.icon.package} Plugins`, short: theme.icon.package, muted: true });
|
|
616
646
|
return [...matched, ...empty];
|
|
@@ -745,6 +775,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
745
775
|
// Preview handlers
|
|
746
776
|
let onPreview: ((value: string) => void | Promise<void>) | undefined;
|
|
747
777
|
let onPreviewCancel: (() => void) | undefined;
|
|
778
|
+
let footer: Component | undefined;
|
|
748
779
|
|
|
749
780
|
const activeThemeBeforePreview = getCurrentThemeName() ?? currentValue;
|
|
750
781
|
if (def.path === "theme.dark" || def.path === "theme.light") {
|
|
@@ -784,6 +815,14 @@ export class SettingsSelectorComponent implements Component {
|
|
|
784
815
|
const separator = settings.get("statusLine.separator");
|
|
785
816
|
this.callbacks.onStatusLinePreview?.({ separator });
|
|
786
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;
|
|
787
826
|
}
|
|
788
827
|
|
|
789
828
|
// Provide status line preview for theme selection
|
|
@@ -806,6 +845,7 @@ export class SettingsSelectorComponent implements Component {
|
|
|
806
845
|
},
|
|
807
846
|
onPreview,
|
|
808
847
|
getPreview,
|
|
848
|
+
footer,
|
|
809
849
|
);
|
|
810
850
|
}
|
|
811
851
|
|
|
@@ -1029,25 +1069,27 @@ export class SettingsSelectorComponent implements Component {
|
|
|
1029
1069
|
this.#endSearch(true);
|
|
1030
1070
|
return;
|
|
1031
1071
|
}
|
|
1032
|
-
if (
|
|
1033
|
-
this.#setSearchQuery([...this.#searchQuery].slice(0, -1).join(""));
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
if (
|
|
1037
|
-
matchesKey(data, "tab") ||
|
|
1038
|
-
matchesKey(data, "shift+tab") ||
|
|
1039
|
-
matchesKey(data, "left") ||
|
|
1040
|
-
matchesKey(data, "right")
|
|
1041
|
-
) {
|
|
1072
|
+
if (matchesKey(data, "tab") || matchesKey(data, "shift+tab")) {
|
|
1042
1073
|
// Jump between tabs that have matches (muted tabs are skipped).
|
|
1043
1074
|
this.#tabBar.handleInput(data);
|
|
1044
1075
|
return;
|
|
1045
1076
|
}
|
|
1046
|
-
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1077
|
+
// Selection, paging, and activation stay with the result list.
|
|
1078
|
+
if (
|
|
1079
|
+
kb.matches(data, "tui.select.up") ||
|
|
1080
|
+
kb.matches(data, "tui.select.down") ||
|
|
1081
|
+
kb.matches(data, "tui.select.pageUp") ||
|
|
1082
|
+
kb.matches(data, "tui.select.pageDown") ||
|
|
1083
|
+
kb.matches(data, "tui.select.confirm") ||
|
|
1084
|
+
data === "\n"
|
|
1085
|
+
) {
|
|
1086
|
+
list.handleInput(data);
|
|
1049
1087
|
return;
|
|
1050
1088
|
}
|
|
1051
|
-
|
|
1089
|
+
// Everything else edits the query like a regular single-line editor:
|
|
1090
|
+
// cursor movement, word ops, kill ring, undo, paste.
|
|
1091
|
+
this.#searchInput.handleInput(data);
|
|
1092
|
+
const value = this.#searchInput.getValue();
|
|
1093
|
+
if (value !== this.#searchQuery) this.#setSearchQuery(value);
|
|
1052
1094
|
}
|
|
1053
1095
|
}
|
|
@@ -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
|
+
}
|
|
@@ -589,6 +589,32 @@ export class ToolExecutionComponent extends Container {
|
|
|
589
589
|
return (this.#result.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
/**
|
|
593
|
+
* Whether this still-live block's settled rows may enter native scrollback
|
|
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
|
|
607
|
+
* polls are removed wholesale by the next poll and must never commit.
|
|
608
|
+
*/
|
|
609
|
+
isTranscriptBlockCommitStable(): boolean {
|
|
610
|
+
if (this.#displaceable) return false;
|
|
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;
|
|
616
|
+
}
|
|
617
|
+
|
|
592
618
|
/**
|
|
593
619
|
* Mark the tool terminal even though no result arrived (the turn aborted or
|
|
594
620
|
* abandoned it) and stop animating, so it can freeze and stops pinning the
|
|
@@ -63,6 +63,19 @@ interface FinalizableBlock {
|
|
|
63
63
|
* never mutate post-finalize simply omit the method.
|
|
64
64
|
*/
|
|
65
65
|
getTranscriptBlockVersion?(): number;
|
|
66
|
+
/**
|
|
67
|
+
* Whether a still-live block's visually settled leading rows are durable —
|
|
68
|
+
* guaranteed to survive the block's remaining transitions (finalize,
|
|
69
|
+
* displacement) byte-stable — and may therefore be promoted as commit-safe
|
|
70
|
+
* by {@link deriveLiveCommitState}. Blocks whose pending render is
|
|
71
|
+
* provisional (a tool call's tail-window streaming preview, replaced
|
|
72
|
+
* wholesale by the result render) return `false`: committing such rows
|
|
73
|
+
* strands a stale copy in immutable terminal history the moment the real
|
|
74
|
+
* content re-lays-out the block (the engine audit recommits below it —
|
|
75
|
+
* "duplication, never loss"). Absent = `true`, the default for blocks
|
|
76
|
+
* whose live rows persist (a streaming assistant message).
|
|
77
|
+
*/
|
|
78
|
+
isTranscriptBlockCommitStable?(): boolean;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
function isBlockFinalized(child: Component): boolean {
|
|
@@ -75,6 +88,11 @@ function getBlockVersion(child: Component): number | undefined {
|
|
|
75
88
|
return fn ? fn.call(child) : undefined;
|
|
76
89
|
}
|
|
77
90
|
|
|
91
|
+
function isBlockCommitStable(child: Component): boolean {
|
|
92
|
+
const fn = (child as Component & FinalizableBlock).isTranscriptBlockCommitStable;
|
|
93
|
+
return fn ? fn.call(child) : true;
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
// A "plain blank" row is empty or whitespace-only with no ANSI bytes. It marks
|
|
79
97
|
// separation padding (a `Spacer`, or a no-background `paddingY` row) as opposed
|
|
80
98
|
// to a background-colored padding row, whose escape sequences contain `\S` and
|
|
@@ -598,7 +616,11 @@ export class TranscriptContainer
|
|
|
598
616
|
previous.generation === this.#generation);
|
|
599
617
|
const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
|
|
600
618
|
let liveCommitState: LiveCommitState | undefined;
|
|
601
|
-
|
|
619
|
+
// Provisional live renders (commit-unstable blocks) never feed the
|
|
620
|
+
// promotion machinery: their settled-looking rows are replaced
|
|
621
|
+
// wholesale on finalize, so offering them would commit a stale
|
|
622
|
+
// preview the result render can only duplicate, never erase.
|
|
623
|
+
if (i >= liveStartIndex && !finalized && isBlockCommitStable(child)) {
|
|
602
624
|
liveCommitState = deriveLiveCommitState(previousSnapshot, contribution, width, this.#generation);
|
|
603
625
|
}
|
|
604
626
|
// Cache the latest contribution as the next frame's diff input.
|
|
@@ -456,7 +456,7 @@ export class CommandController {
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
handleContextCommand(): void {
|
|
459
|
-
const breakdown = computeContextBreakdown(this.ctx.session);
|
|
459
|
+
const breakdown = computeContextBreakdown(this.ctx.session, { snapcompactSavings: true });
|
|
460
460
|
if (breakdown.contextWindow <= 0) {
|
|
461
461
|
this.ctx.showWarning("Context usage is unavailable: no model is selected for this session.");
|
|
462
462
|
return;
|
|
@@ -1528,6 +1528,29 @@ function renderUsageReports(
|
|
|
1528
1528
|
lines.push(` ${uiTheme.fg("accent", "in use by this session:")} ${activeAccountLabel}`);
|
|
1529
1529
|
}
|
|
1530
1530
|
|
|
1531
|
+
const resetAccountLines: string[] = [];
|
|
1532
|
+
for (const report of providerReports) {
|
|
1533
|
+
const count = report.resetCredits?.availableCount ?? 0;
|
|
1534
|
+
if (count <= 0) continue;
|
|
1535
|
+
const label =
|
|
1536
|
+
(report.metadata?.email as string | undefined) ??
|
|
1537
|
+
(report.metadata?.accountId as string | undefined) ??
|
|
1538
|
+
"account";
|
|
1539
|
+
const isActive =
|
|
1540
|
+
!!activeAccount &&
|
|
1541
|
+
((!!activeAccount.accountId && activeAccount.accountId === report.metadata?.accountId) ||
|
|
1542
|
+
(!!activeAccount.email && activeAccount.email === report.metadata?.email));
|
|
1543
|
+
resetAccountLines.push(
|
|
1544
|
+
` • ${label}: ${count} saved reset${count === 1 ? "" : "s"}${isActive ? " (active)" : ""}`,
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
if (resetAccountLines.length > 0) {
|
|
1548
|
+
lines.push(
|
|
1549
|
+
` ${uiTheme.fg("accent", "Saved rate-limit resets")} ${uiTheme.fg("dim", "(/usage reset to spend)")}`,
|
|
1550
|
+
);
|
|
1551
|
+
for (const line of resetAccountLines) lines.push(uiTheme.fg("dim", line));
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1531
1554
|
const renderableGroups = Array.from(limitGroups.values()).map(group => {
|
|
1532
1555
|
const entries = group.limits.map((limit, index) => ({
|
|
1533
1556
|
limit,
|
|
@@ -4,6 +4,7 @@ import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
|
|
|
4
4
|
import { $env, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { getRoleInfo } from "../../config/model-roles";
|
|
6
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
|
+
import { AssistantMessageComponent } from "../../modes/components/assistant-message";
|
|
7
8
|
import { renderSegmentTrack } from "../../modes/components/segment-track";
|
|
8
9
|
import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress";
|
|
9
10
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
@@ -1039,18 +1040,19 @@ export class InputController {
|
|
|
1039
1040
|
|
|
1040
1041
|
toggleThinkingBlockVisibility(): void {
|
|
1041
1042
|
this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
|
|
1042
|
-
settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
|
|
1043
|
+
this.ctx.settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
|
|
1043
1044
|
this.ctx.session.agent.hideThinkingSummary = this.ctx.hideThinkingBlock;
|
|
1044
1045
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1046
|
+
for (const child of this.ctx.chatContainer.children) {
|
|
1047
|
+
if (child instanceof AssistantMessageComponent) {
|
|
1048
|
+
child.setHideThinkingBlock(this.ctx.hideThinkingBlock);
|
|
1049
|
+
child.invalidate();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1048
1052
|
|
|
1049
|
-
// If streaming, re-add the streaming component with updated visibility and re-render
|
|
1050
1053
|
if (this.ctx.streamingComponent && this.ctx.streamingMessage) {
|
|
1051
1054
|
this.ctx.streamingComponent.setHideThinkingBlock(this.ctx.hideThinkingBlock);
|
|
1052
1055
|
this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
|
|
1053
|
-
this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
|
|
1054
1056
|
}
|
|
1055
1057
|
|
|
1056
1058
|
this.ctx.showStatus(`Thinking blocks: ${this.ctx.hideThinkingBlock ? "hidden" : "visible"}`);
|