@oh-my-pi/pi-tui 18.8.4 → 18.8.6
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 +23 -0
- package/dist/types/chat/user-message.d.ts +1 -1
- package/dist/types/overlays/model-browser.d.ts +13 -0
- package/dist/types/overlays/model-hub.d.ts +6 -0
- package/dist/types/overlays/session-selector.d.ts +5 -1
- package/dist/types/overlays/usage-dashboard.d.ts +5 -3
- package/dist/types/prompt/custom-editor.d.ts +2 -2
- package/dist/types/prompt/image-source.d.ts +2 -1
- package/dist/types/theme/theme.d.ts +1 -1
- package/dist/types/theme/tui-adapters.d.ts +3 -0
- package/package.json +13 -9
- package/src/chat/assistant-message.ts +2 -2
- package/src/chat/user-message.ts +1 -1
- package/src/components/editor.ts +6 -3
- package/src/components/markdown.ts +182 -8
- package/src/overlays/model-browser.ts +26 -1
- package/src/overlays/model-hub.ts +172 -21
- package/src/overlays/session-selector.ts +11 -3
- package/src/overlays/usage-dashboard.ts +18 -7
- package/src/prompt/custom-editor.ts +3 -4
- package/src/prompt/image-source.ts +2 -1
- package/src/prompt/tips.txt +2 -1
- package/src/prompt/welcome.ts +46 -9
- package/src/theme/theme.ts +1 -0
- package/src/theme/tui-adapters.ts +68 -0
|
@@ -170,6 +170,12 @@ export interface ModelHubCallbacks {
|
|
|
170
170
|
onSwitchPreset?: (name: string) => void | Promise<void>;
|
|
171
171
|
/** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
|
|
172
172
|
onCycleOrderChange?: (order: string[]) => void;
|
|
173
|
+
/**
|
|
174
|
+
* Persist typed text as `model`'s own compaction point: a token count (`90000`,
|
|
175
|
+
* `90k`, `1M`), a percentage (`80%`), or empty to reset. Returns an error
|
|
176
|
+
* message for input it rejects, which keeps the field open.
|
|
177
|
+
*/
|
|
178
|
+
onCompactionPointChange?: (model: Model, input: string) => string | undefined;
|
|
173
179
|
onCancel: () => void;
|
|
174
180
|
}
|
|
175
181
|
|
|
@@ -224,6 +230,14 @@ type StripState =
|
|
|
224
230
|
kind: "name";
|
|
225
231
|
purpose: "role" | "preset";
|
|
226
232
|
input: Input;
|
|
233
|
+
}
|
|
234
|
+
| {
|
|
235
|
+
/** Footer text input setting `model`'s compaction point; `error` is the last rejection. */
|
|
236
|
+
kind: "name";
|
|
237
|
+
purpose: "compaction";
|
|
238
|
+
model: Model;
|
|
239
|
+
input: Input;
|
|
240
|
+
error?: string;
|
|
227
241
|
};
|
|
228
242
|
|
|
229
243
|
/** A Roles-view command; keys and the picker's action bar both run {@link ModelHubComponent}'s `#runRolesAction`. */
|
|
@@ -236,6 +250,7 @@ type RolesAction =
|
|
|
236
250
|
| "later"
|
|
237
251
|
| "new"
|
|
238
252
|
| "thinking"
|
|
253
|
+
| "compaction"
|
|
239
254
|
| "save"
|
|
240
255
|
| "nextPreset"
|
|
241
256
|
| "prevPreset";
|
|
@@ -249,6 +264,7 @@ const ROLES_ACTION_KEYS: Record<string, RolesAction> = {
|
|
|
249
264
|
"]": "later",
|
|
250
265
|
n: "new",
|
|
251
266
|
t: "thinking",
|
|
267
|
+
k: "compaction",
|
|
252
268
|
s: "save",
|
|
253
269
|
// Letter twins of ctrl+←/→, which macOS reserves for switching Spaces.
|
|
254
270
|
p: "nextPreset",
|
|
@@ -286,6 +302,8 @@ function providerInitials(providerId: string): string {
|
|
|
286
302
|
|
|
287
303
|
const PROVIDER_REFRESH_DEBOUNCE_MS = 120;
|
|
288
304
|
const RECENT_LIMIT = 15;
|
|
305
|
+
/** Accepted compaction point input, shown beside the field. */
|
|
306
|
+
const COMPACTION_INPUT_HINT = "90000 · 90k · 1m · 80% · empty resets";
|
|
289
307
|
const MODEL_KIND_TABS: ReadonlyArray<"all" | ModelKind> = ["all", ...MODEL_KINDS];
|
|
290
308
|
const ROLE_TABS = ["all", "chat", "kind"] as const;
|
|
291
309
|
type RoleTab = (typeof ROLE_TABS)[number];
|
|
@@ -436,6 +454,7 @@ export class ModelHubComponent implements Component {
|
|
|
436
454
|
row: RolesRow | undefined;
|
|
437
455
|
roles: RoleAssignments;
|
|
438
456
|
rows: readonly RolesRow[];
|
|
457
|
+
revision: number;
|
|
439
458
|
children: readonly NativeChild[];
|
|
440
459
|
}
|
|
441
460
|
| undefined;
|
|
@@ -1758,10 +1777,39 @@ export class ModelHubComponent implements Component {
|
|
|
1758
1777
|
this.#strip = { kind: "name", purpose, input: new Input() };
|
|
1759
1778
|
}
|
|
1760
1779
|
|
|
1761
|
-
/**
|
|
1780
|
+
/** The model a Roles row resolves to (assigned role or resolvable fallback), whose compaction point `k` edits. */
|
|
1781
|
+
#roleRowModel(): Model | undefined {
|
|
1782
|
+
const row = this.#rolesRows[this.#roleIndex];
|
|
1783
|
+
if (row?.kind === "role") return this.#roles[row.role]?.model;
|
|
1784
|
+
if (row?.kind === "fallback") return this.#resolveFallbackEntry(row.role, row.chainIndex)?.item.model;
|
|
1785
|
+
return undefined;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/** Open the footer input for the Roles row's model compaction point, prefilled with its own entry. */
|
|
1789
|
+
#openCompactionStrip(): void {
|
|
1790
|
+
if (!this.#callbacks.onCompactionPointChange) return;
|
|
1791
|
+
const model = this.#roleRowModel();
|
|
1792
|
+
if (!model) return;
|
|
1793
|
+
const input = new Input();
|
|
1794
|
+
input.setValue(this.#settings.compactionPointFor?.(model)?.draft ?? "");
|
|
1795
|
+
this.#strip = { kind: "name", purpose: "compaction", model, input };
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/** Validate and commit the name strip: a new role, a preset name, or a compaction point. */
|
|
1762
1799
|
#submitNameStrip(): void {
|
|
1763
1800
|
const strip = this.#strip;
|
|
1764
1801
|
if (strip?.kind !== "name") return;
|
|
1802
|
+
if (strip.purpose === "compaction") {
|
|
1803
|
+
const error = this.#callbacks.onCompactionPointChange?.(strip.model, strip.input.getValue());
|
|
1804
|
+
if (error !== undefined) {
|
|
1805
|
+
strip.error = error;
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
this.#strip = null;
|
|
1809
|
+
this.#frame.chipRanges = [];
|
|
1810
|
+
this.#refreshAfterMutation();
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1765
1813
|
if (strip.purpose === "preset") {
|
|
1766
1814
|
this.#submitPresetName();
|
|
1767
1815
|
return;
|
|
@@ -1943,7 +1991,13 @@ export class ModelHubComponent implements Component {
|
|
|
1943
1991
|
|
|
1944
1992
|
if (rolesView) {
|
|
1945
1993
|
const printable = extractPrintableText(data);
|
|
1946
|
-
|
|
1994
|
+
// Typing searches the catalog from the sidebar, and from the rows
|
|
1995
|
+
// while a query is live (the picker shows its search field only
|
|
1996
|
+
// then); otherwise the rows' letter commands own printable keys.
|
|
1997
|
+
const typed = printable !== undefined && printable.trim().length > 0;
|
|
1998
|
+
const editsQuery =
|
|
1999
|
+
this.#browser.query.length > 0 && (printable !== undefined || matchesKey(data, "backspace"));
|
|
2000
|
+
if ((this.#focus === "scope" && typed) || editsQuery) {
|
|
1947
2001
|
this.#setActiveEntry("all");
|
|
1948
2002
|
this.#focus = "list";
|
|
1949
2003
|
this.#browser.handleInput(data);
|
|
@@ -2020,6 +2074,7 @@ export class ModelHubComponent implements Component {
|
|
|
2020
2074
|
return;
|
|
2021
2075
|
}
|
|
2022
2076
|
strip.input.handleInput(data);
|
|
2077
|
+
if (strip.purpose === "compaction") strip.error = undefined;
|
|
2023
2078
|
return;
|
|
2024
2079
|
}
|
|
2025
2080
|
if (moveStripSelection(strip, data)) return;
|
|
@@ -2183,6 +2238,9 @@ export class ModelHubComponent implements Component {
|
|
|
2183
2238
|
case "prevPreset":
|
|
2184
2239
|
this.#switchPreset(action === "nextPreset" ? 1 : -1);
|
|
2185
2240
|
return;
|
|
2241
|
+
case "compaction":
|
|
2242
|
+
this.#openCompactionStrip();
|
|
2243
|
+
return;
|
|
2186
2244
|
case "thinking":
|
|
2187
2245
|
if (role) {
|
|
2188
2246
|
const target = this.#roleThinkingTarget(role);
|
|
@@ -2382,9 +2440,14 @@ export class ModelHubComponent implements Component {
|
|
|
2382
2440
|
Math.max(0, active),
|
|
2383
2441
|
);
|
|
2384
2442
|
const { names, active: activePreset } = this.#presets();
|
|
2443
|
+
// p/P type into a live query instead of switching presets.
|
|
2444
|
+
const presetKeys =
|
|
2445
|
+
this.#browser.query.length > 0
|
|
2446
|
+
? formatKeyHints(["ctrl+left", "ctrl+right"])
|
|
2447
|
+
: `${formatKeyHints(["ctrl+left", "ctrl+right"])} · ${formatKeyHints(["p", "shift+p"])}`;
|
|
2385
2448
|
const preset =
|
|
2386
2449
|
names.length > 0
|
|
2387
|
-
? ` ${theme.fg("dim", "Preset:")} ${activePreset ? theme.fg("accent", activePreset) : theme.fg("muted", "custom")} ${theme.fg("dim",
|
|
2450
|
+
? ` ${theme.fg("dim", "Preset:")} ${activePreset ? theme.fg("accent", activePreset) : theme.fg("muted", "custom")} ${theme.fg("dim", presetKeys)}`
|
|
2388
2451
|
: "";
|
|
2389
2452
|
return truncateToWidth(
|
|
2390
2453
|
` ${theme.fg("dim", "Roles:")} ${track} ${theme.fg("dim", formatKeyHints(["alt+left", "alt+right"]))}${preset}`,
|
|
@@ -2658,6 +2721,7 @@ export class ModelHubComponent implements Component {
|
|
|
2658
2721
|
const strip = this.#strip;
|
|
2659
2722
|
if (strip) {
|
|
2660
2723
|
if (strip.kind === "name") {
|
|
2724
|
+
if (strip.purpose === "compaction") return `${enter} set compaction point · ${cancel} cancel`;
|
|
2661
2725
|
if (strip.purpose === "preset") {
|
|
2662
2726
|
return `${enter} save preset · ${cancel} cancel`;
|
|
2663
2727
|
}
|
|
@@ -2688,14 +2752,22 @@ export class ModelHubComponent implements Component {
|
|
|
2688
2752
|
this.#presets().names.length > 0 ? ` · ${formatKeyHints(["ctrl+left", "ctrl+right"])} preset` : "";
|
|
2689
2753
|
return `${upDown} providers · ${enterRight} roles · ${altLeftRight} tabs${presets} · ${cancel} close`;
|
|
2690
2754
|
}
|
|
2755
|
+
// A live query takes printable keys and backspace, so the letter commands rest.
|
|
2756
|
+
if (this.#browser.query.length > 0) {
|
|
2757
|
+
return `${upDown} rows · ${enter} pick · type to search · ${left} providers · ${cancel} close`;
|
|
2758
|
+
}
|
|
2691
2759
|
const row = this.#rolesRows[this.#roleIndex];
|
|
2760
|
+
const compaction =
|
|
2761
|
+
this.#callbacks.onCompactionPointChange && this.#roleRowModel()
|
|
2762
|
+
? ` · ${formatKeyHint("k")} compaction limit`
|
|
2763
|
+
: "";
|
|
2692
2764
|
if (row?.kind === "fallback") {
|
|
2693
2765
|
// Advertise `t` only when the entry resolves: wildcards always
|
|
2694
2766
|
// inherit and unknown models have no ladder to offer, so the
|
|
2695
2767
|
// action would be inert there.
|
|
2696
2768
|
const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
|
|
2697
2769
|
const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
|
|
2698
|
-
return `${upDown} rows · ${enter} replace · ${formatKeyHint("f")} add another · ${formatKeyHint("x")} remove${thinking} · [/] reorder · ${left} providers`;
|
|
2770
|
+
return `${upDown} rows · ${enter} replace · ${formatKeyHint("f")} add another · ${formatKeyHint("x")} remove${thinking}${compaction} · [/] reorder · ${left} providers`;
|
|
2699
2771
|
}
|
|
2700
2772
|
if (row?.kind === "chainKey") {
|
|
2701
2773
|
return `${upDown} rows · ${formatKeyHints(["enter", "f"])} add fallback · ${formatKeyHint("x")} clear chain · ${left} providers`;
|
|
@@ -2709,7 +2781,7 @@ export class ModelHubComponent implements Component {
|
|
|
2709
2781
|
const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
|
|
2710
2782
|
const savePreset = this.#callbacks.onSavePreset ? ` · ${formatKeyHint("s")} save preset` : "";
|
|
2711
2783
|
const switchPreset = this.#presets().names.length > 0 ? ` · ${formatKeyHints(["p", "shift+p"])} preset` : "";
|
|
2712
|
-
return `${upDown} rows · ${enter} pick · ${formatKeyHint("f")} fallback · ${formatKeyHint("x")} clear${thinking} · ${formatKeyHint("c")} cycle · [/] reorder · ${formatKeyHint("n")} new${savePreset}${switchPreset}`;
|
|
2784
|
+
return `${upDown} rows · ${enter} pick · ${formatKeyHint("f")} fallback · ${formatKeyHint("x")} clear${thinking}${compaction} · ${formatKeyHint("c")} cycle · [/] reorder · ${formatKeyHint("n")} new${savePreset}${switchPreset}`;
|
|
2713
2785
|
}
|
|
2714
2786
|
if (entry.kind === "provider" && entry.locked) {
|
|
2715
2787
|
return entry.oauth
|
|
@@ -2734,12 +2806,22 @@ export class ModelHubComponent implements Component {
|
|
|
2734
2806
|
|
|
2735
2807
|
#renderStrip(width: number, strip: StripState): string {
|
|
2736
2808
|
if (strip.kind === "name") {
|
|
2737
|
-
const
|
|
2738
|
-
|
|
2809
|
+
const labelText =
|
|
2810
|
+
strip.purpose === "compaction"
|
|
2811
|
+
? `Compact ${strip.model.id} at:`
|
|
2812
|
+
: strip.purpose === "preset"
|
|
2813
|
+
? "Preset name:"
|
|
2814
|
+
: "New role name:";
|
|
2739
2815
|
const label = theme.fg("accent", labelText);
|
|
2740
2816
|
const inputWidth = Math.max(8, Math.min(32, width - visibleWidth(labelText) - 24));
|
|
2741
2817
|
const inputLine = strip.input.render(inputWidth)[0] ?? "";
|
|
2742
|
-
|
|
2818
|
+
const hint =
|
|
2819
|
+
strip.purpose !== "compaction"
|
|
2820
|
+
? theme.fg("dim", "(letters, digits, - and _)")
|
|
2821
|
+
: strip.error
|
|
2822
|
+
? theme.fg("error", strip.error)
|
|
2823
|
+
: theme.fg("dim", COMPACTION_INPUT_HINT);
|
|
2824
|
+
return truncateToWidth(`${label} ${inputLine} ${hint}`, width);
|
|
2743
2825
|
}
|
|
2744
2826
|
|
|
2745
2827
|
const prefix =
|
|
@@ -3161,7 +3243,9 @@ export class ModelHubComponent implements Component {
|
|
|
3161
3243
|
size: "lg",
|
|
3162
3244
|
layout: "rows",
|
|
3163
3245
|
preview: "side",
|
|
3164
|
-
|
|
3246
|
+
// The Roles rows' letter commands own printable keys until a query
|
|
3247
|
+
// is live, so the sheet offers no search field to type into.
|
|
3248
|
+
query: rolesView && this.#browser.query.length === 0 ? null : this.#browser.query,
|
|
3165
3249
|
cursor: this.#browser.cursor,
|
|
3166
3250
|
placeholder: "Search models…",
|
|
3167
3251
|
scopes: this.#pickerScopes(),
|
|
@@ -3390,12 +3474,14 @@ export class ModelHubComponent implements Component {
|
|
|
3390
3474
|
if (strip) {
|
|
3391
3475
|
const apply =
|
|
3392
3476
|
strip.kind === "name"
|
|
3393
|
-
?
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3477
|
+
? strip.purpose === "compaction"
|
|
3478
|
+
? pickerAction("compactionPoint", "Set compaction point", "enter", { primary: true })
|
|
3479
|
+
: pickerAction(
|
|
3480
|
+
strip.purpose === "preset" ? "presetName" : "roleName",
|
|
3481
|
+
strip.purpose === "preset" ? "Save preset" : "Create role",
|
|
3482
|
+
"enter",
|
|
3483
|
+
{ primary: true },
|
|
3484
|
+
)
|
|
3399
3485
|
: pickerAction(
|
|
3400
3486
|
"stripApply",
|
|
3401
3487
|
strip.kind === "thinking" ? "Apply" : strip.kind === "scope" ? "Save to scope" : "Assign / clear",
|
|
@@ -3417,9 +3503,20 @@ export class ModelHubComponent implements Component {
|
|
|
3417
3503
|
}
|
|
3418
3504
|
if (rolesView) {
|
|
3419
3505
|
const row = this.#rolesRows[this.#roleIndex];
|
|
3506
|
+
// A live query takes the letter keys: those buttons keep working but lose their keycaps.
|
|
3507
|
+
const searching = this.#browser.query.length > 0;
|
|
3420
3508
|
const roleAction = (action: RolesAction, label: string, key: string, primary = false) =>
|
|
3421
|
-
pickerAction(
|
|
3509
|
+
pickerAction(
|
|
3510
|
+
`roles:${action}`,
|
|
3511
|
+
label,
|
|
3512
|
+
searching && key !== "enter" ? undefined : key,
|
|
3513
|
+
primary ? { primary: true } : undefined,
|
|
3514
|
+
);
|
|
3422
3515
|
const actions: (TspPickerAction | undefined)[] = [];
|
|
3516
|
+
const compaction =
|
|
3517
|
+
this.#callbacks.onCompactionPointChange && this.#roleRowModel()
|
|
3518
|
+
? roleAction("compaction", "Compaction limit", "k")
|
|
3519
|
+
: undefined;
|
|
3423
3520
|
switch (row?.kind) {
|
|
3424
3521
|
case "role": {
|
|
3425
3522
|
const assigned = this.#roles[row.role];
|
|
@@ -3428,6 +3525,7 @@ export class ModelHubComponent implements Component {
|
|
|
3428
3525
|
roleAction("fallback", "Add fallback", "f"),
|
|
3429
3526
|
assigned && !assigned.autoSelected ? roleAction("clear", "Clear", "x") : undefined,
|
|
3430
3527
|
this.#roleThinkingTarget(row.role) ? roleAction("thinking", "Thinking", "t") : undefined,
|
|
3528
|
+
compaction,
|
|
3431
3529
|
roleAction("cycle", this.#cycleOrder().includes(row.role) ? "Leave cycle" : "Add to cycle", "c"),
|
|
3432
3530
|
roleAction("new", "New role", "n"),
|
|
3433
3531
|
this.#callbacks.onSavePreset ? roleAction("save", "Save preset", "s") : undefined,
|
|
@@ -3443,6 +3541,7 @@ export class ModelHubComponent implements Component {
|
|
|
3443
3541
|
this.#resolveFallbackEntry(row.role, row.chainIndex)
|
|
3444
3542
|
? roleAction("thinking", "Thinking", "t")
|
|
3445
3543
|
: undefined,
|
|
3544
|
+
compaction,
|
|
3446
3545
|
roleAction("earlier", "Earlier", "["),
|
|
3447
3546
|
roleAction("later", "Later", "]"),
|
|
3448
3547
|
);
|
|
@@ -3479,6 +3578,19 @@ export class ModelHubComponent implements Component {
|
|
|
3479
3578
|
/** The open strip as the picker's chip strip (role assignment, save scope, thinking level, new role name). */
|
|
3480
3579
|
#pickerStrip(strip: StripState): NonNullable<TspPickerProps["strip"]> {
|
|
3481
3580
|
if (strip.kind === "name") {
|
|
3581
|
+
if (strip.purpose === "compaction") {
|
|
3582
|
+
return {
|
|
3583
|
+
label: [
|
|
3584
|
+
span("Compact ", "muted"),
|
|
3585
|
+
span(strip.model.id, "mono"),
|
|
3586
|
+
span(" at ", "muted"),
|
|
3587
|
+
span(strip.input.getValue(), "mono"),
|
|
3588
|
+
span("▏", "accent"),
|
|
3589
|
+
span(` ${strip.error ?? COMPACTION_INPUT_HINT}`, strip.error ? "error" : "dim"),
|
|
3590
|
+
],
|
|
3591
|
+
items: [],
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3482
3594
|
return {
|
|
3483
3595
|
label: [
|
|
3484
3596
|
span(strip.purpose === "preset" ? "Preset name " : "New role name ", "muted"),
|
|
@@ -3618,7 +3730,14 @@ export class ModelHubComponent implements Component {
|
|
|
3618
3730
|
/** The selected Roles-view row's preview: its model's facts and its fallback chain. */
|
|
3619
3731
|
#rolePreview(row: RolesRow | undefined): readonly NativeChild[] {
|
|
3620
3732
|
const memo = this.#pickerRolePreview;
|
|
3621
|
-
|
|
3733
|
+
const revision = this.#settings.revision;
|
|
3734
|
+
if (
|
|
3735
|
+
memo !== undefined &&
|
|
3736
|
+
memo.row === row &&
|
|
3737
|
+
memo.roles === this.#roles &&
|
|
3738
|
+
memo.rows === this.#rolesRows &&
|
|
3739
|
+
memo.revision === revision
|
|
3740
|
+
) {
|
|
3622
3741
|
return memo.children;
|
|
3623
3742
|
}
|
|
3624
3743
|
const children: NativeChild[] = [];
|
|
@@ -3692,7 +3811,7 @@ export class ModelHubComponent implements Component {
|
|
|
3692
3811
|
);
|
|
3693
3812
|
break;
|
|
3694
3813
|
}
|
|
3695
|
-
this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, children };
|
|
3814
|
+
this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, revision, children };
|
|
3696
3815
|
return children;
|
|
3697
3816
|
}
|
|
3698
3817
|
|
|
@@ -3792,6 +3911,9 @@ export class ModelHubComponent implements Component {
|
|
|
3792
3911
|
case "presetName":
|
|
3793
3912
|
this.#submitPresetName();
|
|
3794
3913
|
return;
|
|
3914
|
+
case "compactionPoint":
|
|
3915
|
+
this.#submitNameStrip();
|
|
3916
|
+
return;
|
|
3795
3917
|
case "cancel":
|
|
3796
3918
|
if (this.#strip) this.#closeStrip();
|
|
3797
3919
|
return;
|
|
@@ -3842,6 +3964,18 @@ export class ModelHubComponent implements Component {
|
|
|
3842
3964
|
const strip = this.#strip;
|
|
3843
3965
|
if (!strip) return undefined;
|
|
3844
3966
|
if (strip.kind === "name") {
|
|
3967
|
+
if (strip.purpose === "compaction") {
|
|
3968
|
+
return node(
|
|
3969
|
+
"row",
|
|
3970
|
+
{ gap: "sm", align: "center" },
|
|
3971
|
+
[
|
|
3972
|
+
text([span(`Compact ${strip.model.id} at:`, "accent")]),
|
|
3973
|
+
col([strip.input], { grow: 1 }),
|
|
3974
|
+
text([span(strip.error ?? COMPACTION_INPUT_HINT, strip.error ? "error" : "dim")]),
|
|
3975
|
+
],
|
|
3976
|
+
"compactionPoint",
|
|
3977
|
+
);
|
|
3978
|
+
}
|
|
3845
3979
|
const preset = strip.purpose === "preset";
|
|
3846
3980
|
return node(
|
|
3847
3981
|
"row",
|
|
@@ -3895,9 +4029,11 @@ export class ModelHubComponent implements Component {
|
|
|
3895
4029
|
if (strip) {
|
|
3896
4030
|
switch (strip.kind) {
|
|
3897
4031
|
case "name":
|
|
3898
|
-
return strip.purpose === "
|
|
3899
|
-
? [keys("
|
|
3900
|
-
:
|
|
4032
|
+
return strip.purpose === "compaction"
|
|
4033
|
+
? [keys("set compaction point", "enter"), cancel("cancel")]
|
|
4034
|
+
: strip.purpose === "preset"
|
|
4035
|
+
? [keys("save preset", "enter"), cancel("cancel")]
|
|
4036
|
+
: [keys("create + pick model", "enter"), cancel("cancel")];
|
|
3901
4037
|
case "role":
|
|
3902
4038
|
return [keys("choose", "left", "right"), keys("assign/clear", "enter"), cancel("cancel")];
|
|
3903
4039
|
case "scope":
|
|
@@ -3930,7 +4066,20 @@ export class ModelHubComponent implements Component {
|
|
|
3930
4066
|
cancel("close"),
|
|
3931
4067
|
];
|
|
3932
4068
|
}
|
|
4069
|
+
// A live query takes printable keys and backspace, so the letter commands rest.
|
|
4070
|
+
if (this.#browser.query.length > 0) {
|
|
4071
|
+
return [
|
|
4072
|
+
upDown("rows"),
|
|
4073
|
+
keys("pick", "enter"),
|
|
4074
|
+
search,
|
|
4075
|
+
keys("providers", "left"),
|
|
4076
|
+
presetHint,
|
|
4077
|
+
cancel("close"),
|
|
4078
|
+
];
|
|
4079
|
+
}
|
|
3933
4080
|
const row = this.#rolesRows[this.#roleIndex];
|
|
4081
|
+
const compaction =
|
|
4082
|
+
this.#callbacks.onCompactionPointChange && this.#roleRowModel() ? keys("compaction limit", "k") : undefined;
|
|
3934
4083
|
if (row?.kind === "fallback") {
|
|
3935
4084
|
// Advertise `t` only where a strip would open, as the ANSI footer does.
|
|
3936
4085
|
const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
|
|
@@ -3940,6 +4089,7 @@ export class ModelHubComponent implements Component {
|
|
|
3940
4089
|
keys("add another", "f"),
|
|
3941
4090
|
keys("remove", "x"),
|
|
3942
4091
|
editable ? keys("thinking", "t") : undefined,
|
|
4092
|
+
compaction,
|
|
3943
4093
|
reorder,
|
|
3944
4094
|
keys("providers", "left"),
|
|
3945
4095
|
];
|
|
@@ -3962,6 +4112,7 @@ export class ModelHubComponent implements Component {
|
|
|
3962
4112
|
keys("fallback", "f"),
|
|
3963
4113
|
keys("clear", "x"),
|
|
3964
4114
|
editable ? keys("thinking", "t") : undefined,
|
|
4115
|
+
compaction,
|
|
3965
4116
|
keys("cycle", "c"),
|
|
3966
4117
|
reorder,
|
|
3967
4118
|
keys("new", "n"),
|
|
@@ -1330,7 +1330,11 @@ export interface SessionSelectorOptions<T extends SessionSelectorEntry = Session
|
|
|
1330
1330
|
title?: string;
|
|
1331
1331
|
/** Fixed scope label, or false to omit the scope suffix. */
|
|
1332
1332
|
scopeLabel?: string | false;
|
|
1333
|
-
/**
|
|
1333
|
+
/**
|
|
1334
|
+
* Show each session's working directory in the list. Defaults to on when
|
|
1335
|
+
* the session files live in more than one directory (e.g. the folder scope
|
|
1336
|
+
* merging a repository's worktrees).
|
|
1337
|
+
*/
|
|
1334
1338
|
showCwd?: boolean;
|
|
1335
1339
|
/**
|
|
1336
1340
|
* Reads the live terminal height so the visible window fits the viewport.
|
|
@@ -1374,6 +1378,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1374
1378
|
#onRequestRender?: () => void;
|
|
1375
1379
|
readonly #loadAllSessions?: () => Promise<T[]>;
|
|
1376
1380
|
#folderSessions: T[];
|
|
1381
|
+
readonly #folderShowCwd: boolean;
|
|
1377
1382
|
#globalSessions: T[] | null = null;
|
|
1378
1383
|
#scope: "folder" | "all" = "folder";
|
|
1379
1384
|
#toggling = false;
|
|
@@ -1440,6 +1445,9 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1440
1445
|
this.#onDelete = options.onDelete;
|
|
1441
1446
|
this.#loadAllSessions = options.loadAllSessions;
|
|
1442
1447
|
this.#folderSessions = sessions;
|
|
1448
|
+
// Storage directory, not recorded cwd: one folder's sessions may record
|
|
1449
|
+
// symlink aliases of the same path, which must not turn the column on.
|
|
1450
|
+
this.#folderShowCwd = options.showCwd ?? new Set(sessions.map(session => path.dirname(session.path))).size > 1;
|
|
1443
1451
|
this.#globalSessions = options.allSessions ?? null;
|
|
1444
1452
|
this.#getTerminalRows = options.getTerminalRows ?? (() => 24);
|
|
1445
1453
|
this.#fillHeight = options.fillHeight ?? false;
|
|
@@ -1457,7 +1465,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1457
1465
|
// projects' history (issue #3099).
|
|
1458
1466
|
this.#sessionList = new SessionList(
|
|
1459
1467
|
sessions,
|
|
1460
|
-
|
|
1468
|
+
this.#folderShowCwd,
|
|
1461
1469
|
options.historyMatcher,
|
|
1462
1470
|
options.getTerminalRows,
|
|
1463
1471
|
options.pinnedIds,
|
|
@@ -1530,7 +1538,7 @@ export class SessionSelectorComponent<T extends SessionSelectorEntry = SessionSe
|
|
|
1530
1538
|
this.#sessionList.setSessions(global, true);
|
|
1531
1539
|
} else {
|
|
1532
1540
|
this.#scope = "folder";
|
|
1533
|
-
this.#sessionList.setSessions(this.#folderSessions,
|
|
1541
|
+
this.#sessionList.setSessions(this.#folderSessions, this.#folderShowCwd);
|
|
1534
1542
|
}
|
|
1535
1543
|
this.title = this.#headerLabel();
|
|
1536
1544
|
this.#onRequestRender?.();
|
|
@@ -84,7 +84,7 @@ export interface ProviderCard {
|
|
|
84
84
|
/** Number of represented accounts, including unavailable usage lookups. */
|
|
85
85
|
accounts: number;
|
|
86
86
|
unavailableAccounts: string[];
|
|
87
|
-
/** Window rows
|
|
87
|
+
/** Window rows in provider-declared order (e.g. 5h → weekly → monthly); the fullest CARD_MAX_WINDOWS lead. */
|
|
88
88
|
windows: CardWindowRow[];
|
|
89
89
|
/** True when every account reports no limits (e.g. enterprise plans). */
|
|
90
90
|
unlimited: boolean;
|
|
@@ -146,8 +146,10 @@ function compactWindowTag(window: NonNullable<UsageLimit["window"]>): string {
|
|
|
146
146
|
* Collapse usage reports into one compact card per provider: limits grouped by
|
|
147
147
|
* quota bucket (label + window), each bucket showing the mean used fraction
|
|
148
148
|
* across accounts (matching the classic report's aggregate "% free") with the
|
|
149
|
-
* most-used account's reset countdown.
|
|
150
|
-
*
|
|
149
|
+
* most-used account's reset countdown. Rows keep the provider's declared window
|
|
150
|
+
* order so 5h → week → month reads the same every time; cards sort by their
|
|
151
|
+
* fullest window so what's burning is on top-left; fully idle providers
|
|
152
|
+
* collapse into a tick.
|
|
151
153
|
*/
|
|
152
154
|
export function buildProviderCards(
|
|
153
155
|
reports: UsageReport[],
|
|
@@ -181,7 +183,7 @@ export function buildProviderCards(
|
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
|
|
184
|
-
const
|
|
186
|
+
const declared: CardWindowRow[] = [...buckets.values()].map(bucket => {
|
|
185
187
|
const fractions = bucket.limits
|
|
186
188
|
.map(limit => resolveUsedFraction(limit))
|
|
187
189
|
.filter((value): value is number => value !== undefined);
|
|
@@ -200,7 +202,13 @@ export function buildProviderCards(
|
|
|
200
202
|
usedText: fraction === undefined ? formatAbsoluteOnlyAmount(bucket.limits) : undefined,
|
|
201
203
|
};
|
|
202
204
|
});
|
|
203
|
-
|
|
205
|
+
// Cards render at most CARD_MAX_WINDOWS rows; when a provider declares
|
|
206
|
+
// more, keep the fullest ones visible (still in declared order) so an
|
|
207
|
+
// exhausted bucket never hides behind "+N more".
|
|
208
|
+
const visible = new Set(
|
|
209
|
+
[...declared].sort((a, b) => (b.fraction ?? -1) - (a.fraction ?? -1)).slice(0, CARD_MAX_WINDOWS),
|
|
210
|
+
);
|
|
211
|
+
const windows = [...declared.filter(row => visible.has(row)), ...declared.filter(row => !visible.has(row))];
|
|
204
212
|
// The window tag earns its columns only when sibling rows would otherwise
|
|
205
213
|
// be indistinguishable (e.g. Antigravity's daily vs weekly "Usage (Google)").
|
|
206
214
|
for (const window of windows) {
|
|
@@ -264,9 +272,12 @@ export function buildProviderCards(
|
|
|
264
272
|
});
|
|
265
273
|
}
|
|
266
274
|
|
|
275
|
+
const worstFraction = new Map(
|
|
276
|
+
cards.map(card => [card, card.windows.reduce((max, window) => Math.max(max, window.fraction ?? -1), -1)]),
|
|
277
|
+
);
|
|
267
278
|
cards.sort((a, b) => {
|
|
268
|
-
const aWorst = a
|
|
269
|
-
const bWorst = b
|
|
279
|
+
const aWorst = worstFraction.get(a)!;
|
|
280
|
+
const bWorst = worstFraction.get(b)!;
|
|
270
281
|
if (aWorst !== bWorst) return bWorst - aWorst;
|
|
271
282
|
return a.name.localeCompare(b.name);
|
|
272
283
|
});
|
|
@@ -20,7 +20,6 @@ import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node"
|
|
|
20
20
|
import type { ComposerFacts, ComposerFactsSource } from "../status-line/types";
|
|
21
21
|
import { allowsModelMentions, allowsSkillTokens, SKILL_TOKEN_RE } from "./skill-tokens";
|
|
22
22
|
import { expandModelMentionTags, MODEL_MENTION_RE, modelMentionToken } from "./model-mention-syntax";
|
|
23
|
-
import { imageAttachmentSource } from "./image-source";
|
|
24
23
|
import { isVideoPath } from "./video";
|
|
25
24
|
import {
|
|
26
25
|
attachmentSgr,
|
|
@@ -480,8 +479,8 @@ export class CustomEditor extends Editor {
|
|
|
480
479
|
chips: ComposerChipDescriptor[];
|
|
481
480
|
}
|
|
482
481
|
| undefined;
|
|
483
|
-
/** Host-wired producer of per-image
|
|
484
|
-
* chip tokens for restored drafts (esc-esc, `/tree`, branch). */
|
|
482
|
+
/** Host-wired producer of per-image chip targets (a file on disk, or a session blob copy);
|
|
483
|
+
* drives clickable chip tokens for restored drafts (esc-esc, `/tree`, branch). */
|
|
485
484
|
draftImageLinkMaterializer?: (images: readonly ImageContent[]) => Promise<(string | undefined)[] | undefined>;
|
|
486
485
|
|
|
487
486
|
/**
|
|
@@ -791,7 +790,7 @@ export class CustomEditor extends Editor {
|
|
|
791
790
|
if (!materialize || images.length === 0) return;
|
|
792
791
|
const links = await materialize(images);
|
|
793
792
|
if (!links || this.pendingImages !== images) return;
|
|
794
|
-
this.pendingImageLinks =
|
|
793
|
+
this.pendingImageLinks = links;
|
|
795
794
|
this.imageLinks = this.pendingImageLinks;
|
|
796
795
|
this.#requestShimmerRepaint?.();
|
|
797
796
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* to the session's `local://` root, and generated video contact-sheet previews.
|
|
5
5
|
* Symbol metadata stays out of serialized/model-bound image data while traveling
|
|
6
6
|
* with the draft object, until AgentSession creates the hidden companion message
|
|
7
|
-
* that tells the model the path
|
|
7
|
+
* that tells the model the path. The model keeps `local://` references across
|
|
8
|
+
* `/move`; chips for internal URLs instead use a stable blob copy.
|
|
8
9
|
*/
|
|
9
10
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
10
11
|
import { isRecord } from "@oh-my-pi/pi-utils";
|
package/src/prompt/tips.txt
CHANGED
|
@@ -30,4 +30,5 @@ Turn on `/advisor` to attach a second model that reviews every turn and quietly
|
|
|
30
30
|
Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
|
|
31
31
|
Press {key:shift+tab} to cycle through reasoning effort levels
|
|
32
32
|
omp can reconfigure and extend itself — ask any questions you have!
|
|
33
|
-
Lint/type errors piling up? `omp cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — {key:escape} cancels
|
|
33
|
+
Lint/type errors piling up? `omp cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — {key:escape} cancels
|
|
34
|
+
[NO-TERN] Psst, omp has a natural habitat 🐦 [tern](https://scl.so/tern)
|