@oh-my-pi/pi-coding-agent 14.0.4 → 14.1.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 +83 -0
- package/package.json +11 -8
- package/src/async/index.ts +1 -0
- package/src/async/support.ts +5 -0
- package/src/cli/list-models.ts +96 -57
- package/src/commit/model-selection.ts +16 -13
- package/src/config/model-equivalence.ts +674 -0
- package/src/config/model-registry.ts +182 -13
- package/src/config/model-resolver.ts +203 -74
- package/src/config/settings-schema.ts +23 -0
- package/src/config/settings.ts +9 -2
- package/src/dap/session.ts +31 -39
- package/src/debug/log-formatting.ts +2 -2
- package/src/edit/modes/chunk.ts +8 -3
- package/src/export/html/template.css +82 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +612 -97
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/jobs-protocol.ts +2 -1
- package/src/lsp/client.ts +5 -3
- package/src/lsp/index.ts +4 -9
- package/src/lsp/utils.ts +26 -0
- package/src/main.ts +6 -1
- package/src/memories/index.ts +7 -6
- package/src/modes/components/diff.ts +1 -1
- package/src/modes/components/model-selector.ts +221 -64
- package/src/modes/controllers/command-controller.ts +18 -0
- package/src/modes/controllers/event-controller.ts +438 -426
- package/src/modes/controllers/selector-controller.ts +13 -5
- package/src/modes/theme/mermaid-cache.ts +5 -7
- package/src/priority.json +8 -0
- package/src/prompts/agents/designer.md +1 -2
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/bash.md +15 -0
- package/src/prompts/tools/cancel-job.md +1 -1
- package/src/prompts/tools/chunk-edit.md +39 -40
- package/src/prompts/tools/read-chunk.md +13 -1
- package/src/prompts/tools/read.md +9 -0
- package/src/prompts/tools/write.md +1 -0
- package/src/sdk.ts +7 -4
- package/src/session/agent-session.ts +33 -6
- package/src/session/compaction/compaction.ts +1 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/await-tool.ts +2 -1
- package/src/tools/bash.ts +221 -56
- package/src/tools/browser.ts +84 -21
- package/src/tools/cancel-job.ts +2 -1
- package/src/tools/fetch.ts +1 -1
- package/src/tools/find.ts +40 -94
- package/src/tools/gemini-image.ts +1 -0
- package/src/tools/inspect-image.ts +1 -1
- package/src/tools/read.ts +218 -1
- package/src/tools/render-utils.ts +1 -1
- package/src/tools/sqlite-reader.ts +623 -0
- package/src/tools/write.ts +187 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +24 -1
- package/src/utils/image-resize.ts +73 -37
- package/src/utils/title-generator.ts +1 -1
- package/src/web/scrapers/types.ts +50 -32
- package/src/web/search/providers/codex.ts +21 -2
|
@@ -28,10 +28,38 @@ function makeInvertedBadge(label: string, color: ThemeColor): string {
|
|
|
28
28
|
return `${bgAnsi}\x1b[30m ${label} \x1b[39m\x1b[49m`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function normalizeSearchText(value: string): string {
|
|
32
|
+
return value
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
35
|
+
.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compactSearchText(value: string): string {
|
|
39
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getAlphaSearchTokens(query: string): string[] {
|
|
43
|
+
return [...normalizeSearchText(query).matchAll(/[a-z]+/g)].map(match => match[0]).filter(token => token.length > 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
interface ModelItem {
|
|
47
|
+
kind: "provider";
|
|
32
48
|
provider: string;
|
|
33
49
|
id: string;
|
|
34
50
|
model: Model;
|
|
51
|
+
selector: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CanonicalModelItem {
|
|
55
|
+
kind: "canonical";
|
|
56
|
+
id: string;
|
|
57
|
+
model: Model;
|
|
58
|
+
selector: string;
|
|
59
|
+
variantCount: number;
|
|
60
|
+
searchText: string;
|
|
61
|
+
normalizedSearchText: string;
|
|
62
|
+
compactSearchText: string;
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
interface ScopedModelItem {
|
|
@@ -44,7 +72,7 @@ interface RoleAssignment {
|
|
|
44
72
|
thinkingLevel: ThinkingLevel;
|
|
45
73
|
}
|
|
46
74
|
|
|
47
|
-
type RoleSelectCallback = (model: Model, role: string | null, thinkingLevel?: ThinkingLevel) => void;
|
|
75
|
+
type RoleSelectCallback = (model: Model, role: string | null, thinkingLevel?: ThinkingLevel, selector?: string) => void;
|
|
48
76
|
type CancelCallback = () => void;
|
|
49
77
|
interface MenuRoleAction {
|
|
50
78
|
label: string;
|
|
@@ -52,6 +80,7 @@ interface MenuRoleAction {
|
|
|
52
80
|
}
|
|
53
81
|
|
|
54
82
|
const ALL_TAB = "ALL";
|
|
83
|
+
const CANONICAL_TAB = "CANONICAL";
|
|
55
84
|
|
|
56
85
|
/**
|
|
57
86
|
* Component that renders a model selector with provider tabs and context menu.
|
|
@@ -68,6 +97,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
68
97
|
#menuContainer: Container;
|
|
69
98
|
#allModels: ModelItem[] = [];
|
|
70
99
|
#filteredModels: ModelItem[] = [];
|
|
100
|
+
#canonicalModels: CanonicalModelItem[] = [];
|
|
101
|
+
#filteredCanonicalModels: CanonicalModelItem[] = [];
|
|
71
102
|
#selectedIndex: number = 0;
|
|
72
103
|
#roles = {} as Record<string, RoleAssignment | undefined>;
|
|
73
104
|
#settings = null as unknown as Settings;
|
|
@@ -97,7 +128,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
97
128
|
settings: Settings,
|
|
98
129
|
modelRegistry: ModelRegistry,
|
|
99
130
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
100
|
-
onSelect: (model: Model, role: string | null, thinkingLevel?: ThinkingLevel) => void,
|
|
131
|
+
onSelect: (model: Model, role: string | null, thinkingLevel?: ThinkingLevel, selector?: string) => void,
|
|
101
132
|
onCancel: () => void,
|
|
102
133
|
options?: { temporaryOnly?: boolean; initialSearchInput?: string },
|
|
103
134
|
) {
|
|
@@ -202,6 +233,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
202
233
|
const resolved = resolveModelRoleValue(roleValue, allModels, {
|
|
203
234
|
settings: this.#settings,
|
|
204
235
|
matchPreferences,
|
|
236
|
+
modelRegistry: this.#modelRegistry,
|
|
205
237
|
});
|
|
206
238
|
if (resolved.model) {
|
|
207
239
|
this.#roles[role] = {
|
|
@@ -237,8 +269,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
237
269
|
const latestRe = /-latest$/;
|
|
238
270
|
|
|
239
271
|
models.sort((a, b) => {
|
|
240
|
-
const aKey =
|
|
241
|
-
const bKey =
|
|
272
|
+
const aKey = a.selector;
|
|
273
|
+
const bKey = b.selector;
|
|
242
274
|
|
|
243
275
|
const aRank = modelRank(a);
|
|
244
276
|
const bRank = modelRank(b);
|
|
@@ -289,15 +321,50 @@ export class ModelSelectorComponent extends Container {
|
|
|
289
321
|
});
|
|
290
322
|
}
|
|
291
323
|
|
|
324
|
+
#sortCanonicalModels(models: CanonicalModelItem[]): void {
|
|
325
|
+
const mruOrder = this.#settings.getStorage()?.getModelUsageOrder() ?? [];
|
|
326
|
+
const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
|
|
327
|
+
|
|
328
|
+
const modelRank = (model: CanonicalModelItem) => {
|
|
329
|
+
let i = 0;
|
|
330
|
+
while (i < MODEL_ROLE_IDS.length) {
|
|
331
|
+
const role = MODEL_ROLE_IDS[i];
|
|
332
|
+
const assigned = this.#roles[role];
|
|
333
|
+
if (assigned && modelsAreEqual(assigned.model, model.model)) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
i++;
|
|
337
|
+
}
|
|
338
|
+
return i;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
models.sort((a, b) => {
|
|
342
|
+
const aRank = modelRank(a);
|
|
343
|
+
const bRank = modelRank(b);
|
|
344
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
345
|
+
|
|
346
|
+
const aMru = mruIndex.get(`${a.model.provider}/${a.model.id}`) ?? Number.MAX_SAFE_INTEGER;
|
|
347
|
+
const bMru = mruIndex.get(`${b.model.provider}/${b.model.id}`) ?? Number.MAX_SAFE_INTEGER;
|
|
348
|
+
if (aMru !== bMru) return aMru - bMru;
|
|
349
|
+
|
|
350
|
+
const providerCmp = a.model.provider.localeCompare(b.model.provider);
|
|
351
|
+
if (providerCmp !== 0) return providerCmp;
|
|
352
|
+
|
|
353
|
+
return a.id.localeCompare(b.id);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
292
357
|
async #loadModels(): Promise<void> {
|
|
293
358
|
let models: ModelItem[];
|
|
294
359
|
|
|
295
360
|
// Use scoped models if provided via --models flag
|
|
296
361
|
if (this.#scopedModels.length > 0) {
|
|
297
362
|
models = this.#scopedModels.map(scoped => ({
|
|
363
|
+
kind: "provider",
|
|
298
364
|
provider: scoped.model.provider,
|
|
299
365
|
id: scoped.model.id,
|
|
300
366
|
model: scoped.model,
|
|
367
|
+
selector: `${scoped.model.provider}/${scoped.model.id}`,
|
|
301
368
|
}));
|
|
302
369
|
} else {
|
|
303
370
|
// Reload config and cached discovery state without blocking on live provider refresh
|
|
@@ -315,22 +382,61 @@ export class ModelSelectorComponent extends Container {
|
|
|
315
382
|
try {
|
|
316
383
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
317
384
|
models = availableModels.map((model: Model) => ({
|
|
385
|
+
kind: "provider",
|
|
318
386
|
provider: model.provider,
|
|
319
387
|
id: model.id,
|
|
320
388
|
model,
|
|
389
|
+
selector: `${model.provider}/${model.id}`,
|
|
321
390
|
}));
|
|
322
391
|
} catch (error) {
|
|
323
392
|
this.#allModels = [];
|
|
324
393
|
this.#filteredModels = [];
|
|
394
|
+
this.#canonicalModels = [];
|
|
395
|
+
this.#filteredCanonicalModels = [];
|
|
325
396
|
this.#errorMessage = error instanceof Error ? error.message : String(error);
|
|
326
397
|
return;
|
|
327
398
|
}
|
|
328
399
|
}
|
|
329
400
|
|
|
401
|
+
const canonicalRecords = this.#modelRegistry.getCanonicalModels({
|
|
402
|
+
availableOnly: this.#scopedModels.length === 0,
|
|
403
|
+
candidates: models.map(item => item.model),
|
|
404
|
+
});
|
|
405
|
+
const canonicalModels = canonicalRecords
|
|
406
|
+
.map(record => {
|
|
407
|
+
const selectedModel = this.#modelRegistry.resolveCanonicalModel(record.id, {
|
|
408
|
+
availableOnly: this.#scopedModels.length === 0,
|
|
409
|
+
candidates: models.map(item => item.model),
|
|
410
|
+
});
|
|
411
|
+
if (!selectedModel) return undefined;
|
|
412
|
+
const searchText = [
|
|
413
|
+
record.id,
|
|
414
|
+
record.name,
|
|
415
|
+
selectedModel.provider,
|
|
416
|
+
selectedModel.id,
|
|
417
|
+
selectedModel.name,
|
|
418
|
+
...record.variants.flatMap(variant => [variant.selector, variant.model.name]),
|
|
419
|
+
].join(" ");
|
|
420
|
+
return {
|
|
421
|
+
kind: "canonical" as const,
|
|
422
|
+
id: record.id,
|
|
423
|
+
model: selectedModel,
|
|
424
|
+
selector: record.id,
|
|
425
|
+
variantCount: record.variants.length,
|
|
426
|
+
searchText,
|
|
427
|
+
normalizedSearchText: normalizeSearchText(searchText),
|
|
428
|
+
compactSearchText: compactSearchText(searchText),
|
|
429
|
+
};
|
|
430
|
+
})
|
|
431
|
+
.filter((item): item is CanonicalModelItem => item !== undefined);
|
|
432
|
+
|
|
330
433
|
this.#sortModels(models);
|
|
434
|
+
this.#sortCanonicalModels(canonicalModels);
|
|
331
435
|
|
|
332
436
|
this.#allModels = models;
|
|
333
437
|
this.#filteredModels = models;
|
|
438
|
+
this.#canonicalModels = canonicalModels;
|
|
439
|
+
this.#filteredCanonicalModels = canonicalModels;
|
|
334
440
|
this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, models.length - 1));
|
|
335
441
|
}
|
|
336
442
|
|
|
@@ -343,12 +449,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
343
449
|
providerSet.add(provider.toUpperCase());
|
|
344
450
|
}
|
|
345
451
|
const sortedProviders = Array.from(providerSet).sort();
|
|
346
|
-
this.#providers = [ALL_TAB, ...sortedProviders];
|
|
452
|
+
this.#providers = [ALL_TAB, CANONICAL_TAB, ...sortedProviders];
|
|
347
453
|
}
|
|
348
454
|
|
|
349
455
|
async #refreshSelectedProvider(): Promise<void> {
|
|
350
456
|
const activeProvider = this.#getActiveProvider();
|
|
351
|
-
if (this.#scopedModels.length > 0 || activeProvider === ALL_TAB) {
|
|
457
|
+
if (this.#scopedModels.length > 0 || activeProvider === ALL_TAB || activeProvider === CANONICAL_TAB) {
|
|
352
458
|
return;
|
|
353
459
|
}
|
|
354
460
|
await this.#modelRegistry.refreshProvider(activeProvider.toLowerCase());
|
|
@@ -382,19 +488,25 @@ export class ModelSelectorComponent extends Container {
|
|
|
382
488
|
return this.#providers[this.#activeTabIndex] ?? ALL_TAB;
|
|
383
489
|
}
|
|
384
490
|
|
|
491
|
+
#isCanonicalTab(): boolean {
|
|
492
|
+
return this.#getActiveProvider() === CANONICAL_TAB;
|
|
493
|
+
}
|
|
494
|
+
|
|
385
495
|
#filterModels(query: string): void {
|
|
386
496
|
const activeProvider = this.#getActiveProvider();
|
|
497
|
+
const isCanonicalTab = activeProvider === CANONICAL_TAB;
|
|
387
498
|
|
|
388
|
-
// Start with all models or filter by provider
|
|
499
|
+
// Start with all models or filter by provider/canonical view
|
|
389
500
|
let baseModels = this.#allModels;
|
|
390
|
-
|
|
501
|
+
const baseCanonicalModels = this.#canonicalModels;
|
|
502
|
+
if (!isCanonicalTab && activeProvider !== ALL_TAB) {
|
|
391
503
|
baseModels = this.#allModels.filter(m => m.provider.toUpperCase() === activeProvider);
|
|
392
504
|
}
|
|
393
505
|
|
|
394
506
|
// Apply fuzzy filter if query is present
|
|
395
507
|
if (query.trim()) {
|
|
396
|
-
// If user is searching, auto-switch to ALL
|
|
397
|
-
if (activeProvider !== ALL_TAB) {
|
|
508
|
+
// If user is searching from a provider tab, auto-switch to ALL to show global provider results.
|
|
509
|
+
if (activeProvider !== ALL_TAB && !isCanonicalTab) {
|
|
398
510
|
this.#activeTabIndex = 0;
|
|
399
511
|
if (this.#tabBar && this.#tabBar.getActiveIndex() !== 0) {
|
|
400
512
|
this.#tabBar.setActiveIndex(0);
|
|
@@ -403,14 +515,41 @@ export class ModelSelectorComponent extends Container {
|
|
|
403
515
|
this.#updateTabBar();
|
|
404
516
|
baseModels = this.#allModels;
|
|
405
517
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
518
|
+
|
|
519
|
+
if (isCanonicalTab) {
|
|
520
|
+
const alphaTokens = getAlphaSearchTokens(query);
|
|
521
|
+
const alphaFiltered =
|
|
522
|
+
alphaTokens.length === 0
|
|
523
|
+
? baseCanonicalModels
|
|
524
|
+
: baseCanonicalModels.filter(item =>
|
|
525
|
+
alphaTokens.every(token => item.normalizedSearchText.includes(token)),
|
|
526
|
+
);
|
|
527
|
+
const compactQuery = compactSearchText(query);
|
|
528
|
+
const substringFiltered =
|
|
529
|
+
compactQuery.length === 0
|
|
530
|
+
? alphaFiltered
|
|
531
|
+
: alphaFiltered.filter(item => item.compactSearchText.includes(compactQuery));
|
|
532
|
+
const fuzzySource =
|
|
533
|
+
substringFiltered.length > 0
|
|
534
|
+
? substringFiltered
|
|
535
|
+
: alphaFiltered.length > 0
|
|
536
|
+
? alphaFiltered
|
|
537
|
+
: baseCanonicalModels;
|
|
538
|
+
const fuzzyMatches = fuzzyFilter(fuzzySource, query, ({ searchText }) => searchText);
|
|
539
|
+
this.#sortCanonicalModels(fuzzyMatches);
|
|
540
|
+
this.#filteredCanonicalModels = fuzzyMatches;
|
|
541
|
+
} else {
|
|
542
|
+
const fuzzyMatches = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
|
|
543
|
+
this.#sortModels(fuzzyMatches);
|
|
544
|
+
this.#filteredModels = fuzzyMatches;
|
|
545
|
+
}
|
|
409
546
|
} else {
|
|
410
547
|
this.#filteredModels = baseModels;
|
|
548
|
+
this.#filteredCanonicalModels = baseCanonicalModels;
|
|
411
549
|
}
|
|
412
550
|
|
|
413
|
-
|
|
551
|
+
const visibleCount = isCanonicalTab ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
|
|
552
|
+
this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, visibleCount - 1));
|
|
414
553
|
this.#updateList();
|
|
415
554
|
}
|
|
416
555
|
|
|
@@ -433,7 +572,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
433
572
|
|
|
434
573
|
#getProviderEmptyStateMessage(): string | undefined {
|
|
435
574
|
const activeProvider = this.#getActiveProvider();
|
|
436
|
-
if (activeProvider === ALL_TAB || this.#searchInput.getValue().trim()) {
|
|
575
|
+
if (activeProvider === ALL_TAB || activeProvider === CANONICAL_TAB || this.#searchInput.getValue().trim()) {
|
|
437
576
|
return undefined;
|
|
438
577
|
}
|
|
439
578
|
const state = this.#modelRegistry.getProviderDiscoveryState(activeProvider.toLowerCase());
|
|
@@ -459,21 +598,25 @@ export class ModelSelectorComponent extends Container {
|
|
|
459
598
|
|
|
460
599
|
#updateList(): void {
|
|
461
600
|
this.#listContainer.clear();
|
|
601
|
+
const isCanonicalTab = this.#isCanonicalTab();
|
|
602
|
+
const visibleItems = isCanonicalTab ? this.#filteredCanonicalModels : this.#filteredModels;
|
|
462
603
|
|
|
463
604
|
const maxVisible = 10;
|
|
464
605
|
const startIndex = Math.max(
|
|
465
606
|
0,
|
|
466
|
-
Math.min(this.#selectedIndex - Math.floor(maxVisible / 2),
|
|
607
|
+
Math.min(this.#selectedIndex - Math.floor(maxVisible / 2), visibleItems.length - maxVisible),
|
|
467
608
|
);
|
|
468
|
-
const endIndex = Math.min(startIndex + maxVisible,
|
|
609
|
+
const endIndex = Math.min(startIndex + maxVisible, visibleItems.length);
|
|
469
610
|
|
|
470
611
|
const activeProvider = this.#getActiveProvider();
|
|
471
612
|
const showProvider = activeProvider === ALL_TAB;
|
|
472
613
|
|
|
473
614
|
// Show visible slice of filtered models
|
|
474
615
|
for (let i = startIndex; i < endIndex; i++) {
|
|
475
|
-
const item =
|
|
616
|
+
const item = visibleItems[i];
|
|
476
617
|
if (!item) continue;
|
|
618
|
+
const canonicalItem = isCanonicalTab ? (item as CanonicalModelItem) : undefined;
|
|
619
|
+
const providerItem = isCanonicalTab ? undefined : (item as ModelItem);
|
|
477
620
|
|
|
478
621
|
const isSelected = i === this.#selectedIndex;
|
|
479
622
|
|
|
@@ -502,17 +645,25 @@ export class ModelSelectorComponent extends Container {
|
|
|
502
645
|
let line = "";
|
|
503
646
|
if (isSelected) {
|
|
504
647
|
const prefix = theme.fg("accent", `${theme.nav.cursor} `);
|
|
505
|
-
if (
|
|
506
|
-
const
|
|
507
|
-
|
|
648
|
+
if (isCanonicalTab) {
|
|
649
|
+
const variants = theme.fg("dim", ` [${canonicalItem?.variantCount ?? 0}]`);
|
|
650
|
+
const backing = theme.fg("dim", ` -> ${item.model.provider}/${item.model.id}`);
|
|
651
|
+
line = `${prefix}${theme.fg("accent", item.id)}${variants}${backing}${badgeText}`;
|
|
652
|
+
} else if (showProvider) {
|
|
653
|
+
const providerPrefix = theme.fg("dim", `${providerItem?.provider ?? ""}/`);
|
|
654
|
+
line = `${prefix}${providerPrefix}${theme.fg("accent", providerItem?.id ?? item.id)}${badgeText}`;
|
|
508
655
|
} else {
|
|
509
656
|
line = `${prefix}${theme.fg("accent", item.id)}${badgeText}`;
|
|
510
657
|
}
|
|
511
658
|
} else {
|
|
512
659
|
const prefix = " ";
|
|
513
|
-
if (
|
|
514
|
-
const
|
|
515
|
-
|
|
660
|
+
if (isCanonicalTab) {
|
|
661
|
+
const variants = theme.fg("dim", ` [${canonicalItem?.variantCount ?? 0}]`);
|
|
662
|
+
const backing = theme.fg("dim", ` -> ${item.model.provider}/${item.model.id}`);
|
|
663
|
+
line = `${prefix}${item.id}${variants}${backing}${badgeText}`;
|
|
664
|
+
} else if (showProvider) {
|
|
665
|
+
const providerPrefix = theme.fg("dim", `${providerItem?.provider ?? ""}/`);
|
|
666
|
+
line = `${prefix}${providerPrefix}${providerItem?.id ?? item.id}${badgeText}`;
|
|
516
667
|
} else {
|
|
517
668
|
line = `${prefix}${item.id}${badgeText}`;
|
|
518
669
|
}
|
|
@@ -522,8 +673,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
522
673
|
}
|
|
523
674
|
|
|
524
675
|
// Add scroll indicator if needed
|
|
525
|
-
if (startIndex > 0 || endIndex <
|
|
526
|
-
const scrollInfo = theme.fg("muted", ` (${this.#selectedIndex + 1}/${
|
|
676
|
+
if (startIndex > 0 || endIndex < visibleItems.length) {
|
|
677
|
+
const scrollInfo = theme.fg("muted", ` (${this.#selectedIndex + 1}/${visibleItems.length})`);
|
|
527
678
|
this.#listContainer.addChild(new Text(scrollInfo, 0, 0));
|
|
528
679
|
}
|
|
529
680
|
|
|
@@ -533,13 +684,21 @@ export class ModelSelectorComponent extends Container {
|
|
|
533
684
|
for (const line of errorLines) {
|
|
534
685
|
this.#listContainer.addChild(new Text(theme.fg("error", line), 0, 0));
|
|
535
686
|
}
|
|
536
|
-
} else if (
|
|
687
|
+
} else if (visibleItems.length === 0) {
|
|
537
688
|
const statusMessage = this.#getProviderEmptyStateMessage();
|
|
538
689
|
this.#listContainer.addChild(new Text(theme.fg("muted", statusMessage ?? " No matching models"), 0, 0));
|
|
539
690
|
} else {
|
|
540
|
-
const selected =
|
|
691
|
+
const selected = visibleItems[this.#selectedIndex];
|
|
692
|
+
if (!selected) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
541
695
|
this.#listContainer.addChild(new Spacer(1));
|
|
542
|
-
|
|
696
|
+
const suffix = isCanonicalTab
|
|
697
|
+
? ` (${selected.model.provider}/${selected.model.id}, ${(selected as CanonicalModelItem).variantCount} variants)`
|
|
698
|
+
: "";
|
|
699
|
+
this.#listContainer.addChild(
|
|
700
|
+
new Text(theme.fg("muted", ` Model Name: ${selected.model.name}${suffix}`), 0, 0),
|
|
701
|
+
);
|
|
543
702
|
}
|
|
544
703
|
}
|
|
545
704
|
#getThinkingLevelsForModel(model: Model): ReadonlyArray<ThinkingLevel> {
|
|
@@ -557,8 +716,14 @@ export class ModelSelectorComponent extends Container {
|
|
|
557
716
|
return foundIndex >= 0 ? foundIndex : 0;
|
|
558
717
|
}
|
|
559
718
|
|
|
719
|
+
#getSelectedItem(): ModelItem | CanonicalModelItem | undefined {
|
|
720
|
+
return this.#isCanonicalTab()
|
|
721
|
+
? this.#filteredCanonicalModels[this.#selectedIndex]
|
|
722
|
+
: this.#filteredModels[this.#selectedIndex];
|
|
723
|
+
}
|
|
724
|
+
|
|
560
725
|
#openMenu(): void {
|
|
561
|
-
if (this.#
|
|
726
|
+
if (!this.#getSelectedItem()) return;
|
|
562
727
|
|
|
563
728
|
this.#isMenuOpen = true;
|
|
564
729
|
this.#menuStep = "role";
|
|
@@ -577,11 +742,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
577
742
|
#updateMenu(): void {
|
|
578
743
|
this.#menuContainer.clear();
|
|
579
744
|
|
|
580
|
-
const
|
|
581
|
-
if (!
|
|
745
|
+
const selectedItem = this.#getSelectedItem();
|
|
746
|
+
if (!selectedItem) return;
|
|
582
747
|
|
|
583
748
|
const showingThinking = this.#menuStep === "thinking" && this.#menuSelectedRole !== null;
|
|
584
|
-
const thinkingOptions = showingThinking ? this.#getThinkingLevelsForModel(
|
|
749
|
+
const thinkingOptions = showingThinking ? this.#getThinkingLevelsForModel(selectedItem.model) : [];
|
|
585
750
|
const optionLines = showingThinking
|
|
586
751
|
? thinkingOptions.map((thinkingLevel, index) => {
|
|
587
752
|
const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
|
|
@@ -596,8 +761,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
596
761
|
const selectedRoleName = this.#menuSelectedRole ? getRoleInfo(this.#menuSelectedRole, this.#settings).name : "";
|
|
597
762
|
const headerText =
|
|
598
763
|
showingThinking && this.#menuSelectedRole
|
|
599
|
-
? ` Thinking for: ${selectedRoleName} (${
|
|
600
|
-
: ` Action for: ${
|
|
764
|
+
? ` Thinking for: ${selectedRoleName} (${selectedItem.id})`
|
|
765
|
+
: ` Action for: ${selectedItem.id}`;
|
|
601
766
|
const hintText = showingThinking ? " Enter: confirm Esc: back" : " Enter: continue Esc: cancel";
|
|
602
767
|
const menuWidth = Math.max(
|
|
603
768
|
visibleWidth(headerText),
|
|
@@ -610,15 +775,13 @@ export class ModelSelectorComponent extends Container {
|
|
|
610
775
|
if (showingThinking && this.#menuSelectedRole) {
|
|
611
776
|
this.#menuContainer.addChild(
|
|
612
777
|
new Text(
|
|
613
|
-
theme.fg("text", ` Thinking for: ${theme.bold(selectedRoleName)} (${theme.bold(
|
|
778
|
+
theme.fg("text", ` Thinking for: ${theme.bold(selectedRoleName)} (${theme.bold(selectedItem.id)})`),
|
|
614
779
|
0,
|
|
615
780
|
0,
|
|
616
781
|
),
|
|
617
782
|
);
|
|
618
783
|
} else {
|
|
619
|
-
this.#menuContainer.addChild(
|
|
620
|
-
new Text(theme.fg("text", ` Action for: ${theme.bold(selectedModel.id)}`), 0, 0),
|
|
621
|
-
);
|
|
784
|
+
this.#menuContainer.addChild(new Text(theme.fg("text", ` Action for: ${theme.bold(selectedItem.id)}`), 0, 0));
|
|
622
785
|
}
|
|
623
786
|
this.#menuContainer.addChild(new Spacer(1));
|
|
624
787
|
|
|
@@ -648,27 +811,29 @@ export class ModelSelectorComponent extends Container {
|
|
|
648
811
|
|
|
649
812
|
// Up arrow - navigate list (wrap to bottom when at top)
|
|
650
813
|
if (matchesKey(keyData, "up")) {
|
|
651
|
-
|
|
652
|
-
|
|
814
|
+
const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
|
|
815
|
+
if (itemCount === 0) return;
|
|
816
|
+
this.#selectedIndex = this.#selectedIndex === 0 ? itemCount - 1 : this.#selectedIndex - 1;
|
|
653
817
|
this.#updateList();
|
|
654
818
|
return;
|
|
655
819
|
}
|
|
656
820
|
|
|
657
821
|
// Down arrow - navigate list (wrap to top when at bottom)
|
|
658
822
|
if (matchesKey(keyData, "down")) {
|
|
659
|
-
|
|
660
|
-
|
|
823
|
+
const itemCount = this.#isCanonicalTab() ? this.#filteredCanonicalModels.length : this.#filteredModels.length;
|
|
824
|
+
if (itemCount === 0) return;
|
|
825
|
+
this.#selectedIndex = this.#selectedIndex === itemCount - 1 ? 0 : this.#selectedIndex + 1;
|
|
661
826
|
this.#updateList();
|
|
662
827
|
return;
|
|
663
828
|
}
|
|
664
829
|
|
|
665
830
|
// Enter - open context menu or select directly in temporary mode
|
|
666
831
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
667
|
-
const
|
|
668
|
-
if (
|
|
832
|
+
const selectedItem = this.#getSelectedItem();
|
|
833
|
+
if (selectedItem) {
|
|
669
834
|
if (this.#temporaryOnly) {
|
|
670
835
|
// In temporary mode, skip menu and select directly
|
|
671
|
-
this.#handleSelect(
|
|
836
|
+
this.#handleSelect(selectedItem, null);
|
|
672
837
|
} else {
|
|
673
838
|
this.#openMenu();
|
|
674
839
|
}
|
|
@@ -687,12 +852,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
687
852
|
this.#filterModels(this.#searchInput.getValue());
|
|
688
853
|
}
|
|
689
854
|
#handleMenuInput(keyData: string): void {
|
|
690
|
-
const
|
|
691
|
-
if (!
|
|
855
|
+
const selectedItem = this.#getSelectedItem();
|
|
856
|
+
if (!selectedItem) return;
|
|
692
857
|
|
|
693
858
|
const optionCount =
|
|
694
859
|
this.#menuStep === "thinking" && this.#menuSelectedRole !== null
|
|
695
|
-
? this.#getThinkingLevelsForModel(
|
|
860
|
+
? this.#getThinkingLevelsForModel(selectedItem.model).length
|
|
696
861
|
: this.#menuRoleActions.length;
|
|
697
862
|
if (optionCount === 0) return;
|
|
698
863
|
|
|
@@ -714,16 +879,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
714
879
|
if (!action) return;
|
|
715
880
|
this.#menuSelectedRole = action.role;
|
|
716
881
|
this.#menuStep = "thinking";
|
|
717
|
-
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(action.role,
|
|
882
|
+
this.#menuSelectedIndex = this.#getThinkingPreselectIndex(action.role, selectedItem.model);
|
|
718
883
|
this.#updateMenu();
|
|
719
884
|
return;
|
|
720
885
|
}
|
|
721
886
|
|
|
722
887
|
if (!this.#menuSelectedRole) return;
|
|
723
|
-
const thinkingOptions = this.#getThinkingLevelsForModel(
|
|
888
|
+
const thinkingOptions = this.#getThinkingLevelsForModel(selectedItem.model);
|
|
724
889
|
const thinkingLevel = thinkingOptions[this.#menuSelectedIndex];
|
|
725
890
|
if (!thinkingLevel) return;
|
|
726
|
-
this.#handleSelect(
|
|
891
|
+
this.#handleSelect(selectedItem, this.#menuSelectedRole, thinkingLevel);
|
|
727
892
|
this.#closeMenu();
|
|
728
893
|
return;
|
|
729
894
|
}
|
|
@@ -742,28 +907,20 @@ export class ModelSelectorComponent extends Container {
|
|
|
742
907
|
}
|
|
743
908
|
}
|
|
744
909
|
|
|
745
|
-
#
|
|
746
|
-
const modelKey = `${model.provider}/${model.id}`;
|
|
747
|
-
if (thinkingLevel === ThinkingLevel.Inherit) return modelKey;
|
|
748
|
-
return `${modelKey}:${thinkingLevel}`;
|
|
749
|
-
}
|
|
750
|
-
#handleSelect(model: Model, role: string | null, thinkingLevel?: ThinkingLevel): void {
|
|
910
|
+
#handleSelect(item: ModelItem | CanonicalModelItem, role: string | null, thinkingLevel?: ThinkingLevel): void {
|
|
751
911
|
// For temporary role, don't save to settings - just notify caller
|
|
752
912
|
if (role === null) {
|
|
753
|
-
this.#onSelectCallback(model, null);
|
|
913
|
+
this.#onSelectCallback(item.model, null, undefined, item.selector);
|
|
754
914
|
return;
|
|
755
915
|
}
|
|
756
916
|
|
|
757
917
|
const selectedThinkingLevel = thinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
|
|
758
918
|
|
|
759
|
-
// Save to settings
|
|
760
|
-
this.#settings.setModelRole(role, this.#formatRoleModelValue(model, selectedThinkingLevel));
|
|
761
|
-
|
|
762
919
|
// Update local state for UI
|
|
763
|
-
this.#roles[role] = { model, thinkingLevel: selectedThinkingLevel };
|
|
920
|
+
this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel };
|
|
764
921
|
|
|
765
922
|
// Notify caller (for updating agent state if needed)
|
|
766
|
-
this.#onSelectCallback(model, role, selectedThinkingLevel);
|
|
923
|
+
this.#onSelectCallback(item.model, role, selectedThinkingLevel, item.selector);
|
|
767
924
|
|
|
768
925
|
// Update list to show new badges
|
|
769
926
|
this.#updateList();
|
|
@@ -1002,6 +1002,14 @@ function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: numbe
|
|
|
1002
1002
|
return `account ${index + 1}`;
|
|
1003
1003
|
}
|
|
1004
1004
|
|
|
1005
|
+
function formatUnlimitedReportLabel(report: UsageReport, index: number): string {
|
|
1006
|
+
const email = report.metadata?.email as string | undefined;
|
|
1007
|
+
if (email) return email;
|
|
1008
|
+
const accountId = report.metadata?.accountId as string | undefined;
|
|
1009
|
+
if (accountId) return accountId;
|
|
1010
|
+
return `account ${index + 1}`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1005
1013
|
function formatResetShort(limit: UsageLimit, nowMs: number): string | undefined {
|
|
1006
1014
|
if (limit.window?.resetsAt !== undefined) {
|
|
1007
1015
|
return formatDuration(limit.window.resetsAt - nowMs);
|
|
@@ -1188,6 +1196,16 @@ function renderUsageReports(reports: UsageReport[], uiTheme: typeof theme, nowMs
|
|
|
1188
1196
|
}
|
|
1189
1197
|
}
|
|
1190
1198
|
|
|
1199
|
+
// Render accounts with no rate limits (e.g. business/enterprise plans).
|
|
1200
|
+
const unlimitedReports = providerReports.filter(report => report.limits.length === 0);
|
|
1201
|
+
for (const report of unlimitedReports) {
|
|
1202
|
+
const label = formatUnlimitedReportLabel(report, 0);
|
|
1203
|
+
const tier = report.metadata?.planType as string | undefined;
|
|
1204
|
+
const tierSuffix = tier ? ` ${uiTheme.fg("dim", `(${tier})`)}` : "";
|
|
1205
|
+
lines.push(
|
|
1206
|
+
`${uiTheme.fg("success", uiTheme.status.success)} ${label}${tierSuffix} ${uiTheme.fg("dim", "-- no limits")}`,
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1191
1209
|
// No per-provider footer; global header shows last check.
|
|
1192
1210
|
}
|
|
1193
1211
|
|