@oh-my-pi/pi-tui 18.8.3 → 18.8.5
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 +22 -0
- package/dist/types/apps/git/help.d.ts +3 -1
- package/dist/types/chat/user-message.d.ts +1 -1
- package/dist/types/components/markdown.d.ts +11 -0
- package/dist/types/native/overlay.d.ts +5 -0
- package/dist/types/overlays/agent-hub-projection.d.ts +2 -2
- package/dist/types/overlays/agent-transcript-viewer.d.ts +3 -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/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/package.json +9 -9
- package/src/apps/autoresearch-dashboard.ts +7 -3
- package/src/apps/git/help.ts +29 -20
- package/src/chat/assistant-message.ts +20 -5
- package/src/chat/user-message.ts +1 -1
- package/src/components/editor.ts +6 -3
- package/src/components/markdown.ts +114 -0
- package/src/native/overlay.ts +13 -0
- package/src/overlays/agent-hub-projection.ts +7 -1
- package/src/overlays/agent-hub.ts +29 -24
- package/src/overlays/agent-transcript-viewer.ts +25 -10
- package/src/overlays/annotation-overlay.ts +25 -3
- package/src/overlays/btw-history-panel.ts +12 -1
- package/src/overlays/model-browser.ts +26 -1
- package/src/overlays/model-hub.ts +133 -17
- package/src/overlays/move-overlay.ts +28 -14
- package/src/overlays/plan-review-overlay.ts +11 -2
- package/src/overlays/usage-dashboard.ts +18 -7
- package/src/prompt/custom-editor.ts +3 -4
- package/src/prompt/image-source.ts +2 -1
|
@@ -200,6 +200,12 @@ const DATA_CHANGE_RENDER_COALESCE_MS = 100;
|
|
|
200
200
|
/** Double-tap window for the table's left-left "close hub" gesture. */
|
|
201
201
|
const LEFT_TAP_WINDOW_MS = 500;
|
|
202
202
|
|
|
203
|
+
function compareRosterAgents(a: AgentRecordLike, b: AgentRecordLike): number {
|
|
204
|
+
return (
|
|
205
|
+
STATUS_ORDER[a.status] - STATUS_ORDER[b.status] || b.lastActivity - a.lastActivity || a.id.localeCompare(b.id)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
203
209
|
function activityGlyph(row: AgentActivityRow): string {
|
|
204
210
|
if (row.status === "error") return theme.fg("error", theme.status.error);
|
|
205
211
|
if (row.status === "aborted") return theme.fg("warning", theme.status.aborted);
|
|
@@ -358,11 +364,10 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
358
364
|
#rows: TRecord[] = [];
|
|
359
365
|
#statusCounts: Record<AgentStatus, number> = { running: 0, idle: 0, parked: 0, aborted: 0 };
|
|
360
366
|
#selectedRow = 0;
|
|
361
|
-
/**
|
|
362
|
-
*
|
|
363
|
-
* while the hub is open; newly appearing generations append at the end. */
|
|
367
|
+
/** Initial status/recency rank stays fixed while open; new generations
|
|
368
|
+
* prepend without changing the relative order of existing rows. */
|
|
364
369
|
#rowOrder: Map<TRecord, number> | undefined;
|
|
365
|
-
#
|
|
370
|
+
#nextNewRowOrder = 0;
|
|
366
371
|
#hoveredRow: number | null = null;
|
|
367
372
|
/** Per-render screen-line to agent-row map, shared by click and hover routing. */
|
|
368
373
|
#hitRows: Array<number | undefined> = [];
|
|
@@ -1524,35 +1529,35 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
1524
1529
|
const refs = this.#registry.list().filter(ref => ref.id !== MAIN_AGENT_ID);
|
|
1525
1530
|
this.#observedById = new Map();
|
|
1526
1531
|
for (const session of this.#observers.getSessions()) this.#observedById.set(session.id, session);
|
|
1527
|
-
//
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
// Defer the capture until persisted-subagent discovery settles so a
|
|
1531
|
-
// mid-scan refresh cannot freeze a partial roster (the remaining agents
|
|
1532
|
-
// would otherwise append in readdir order instead of being ranked).
|
|
1532
|
+
// Capture the status+recency ranking once so keyboard navigation does
|
|
1533
|
+
// not jump on heartbeats. Defer until persisted-subagent discovery
|
|
1534
|
+
// settles so a partial roster is not frozen in readdir order.
|
|
1533
1535
|
const rowOrder = this.#rowOrder;
|
|
1534
1536
|
let ordered: TRecord[];
|
|
1535
1537
|
if (!rowOrder) {
|
|
1536
|
-
ordered = refs.sort(
|
|
1537
|
-
(a, b) =>
|
|
1538
|
-
STATUS_ORDER[a.status] - STATUS_ORDER[b.status] ||
|
|
1539
|
-
b.lastActivity - a.lastActivity ||
|
|
1540
|
-
a.id.localeCompare(b.id),
|
|
1541
|
-
);
|
|
1538
|
+
ordered = refs.sort(compareRosterAgents);
|
|
1542
1539
|
if (!this.#loadingPersistedSubagents && ordered.length > 0) {
|
|
1543
|
-
|
|
1544
|
-
for (const ref of ordered)
|
|
1540
|
+
const rowOrder = new Map<TRecord, number>();
|
|
1541
|
+
for (const [index, ref] of ordered.entries()) rowOrder.set(ref, index);
|
|
1542
|
+
this.#rowOrder = rowOrder;
|
|
1545
1543
|
}
|
|
1546
1544
|
} else {
|
|
1547
1545
|
for (const rankedRef of rowOrder.keys()) {
|
|
1548
1546
|
if (!refs.includes(rankedRef)) rowOrder.delete(rankedRef);
|
|
1549
1547
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
for (const ref of
|
|
1554
|
-
if (!rowOrder.has(ref))
|
|
1548
|
+
// Each batch of newly appearing generations uses the initial sort,
|
|
1549
|
+
// then takes ranks above every agent already on screen.
|
|
1550
|
+
let newcomers: TRecord[] | undefined;
|
|
1551
|
+
for (const ref of refs) {
|
|
1552
|
+
if (!rowOrder.has(ref)) (newcomers ??= []).push(ref);
|
|
1553
|
+
}
|
|
1554
|
+
if (newcomers) {
|
|
1555
|
+
newcomers.sort(compareRosterAgents);
|
|
1556
|
+
for (let i = newcomers.length - 1; i >= 0; i--) {
|
|
1557
|
+
rowOrder.set(newcomers[i]!, --this.#nextNewRowOrder);
|
|
1558
|
+
}
|
|
1555
1559
|
}
|
|
1560
|
+
ordered = refs.sort((a, b) => rowOrder.get(a)! - rowOrder.get(b)!);
|
|
1556
1561
|
}
|
|
1557
1562
|
const query = this.#agentFilter.getValue().trim();
|
|
1558
1563
|
const rosterRows =
|
|
@@ -1561,7 +1566,7 @@ export class AgentHubOverlayComponent<TRecord extends AgentRecordLike = AgentRec
|
|
|
1561
1566
|
: ordered;
|
|
1562
1567
|
|
|
1563
1568
|
if (this.#viewMode === "tree") {
|
|
1564
|
-
const tree = projectAgentTree(rosterRows);
|
|
1569
|
+
const tree = projectAgentTree(rosterRows, rowOrder);
|
|
1565
1570
|
this.#rows = tree.rows;
|
|
1566
1571
|
this.#treeDepthById = tree.depthById;
|
|
1567
1572
|
this.#treeParentById = tree.parentById;
|
|
@@ -41,8 +41,8 @@ import { sanitizeErrorLine } from "../chrome/error-block";
|
|
|
41
41
|
import type { ScrollRangeAnchor } from "../components/scroll-view";
|
|
42
42
|
import { formatContextUsage } from "../chrome/context-thresholds";
|
|
43
43
|
import { node, span, text } from "../native/describe";
|
|
44
|
-
import type { DescribeContext, NativeChild, NativeNode } from "../native/node";
|
|
45
|
-
import { actionHint, hintsRow, overlayCard } from "../native/overlay";
|
|
44
|
+
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
45
|
+
import { actionHint, escCloseButton, hintsRow, overlayCard } from "../native/overlay";
|
|
46
46
|
|
|
47
47
|
/** Parsed message and model metadata relevant to a transcript viewer. */
|
|
48
48
|
export type AgentTranscriptEntry = SessionMessageEntryLike | { type: "model_change"; model: string };
|
|
@@ -559,12 +559,7 @@ export class AgentTranscriptViewer implements Component {
|
|
|
559
559
|
}
|
|
560
560
|
|
|
561
561
|
if (matchesKey(data, "escape")) {
|
|
562
|
-
|
|
563
|
-
this.#editor.setText("");
|
|
564
|
-
this.#deps.requestRender();
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
this.#deps.onClose();
|
|
562
|
+
this.#escape();
|
|
568
563
|
return;
|
|
569
564
|
}
|
|
570
565
|
|
|
@@ -647,6 +642,21 @@ export class AgentTranscriptViewer implements Component {
|
|
|
647
642
|
return lines;
|
|
648
643
|
}
|
|
649
644
|
|
|
645
|
+
/** The top-right `esc` runs Esc. */
|
|
646
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
647
|
+
if (event.type === "action" && event.act === "close") this.#escape();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Esc: clear a non-empty draft first, else close the viewer. */
|
|
651
|
+
#escape(): void {
|
|
652
|
+
if (this.#editor && this.#editor.getText().trim() !== "") {
|
|
653
|
+
this.#editor.setText("");
|
|
654
|
+
this.#deps.requestRender();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
this.#deps.onClose();
|
|
658
|
+
}
|
|
659
|
+
|
|
650
660
|
/**
|
|
651
661
|
* Header, transcript body (the builder's container, which describes its own
|
|
652
662
|
* blocks), notice, message editor, stats and key hints. Scrolling is the
|
|
@@ -675,6 +685,8 @@ export class AgentTranscriptViewer implements Component {
|
|
|
675
685
|
|
|
676
686
|
const id = this.#deps.agentId;
|
|
677
687
|
const children: NativeChild[] = [];
|
|
688
|
+
// Top row: the agent's meta on the left, a clickable `esc` (close) on the right.
|
|
689
|
+
const top: NativeChild[] = [];
|
|
678
690
|
if (ref) {
|
|
679
691
|
const kindTag = ref.parentId ? `${ref.kind} ${theme.sep.dot} of ${ref.parentId}` : ref.kind;
|
|
680
692
|
const meta: NativeChild[] = [
|
|
@@ -683,8 +695,12 @@ export class AgentTranscriptViewer implements Component {
|
|
|
683
695
|
text([span(kindTag, "dim")], { truncate: "end" }),
|
|
684
696
|
];
|
|
685
697
|
if (this.#model) meta.push(text([span(this.#model, "muted")], { truncate: "end" }));
|
|
686
|
-
|
|
698
|
+
top.push(node("row", { gap: "sm", align: "center", grow: 1, min: { w: 0 } }, meta, "meta"));
|
|
699
|
+
} else {
|
|
700
|
+
top.push(node("spacer", { grow: 1 }));
|
|
687
701
|
}
|
|
702
|
+
top.push(escCloseButton());
|
|
703
|
+
children.push(node("row", { gap: "md", align: "center" }, top, "top"));
|
|
688
704
|
children.push(
|
|
689
705
|
placeholder === undefined
|
|
690
706
|
? node("col", { grow: 1 }, [this.#builder.container], "transcript")
|
|
@@ -747,7 +763,6 @@ export class AgentTranscriptViewer implements Component {
|
|
|
747
763
|
children.push(
|
|
748
764
|
hintsRow([
|
|
749
765
|
this.#editor ? actionHint("tui.input.submit", "send") : undefined,
|
|
750
|
-
{ keys: ["escape"], label: "close" },
|
|
751
766
|
{ keys: [this.#deps.expandKeys[0] ?? "ctrl+o"], label: "expand" },
|
|
752
767
|
]),
|
|
753
768
|
);
|
|
@@ -42,7 +42,15 @@ import { matchesAppExternalEditor } from "../keybinding-matchers";
|
|
|
42
42
|
import type { KeyName } from "../key-hint-format";
|
|
43
43
|
import { item, keyed, node, row as rowNode, span, text } from "../native/describe";
|
|
44
44
|
import { leafKey, type NativeChild, type NativeNode, type NativeUiEvent } from "../native/node";
|
|
45
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
escCloseButton,
|
|
47
|
+
hintsRow,
|
|
48
|
+
itemIndex,
|
|
49
|
+
type NativeHint,
|
|
50
|
+
noteSpans,
|
|
51
|
+
overlayCard,
|
|
52
|
+
selectList,
|
|
53
|
+
} from "../native/overlay";
|
|
46
54
|
|
|
47
55
|
function fit(text: string, width: number): string {
|
|
48
56
|
if (width <= 0) return "";
|
|
@@ -317,7 +325,7 @@ export class AnnotationOverlay implements Focusable {
|
|
|
317
325
|
return;
|
|
318
326
|
}
|
|
319
327
|
if (this.#keybindings.matches(data, "tui.select.cancel")) {
|
|
320
|
-
this.#
|
|
328
|
+
this.#escape();
|
|
321
329
|
return;
|
|
322
330
|
}
|
|
323
331
|
if (data === "A" && (this.#focus === "files" || this.#focus === "diff")) {
|
|
@@ -369,6 +377,13 @@ export class AnnotationOverlay implements Focusable {
|
|
|
369
377
|
}
|
|
370
378
|
}
|
|
371
379
|
|
|
380
|
+
/** Esc, by state: leave the note chooser, drop the note draft, else close the review. */
|
|
381
|
+
#escape(): void {
|
|
382
|
+
if (this.#annotationChooser) this.#annotationChooser = undefined;
|
|
383
|
+
else if (this.#annotating) this.#cancelAnnotation();
|
|
384
|
+
else this.#finish(undefined);
|
|
385
|
+
}
|
|
386
|
+
|
|
372
387
|
#cycleFocus(direction: number): void {
|
|
373
388
|
const regions: FocusRegion[] = this.#sidebarShown ? ["files", "diff", "actions"] : ["diff", "actions"];
|
|
374
389
|
const current = regions.indexOf(this.#focus);
|
|
@@ -1263,7 +1278,10 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1263
1278
|
role: this.#textSource ? "omp.overlay.annotateText.lines" : "omp.overlay.codeReview.diff",
|
|
1264
1279
|
tone: this.#focus === "diff" ? "accent" : undefined,
|
|
1265
1280
|
});
|
|
1266
|
-
|
|
1281
|
+
// The header row ends in a clickable `esc` at the top right.
|
|
1282
|
+
const title = node("col", { grow: 1, min: { w: 0 } }, [this.#describeHeader()], "title");
|
|
1283
|
+
const head = keyed(rowNode([title, escCloseButton()], { gap: "md", align: "center" }), "head");
|
|
1284
|
+
const main = node("col", { grow: 1 }, [head, lines], "main");
|
|
1267
1285
|
if (this.#sidebarShown) {
|
|
1268
1286
|
const files = selectList(
|
|
1269
1287
|
"files",
|
|
@@ -1312,6 +1330,10 @@ export class AnnotationOverlay implements Focusable {
|
|
|
1312
1330
|
|
|
1313
1331
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
1314
1332
|
if (this.#finished || this.#externalOperation) return;
|
|
1333
|
+
if (event.type === "action" && event.act === "close") {
|
|
1334
|
+
this.#escape();
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1315
1337
|
if (event.type !== "select" && event.type !== "activate") return;
|
|
1316
1338
|
const activate = event.type === "activate";
|
|
1317
1339
|
const leaf = leafKey(event.key);
|
|
@@ -37,7 +37,7 @@ import { clampSelection, contentRowWidth, padLinesToHeight, renderScrollableList
|
|
|
37
37
|
import type { TspScrollBy, TspSpan } from "@oh-my-pi/pi-wire";
|
|
38
38
|
import type { NativeChild, NativeNode, NativeScroll, NativeUiEvent } from "../native/node";
|
|
39
39
|
import { col, md, node, span, text } from "../native/describe";
|
|
40
|
-
import { actionHint, hintsRow, type NativeHint, statusHintsRow } from "../native/overlay";
|
|
40
|
+
import { actionBar, actionButton, actionHint, hintsRow, type NativeHint, statusHintsRow } from "../native/overlay";
|
|
41
41
|
|
|
42
42
|
interface BtwHistoryPanelOptions {
|
|
43
43
|
records: readonly BtwHistoryRecord[];
|
|
@@ -485,6 +485,13 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
485
485
|
children.push(node("col", undefined, lines, "composer"));
|
|
486
486
|
}
|
|
487
487
|
children.push({ ...this.#describeFooter(record, canFollowUp, copied), key: "footer" });
|
|
488
|
+
// Clicking Close always dismisses the sheet; the Esc keycap only shows where Esc does too.
|
|
489
|
+
const running = record !== undefined && getBtwLatestTurn(record).status === "running";
|
|
490
|
+
const escapeCloses = !composer && !(running && !this.#options.escapeHides);
|
|
491
|
+
children.push({
|
|
492
|
+
...actionBar([null, actionButton("Close", "close", escapeCloses ? { keys: "escape" } : undefined)]),
|
|
493
|
+
key: "actions",
|
|
494
|
+
});
|
|
488
495
|
// The sheet is the frame (`nativeOverlay`): a borderless column.
|
|
489
496
|
const described = col(children, { gap: "md" });
|
|
490
497
|
this.#native = {
|
|
@@ -504,6 +511,10 @@ export class BtwHistoryPanel implements Component, Focusable {
|
|
|
504
511
|
}
|
|
505
512
|
|
|
506
513
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
514
|
+
if (event.type === "action" && event.act === "close") {
|
|
515
|
+
this.#options.onClose();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
507
518
|
if ((event.type !== "select" && event.type !== "activate") || this.#composer) return;
|
|
508
519
|
const index = this.#records.findIndex(record => record.id === event.item);
|
|
509
520
|
if (index === -1) return;
|
|
@@ -122,6 +122,18 @@ export interface ResolvedModelRoleValue {
|
|
|
122
122
|
warning?: string;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** Where auto-compaction triggers for one model, as the model hub's preview shows it (edited from the Roles view). */
|
|
126
|
+
export interface ModelCompactionPoint {
|
|
127
|
+
/** Context tokens that trigger auto-compaction; undefined when auto-compaction is off or the window is unknown. */
|
|
128
|
+
tokens: number | undefined;
|
|
129
|
+
/** The configured percentage of the window, when the trigger is percent-based. */
|
|
130
|
+
percent?: number;
|
|
131
|
+
/** What sets it: the matching `compaction.modelThresholds` key, `global`, or `default`. */
|
|
132
|
+
source: string;
|
|
133
|
+
/** The model's own exact entry as editable text (`90k`, `80%`); absent when it has none. */
|
|
134
|
+
draft?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
/** Host-provided preferences and model-role resolution for the browser. */
|
|
126
138
|
export interface ModelBrowserSource extends ModelRoleLookup {
|
|
127
139
|
/**
|
|
@@ -143,6 +155,8 @@ export interface ModelBrowserSource extends ModelRoleLookup {
|
|
|
143
155
|
getRoleInfo(role: string): ModelBrowserRoleInfo;
|
|
144
156
|
defaultRoleChain(role: string): string[];
|
|
145
157
|
resolveRoleValue(value: string | undefined, models: Model[], roleLookup?: ModelRoleLookup): ResolvedModelRoleValue;
|
|
158
|
+
/** Auto-compaction point for `model`; absent hosts show no compaction row in the preview. */
|
|
159
|
+
compactionPointFor?(model: Model): ModelCompactionPoint | undefined;
|
|
146
160
|
}
|
|
147
161
|
|
|
148
162
|
/** Read-only catalog surface consumed by model browsers. */
|
|
@@ -2061,7 +2075,7 @@ export class ModelBrowser implements Component {
|
|
|
2061
2075
|
pickerPreview(mode: "full" | "compact", current?: string): readonly NativeChild[] {
|
|
2062
2076
|
const selected = this.getSelected();
|
|
2063
2077
|
const item = selected && !this.#isDisabled(selected) ? selected : undefined;
|
|
2064
|
-
const key = `${mode}\0${current ?? ""}`;
|
|
2078
|
+
const key = `${mode}\0${current ?? ""}\0${this.#settings.revision}`;
|
|
2065
2079
|
const memo = this.#pickerPreview;
|
|
2066
2080
|
if (
|
|
2067
2081
|
memo !== undefined &&
|
|
@@ -2155,6 +2169,17 @@ export class ModelBrowser implements Component {
|
|
|
2155
2169
|
if (v) facts.push({ k: [span(k, "muted")], v: [span(v, "mono")] });
|
|
2156
2170
|
};
|
|
2157
2171
|
fact("Context", ctx > 0 ? ctx.toLocaleString("en-US") : undefined);
|
|
2172
|
+
const compaction = this.#settings.compactionPointFor?.(model);
|
|
2173
|
+
if (compaction) {
|
|
2174
|
+
const value =
|
|
2175
|
+
compaction.tokens === undefined
|
|
2176
|
+
? "off"
|
|
2177
|
+
: `${compaction.tokens.toLocaleString("en-US")}${compaction.percent !== undefined ? ` · ${compaction.percent}%` : ""}`;
|
|
2178
|
+
facts.push({
|
|
2179
|
+
k: [span("Compacts at", "muted")],
|
|
2180
|
+
v: [span(value, "mono"), span(` · ${compaction.source}`, "dim")],
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2158
2183
|
fact("Max output", out > 0 ? out.toLocaleString("en-US") : undefined);
|
|
2159
2184
|
fact("Price", isFreeModel(model) ? "free" : `${previewPrice(model)} per M`);
|
|
2160
2185
|
fact("Speed", speed.length > 0 ? speed.join(" · ") : undefined);
|
|
@@ -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;
|
|
@@ -2020,6 +2068,7 @@ export class ModelHubComponent implements Component {
|
|
|
2020
2068
|
return;
|
|
2021
2069
|
}
|
|
2022
2070
|
strip.input.handleInput(data);
|
|
2071
|
+
if (strip.purpose === "compaction") strip.error = undefined;
|
|
2023
2072
|
return;
|
|
2024
2073
|
}
|
|
2025
2074
|
if (moveStripSelection(strip, data)) return;
|
|
@@ -2183,6 +2232,9 @@ export class ModelHubComponent implements Component {
|
|
|
2183
2232
|
case "prevPreset":
|
|
2184
2233
|
this.#switchPreset(action === "nextPreset" ? 1 : -1);
|
|
2185
2234
|
return;
|
|
2235
|
+
case "compaction":
|
|
2236
|
+
this.#openCompactionStrip();
|
|
2237
|
+
return;
|
|
2186
2238
|
case "thinking":
|
|
2187
2239
|
if (role) {
|
|
2188
2240
|
const target = this.#roleThinkingTarget(role);
|
|
@@ -2658,6 +2710,7 @@ export class ModelHubComponent implements Component {
|
|
|
2658
2710
|
const strip = this.#strip;
|
|
2659
2711
|
if (strip) {
|
|
2660
2712
|
if (strip.kind === "name") {
|
|
2713
|
+
if (strip.purpose === "compaction") return `${enter} set compaction point · ${cancel} cancel`;
|
|
2661
2714
|
if (strip.purpose === "preset") {
|
|
2662
2715
|
return `${enter} save preset · ${cancel} cancel`;
|
|
2663
2716
|
}
|
|
@@ -2689,13 +2742,17 @@ export class ModelHubComponent implements Component {
|
|
|
2689
2742
|
return `${upDown} providers · ${enterRight} roles · ${altLeftRight} tabs${presets} · ${cancel} close`;
|
|
2690
2743
|
}
|
|
2691
2744
|
const row = this.#rolesRows[this.#roleIndex];
|
|
2745
|
+
const compaction =
|
|
2746
|
+
this.#callbacks.onCompactionPointChange && this.#roleRowModel()
|
|
2747
|
+
? ` · ${formatKeyHint("k")} compaction limit`
|
|
2748
|
+
: "";
|
|
2692
2749
|
if (row?.kind === "fallback") {
|
|
2693
2750
|
// Advertise `t` only when the entry resolves: wildcards always
|
|
2694
2751
|
// inherit and unknown models have no ladder to offer, so the
|
|
2695
2752
|
// action would be inert there.
|
|
2696
2753
|
const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
|
|
2697
2754
|
const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
|
|
2698
|
-
return `${upDown} rows · ${enter} replace · ${formatKeyHint("f")} add another · ${formatKeyHint("x")} remove${thinking} · [/] reorder · ${left} providers`;
|
|
2755
|
+
return `${upDown} rows · ${enter} replace · ${formatKeyHint("f")} add another · ${formatKeyHint("x")} remove${thinking}${compaction} · [/] reorder · ${left} providers`;
|
|
2699
2756
|
}
|
|
2700
2757
|
if (row?.kind === "chainKey") {
|
|
2701
2758
|
return `${upDown} rows · ${formatKeyHints(["enter", "f"])} add fallback · ${formatKeyHint("x")} clear chain · ${left} providers`;
|
|
@@ -2709,7 +2766,7 @@ export class ModelHubComponent implements Component {
|
|
|
2709
2766
|
const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
|
|
2710
2767
|
const savePreset = this.#callbacks.onSavePreset ? ` · ${formatKeyHint("s")} save preset` : "";
|
|
2711
2768
|
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}`;
|
|
2769
|
+
return `${upDown} rows · ${enter} pick · ${formatKeyHint("f")} fallback · ${formatKeyHint("x")} clear${thinking}${compaction} · ${formatKeyHint("c")} cycle · [/] reorder · ${formatKeyHint("n")} new${savePreset}${switchPreset}`;
|
|
2713
2770
|
}
|
|
2714
2771
|
if (entry.kind === "provider" && entry.locked) {
|
|
2715
2772
|
return entry.oauth
|
|
@@ -2734,12 +2791,22 @@ export class ModelHubComponent implements Component {
|
|
|
2734
2791
|
|
|
2735
2792
|
#renderStrip(width: number, strip: StripState): string {
|
|
2736
2793
|
if (strip.kind === "name") {
|
|
2737
|
-
const
|
|
2738
|
-
|
|
2794
|
+
const labelText =
|
|
2795
|
+
strip.purpose === "compaction"
|
|
2796
|
+
? `Compact ${strip.model.id} at:`
|
|
2797
|
+
: strip.purpose === "preset"
|
|
2798
|
+
? "Preset name:"
|
|
2799
|
+
: "New role name:";
|
|
2739
2800
|
const label = theme.fg("accent", labelText);
|
|
2740
2801
|
const inputWidth = Math.max(8, Math.min(32, width - visibleWidth(labelText) - 24));
|
|
2741
2802
|
const inputLine = strip.input.render(inputWidth)[0] ?? "";
|
|
2742
|
-
|
|
2803
|
+
const hint =
|
|
2804
|
+
strip.purpose !== "compaction"
|
|
2805
|
+
? theme.fg("dim", "(letters, digits, - and _)")
|
|
2806
|
+
: strip.error
|
|
2807
|
+
? theme.fg("error", strip.error)
|
|
2808
|
+
: theme.fg("dim", COMPACTION_INPUT_HINT);
|
|
2809
|
+
return truncateToWidth(`${label} ${inputLine} ${hint}`, width);
|
|
2743
2810
|
}
|
|
2744
2811
|
|
|
2745
2812
|
const prefix =
|
|
@@ -3390,12 +3457,14 @@ export class ModelHubComponent implements Component {
|
|
|
3390
3457
|
if (strip) {
|
|
3391
3458
|
const apply =
|
|
3392
3459
|
strip.kind === "name"
|
|
3393
|
-
?
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3460
|
+
? strip.purpose === "compaction"
|
|
3461
|
+
? pickerAction("compactionPoint", "Set compaction point", "enter", { primary: true })
|
|
3462
|
+
: pickerAction(
|
|
3463
|
+
strip.purpose === "preset" ? "presetName" : "roleName",
|
|
3464
|
+
strip.purpose === "preset" ? "Save preset" : "Create role",
|
|
3465
|
+
"enter",
|
|
3466
|
+
{ primary: true },
|
|
3467
|
+
)
|
|
3399
3468
|
: pickerAction(
|
|
3400
3469
|
"stripApply",
|
|
3401
3470
|
strip.kind === "thinking" ? "Apply" : strip.kind === "scope" ? "Save to scope" : "Assign / clear",
|
|
@@ -3420,6 +3489,10 @@ export class ModelHubComponent implements Component {
|
|
|
3420
3489
|
const roleAction = (action: RolesAction, label: string, key: string, primary = false) =>
|
|
3421
3490
|
pickerAction(`roles:${action}`, label, key, primary ? { primary: true } : undefined);
|
|
3422
3491
|
const actions: (TspPickerAction | undefined)[] = [];
|
|
3492
|
+
const compaction =
|
|
3493
|
+
this.#callbacks.onCompactionPointChange && this.#roleRowModel()
|
|
3494
|
+
? roleAction("compaction", "Compaction limit", "k")
|
|
3495
|
+
: undefined;
|
|
3423
3496
|
switch (row?.kind) {
|
|
3424
3497
|
case "role": {
|
|
3425
3498
|
const assigned = this.#roles[row.role];
|
|
@@ -3428,6 +3501,7 @@ export class ModelHubComponent implements Component {
|
|
|
3428
3501
|
roleAction("fallback", "Add fallback", "f"),
|
|
3429
3502
|
assigned && !assigned.autoSelected ? roleAction("clear", "Clear", "x") : undefined,
|
|
3430
3503
|
this.#roleThinkingTarget(row.role) ? roleAction("thinking", "Thinking", "t") : undefined,
|
|
3504
|
+
compaction,
|
|
3431
3505
|
roleAction("cycle", this.#cycleOrder().includes(row.role) ? "Leave cycle" : "Add to cycle", "c"),
|
|
3432
3506
|
roleAction("new", "New role", "n"),
|
|
3433
3507
|
this.#callbacks.onSavePreset ? roleAction("save", "Save preset", "s") : undefined,
|
|
@@ -3443,6 +3517,7 @@ export class ModelHubComponent implements Component {
|
|
|
3443
3517
|
this.#resolveFallbackEntry(row.role, row.chainIndex)
|
|
3444
3518
|
? roleAction("thinking", "Thinking", "t")
|
|
3445
3519
|
: undefined,
|
|
3520
|
+
compaction,
|
|
3446
3521
|
roleAction("earlier", "Earlier", "["),
|
|
3447
3522
|
roleAction("later", "Later", "]"),
|
|
3448
3523
|
);
|
|
@@ -3479,6 +3554,19 @@ export class ModelHubComponent implements Component {
|
|
|
3479
3554
|
/** The open strip as the picker's chip strip (role assignment, save scope, thinking level, new role name). */
|
|
3480
3555
|
#pickerStrip(strip: StripState): NonNullable<TspPickerProps["strip"]> {
|
|
3481
3556
|
if (strip.kind === "name") {
|
|
3557
|
+
if (strip.purpose === "compaction") {
|
|
3558
|
+
return {
|
|
3559
|
+
label: [
|
|
3560
|
+
span("Compact ", "muted"),
|
|
3561
|
+
span(strip.model.id, "mono"),
|
|
3562
|
+
span(" at ", "muted"),
|
|
3563
|
+
span(strip.input.getValue(), "mono"),
|
|
3564
|
+
span("▏", "accent"),
|
|
3565
|
+
span(` ${strip.error ?? COMPACTION_INPUT_HINT}`, strip.error ? "error" : "dim"),
|
|
3566
|
+
],
|
|
3567
|
+
items: [],
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3482
3570
|
return {
|
|
3483
3571
|
label: [
|
|
3484
3572
|
span(strip.purpose === "preset" ? "Preset name " : "New role name ", "muted"),
|
|
@@ -3618,7 +3706,14 @@ export class ModelHubComponent implements Component {
|
|
|
3618
3706
|
/** The selected Roles-view row's preview: its model's facts and its fallback chain. */
|
|
3619
3707
|
#rolePreview(row: RolesRow | undefined): readonly NativeChild[] {
|
|
3620
3708
|
const memo = this.#pickerRolePreview;
|
|
3621
|
-
|
|
3709
|
+
const revision = this.#settings.revision;
|
|
3710
|
+
if (
|
|
3711
|
+
memo !== undefined &&
|
|
3712
|
+
memo.row === row &&
|
|
3713
|
+
memo.roles === this.#roles &&
|
|
3714
|
+
memo.rows === this.#rolesRows &&
|
|
3715
|
+
memo.revision === revision
|
|
3716
|
+
) {
|
|
3622
3717
|
return memo.children;
|
|
3623
3718
|
}
|
|
3624
3719
|
const children: NativeChild[] = [];
|
|
@@ -3692,7 +3787,7 @@ export class ModelHubComponent implements Component {
|
|
|
3692
3787
|
);
|
|
3693
3788
|
break;
|
|
3694
3789
|
}
|
|
3695
|
-
this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, children };
|
|
3790
|
+
this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, revision, children };
|
|
3696
3791
|
return children;
|
|
3697
3792
|
}
|
|
3698
3793
|
|
|
@@ -3792,6 +3887,9 @@ export class ModelHubComponent implements Component {
|
|
|
3792
3887
|
case "presetName":
|
|
3793
3888
|
this.#submitPresetName();
|
|
3794
3889
|
return;
|
|
3890
|
+
case "compactionPoint":
|
|
3891
|
+
this.#submitNameStrip();
|
|
3892
|
+
return;
|
|
3795
3893
|
case "cancel":
|
|
3796
3894
|
if (this.#strip) this.#closeStrip();
|
|
3797
3895
|
return;
|
|
@@ -3842,6 +3940,18 @@ export class ModelHubComponent implements Component {
|
|
|
3842
3940
|
const strip = this.#strip;
|
|
3843
3941
|
if (!strip) return undefined;
|
|
3844
3942
|
if (strip.kind === "name") {
|
|
3943
|
+
if (strip.purpose === "compaction") {
|
|
3944
|
+
return node(
|
|
3945
|
+
"row",
|
|
3946
|
+
{ gap: "sm", align: "center" },
|
|
3947
|
+
[
|
|
3948
|
+
text([span(`Compact ${strip.model.id} at:`, "accent")]),
|
|
3949
|
+
col([strip.input], { grow: 1 }),
|
|
3950
|
+
text([span(strip.error ?? COMPACTION_INPUT_HINT, strip.error ? "error" : "dim")]),
|
|
3951
|
+
],
|
|
3952
|
+
"compactionPoint",
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3845
3955
|
const preset = strip.purpose === "preset";
|
|
3846
3956
|
return node(
|
|
3847
3957
|
"row",
|
|
@@ -3895,9 +4005,11 @@ export class ModelHubComponent implements Component {
|
|
|
3895
4005
|
if (strip) {
|
|
3896
4006
|
switch (strip.kind) {
|
|
3897
4007
|
case "name":
|
|
3898
|
-
return strip.purpose === "
|
|
3899
|
-
? [keys("
|
|
3900
|
-
:
|
|
4008
|
+
return strip.purpose === "compaction"
|
|
4009
|
+
? [keys("set compaction point", "enter"), cancel("cancel")]
|
|
4010
|
+
: strip.purpose === "preset"
|
|
4011
|
+
? [keys("save preset", "enter"), cancel("cancel")]
|
|
4012
|
+
: [keys("create + pick model", "enter"), cancel("cancel")];
|
|
3901
4013
|
case "role":
|
|
3902
4014
|
return [keys("choose", "left", "right"), keys("assign/clear", "enter"), cancel("cancel")];
|
|
3903
4015
|
case "scope":
|
|
@@ -3931,6 +4043,8 @@ export class ModelHubComponent implements Component {
|
|
|
3931
4043
|
];
|
|
3932
4044
|
}
|
|
3933
4045
|
const row = this.#rolesRows[this.#roleIndex];
|
|
4046
|
+
const compaction =
|
|
4047
|
+
this.#callbacks.onCompactionPointChange && this.#roleRowModel() ? keys("compaction limit", "k") : undefined;
|
|
3934
4048
|
if (row?.kind === "fallback") {
|
|
3935
4049
|
// Advertise `t` only where a strip would open, as the ANSI footer does.
|
|
3936
4050
|
const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
|
|
@@ -3940,6 +4054,7 @@ export class ModelHubComponent implements Component {
|
|
|
3940
4054
|
keys("add another", "f"),
|
|
3941
4055
|
keys("remove", "x"),
|
|
3942
4056
|
editable ? keys("thinking", "t") : undefined,
|
|
4057
|
+
compaction,
|
|
3943
4058
|
reorder,
|
|
3944
4059
|
keys("providers", "left"),
|
|
3945
4060
|
];
|
|
@@ -3962,6 +4077,7 @@ export class ModelHubComponent implements Component {
|
|
|
3962
4077
|
keys("fallback", "f"),
|
|
3963
4078
|
keys("clear", "x"),
|
|
3964
4079
|
editable ? keys("thinking", "t") : undefined,
|
|
4080
|
+
compaction,
|
|
3965
4081
|
keys("cycle", "c"),
|
|
3966
4082
|
reorder,
|
|
3967
4083
|
keys("new", "n"),
|