@oh-my-pi/pi-coding-agent 16.4.6 → 16.5.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 +88 -0
- package/dist/cli.js +3312 -3254
- package/dist/types/cli/args.d.ts +5 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +15 -0
- package/dist/types/config/model-resolver.d.ts +4 -3
- package/dist/types/config/model-roles.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +46 -14
- package/dist/types/extensibility/extensions/types.d.ts +1 -1
- package/dist/types/launch/broker.d.ts +2 -0
- package/dist/types/launch/client.d.ts +22 -0
- package/dist/types/launch/paths.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +8 -0
- package/dist/types/launch/protocol.d.ts +170 -0
- package/dist/types/launch/terminal-output.d.ts +7 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -1
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-browser.d.ts +28 -3
- package/dist/types/modes/components/model-hub.d.ts +0 -10
- package/dist/types/modes/components/model-picker.d.ts +44 -0
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/print-mode.d.ts +14 -8
- package/dist/types/modes/print-mode.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/sdk.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +42 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/thinking.d.ts +2 -2
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
- package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
- package/dist/types/tools/browser/tab-worker.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/launch.d.ts +121 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/terminal-output.d.ts +5 -0
- package/dist/types/vibe/runtime.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +0 -8
- package/package.json +20 -20
- package/src/cli/args.ts +11 -0
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gallery-fixtures/shell.ts +82 -1
- package/src/cli.ts +10 -0
- package/src/commands/launch.ts +17 -0
- package/src/config/model-resolver.ts +110 -31
- package/src/config/model-roles.ts +14 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +2 -2
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/completion-bridge.ts +4 -4
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/extensions/model-api.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/internal-urls/docs-index.ts +4 -4
- package/src/launch/broker.ts +1017 -0
- package/src/launch/client.ts +344 -0
- package/src/launch/paths.ts +17 -0
- package/src/launch/presence.ts +82 -0
- package/src/launch/protocol.ts +386 -0
- package/src/launch/terminal-output.ts +46 -0
- package/src/main.ts +50 -1
- package/src/modes/acp/acp-agent.ts +8 -1
- package/src/modes/components/agent-hub.ts +101 -31
- package/src/modes/components/compaction-summary-message.ts +8 -2
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/model-browser.ts +152 -49
- package/src/modes/components/model-hub.ts +55 -142
- package/src/modes/components/model-picker.ts +233 -0
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
- package/src/modes/components/status-line/component.test.ts +41 -2
- package/src/modes/components/status-line/component.ts +4 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +9 -1
- package/src/modes/controllers/event-controller.ts +31 -32
- package/src/modes/controllers/selector-controller.ts +96 -22
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +20 -3
- package/src/modes/print-mode.test.ts +71 -0
- package/src/modes/print-mode.ts +51 -2
- package/src/modes/theme/theme.ts +9 -0
- package/src/modes/utils/ui-helpers.ts +25 -4
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/scout.md +1 -1
- package/src/prompts/system/plan-yolo-handoff.md +5 -0
- package/src/prompts/system/prewalk-checklist.md +7 -0
- package/src/prompts/system/prewalk-continue.md +1 -0
- package/src/prompts/system/prewalk-plan.md +13 -0
- package/src/prompts/system/system-prompt.md +9 -7
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/browser.md +4 -4
- package/src/prompts/tools/launch.md +25 -0
- package/src/sdk.ts +8 -2
- package/src/session/agent-session.ts +550 -87
- package/src/session/session-context.test.ts +10 -5
- package/src/session/session-context.ts +25 -8
- package/src/session/session-entries.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +25 -0
- package/src/task/agents.ts +2 -2
- package/src/thinking.ts +10 -3
- package/src/tiny/models.ts +7 -7
- package/src/tools/bash-interactive.ts +5 -8
- package/src/tools/bash.ts +38 -24
- package/src/tools/browser/cmux/cmux-tab.ts +17 -2
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +66 -6
- package/src/tools/browser/tab-protocol.ts +6 -0
- package/src/tools/browser/tab-supervisor.ts +17 -1
- package/src/tools/browser/tab-worker.ts +140 -21
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/index.ts +5 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/launch.ts +643 -0
- package/src/tools/render-utils.ts +3 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/terminal-output.ts +141 -0
- package/src/tts/speech-enhancer.ts +2 -2
- package/src/utils/image-vision-fallback.ts +3 -3
- package/src/vibe/runtime.ts +2 -2
- package/src/web/search/provider.ts +0 -10
- package/src/web/search/providers/perplexity.ts +18 -2
- package/src/web/search/providers/public.ts +2 -4
- package/src/web/search/types.ts +0 -10
- package/dist/types/web/search/providers/bing.d.ts +0 -14
- package/dist/types/web/search/providers/yahoo.d.ts +0 -14
- package/src/web/search/providers/bing.ts +0 -197
- package/src/web/search/providers/yahoo.ts +0 -179
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* {@link ModelBrowser} body. The Roles view manages assignments directly:
|
|
7
7
|
* pick a role, pick a model, adjust thinking in an inline strip, or clear the
|
|
8
8
|
* role back to auto-selection. Locked providers forward to the /login flow.
|
|
9
|
-
* Fully mouse-navigable (hover, wheel, click).
|
|
9
|
+
* Fully mouse-navigable (hover, wheel, click). Session-only switching lives
|
|
10
|
+
* in the compact alt+p picker ({@link ./model-picker}).
|
|
10
11
|
*/
|
|
11
12
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
12
13
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
@@ -27,15 +28,9 @@ import {
|
|
|
27
28
|
visibleWidth,
|
|
28
29
|
} from "@oh-my-pi/pi-tui";
|
|
29
30
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
30
|
-
import { getModelMatchPreferences, resolveModelRoleValue } from "../../config/model-resolver";
|
|
31
31
|
import { getKnownRoleIds, getRoleInfo } from "../../config/model-roles";
|
|
32
32
|
import type { Settings } from "../../config/settings";
|
|
33
|
-
import {
|
|
34
|
-
AUTO_THINKING,
|
|
35
|
-
type ConfiguredThinkingLevel,
|
|
36
|
-
getConfiguredThinkingLevelMetadata,
|
|
37
|
-
parseConfiguredThinkingLevel,
|
|
38
|
-
} from "../../thinking";
|
|
33
|
+
import { AUTO_THINKING, type ConfiguredThinkingLevel, getConfiguredThinkingLevelMetadata } from "../../thinking";
|
|
39
34
|
import { theme } from "../theme/theme";
|
|
40
35
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../utils/keybinding-matchers";
|
|
41
36
|
import {
|
|
@@ -43,15 +38,13 @@ import {
|
|
|
43
38
|
ModelBrowser,
|
|
44
39
|
type ModelBrowserItem,
|
|
45
40
|
type RoleAssignments,
|
|
41
|
+
resolveRoleAssignments,
|
|
46
42
|
sortModelItems,
|
|
47
43
|
thinkingLevelGlyph,
|
|
48
44
|
} from "./model-browser";
|
|
49
45
|
import { bottomBorder, dividerSplit, row, splitBodyWidth, splitRow, topBorderSplit } from "./overlay-box";
|
|
50
46
|
import { renderSegmentTrack } from "./segment-track";
|
|
51
47
|
|
|
52
|
-
/** `roles` is the full /models hub; `pick` is a one-shot session/embedded picker. */
|
|
53
|
-
export type ModelHubMode = "roles" | "pick";
|
|
54
|
-
|
|
55
48
|
/**
|
|
56
49
|
* A row of the Roles view: a role, a model/wildcard chain-key header, one of a
|
|
57
50
|
* chain's fallback entries, or the trailing "+ New role…". Fallback rows under
|
|
@@ -89,8 +82,6 @@ export interface ModelHubCallbacks {
|
|
|
89
82
|
onUnassign: (role: string) => void;
|
|
90
83
|
/** Persist a `retry.fallbackChains` entry — keyed by a role, `provider/model-id`, or `provider/*`; an empty chain clears the key. */
|
|
91
84
|
onFallbackChainChange?: (role: string, chain: string[]) => void;
|
|
92
|
-
/** Pick-mode activation: session-only switch or embedded pick. */
|
|
93
|
-
onPick?: (model: Model, selector: string) => void;
|
|
94
85
|
/** Locked provider activation: forward to the /login flow. */
|
|
95
86
|
onLoginRequest?: (providerId: string) => void;
|
|
96
87
|
/** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
|
|
@@ -99,14 +90,8 @@ export interface ModelHubCallbacks {
|
|
|
99
90
|
}
|
|
100
91
|
|
|
101
92
|
export interface ModelHubOptions {
|
|
102
|
-
mode?: ModelHubMode;
|
|
103
|
-
/** Session token count; in pick mode, models with smaller context windows are disabled. */
|
|
104
|
-
currentContextTokens?: number;
|
|
105
93
|
/** Preselect this provider's sidebar entry (e.g. when reopening after /login). */
|
|
106
94
|
initialProviderId?: string;
|
|
107
|
-
/** Status-row hint shown in pick mode. */
|
|
108
|
-
pickerHint?: string;
|
|
109
|
-
initialQuery?: string;
|
|
110
95
|
}
|
|
111
96
|
|
|
112
97
|
interface SidebarEntry {
|
|
@@ -158,8 +143,6 @@ const RECENT_LIMIT = 15;
|
|
|
158
143
|
const SIDEBAR_MIN_WIDTH = 18;
|
|
159
144
|
const SIDEBAR_MAX_WIDTH = 26;
|
|
160
145
|
|
|
161
|
-
const PICK_MODE_HINT = "Session-only switch — role models stay unchanged";
|
|
162
|
-
|
|
163
146
|
/**
|
|
164
147
|
* Providers already auto-refreshed this process. Selecting a provider fetches
|
|
165
148
|
* its live model list at most once per application lifetime (surviving hub
|
|
@@ -182,8 +165,6 @@ export class ModelHubComponent implements Component {
|
|
|
182
165
|
#registry: ModelRegistry;
|
|
183
166
|
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
184
167
|
#callbacks: ModelHubCallbacks;
|
|
185
|
-
#mode: ModelHubMode;
|
|
186
|
-
#pickerHint: string;
|
|
187
168
|
|
|
188
169
|
#browser: ModelBrowser;
|
|
189
170
|
#roles: RoleAssignments = {};
|
|
@@ -250,14 +231,9 @@ export class ModelHubComponent implements Component {
|
|
|
250
231
|
this.#registry = registry;
|
|
251
232
|
this.#scopedModels = scopedModels;
|
|
252
233
|
this.#callbacks = callbacks;
|
|
253
|
-
this.#mode = options.mode ?? "roles";
|
|
254
|
-
this.#pickerHint = options.pickerHint ?? PICK_MODE_HINT;
|
|
255
234
|
|
|
256
235
|
this.#browser = new ModelBrowser(settings, {
|
|
257
|
-
currentContextTokens: options.currentContextTokens,
|
|
258
|
-
disableOverContext: this.#mode === "pick",
|
|
259
236
|
emptyText: () => this.#emptyStateMessage(),
|
|
260
|
-
initialQuery: options.initialQuery,
|
|
261
237
|
});
|
|
262
238
|
this.#browser.onActivate = item => this.#activateItem(item);
|
|
263
239
|
this.#browser.onCancel = () => this.#callbacks.onCancel();
|
|
@@ -275,10 +251,6 @@ export class ModelHubComponent implements Component {
|
|
|
275
251
|
this.#setActiveEntry("all");
|
|
276
252
|
}
|
|
277
253
|
|
|
278
|
-
if (this.#mode === "pick") {
|
|
279
|
-
this.#focus = "list";
|
|
280
|
-
}
|
|
281
|
-
|
|
282
254
|
// Reconcile with cached discovery state in the background. A --models
|
|
283
255
|
// scope is registry-independent, so the offline reload would only repeat
|
|
284
256
|
// the synchronous hydration above.
|
|
@@ -314,62 +286,10 @@ export class ModelHubComponent implements Component {
|
|
|
314
286
|
return getKnownRoleIds(this.#settings).filter(role => !getRoleInfo(role, this.#settings).hidden);
|
|
315
287
|
}
|
|
316
288
|
|
|
317
|
-
#getResolvedRoleThinkingLevel(
|
|
318
|
-
role: string,
|
|
319
|
-
resolved: { explicitThinkingLevel: boolean; thinkingLevel?: ConfiguredThinkingLevel },
|
|
320
|
-
): ConfiguredThinkingLevel {
|
|
321
|
-
if (resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined) {
|
|
322
|
-
return resolved.thinkingLevel;
|
|
323
|
-
}
|
|
324
|
-
if (role === "default") {
|
|
325
|
-
return parseConfiguredThinkingLevel(this.#settings.get("defaultThinkingLevel")) ?? ThinkingLevel.Inherit;
|
|
326
|
-
}
|
|
327
|
-
return ThinkingLevel.Inherit;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
289
|
/** Resolve every known role: configured values first, auto-selection for the rest. */
|
|
331
290
|
#reloadRoles(autoCandidates: ReadonlyArray<Model>): void {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const matchPreferences = getModelMatchPreferences(this.#settings);
|
|
335
|
-
const knownRoles = getKnownRoleIds(this.#settings);
|
|
336
|
-
const configuredRoles = new Set<string>();
|
|
337
|
-
|
|
338
|
-
for (const role of knownRoles) {
|
|
339
|
-
const roleValue = this.#settings.getModelRole(role);
|
|
340
|
-
if (!roleValue) continue;
|
|
341
|
-
configuredRoles.add(role);
|
|
342
|
-
const resolved = resolveModelRoleValue(roleValue, allModels, {
|
|
343
|
-
settings: this.#settings,
|
|
344
|
-
matchPreferences,
|
|
345
|
-
});
|
|
346
|
-
if (resolved.model) {
|
|
347
|
-
nextRoles[role] = {
|
|
348
|
-
model: resolved.model,
|
|
349
|
-
thinkingLevel: this.#getResolvedRoleThinkingLevel(role, resolved),
|
|
350
|
-
autoSelected: false,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (autoCandidates.length > 0) {
|
|
356
|
-
const candidates = [...autoCandidates];
|
|
357
|
-
for (const role of knownRoles) {
|
|
358
|
-
if (configuredRoles.has(role)) continue;
|
|
359
|
-
const resolved = resolveModelRoleValue(`pi/${role}`, candidates, {
|
|
360
|
-
settings: this.#settings,
|
|
361
|
-
matchPreferences,
|
|
362
|
-
});
|
|
363
|
-
if (!resolved.model) continue;
|
|
364
|
-
nextRoles[role] = {
|
|
365
|
-
model: resolved.model,
|
|
366
|
-
thinkingLevel: this.#getResolvedRoleThinkingLevel(role, resolved),
|
|
367
|
-
autoSelected: true,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
this.#roles = nextRoles;
|
|
291
|
+
const allModels = this.#scopedModels.length > 0 ? autoCandidates : this.#registry.getAll();
|
|
292
|
+
this.#roles = resolveRoleAssignments(this.#settings, allModels, autoCandidates);
|
|
373
293
|
}
|
|
374
294
|
|
|
375
295
|
/** Rebuild items, roles, and the sidebar from the registry's in-memory state. */
|
|
@@ -461,7 +381,7 @@ export class ModelHubComponent implements Component {
|
|
|
461
381
|
label: providerId,
|
|
462
382
|
providerId,
|
|
463
383
|
locked: isLocked,
|
|
464
|
-
annotation: isLocked ?
|
|
384
|
+
annotation: isLocked ? undefined : String(availableCounts.get(providerId) ?? 0),
|
|
465
385
|
oauth: oauthIds.has(providerId),
|
|
466
386
|
catalogCount: catalogCounts.get(providerId) ?? 0,
|
|
467
387
|
});
|
|
@@ -475,16 +395,15 @@ export class ModelHubComponent implements Component {
|
|
|
475
395
|
|
|
476
396
|
// Roles leads the fixed section so downward hops from Recent head into
|
|
477
397
|
// model scopes instead of being captured by the roles view.
|
|
478
|
-
const fixed: SidebarEntry[] = [
|
|
479
|
-
|
|
480
|
-
fixed.push({
|
|
398
|
+
const fixed: SidebarEntry[] = [
|
|
399
|
+
{
|
|
481
400
|
id: "roles",
|
|
482
401
|
kind: "roles",
|
|
483
402
|
label: "Roles",
|
|
484
403
|
annotation: `${assignedCount}/${visibleRoles.length}`,
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
404
|
+
},
|
|
405
|
+
{ id: "all", kind: "all", label: "All models", annotation: String(availableModels.length) },
|
|
406
|
+
];
|
|
488
407
|
|
|
489
408
|
this.#fixedEntries = fixed;
|
|
490
409
|
this.#unlockedProviderEntries = [...unlocked]
|
|
@@ -811,10 +730,6 @@ export class ModelHubComponent implements Component {
|
|
|
811
730
|
// ═══════════════════════════════════════════════════════════════════════
|
|
812
731
|
|
|
813
732
|
#activateItem(item: ModelBrowserItem): void {
|
|
814
|
-
if (this.#mode === "pick") {
|
|
815
|
-
this.#callbacks.onPick?.(item.model, item.selector);
|
|
816
|
-
return;
|
|
817
|
-
}
|
|
818
733
|
if (this.#assigning) {
|
|
819
734
|
const target = this.#assigning;
|
|
820
735
|
this.#assigning = null;
|
|
@@ -917,7 +832,7 @@ export class ModelHubComponent implements Component {
|
|
|
917
832
|
const strip = this.#strip;
|
|
918
833
|
this.#strip = null;
|
|
919
834
|
this.#chipRanges = [];
|
|
920
|
-
if (strip?.kind === "thinking" && strip.returnToRoles
|
|
835
|
+
if (strip?.kind === "thinking" && strip.returnToRoles) {
|
|
921
836
|
this.#setActiveEntry("roles");
|
|
922
837
|
this.#focus = "list";
|
|
923
838
|
}
|
|
@@ -1030,14 +945,12 @@ export class ModelHubComponent implements Component {
|
|
|
1030
945
|
}
|
|
1031
946
|
this.#setFallbackChain(target.role, chain);
|
|
1032
947
|
this.#browser.setQuery("");
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
if (rowIndex >= 0) this.#roleIndex = rowIndex;
|
|
1040
|
-
}
|
|
948
|
+
this.#setActiveEntry("roles");
|
|
949
|
+
this.#focus = "list";
|
|
950
|
+
const rowIndex = this.#rolesRows.findIndex(
|
|
951
|
+
row => row.kind === "fallback" && row.role === target.role && row.selector === selector,
|
|
952
|
+
);
|
|
953
|
+
if (rowIndex >= 0) this.#roleIndex = rowIndex;
|
|
1041
954
|
}
|
|
1042
955
|
|
|
1043
956
|
/** Persist `role`'s chain through the host callback and rebuild dependent state. */
|
|
@@ -1076,10 +989,8 @@ export class ModelHubComponent implements Component {
|
|
|
1076
989
|
#cancelAssign(): void {
|
|
1077
990
|
this.#assigning = null;
|
|
1078
991
|
this.#browser.setQuery("");
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
this.#focus = "list";
|
|
1082
|
-
}
|
|
992
|
+
this.#setActiveEntry("roles");
|
|
993
|
+
this.#focus = "list";
|
|
1083
994
|
}
|
|
1084
995
|
|
|
1085
996
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1468,6 +1379,10 @@ export class ModelHubComponent implements Component {
|
|
|
1468
1379
|
this.#roleHover = null;
|
|
1469
1380
|
if (overBody && this.#isBrowserView(entry)) {
|
|
1470
1381
|
this.#browser.routeMouse(event, bodyLine);
|
|
1382
|
+
} else {
|
|
1383
|
+
// Pointer left the browser pane: without this, the last
|
|
1384
|
+
// hovered row keeps its band while the sidebar hovers too.
|
|
1385
|
+
this.#browser.clearHover();
|
|
1471
1386
|
}
|
|
1472
1387
|
}
|
|
1473
1388
|
return true;
|
|
@@ -1578,11 +1493,10 @@ export class ModelHubComponent implements Component {
|
|
|
1578
1493
|
// While searching, entries the hop skips gray out: locked and
|
|
1579
1494
|
// zero-match providers, an empty Recent, and the Roles view.
|
|
1580
1495
|
const muted = entry.locked || matchCount === 0 || (searching && entry.kind === "roles");
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
: " ";
|
|
1496
|
+
// The sidebar's active entry is state, not a cursor: accent label
|
|
1497
|
+
// plus a cursor glyph while the sidebar owns the arrows. The band
|
|
1498
|
+
// stays in the body pane so the two never look alike.
|
|
1499
|
+
const cursor = active && this.#focus === "scope" ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
1586
1500
|
|
|
1587
1501
|
let icon: string;
|
|
1588
1502
|
if (entry.kind === "recent") {
|
|
@@ -1597,7 +1511,7 @@ export class ModelHubComponent implements Component {
|
|
|
1597
1511
|
const labelStyled = muted
|
|
1598
1512
|
? theme.fg("dim", entry.label)
|
|
1599
1513
|
: active
|
|
1600
|
-
? theme.fg("accent", entry.label)
|
|
1514
|
+
? theme.bold(theme.fg("accent", entry.label))
|
|
1601
1515
|
: entry.label;
|
|
1602
1516
|
|
|
1603
1517
|
const refreshing = entry.providerId ? this.#refreshingProviders.has(entry.providerId) : false;
|
|
@@ -1614,8 +1528,10 @@ export class ModelHubComponent implements Component {
|
|
|
1614
1528
|
line = `${left}${" ".repeat(width - leftWidth - annWidth)}${annotationStyled}`;
|
|
1615
1529
|
} else {
|
|
1616
1530
|
line = truncateToWidth(left, width);
|
|
1531
|
+
const lineWidth = visibleWidth(line);
|
|
1532
|
+
if (lineWidth < width) line += " ".repeat(width - lineWidth);
|
|
1617
1533
|
}
|
|
1618
|
-
if (hovered
|
|
1534
|
+
if (hovered) {
|
|
1619
1535
|
line = theme.bg("selectedBg", line);
|
|
1620
1536
|
}
|
|
1621
1537
|
lines.push(line);
|
|
@@ -1650,7 +1566,7 @@ export class ModelHubComponent implements Component {
|
|
|
1650
1566
|
let text: string;
|
|
1651
1567
|
switch (entry.kind) {
|
|
1652
1568
|
case "recent":
|
|
1653
|
-
text =
|
|
1569
|
+
text = `Recently used models${scopedSuffix}`;
|
|
1654
1570
|
break;
|
|
1655
1571
|
case "roles":
|
|
1656
1572
|
text = "Model roles — f adds a retry fallback, cleared roles fall back to auto-selection";
|
|
@@ -1665,7 +1581,7 @@ export class ModelHubComponent implements Component {
|
|
|
1665
1581
|
}
|
|
1666
1582
|
break;
|
|
1667
1583
|
default:
|
|
1668
|
-
text =
|
|
1584
|
+
text = `All available models${scopedSuffix}`;
|
|
1669
1585
|
break;
|
|
1670
1586
|
}
|
|
1671
1587
|
if (this.#configError && entry.kind !== "provider") {
|
|
@@ -1675,6 +1591,17 @@ export class ModelHubComponent implements Component {
|
|
|
1675
1591
|
return truncateToWidth(theme.fg("muted", ` ${text}`), width);
|
|
1676
1592
|
}
|
|
1677
1593
|
|
|
1594
|
+
/** Clamp a roles row to `width`; the bg band is reserved for mouse hover. */
|
|
1595
|
+
#finishRolesRow(line: string, width: number, hovered: boolean): string {
|
|
1596
|
+
let out = truncateToWidth(line, width);
|
|
1597
|
+
if (hovered) {
|
|
1598
|
+
const w = visibleWidth(out);
|
|
1599
|
+
if (w < width) out += " ".repeat(width - w);
|
|
1600
|
+
return theme.bg("selectedBg", out);
|
|
1601
|
+
}
|
|
1602
|
+
return out;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1678
1605
|
#renderRolesView(width: number, rows: number): string[] {
|
|
1679
1606
|
const lines: string[] = [];
|
|
1680
1607
|
lines.push("");
|
|
@@ -1691,12 +1618,14 @@ export class ModelHubComponent implements Component {
|
|
|
1691
1618
|
}
|
|
1692
1619
|
|
|
1693
1620
|
const cycleOrder = this.#cycleOrder();
|
|
1621
|
+
const listFocused = this.#focus === "list";
|
|
1694
1622
|
for (let i = 0; i < this.#rolesRows.length && lines.length < rows - 2; i++) {
|
|
1695
1623
|
const rowDef = this.#rolesRows[i];
|
|
1696
1624
|
if (!rowDef) continue;
|
|
1697
1625
|
const selected = i === this.#roleIndex;
|
|
1698
1626
|
const hovered = i === this.#roleHover;
|
|
1699
|
-
|
|
1627
|
+
// The unfocused pane draws no cursor; accent text still marks the row.
|
|
1628
|
+
const cursor = selected && listFocused ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
1700
1629
|
|
|
1701
1630
|
if (rowDef.kind === "separator") {
|
|
1702
1631
|
lines.push(` ${theme.fg("border", "─".repeat(Math.max(1, width - 6)))}`);
|
|
@@ -1706,10 +1635,7 @@ export class ModelHubComponent implements Component {
|
|
|
1706
1635
|
if (rowDef.kind === "newRole" || rowDef.kind === "newFallback") {
|
|
1707
1636
|
const label = rowDef.kind === "newRole" ? "+ New role…" : "+ New fallback…";
|
|
1708
1637
|
let line = ` ${cursor} ${theme.fg(selected ? "accent" : "dim", label)}`;
|
|
1709
|
-
line =
|
|
1710
|
-
if (hovered && !selected) {
|
|
1711
|
-
line = theme.bg("selectedBg", line);
|
|
1712
|
-
}
|
|
1638
|
+
line = this.#finishRolesRow(line, width, hovered);
|
|
1713
1639
|
lines.push(line);
|
|
1714
1640
|
continue;
|
|
1715
1641
|
}
|
|
@@ -1720,10 +1646,7 @@ export class ModelHubComponent implements Component {
|
|
|
1720
1646
|
const tail = key.slice(slash + 1);
|
|
1721
1647
|
const keyStyled = theme.fg("dim", key.slice(0, slash + 1)) + (selected ? theme.fg("accent", tail) : tail);
|
|
1722
1648
|
let line = ` ${cursor} ${theme.fg("dim", theme.status.shadowed)} ${keyStyled}`;
|
|
1723
|
-
line =
|
|
1724
|
-
if (hovered && !selected) {
|
|
1725
|
-
line = theme.bg("selectedBg", line);
|
|
1726
|
-
}
|
|
1649
|
+
line = this.#finishRolesRow(line, width, hovered);
|
|
1727
1650
|
lines.push(line);
|
|
1728
1651
|
continue;
|
|
1729
1652
|
}
|
|
@@ -1732,10 +1655,7 @@ export class ModelHubComponent implements Component {
|
|
|
1732
1655
|
const branch = theme.fg("dim", `${"".padEnd(tagWidth + 3)}↳`);
|
|
1733
1656
|
const selector = selected ? theme.fg("accent", rowDef.selector) : theme.fg("muted", rowDef.selector);
|
|
1734
1657
|
let line = ` ${cursor} ${branch} ${selector}`;
|
|
1735
|
-
line =
|
|
1736
|
-
if (hovered && !selected) {
|
|
1737
|
-
line = theme.bg("selectedBg", line);
|
|
1738
|
-
}
|
|
1658
|
+
line = this.#finishRolesRow(line, width, hovered);
|
|
1739
1659
|
lines.push(line);
|
|
1740
1660
|
continue;
|
|
1741
1661
|
}
|
|
@@ -1778,12 +1698,8 @@ export class ModelHubComponent implements Component {
|
|
|
1778
1698
|
const lineWidth = visibleWidth(line);
|
|
1779
1699
|
if (rightWidth > 0 && lineWidth + rightWidth + 2 <= width) {
|
|
1780
1700
|
line = `${line}${" ".repeat(width - lineWidth - rightWidth - 1)}${right}`;
|
|
1781
|
-
} else {
|
|
1782
|
-
line = truncateToWidth(line, width);
|
|
1783
|
-
}
|
|
1784
|
-
if (hovered && !selected) {
|
|
1785
|
-
line = theme.bg("selectedBg", line);
|
|
1786
1701
|
}
|
|
1702
|
+
line = this.#finishRolesRow(line, width, hovered);
|
|
1787
1703
|
lines.push(line);
|
|
1788
1704
|
}
|
|
1789
1705
|
|
|
@@ -1890,9 +1806,6 @@ export class ModelHubComponent implements Component {
|
|
|
1890
1806
|
}
|
|
1891
1807
|
const arrows = this.#focus === "scope" ? "↑/↓ providers · → models" : "↑/↓ models · ← providers";
|
|
1892
1808
|
const refresh = entry.kind === "provider" ? " · F5 refresh" : "";
|
|
1893
|
-
if (this.#mode === "pick") {
|
|
1894
|
-
return `Enter use for this session · ${arrows} · type to search${refresh} · Esc close`;
|
|
1895
|
-
}
|
|
1896
1809
|
return `Enter assign roles · ${arrows} · type to search${refresh} · Esc close`;
|
|
1897
1810
|
}
|
|
1898
1811
|
|
|
@@ -1981,14 +1894,14 @@ export class ModelHubComponent implements Component {
|
|
|
1981
1894
|
bodyLines.push(...this.#renderLockedView(entry, bodyWidth, contentRows - 1));
|
|
1982
1895
|
} else {
|
|
1983
1896
|
this.#browser.setMaxVisible(contentRows - 1 - 5);
|
|
1897
|
+
this.#browser.setFocused(this.#focus === "list");
|
|
1984
1898
|
bodyLines.push(...this.#browser.render(bodyWidth));
|
|
1985
1899
|
}
|
|
1986
1900
|
|
|
1987
1901
|
const sidebarLines = this.#renderSidebar(sidebarWidth, contentRows);
|
|
1988
1902
|
|
|
1989
|
-
const title = this.#mode === "pick" ? "Switch Model" : "Models";
|
|
1990
1903
|
const out: string[] = [];
|
|
1991
|
-
out.push(topBorderSplit(width,
|
|
1904
|
+
out.push(topBorderSplit(width, "Models", sidebarWidth));
|
|
1992
1905
|
this.#contentRowStart = out.length;
|
|
1993
1906
|
for (let i = 0; i < contentRows; i++) {
|
|
1994
1907
|
out.push(splitRow(sidebarLines[i] ?? "", bodyLines[i] ?? "", width, sidebarWidth));
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact session-model picker (alt+p / `/switch`): a bottom-anchored
|
|
3
|
+
* floating overlay hosting just a {@link ModelBrowser} — no provider sidebar.
|
|
4
|
+
* Model entries switch the current session only; a search beginning with `@`
|
|
5
|
+
* exposes the configured ctrl+p quick roles.
|
|
6
|
+
*/
|
|
7
|
+
import type { Model } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import type { Component, TUI } from "@oh-my-pi/pi-tui";
|
|
9
|
+
import type { ModelRegistry } from "../../config/model-registry";
|
|
10
|
+
import type { Settings } from "../../config/settings";
|
|
11
|
+
import type { ResolvedRoleModel } from "../../session/agent-session";
|
|
12
|
+
import { theme } from "../theme/theme";
|
|
13
|
+
import {
|
|
14
|
+
buildBrowserItems,
|
|
15
|
+
ModelBrowser,
|
|
16
|
+
type ModelBrowserItem,
|
|
17
|
+
resolveRoleAssignments,
|
|
18
|
+
sortModelItems,
|
|
19
|
+
} from "./model-browser";
|
|
20
|
+
import type { ScopedModelItem } from "./model-hub";
|
|
21
|
+
import { bottomBorder, row, topBorder } from "./overlay-box";
|
|
22
|
+
import { resolveSegmentPalette } from "./segment-track";
|
|
23
|
+
|
|
24
|
+
export interface ModelPickerCallbacks {
|
|
25
|
+
/** A model was chosen for a session-only switch. `selector` is `provider/id`. */
|
|
26
|
+
onPick: (model: Model, selector: string) => void;
|
|
27
|
+
/** A configured ctrl+p quick role was chosen. */
|
|
28
|
+
onPickRole?: (entry: ResolvedRoleModel) => void;
|
|
29
|
+
/** The picker was dismissed. */
|
|
30
|
+
onCancel: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ModelPickerOptions {
|
|
34
|
+
/** Session token count; models with smaller context windows are disabled. */
|
|
35
|
+
currentContextTokens?: number;
|
|
36
|
+
/** `provider/id` of the session's active model; highlighted and preselected. */
|
|
37
|
+
currentSelector?: string;
|
|
38
|
+
/** Resolved role models in the same order used by the ctrl+p quick-role cycle. */
|
|
39
|
+
quickRoles?: ReadonlyArray<ResolvedRoleModel>;
|
|
40
|
+
/** Complete ctrl+p order, including unavailable roles, to preserve segment colors. */
|
|
41
|
+
quickRoleOrder?: ReadonlyArray<string>;
|
|
42
|
+
/** Active quick role, highlighted when the search begins with `@`. */
|
|
43
|
+
currentQuickRole?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fixed chrome rows: top border, status row, footer, bottom border. */
|
|
47
|
+
const CHROME_ROWS = 4;
|
|
48
|
+
/** Rows the browser renders around its list window (search + blank, blank + two detail rows). */
|
|
49
|
+
const BROWSER_FRAME_ROWS = 5;
|
|
50
|
+
/** Minimum rows for the browser list window on short terminals. */
|
|
51
|
+
const MIN_VISIBLE = 5;
|
|
52
|
+
/** Fraction of the terminal height the floating overlay occupies. */
|
|
53
|
+
const HEIGHT_FRACTION = 0.4;
|
|
54
|
+
|
|
55
|
+
const STATUS_HINT = "Session-only switch — role models stay unchanged";
|
|
56
|
+
const QUICK_ROLE_STATUS_HINT = "Quick role switch — applies its model and thinking for this session";
|
|
57
|
+
const FOOTER_HINT = "↑/↓ models · Enter use for this session · type to search · @ quick roles · Esc close";
|
|
58
|
+
const QUICK_ROLE_FOOTER_HINT = "↑/↓ roles · Enter apply role model · type to search · Esc close";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The alt+p picker component. Hosted as a non-fullscreen bottom-anchored
|
|
62
|
+
* overlay (`ui.showOverlay(..., { anchor: "bottom-center" })`); keyboard-only,
|
|
63
|
+
* since mouse tracking is reserved for fullscreen overlays.
|
|
64
|
+
*/
|
|
65
|
+
export class ModelPickerComponent implements Component {
|
|
66
|
+
#tui: TUI;
|
|
67
|
+
#settings: Settings;
|
|
68
|
+
#registry: ModelRegistry;
|
|
69
|
+
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
70
|
+
#browser: ModelBrowser;
|
|
71
|
+
#configError: string | undefined;
|
|
72
|
+
#currentSelector: string | undefined;
|
|
73
|
+
#currentQuickRoleSelector: string | undefined;
|
|
74
|
+
#modelItems: ModelBrowserItem[] = [];
|
|
75
|
+
#quickRoleItems: ModelBrowserItem[] = [];
|
|
76
|
+
#quickRoles = new Map<string, ResolvedRoleModel>();
|
|
77
|
+
#roleMode = false;
|
|
78
|
+
|
|
79
|
+
constructor(
|
|
80
|
+
tui: TUI,
|
|
81
|
+
settings: Settings,
|
|
82
|
+
registry: ModelRegistry,
|
|
83
|
+
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
84
|
+
callbacks: ModelPickerCallbacks,
|
|
85
|
+
options: ModelPickerOptions = {},
|
|
86
|
+
) {
|
|
87
|
+
this.#tui = tui;
|
|
88
|
+
this.#settings = settings;
|
|
89
|
+
this.#registry = registry;
|
|
90
|
+
this.#scopedModels = scopedModels;
|
|
91
|
+
this.#currentSelector = options.currentSelector;
|
|
92
|
+
this.#currentQuickRoleSelector = options.currentQuickRole ? `@${options.currentQuickRole}` : undefined;
|
|
93
|
+
this.#quickRoleItems = this.#buildQuickRoleItems(
|
|
94
|
+
options.quickRoles ?? [],
|
|
95
|
+
options.quickRoleOrder ?? options.quickRoles?.map(entry => entry.role) ?? [],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
this.#browser = new ModelBrowser(settings, {
|
|
99
|
+
currentContextTokens: options.currentContextTokens,
|
|
100
|
+
disableOverContext: true,
|
|
101
|
+
emptyText: () => (this.#roleMode ? " No quick roles in the Ctrl+P cycle" : undefined),
|
|
102
|
+
});
|
|
103
|
+
this.#browser.onActivate = item => {
|
|
104
|
+
const quickRole = this.#quickRoles.get(item.selector);
|
|
105
|
+
if (quickRole) {
|
|
106
|
+
callbacks.onPickRole?.(quickRole);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
callbacks.onPick(item.model, item.selector);
|
|
110
|
+
};
|
|
111
|
+
this.#browser.onCancel = () => callbacks.onCancel();
|
|
112
|
+
this.#browser.onQueryChange = query => this.#syncItemsForQuery(query);
|
|
113
|
+
|
|
114
|
+
// Hydrate synchronously from the current registry snapshot so the first
|
|
115
|
+
// Enter after opening acts on cached models instead of being dropped
|
|
116
|
+
// while the offline refresh promise is still pending.
|
|
117
|
+
this.#syncFromRegistryState();
|
|
118
|
+
if (options.currentSelector) {
|
|
119
|
+
this.#browser.selectSelector(options.currentSelector);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Reconcile with cached discovery state in the background. A --models
|
|
123
|
+
// scope is registry-independent, so the offline reload would only repeat
|
|
124
|
+
// the synchronous hydration above.
|
|
125
|
+
if (this.#scopedModels.length === 0) {
|
|
126
|
+
this.#registry
|
|
127
|
+
.refresh("offline")
|
|
128
|
+
.then(() => this.#syncFromRegistryState())
|
|
129
|
+
.catch(error => {
|
|
130
|
+
this.#configError = error instanceof Error ? error.message : String(error);
|
|
131
|
+
})
|
|
132
|
+
.finally(() => this.#tui.requestRender());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
invalidate(): void {}
|
|
137
|
+
|
|
138
|
+
/** Rebuild model items and role chips from the registry's in-memory state. */
|
|
139
|
+
#syncFromRegistryState(): void {
|
|
140
|
+
let models: ReadonlyArray<Model>;
|
|
141
|
+
if (this.#scopedModels.length > 0) {
|
|
142
|
+
models = this.#scopedModels.map(scoped => scoped.model);
|
|
143
|
+
this.#configError = undefined;
|
|
144
|
+
} else {
|
|
145
|
+
const loadError = this.#registry.getError();
|
|
146
|
+
this.#configError = loadError ? String(loadError) : undefined;
|
|
147
|
+
try {
|
|
148
|
+
models = this.#registry.getAvailable();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
this.#configError = error instanceof Error ? error.message : String(error);
|
|
151
|
+
models = [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const allModels = this.#scopedModels.length > 0 ? models : this.#registry.getAll();
|
|
156
|
+
const roles = resolveRoleAssignments(this.#settings, allModels, models);
|
|
157
|
+
const storage = this.#settings.getStorage();
|
|
158
|
+
const mruOrder = storage?.getModelUsageOrder() ?? [];
|
|
159
|
+
this.#modelItems = buildBrowserItems(models);
|
|
160
|
+
sortModelItems(this.#modelItems, { roles, mruOrder });
|
|
161
|
+
this.#browser.setRoles(roles);
|
|
162
|
+
this.#browser.setMruOrder(mruOrder);
|
|
163
|
+
this.#browser.setPerfStats(storage?.getModelPerf() ?? new Map());
|
|
164
|
+
this.#syncItemsForQuery(this.#browser.query, true);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Build virtual `@role` rows, colored by their ctrl+p segment position. */
|
|
168
|
+
#buildQuickRoleItems(
|
|
169
|
+
quickRoles: ReadonlyArray<ResolvedRoleModel>,
|
|
170
|
+
quickRoleOrder: ReadonlyArray<string>,
|
|
171
|
+
): ModelBrowserItem[] {
|
|
172
|
+
const order = quickRoleOrder.length > 0 ? quickRoleOrder : quickRoles.map(entry => entry.role);
|
|
173
|
+
const palette = resolveSegmentPalette(order.length);
|
|
174
|
+
return quickRoles.map((entry, index) => {
|
|
175
|
+
const selector = `@${entry.role}`;
|
|
176
|
+
this.#quickRoles.set(selector, entry);
|
|
177
|
+
const orderIndex = order.indexOf(entry.role);
|
|
178
|
+
return {
|
|
179
|
+
provider: "",
|
|
180
|
+
id: selector,
|
|
181
|
+
model: entry.model,
|
|
182
|
+
selector,
|
|
183
|
+
labelColor: palette[(orderIndex >= 0 ? orderIndex : index) % palette.length],
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Switch browser content only when a leading `@` changes the search mode. */
|
|
189
|
+
#syncItemsForQuery(query: string, refresh = false): void {
|
|
190
|
+
const roleMode = query.startsWith("@");
|
|
191
|
+
const modeChanged = roleMode !== this.#roleMode;
|
|
192
|
+
if (!modeChanged && !refresh) return;
|
|
193
|
+
|
|
194
|
+
this.#roleMode = roleMode;
|
|
195
|
+
this.#browser.setShowProvider(!roleMode);
|
|
196
|
+
this.#browser.setDisableOverContext(!roleMode);
|
|
197
|
+
this.#browser.setPreserveQueryOrder(roleMode);
|
|
198
|
+
const currentSelector = roleMode ? this.#currentQuickRoleSelector : this.#currentSelector;
|
|
199
|
+
this.#browser.setCurrentSelector(currentSelector);
|
|
200
|
+
this.#browser.setItems(roleMode ? this.#quickRoleItems : this.#modelItems);
|
|
201
|
+
if (modeChanged && currentSelector) {
|
|
202
|
+
this.#browser.selectSelector(currentSelector);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
handleInput(data: string): void {
|
|
207
|
+
// Mouse tracking is off outside fullscreen overlays; drop any stray SGR
|
|
208
|
+
// reports instead of feeding them to the search input.
|
|
209
|
+
if (data.startsWith("\x1b[<")) return;
|
|
210
|
+
this.#browser.handleInput(data);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
render(width: number): string[] {
|
|
214
|
+
const termRows = Math.max(16, this.#tui.terminal?.rows || process.stdout.rows || 40);
|
|
215
|
+
const listBudget = Math.floor(termRows * HEIGHT_FRACTION) - CHROME_ROWS - BROWSER_FRAME_ROWS;
|
|
216
|
+
this.#browser.setMaxVisible(Math.max(MIN_VISIBLE, listBudget));
|
|
217
|
+
|
|
218
|
+
const inner = Math.max(1, width - 4);
|
|
219
|
+
const status = this.#configError
|
|
220
|
+
? theme.fg("error", ` ${this.#configError}`)
|
|
221
|
+
: theme.fg("muted", ` ${this.#roleMode ? QUICK_ROLE_STATUS_HINT : STATUS_HINT}`);
|
|
222
|
+
|
|
223
|
+
const out: string[] = [];
|
|
224
|
+
out.push(topBorder(width, "Switch Model"));
|
|
225
|
+
out.push(row(status, width));
|
|
226
|
+
for (const line of this.#browser.render(inner)) {
|
|
227
|
+
out.push(row(line, width));
|
|
228
|
+
}
|
|
229
|
+
out.push(row(theme.fg("dim", this.#roleMode ? QUICK_ROLE_FOOTER_HINT : FOOTER_HINT), width));
|
|
230
|
+
out.push(bottomBorder(width));
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
}
|