@oh-my-pi/pi-tui 18.7.0 → 18.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -52
- package/dist/types/apps/debug/log-viewer.d.ts +4 -0
- package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
- package/dist/types/chat/fence-figure.d.ts +42 -0
- package/dist/types/chat/svg-figure.d.ts +1 -1
- package/dist/types/chat/tool-execution.d.ts +2 -0
- package/dist/types/components/editor.d.ts +8 -3
- package/dist/types/components/markdown.d.ts +11 -6
- package/dist/types/fuzzy.d.ts +17 -3
- package/dist/types/native/backend.d.ts +2 -1
- package/dist/types/native/blobs.d.ts +4 -2
- package/dist/types/native/encode.d.ts +3 -4
- package/dist/types/overlays/agent-transcript-viewer.d.ts +8 -1
- package/dist/types/overlays/history-search.d.ts +10 -0
- package/dist/types/overlays/model-browser.d.ts +6 -0
- package/dist/types/prompt/custom-editor.d.ts +7 -4
- package/dist/types/prompt/interactive-context-helpers.d.ts +2 -1
- package/dist/types/tools/renderer.d.ts +22 -0
- package/dist/types/tools/streaming-output.d.ts +11 -1
- package/dist/types/tools/write.d.ts +8 -1
- package/package.json +9 -9
- package/src/apps/debug/log-viewer.ts +188 -72
- package/src/apps/debug/raw-sse-buffer.ts +8 -0
- package/src/apps/debug/raw-sse.ts +39 -5
- package/src/apps/git/diff-pane.ts +26 -5
- package/src/autocomplete.ts +59 -33
- package/src/chat/assistant-message.ts +30 -18
- package/src/chat/fence-figure.ts +80 -0
- package/src/chat/svg-figure.ts +2 -4
- package/src/chat/tool-execution.ts +53 -4
- package/src/components/editor.ts +16 -3
- package/src/components/markdown.ts +80 -307
- package/src/fuzzy.ts +59 -14
- package/src/native/backend.ts +170 -15
- package/src/native/blobs.ts +4 -2
- package/src/native/encode.ts +58 -31
- package/src/overlays/agent-hub-projection.ts +70 -2
- package/src/overlays/agent-hub.ts +3 -2
- package/src/overlays/agent-transcript-viewer.ts +64 -13
- package/src/overlays/agents-hub.ts +14 -7
- package/src/overlays/copy-selector.ts +30 -8
- package/src/overlays/extensions/state-manager.ts +52 -12
- package/src/overlays/history-search.ts +100 -7
- package/src/overlays/jobs-panel.ts +30 -2
- package/src/overlays/model-browser.ts +107 -21
- package/src/overlays/model-hub.ts +34 -7
- package/src/overlays/oauth-selector.ts +41 -26
- package/src/overlays/plan-review-overlay.ts +142 -64
- package/src/overlays/rewind-selector.ts +86 -17
- package/src/overlays/session-selector.ts +24 -4
- package/src/overlays/settings-selector.ts +21 -6
- package/src/overlays/tree-selector.ts +20 -4
- package/src/overlays/usage-dashboard.ts +12 -15
- package/src/prompt/attachment-chips.ts +2 -1
- package/src/prompt/composer-attachments.ts +6 -4
- package/src/prompt/custom-editor.ts +48 -19
- package/src/prompt/interactive-context-helpers.ts +27 -18
- package/src/prompt/macos-spelling.ts +7 -0
- package/src/prompt/magic-keywords.ts +11 -2
- package/src/prompt/word-completion.ts +38 -5
- package/src/setup/scenes/splash.ts +109 -45
- package/src/status-line/component.ts +220 -64
- package/src/status-line/segments.ts +5 -2
- package/src/terminal.ts +3 -2
- package/src/tools/ast-edit.ts +59 -35
- package/src/tools/ast-grep.ts +35 -16
- package/src/tools/default-renderer.ts +35 -19
- package/src/tools/eval-format/index.ts +28 -2
- package/src/tools/eval-format/javascript.ts +7 -5
- package/src/tools/eval.ts +164 -78
- package/src/tools/grep.ts +81 -48
- package/src/tools/lsp.ts +81 -59
- package/src/tools/mcp.ts +98 -68
- package/src/tools/renderer.ts +27 -0
- package/src/tools/streaming-output.ts +120 -48
- package/src/tools/task.ts +111 -54
- package/src/tools/todo.ts +115 -29
- package/src/tools/wait.ts +34 -24
- package/src/tools/write.ts +77 -39
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
parseConfiguredThinkingLevel,
|
|
28
28
|
} from "../thinking";
|
|
29
29
|
import { thinkingLevelGlyph } from "../render/render-utils";
|
|
30
|
-
import { type ThemeColor, theme } from "../theme/theme";
|
|
30
|
+
import { type Theme, type ThemeColor, theme } from "../theme/theme";
|
|
31
31
|
import {
|
|
32
32
|
matchesSelectCancel,
|
|
33
33
|
matchesSelectDown,
|
|
@@ -887,6 +887,24 @@ function metaColumnsWidth(widths: readonly number[]): number {
|
|
|
887
887
|
/** What the per-row perf column shows at the current width. */
|
|
888
888
|
type PerfMode = "off" | "tps" | "full";
|
|
889
889
|
|
|
890
|
+
/** A measured perf aggregate and, when it came from one, its service tier. */
|
|
891
|
+
interface MeasuredPerf {
|
|
892
|
+
perf: ModelBrowserPerf;
|
|
893
|
+
tier?: string;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** Plain catalog metric cells of one row and their terminal widths, valid for one model and theme. */
|
|
897
|
+
interface MetricCells {
|
|
898
|
+
model: Model;
|
|
899
|
+
theme: Theme;
|
|
900
|
+
intelligence: string;
|
|
901
|
+
intelligenceWidth: number;
|
|
902
|
+
ctx: string;
|
|
903
|
+
ctxWidth: number;
|
|
904
|
+
cost: string;
|
|
905
|
+
costWidth: number;
|
|
906
|
+
}
|
|
907
|
+
|
|
890
908
|
/**
|
|
891
909
|
* The reusable browser component. Renders a fixed-height block
|
|
892
910
|
* (`maxVisible + LIST_ROW_START + DETAIL_ROWS` rows) so host mouse geometry
|
|
@@ -907,6 +925,14 @@ export class ModelBrowser implements Component {
|
|
|
907
925
|
/** Ranker for the menu's current item array; rebuilt when items or affinity change. */
|
|
908
926
|
#ranker?: { source: readonly ModelBrowserItem[]; ranker: ModelItemRanker };
|
|
909
927
|
#perf: ReadonlyMap<string, ModelBrowserPerf> = new Map();
|
|
928
|
+
/** Fastest tier aggregate per selector in {@link #perf}, built on the first lookup miss. */
|
|
929
|
+
#perfTiers:
|
|
930
|
+
| { perf: ReadonlyMap<string, ModelBrowserPerf>; size: number; best: Map<string, MeasuredPerf> }
|
|
931
|
+
| undefined;
|
|
932
|
+
/** Theme-formatted catalog metric cells per item; rebuilt when the item's model or the theme changes. */
|
|
933
|
+
#metricCells = new WeakMap<ModelBrowserItem, MetricCells>();
|
|
934
|
+
/** Non-separator items of the last ranked visible list, for {@link queryMatches}. */
|
|
935
|
+
#queryMatches: { visible: readonly ModelBrowserItem[]; matches: readonly ModelBrowserItem[] } | undefined;
|
|
910
936
|
#hoveredIndex: number | null = null;
|
|
911
937
|
#maxVisible = 10;
|
|
912
938
|
#showProvider: boolean;
|
|
@@ -1075,6 +1101,22 @@ export class ModelBrowser implements Component {
|
|
|
1075
1101
|
return this.#menu.visibleItems.length;
|
|
1076
1102
|
}
|
|
1077
1103
|
|
|
1104
|
+
/**
|
|
1105
|
+
* Base items matching the live query, best first, without the separator
|
|
1106
|
+
* row; undefined while the query is blank. Reuses the ranking the list
|
|
1107
|
+
* already ran, so hosts can derive match counts without ranking again.
|
|
1108
|
+
*/
|
|
1109
|
+
get queryMatches(): readonly ModelBrowserItem[] | undefined {
|
|
1110
|
+
if (!this.query.trim()) return undefined;
|
|
1111
|
+
const visible = this.#menu.visibleItems;
|
|
1112
|
+
let cached = this.#queryMatches;
|
|
1113
|
+
if (cached?.visible !== visible) {
|
|
1114
|
+
cached = { visible, matches: visible.filter(item => !this.#isDisabled(item)) };
|
|
1115
|
+
this.#queryMatches = cached;
|
|
1116
|
+
}
|
|
1117
|
+
return cached.matches;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1078
1120
|
/** Move selection to `selector`; false when it is not in the current view. */
|
|
1079
1121
|
selectSelector(selector: string): boolean {
|
|
1080
1122
|
if (!this.#menu.setSelectedKey(selector)) return false;
|
|
@@ -1339,7 +1381,8 @@ export class ModelBrowser implements Component {
|
|
|
1339
1381
|
* on a non-default tier still shows its real speed. `tier` is set whenever
|
|
1340
1382
|
* the numbers come from a tier aggregate, for the caller to label.
|
|
1341
1383
|
*/
|
|
1342
|
-
#perfFor(item: ModelBrowserItem):
|
|
1384
|
+
#perfFor(item: ModelBrowserItem): MeasuredPerf | undefined {
|
|
1385
|
+
if (this.#perf.size === 0) return undefined;
|
|
1343
1386
|
const tier = this.#settings.serviceTierFor?.(item.model);
|
|
1344
1387
|
if (tier) {
|
|
1345
1388
|
const tiered = this.#perf.get(`${item.selector}@${tier}`);
|
|
@@ -1347,13 +1390,48 @@ export class ModelBrowser implements Component {
|
|
|
1347
1390
|
}
|
|
1348
1391
|
const standard = this.#perf.get(item.selector);
|
|
1349
1392
|
if (standard) return { perf: standard };
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1393
|
+
return this.#fastestTierPerf(item.selector);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/** Fastest `selector@tier` aggregate (first wins ties), from an index built once per perf map. */
|
|
1397
|
+
#fastestTierPerf(selector: string): MeasuredPerf | undefined {
|
|
1398
|
+
const perfMap = this.#perf;
|
|
1399
|
+
let index = this.#perfTiers;
|
|
1400
|
+
if (index?.perf !== perfMap || index.size !== perfMap.size) {
|
|
1401
|
+
const best = new Map<string, MeasuredPerf>();
|
|
1402
|
+
for (const [key, perf] of perfMap) {
|
|
1403
|
+
// Every `@` may end the selector: `a@b@t` is tier `b@t` of `a` and tier `t` of `a@b`.
|
|
1404
|
+
for (let at = key.indexOf("@"); at >= 0; at = key.indexOf("@", at + 1)) {
|
|
1405
|
+
const owner = key.slice(0, at);
|
|
1406
|
+
const current = best.get(owner);
|
|
1407
|
+
if (!current || perf.tps > current.perf.tps) best.set(owner, { perf, tier: key.slice(at + 1) });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
index = { perf: perfMap, size: perfMap.size, best };
|
|
1411
|
+
this.#perfTiers = index;
|
|
1355
1412
|
}
|
|
1356
|
-
return best;
|
|
1413
|
+
return index.best.get(selector);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** Catalog metric cells of a row, formatted once per model and theme. */
|
|
1417
|
+
#metricCellsFor(item: ModelBrowserItem): MetricCells {
|
|
1418
|
+
const cached = this.#metricCells.get(item);
|
|
1419
|
+
if (cached?.model === item.model && cached.theme === theme) return cached;
|
|
1420
|
+
const intelligence = formatIntelligence(item.model);
|
|
1421
|
+
const ctx = formatContext(item.model);
|
|
1422
|
+
const cost = formatCostPair(item.model);
|
|
1423
|
+
const cells: MetricCells = {
|
|
1424
|
+
model: item.model,
|
|
1425
|
+
theme,
|
|
1426
|
+
intelligence,
|
|
1427
|
+
intelligenceWidth: visibleWidth(intelligence),
|
|
1428
|
+
ctx,
|
|
1429
|
+
ctxWidth: visibleWidth(ctx),
|
|
1430
|
+
cost,
|
|
1431
|
+
costWidth: visibleWidth(cost),
|
|
1432
|
+
};
|
|
1433
|
+
this.#metricCells.set(item, cells);
|
|
1434
|
+
return cells;
|
|
1357
1435
|
}
|
|
1358
1436
|
|
|
1359
1437
|
/** Measured TPS/TTFT, falling back to the catalog TPS as an estimated `~118t/s`. */
|
|
@@ -1381,7 +1459,8 @@ export class ModelBrowser implements Component {
|
|
|
1381
1459
|
costWidth: number,
|
|
1382
1460
|
intelligenceWidth: number,
|
|
1383
1461
|
perfWidth: number,
|
|
1384
|
-
|
|
1462
|
+
metrics: MetricCells,
|
|
1463
|
+
perfCell: string,
|
|
1385
1464
|
): string {
|
|
1386
1465
|
if (item.id === "separator") {
|
|
1387
1466
|
const dashCount = Math.max(0, width - 4);
|
|
@@ -1405,11 +1484,10 @@ export class ModelBrowser implements Component {
|
|
|
1405
1484
|
|
|
1406
1485
|
// Metric columns collapse when empty or when the row needs room for its name.
|
|
1407
1486
|
const cols: string[] = [];
|
|
1408
|
-
if (intelligenceWidth > 0)
|
|
1409
|
-
|
|
1410
|
-
if (
|
|
1411
|
-
if (
|
|
1412
|
-
if (costWidth > 0) cols.push(theme.fg("dim", padLeftVisible(formatCostPair(item.model), costWidth)));
|
|
1487
|
+
if (intelligenceWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.intelligence, intelligenceWidth)));
|
|
1488
|
+
if (perfWidth > 0) cols.push(theme.fg("dim", padLeftVisible(perfCell, perfWidth)));
|
|
1489
|
+
if (ctxWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.ctx, ctxWidth)));
|
|
1490
|
+
if (costWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.cost, costWidth)));
|
|
1413
1491
|
const metaWidth = metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
|
|
1414
1492
|
const available = Math.max(1, width - metaWidth - (cols.length > 0 ? 1 : 0));
|
|
1415
1493
|
left = truncateToWidth(left, available);
|
|
@@ -1515,15 +1593,22 @@ export class ModelBrowser implements Component {
|
|
|
1515
1593
|
const perfMode: PerfMode = width >= PERF_FULL_MIN_WIDTH ? "full" : width >= PERF_TPS_MIN_WIDTH ? "tps" : "off";
|
|
1516
1594
|
let intelligenceWidth = 0;
|
|
1517
1595
|
let perfWidth = 0;
|
|
1596
|
+
const visible = this.#menu.visibleItems;
|
|
1597
|
+
const windowMetrics: MetricCells[] = [];
|
|
1598
|
+
const windowPerf: string[] = [];
|
|
1518
1599
|
for (let i = startIndex; i < endIndex; i++) {
|
|
1519
|
-
const item =
|
|
1600
|
+
const item = visible[i];
|
|
1520
1601
|
if (!item) continue;
|
|
1521
|
-
|
|
1522
|
-
|
|
1602
|
+
const metrics = this.#metricCellsFor(item);
|
|
1603
|
+
const perfCell = this.#perfCell(item, perfMode);
|
|
1604
|
+
windowMetrics[i - startIndex] = metrics;
|
|
1605
|
+
windowPerf[i - startIndex] = perfCell;
|
|
1606
|
+
ctxWidth = Math.max(ctxWidth, metrics.ctxWidth);
|
|
1607
|
+
costWidth = Math.max(costWidth, metrics.costWidth);
|
|
1523
1608
|
if (perfMode !== "off") {
|
|
1524
|
-
intelligenceWidth = Math.max(intelligenceWidth,
|
|
1609
|
+
intelligenceWidth = Math.max(intelligenceWidth, metrics.intelligenceWidth);
|
|
1525
1610
|
}
|
|
1526
|
-
perfWidth = Math.max(perfWidth, visibleWidth(
|
|
1611
|
+
perfWidth = Math.max(perfWidth, visibleWidth(perfCell));
|
|
1527
1612
|
}
|
|
1528
1613
|
// Preserve at least a readable name by dropping cost, then context.
|
|
1529
1614
|
let nameRoom = width - 2 - metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
|
|
@@ -1533,7 +1618,7 @@ export class ModelBrowser implements Component {
|
|
|
1533
1618
|
|
|
1534
1619
|
const rows: string[] = [];
|
|
1535
1620
|
for (let i = startIndex; i < endIndex; i++) {
|
|
1536
|
-
const item =
|
|
1621
|
+
const item = visible[i];
|
|
1537
1622
|
if (!item) continue;
|
|
1538
1623
|
rows.push(
|
|
1539
1624
|
this.#renderRow(
|
|
@@ -1545,7 +1630,8 @@ export class ModelBrowser implements Component {
|
|
|
1545
1630
|
costWidth,
|
|
1546
1631
|
intelligenceWidth,
|
|
1547
1632
|
perfWidth,
|
|
1548
|
-
|
|
1633
|
+
windowMetrics[i - startIndex]!,
|
|
1634
|
+
windowPerf[i - startIndex]!,
|
|
1549
1635
|
),
|
|
1550
1636
|
);
|
|
1551
1637
|
}
|
|
@@ -28,7 +28,7 @@ import { providerEntry } from "@oh-my-pi/pi-catalog/compat/providers";
|
|
|
28
28
|
import { MODEL_KINDS, modelKind, type ModelKind } from "@oh-my-pi/pi-catalog/types";
|
|
29
29
|
import type { Component, TUI } from "../tui";
|
|
30
30
|
import { extractPrintableText, matchesKey } from "../keys";
|
|
31
|
-
import {
|
|
31
|
+
import { FuzzyCorpus } from "../fuzzy";
|
|
32
32
|
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
33
33
|
import { boundKeys, editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
34
34
|
import type { KeyName } from "../key-hint-format";
|
|
@@ -334,7 +334,15 @@ export class ModelHubComponent implements Component {
|
|
|
334
334
|
#roles: RoleAssignments = {};
|
|
335
335
|
#availableItems: ModelBrowserItem[] = [];
|
|
336
336
|
#recentItems: ModelBrowserItem[] = [];
|
|
337
|
+
/** Selectors of {@link #recentItems}, rebuilt with it, for search hit counts. */
|
|
338
|
+
#recentSelectors: ReadonlySet<string> = new Set();
|
|
337
339
|
#candidateItems: ModelBrowserItem[] = [];
|
|
340
|
+
/** {@link #availableItems} when the candidates are that whole catalog (All scope, no role filter). */
|
|
341
|
+
#candidateCatalog: readonly ModelBrowserItem[] | undefined;
|
|
342
|
+
/** {@link #availableItems} when the browser's base items are that whole catalog; its query ranking then covers every count. */
|
|
343
|
+
#browserCatalog: readonly ModelBrowserItem[] | undefined;
|
|
344
|
+
/** Fuzzy index over {@link #availableItems}, for match counts while the browser holds a narrower scope. */
|
|
345
|
+
#catalogCorpus: { items: readonly ModelBrowserItem[]; corpus: FuzzyCorpus<ModelBrowserItem> } | undefined;
|
|
338
346
|
#modelKindTab: "all" | ModelKind = "all";
|
|
339
347
|
#roleTab: RoleTab = "all";
|
|
340
348
|
#configError: string | undefined;
|
|
@@ -587,6 +595,7 @@ export class ModelHubComponent implements Component {
|
|
|
587
595
|
if (item) this.#recentItems.push(item);
|
|
588
596
|
if (this.#recentItems.length >= RECENT_LIMIT) break;
|
|
589
597
|
}
|
|
598
|
+
this.#recentSelectors = new Set(this.#recentItems.map(item => item.selector));
|
|
590
599
|
|
|
591
600
|
this.#buildSidebar(allModels, availableModels);
|
|
592
601
|
this.#restoreSidebarAnchor(anchor);
|
|
@@ -823,11 +832,13 @@ export class ModelHubComponent implements Component {
|
|
|
823
832
|
? items.filter(item => this.#settings.getRoleInfo(assigning.role).accepts(item.model))
|
|
824
833
|
: items;
|
|
825
834
|
this.#candidateItems = [...scoped];
|
|
835
|
+
this.#candidateCatalog = scoped === this.#availableItems ? this.#availableItems : undefined;
|
|
826
836
|
this.#applyModelKind();
|
|
827
837
|
}
|
|
828
838
|
|
|
829
839
|
#applyModelKind(): void {
|
|
830
840
|
const kind = this.#modelKindTab;
|
|
841
|
+
this.#browserCatalog = kind === "all" ? this.#candidateCatalog : undefined;
|
|
831
842
|
this.#browser.setItems(
|
|
832
843
|
kind === "all"
|
|
833
844
|
? [...this.#candidateItems]
|
|
@@ -936,16 +947,14 @@ export class ModelHubComponent implements Component {
|
|
|
936
947
|
this.#composeEntries();
|
|
937
948
|
return;
|
|
938
949
|
}
|
|
939
|
-
const matches =
|
|
950
|
+
const matches = this.#catalogMatches(query);
|
|
940
951
|
const counts = new Map<string, number>();
|
|
952
|
+
let recentCount = 0;
|
|
941
953
|
for (const item of matches) {
|
|
942
954
|
counts.set(item.provider, (counts.get(item.provider) ?? 0) + 1);
|
|
955
|
+
if (this.#recentSelectors.has(item.selector)) recentCount++;
|
|
943
956
|
}
|
|
944
|
-
|
|
945
|
-
this.#recentSearchCount = matches.reduce(
|
|
946
|
-
(total, item) => total + (recentSelectors.has(item.selector) ? 1 : 0),
|
|
947
|
-
0,
|
|
948
|
-
);
|
|
957
|
+
this.#recentSearchCount = recentCount;
|
|
949
958
|
this.#searchTotal = matches.length;
|
|
950
959
|
this.#searchCounts = counts;
|
|
951
960
|
this.#composeEntries();
|
|
@@ -959,6 +968,24 @@ export class ModelHubComponent implements Component {
|
|
|
959
968
|
}
|
|
960
969
|
}
|
|
961
970
|
|
|
971
|
+
/**
|
|
972
|
+
* Catalog items matching a non-blank `query`. When the browser holds the
|
|
973
|
+
* whole catalog its ranking for this query already ran, so reuse it;
|
|
974
|
+
* otherwise scan a catalog index kept across keystrokes.
|
|
975
|
+
*/
|
|
976
|
+
#catalogMatches(query: string): readonly ModelBrowserItem[] {
|
|
977
|
+
if (this.#browserCatalog === this.#availableItems && this.#browser.query === query) {
|
|
978
|
+
const ranked = this.#browser.queryMatches;
|
|
979
|
+
if (ranked) return ranked;
|
|
980
|
+
}
|
|
981
|
+
let cached = this.#catalogCorpus;
|
|
982
|
+
if (cached?.items !== this.#availableItems) {
|
|
983
|
+
cached = { items: this.#availableItems, corpus: new FuzzyCorpus(this.#availableItems, modelSearchText) };
|
|
984
|
+
this.#catalogCorpus = cached;
|
|
985
|
+
}
|
|
986
|
+
return cached.corpus.rank(query).map(result => result.item);
|
|
987
|
+
}
|
|
988
|
+
|
|
962
989
|
/**
|
|
963
990
|
* Entries the scope hop skips: separators always; while searching, also
|
|
964
991
|
* the Roles view (not a model scope), an empty Recent, locked providers,
|
|
@@ -51,6 +51,8 @@ const ORIGIN_LABELS = {
|
|
|
51
51
|
*/
|
|
52
52
|
export class OAuthSelectorComponent extends OverlayPanel {
|
|
53
53
|
#listContainer: Container;
|
|
54
|
+
/** Provider rows viewport of the last {@link #updateList}; spinner ticks repaint only its rows. */
|
|
55
|
+
#listView: ScrollView | undefined;
|
|
54
56
|
#menu: MenuSelection<OAuthProviderInfo>;
|
|
55
57
|
/** The provider search field; its value drives `#menu`'s query. */
|
|
56
58
|
#search = Object.assign(new Input(), { prompt: "" });
|
|
@@ -213,7 +215,13 @@ export class OAuthSelectorComponent extends OverlayPanel {
|
|
|
213
215
|
if (frameCount > 0) {
|
|
214
216
|
this.#spinnerFrame = (this.#spinnerFrame + 1) % frameCount;
|
|
215
217
|
}
|
|
216
|
-
|
|
218
|
+
// Only the provider rows carry the spinner glyph; the window is unchanged.
|
|
219
|
+
if (this.#listView) {
|
|
220
|
+
const start = this.#scrollStart;
|
|
221
|
+
this.#listView.setLines(this.#providerRows(start, start + this.#visibleCount));
|
|
222
|
+
} else {
|
|
223
|
+
this.#updateList();
|
|
224
|
+
}
|
|
217
225
|
this.#requestRenderCallback?.();
|
|
218
226
|
}, 80);
|
|
219
227
|
}
|
|
@@ -310,37 +318,15 @@ export class OAuthSelectorComponent extends OverlayPanel {
|
|
|
310
318
|
this.#nativeRoot = undefined;
|
|
311
319
|
this.#pickerRoot = undefined;
|
|
312
320
|
this.#listContainer.clear();
|
|
321
|
+
this.#listView = undefined;
|
|
313
322
|
|
|
314
|
-
const
|
|
315
|
-
const total = items.length;
|
|
323
|
+
const total = this.#menu.visibleItems.length;
|
|
316
324
|
const maxVisible = this.#maxVisible;
|
|
317
325
|
const { start: startIndex, end: endIndex } = centeredViewportRange(this.#menu.selectedIndex, total, maxVisible);
|
|
318
326
|
this.#scrollStart = startIndex;
|
|
319
327
|
this.#visibleCount = endIndex - startIndex;
|
|
320
328
|
|
|
321
|
-
const rows
|
|
322
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
323
|
-
const provider = items[i];
|
|
324
|
-
if (!provider) continue;
|
|
325
|
-
const isSelected = i === this.#menu.selectedIndex;
|
|
326
|
-
const isAvailable = provider.available;
|
|
327
|
-
const statusIndicator = this.#getStatusIndicator(provider.id);
|
|
328
|
-
|
|
329
|
-
let line = "";
|
|
330
|
-
if (isSelected) {
|
|
331
|
-
const prefix = theme.fg("accent", `${theme.nav.cursor} `);
|
|
332
|
-
const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
|
|
333
|
-
line = prefix + text + statusIndicator;
|
|
334
|
-
} else {
|
|
335
|
-
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
|
336
|
-
line = text + statusIndicator;
|
|
337
|
-
}
|
|
338
|
-
if (!isSelected && i === this.#hoveredIndex) {
|
|
339
|
-
line = theme.bg("selectedBg", line);
|
|
340
|
-
}
|
|
341
|
-
rows.push(line);
|
|
342
|
-
}
|
|
343
|
-
|
|
329
|
+
const rows = this.#providerRows(startIndex, endIndex);
|
|
344
330
|
if (rows.length > 0) {
|
|
345
331
|
const sv = new ScrollView(rows, {
|
|
346
332
|
height: rows.length,
|
|
@@ -349,6 +335,7 @@ export class OAuthSelectorComponent extends OverlayPanel {
|
|
|
349
335
|
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
350
336
|
});
|
|
351
337
|
sv.setScrollOffset(startIndex);
|
|
338
|
+
this.#listView = sv;
|
|
352
339
|
this.#listContainer.addChild(sv);
|
|
353
340
|
}
|
|
354
341
|
|
|
@@ -371,6 +358,34 @@ export class OAuthSelectorComponent extends OverlayPanel {
|
|
|
371
358
|
this.#listContainer.addChild(new TruncatedText(theme.fg("warning", this.#statusMessage), 0, 0));
|
|
372
359
|
}
|
|
373
360
|
}
|
|
361
|
+
|
|
362
|
+
/** Styled rows for visible providers `[startIndex, endIndex)`: cursor, name, auth status and provenance. */
|
|
363
|
+
#providerRows(startIndex: number, endIndex: number): string[] {
|
|
364
|
+
const items = this.#menu.visibleItems;
|
|
365
|
+
const rows: string[] = [];
|
|
366
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
367
|
+
const provider = items[i];
|
|
368
|
+
if (!provider) continue;
|
|
369
|
+
const isSelected = i === this.#menu.selectedIndex;
|
|
370
|
+
const isAvailable = provider.available;
|
|
371
|
+
const statusIndicator = this.#getStatusIndicator(provider.id);
|
|
372
|
+
|
|
373
|
+
let line = "";
|
|
374
|
+
if (isSelected) {
|
|
375
|
+
const prefix = theme.fg("accent", `${theme.nav.cursor} `);
|
|
376
|
+
const text = isAvailable ? theme.fg("accent", provider.name) : theme.fg("dim", provider.name);
|
|
377
|
+
line = prefix + text + statusIndicator;
|
|
378
|
+
} else {
|
|
379
|
+
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
|
380
|
+
line = text + statusIndicator;
|
|
381
|
+
}
|
|
382
|
+
if (!isSelected && i === this.#hoveredIndex) {
|
|
383
|
+
line = theme.bg("selectedBg", line);
|
|
384
|
+
}
|
|
385
|
+
rows.push(line);
|
|
386
|
+
}
|
|
387
|
+
return rows;
|
|
388
|
+
}
|
|
374
389
|
handleInput(keyData: string): void {
|
|
375
390
|
// Escape or Ctrl+C
|
|
376
391
|
if (matchesSelectCancel(keyData)) {
|