@pellux/goodvibes-tui 0.20.3 → 0.21.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 +27 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +3 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +662 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +14 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +12 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +31 -1
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +3 -3
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +91 -12
- package/src/input/handler-modal-token-routes.ts +3 -0
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +55 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +5 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +4 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +18 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-data.ts +304 -0
- package/src/input/settings-modal-mutations.ts +154 -0
- package/src/input/settings-modal.ts +182 -220
- package/src/main.ts +57 -57
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +4 -1
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/knowledge-panel.ts +3 -5
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +11 -10
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +77 -8
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +14 -0
- package/src/runtime/bootstrap-core.ts +121 -13
- package/src/runtime/bootstrap.ts +2 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +21 -0
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -1,15 +1,53 @@
|
|
|
1
1
|
import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
|
|
2
2
|
import type { FavoritesStore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
3
|
-
import { EFFORT_DESCRIPTIONS } from '@pellux/goodvibes-sdk/platform/providers';
|
|
4
|
-
import { getQualityTier, getQualityTierFromScore, compositeScore, A_TIER_THRESHOLD } from '@pellux/goodvibes-sdk/platform/providers';
|
|
5
3
|
import type { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
6
4
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
7
5
|
import { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
|
|
8
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
BenchmarkSort,
|
|
8
|
+
CapabilityFilter,
|
|
9
|
+
CategoryFilter,
|
|
10
|
+
FilteredModelsCache,
|
|
11
|
+
FilteredProvidersCache,
|
|
12
|
+
GroupByMode,
|
|
13
|
+
ModelItemsCache,
|
|
14
|
+
ModelPickerFocusPane,
|
|
15
|
+
ModelPickerTarget,
|
|
16
|
+
ModelPickerTargetInfo,
|
|
17
|
+
PickerItem,
|
|
18
|
+
PickerMode,
|
|
19
|
+
ProviderItemsCache,
|
|
20
|
+
} from './model-picker-types.ts';
|
|
9
21
|
import { filterProviders, groupProviders } from './model-picker-provider-filter.ts';
|
|
22
|
+
import {
|
|
23
|
+
buildFilteredModels,
|
|
24
|
+
buildFilteredProviders,
|
|
25
|
+
getSyntheticSubgroup,
|
|
26
|
+
setKey,
|
|
27
|
+
orderedListKey,
|
|
28
|
+
mapKey,
|
|
29
|
+
} from './model-picker-filter.ts';
|
|
30
|
+
import {
|
|
31
|
+
buildModelItems,
|
|
32
|
+
buildProviderItems,
|
|
33
|
+
buildEffortItems,
|
|
34
|
+
getModelGroupKey,
|
|
35
|
+
toModelItem,
|
|
36
|
+
} from './model-picker-items.ts';
|
|
10
37
|
|
|
11
38
|
export { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
|
|
12
|
-
export type {
|
|
39
|
+
export type {
|
|
40
|
+
BenchmarkSort,
|
|
41
|
+
CapabilityFilter,
|
|
42
|
+
CategoryFilter,
|
|
43
|
+
GroupByMode,
|
|
44
|
+
ModelFamily,
|
|
45
|
+
ModelPickerFocusPane,
|
|
46
|
+
ModelPickerTarget,
|
|
47
|
+
ModelPickerTargetInfo,
|
|
48
|
+
PickerItem,
|
|
49
|
+
PickerMode,
|
|
50
|
+
} from './model-picker-types.ts';
|
|
13
51
|
|
|
14
52
|
/**
|
|
15
53
|
* ModelPickerModal - Multi-step interactive picker for model, provider, and effort.
|
|
@@ -55,7 +93,7 @@ export class ModelPickerModal {
|
|
|
55
93
|
/** Current input string in contextCap mode. */
|
|
56
94
|
public contextCapQuery = '';
|
|
57
95
|
|
|
58
|
-
// ── Search / filter
|
|
96
|
+
// ── Search / filter ─────────────────────────────────────────────────────────────────────────────
|
|
59
97
|
/** Current search query string (empty = no filter). */
|
|
60
98
|
public query = '';
|
|
61
99
|
/** Active pricing tier filter. */
|
|
@@ -134,7 +172,7 @@ export class ModelPickerModal {
|
|
|
134
172
|
}
|
|
135
173
|
}
|
|
136
174
|
|
|
137
|
-
// ── Category filter cycling
|
|
175
|
+
// ── Category filter cycling ───────────────────────────────────────────────────
|
|
138
176
|
private static readonly CATEGORY_CYCLE: CategoryFilter[] = ['all', 'free', 'paid', 'subscription'];
|
|
139
177
|
/** Cycle to next pricing tier filter. */
|
|
140
178
|
cycleCategory(): void {
|
|
@@ -144,7 +182,7 @@ export class ModelPickerModal {
|
|
|
144
182
|
this._clampSelection();
|
|
145
183
|
}
|
|
146
184
|
|
|
147
|
-
// ── Group-by cycling
|
|
185
|
+
// ── Group-by cycling ────────────────────────────────────────────────────────────────
|
|
148
186
|
private static readonly GROUP_BY_CYCLE: GroupByMode[] = ['provider', 'family', 'pricingTier', 'qualityTier'];
|
|
149
187
|
/** Cycle to next group-by mode. */
|
|
150
188
|
cycleGroupBy(): void {
|
|
@@ -154,7 +192,7 @@ export class ModelPickerModal {
|
|
|
154
192
|
this._clampSelection();
|
|
155
193
|
}
|
|
156
194
|
|
|
157
|
-
// ── Benchmark sort cycling
|
|
195
|
+
// ── Benchmark sort cycling ───────────────────────────────────────────────────────────────
|
|
158
196
|
private static readonly BENCHMARK_SORT_CYCLE: BenchmarkSort[] = ['none', 'composite', 'swe', 'gpqa'];
|
|
159
197
|
/** Cycle to next benchmark sort order. */
|
|
160
198
|
cycleBenchmarkSort(): void {
|
|
@@ -281,7 +319,7 @@ export class ModelPickerModal {
|
|
|
281
319
|
this.clearCaches();
|
|
282
320
|
}
|
|
283
321
|
|
|
284
|
-
// ── Search helpers
|
|
322
|
+
// ── Search helpers ─────────────────────────────────────────────────────────────────────
|
|
285
323
|
|
|
286
324
|
/** Append a character to the search query and clamp selectedIndex. */
|
|
287
325
|
appendChar(ch: string): void {
|
|
@@ -351,175 +389,35 @@ export class ModelPickerModal {
|
|
|
351
389
|
|
|
352
390
|
/** Return providers matching the current query (case-insensitive substring), in grouped order. */
|
|
353
391
|
getFilteredProviders(): string[] {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return cached.result;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const result = filterProviders(this.providers, this.query);
|
|
364
|
-
this.filteredProvidersCache = {
|
|
365
|
-
providersRef: this.providers,
|
|
366
|
-
query: this.query,
|
|
367
|
-
result,
|
|
368
|
-
};
|
|
392
|
+
const { result, cache } = buildFilteredProviders(
|
|
393
|
+
this.providers,
|
|
394
|
+
this.query,
|
|
395
|
+
this.filteredProvidersCache,
|
|
396
|
+
);
|
|
397
|
+
this.filteredProvidersCache = cache;
|
|
369
398
|
return result;
|
|
370
399
|
}
|
|
371
400
|
|
|
372
401
|
/** Return models matching all current filters, sorted per benchmarkSort. */
|
|
373
402
|
getFilteredModels(): ModelDefinition[] {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
let result = this.models;
|
|
395
|
-
|
|
396
|
-
// Available-only filter
|
|
397
|
-
if (this.availableOnly && this.configuredProviders.size > 0) {
|
|
398
|
-
result = result.filter(m => this.configuredProviders.has(m.provider));
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Pricing tier / category filter
|
|
402
|
-
if (this.categoryFilter === 'free') {
|
|
403
|
-
result = result.filter(m => m.tier === 'free');
|
|
404
|
-
} else if (this.categoryFilter === 'paid') {
|
|
405
|
-
result = result.filter(m => m.tier === 'standard' || m.tier === 'premium' || m.tier == null);
|
|
406
|
-
} else if (this.categoryFilter === 'subscription') {
|
|
407
|
-
result = result.filter(m => tierToCategoryFilter(m.tier) === 'subscription');
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Capability filter
|
|
411
|
-
if (this.capabilityFilter === 'reasoning') {
|
|
412
|
-
result = result.filter(m => m.capabilities?.reasoning === true);
|
|
413
|
-
} else if (this.capabilityFilter === 'toolUse') {
|
|
414
|
-
result = result.filter(m => m.capabilities?.toolCalling === true);
|
|
415
|
-
} else if (this.capabilityFilter === 'multimodal') {
|
|
416
|
-
result = result.filter(m => m.capabilities?.multimodal === true);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Query filter — fuzzy: every space-separated word must appear somewhere
|
|
420
|
-
if (this.query.trim().length > 0) {
|
|
421
|
-
const words = this.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
422
|
-
result = result.filter(m => {
|
|
423
|
-
const haystack = `${m.id} ${m.displayName} ${m.provider}`.toLowerCase();
|
|
424
|
-
return words.every(w => haystack.includes(w));
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Benchmark sort
|
|
429
|
-
if (this.benchmarkSort !== 'none') {
|
|
430
|
-
result = [...result].sort((a, b) => {
|
|
431
|
-
let scoreA: number | null = null;
|
|
432
|
-
let scoreB: number | null = null;
|
|
433
|
-
|
|
434
|
-
// For synthetic models, use pre-computed bestCompositeScore from backend lookup
|
|
435
|
-
// (synthetic canonical slugs don't exist in ZeroEval benchmark data)
|
|
436
|
-
if (this.benchmarkSort === 'composite') {
|
|
437
|
-
if (a.provider === 'synthetic') {
|
|
438
|
-
scoreA = this.providerRegistry.getSyntheticModelInfoFromCatalog(a.id)?.bestCompositeScore ?? null;
|
|
439
|
-
} else {
|
|
440
|
-
const bA = this.benchmarkStore.getBenchmarks(a.id) ?? this.benchmarkStore.getBenchmarks(a.displayName);
|
|
441
|
-
scoreA = bA ? compositeScore(bA.benchmarks) : null;
|
|
442
|
-
}
|
|
443
|
-
if (b.provider === 'synthetic') {
|
|
444
|
-
scoreB = this.providerRegistry.getSyntheticModelInfoFromCatalog(b.id)?.bestCompositeScore ?? null;
|
|
445
|
-
} else {
|
|
446
|
-
const bB = this.benchmarkStore.getBenchmarks(b.id) ?? this.benchmarkStore.getBenchmarks(b.displayName);
|
|
447
|
-
scoreB = bB ? compositeScore(bB.benchmarks) : null;
|
|
448
|
-
}
|
|
449
|
-
} else {
|
|
450
|
-
// swe/gpqa sort — individual benchmark scores not available for synthetic models — only composite is cached
|
|
451
|
-
const bA = a.provider === 'synthetic' ? null : (this.benchmarkStore.getBenchmarks(a.id) ?? this.benchmarkStore.getBenchmarks(a.displayName));
|
|
452
|
-
const bB = b.provider === 'synthetic' ? null : (this.benchmarkStore.getBenchmarks(b.id) ?? this.benchmarkStore.getBenchmarks(b.displayName));
|
|
453
|
-
if (this.benchmarkSort === 'swe') {
|
|
454
|
-
scoreA = bA?.benchmarks.swe ?? null;
|
|
455
|
-
scoreB = bB?.benchmarks.swe ?? null;
|
|
456
|
-
} else if (this.benchmarkSort === 'gpqa') {
|
|
457
|
-
scoreA = bA?.benchmarks.gpqa ?? null;
|
|
458
|
-
scoreB = bB?.benchmarks.gpqa ?? null;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
// Models with no score sink to the end
|
|
462
|
-
if (scoreA == null && scoreB == null) return 0;
|
|
463
|
-
if (scoreA == null) return 1;
|
|
464
|
-
if (scoreB == null) return -1;
|
|
465
|
-
return scoreB - scoreA; // descending
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Synthetic sub-grouping: when groupBy is 'provider', order synthetic models so that
|
|
470
|
-
// "Top Models" (score ≥ 0.65) appear before "All Synthetic", each sub-group internally
|
|
471
|
-
// sorted: top by composite score desc, all alphabetically by id.
|
|
472
|
-
if (this.groupBy === 'provider' && this.benchmarkSort === 'none') {
|
|
473
|
-
const nonSynthetic = result.filter(m => m.provider !== 'synthetic');
|
|
474
|
-
const synthetic = result.filter(m => m.provider === 'synthetic');
|
|
475
|
-
|
|
476
|
-
if (synthetic.length > 0) {
|
|
477
|
-
const topModels = synthetic.filter(m => this._getSyntheticSubgroup(m) === 'top');
|
|
478
|
-
const allModels = synthetic.filter(m => this._getSyntheticSubgroup(m) === 'all');
|
|
479
|
-
|
|
480
|
-
// Sort top models by composite score descending
|
|
481
|
-
topModels.sort((a, b) => {
|
|
482
|
-
const sA = this.providerRegistry.getSyntheticModelInfoFromCatalog(a.id)?.bestCompositeScore ?? null;
|
|
483
|
-
const sB = this.providerRegistry.getSyntheticModelInfoFromCatalog(b.id)?.bestCompositeScore ?? null;
|
|
484
|
-
if (sA == null && sB == null) return 0;
|
|
485
|
-
if (sA == null) return 1;
|
|
486
|
-
if (sB == null) return -1;
|
|
487
|
-
return sB - sA;
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// Sort remaining alphabetically by id
|
|
491
|
-
allModels.sort((a, b) => a.id.localeCompare(b.id));
|
|
492
|
-
|
|
493
|
-
result = [...nonSynthetic, ...topModels, ...allModels];
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Boost recent (non-pinned) models to the front of the list,
|
|
498
|
-
// preserving relative order within the recent group and within the rest.
|
|
499
|
-
if (this.recentIds.length > 0) {
|
|
500
|
-
const recentSet = new Set(this.recentIds);
|
|
501
|
-
const recent = this.recentIds
|
|
502
|
-
.filter(id => result.some(m => m.id === id && !this.pinnedIds.has(id)))
|
|
503
|
-
.map(id => result.find(m => m.id === id)!)
|
|
504
|
-
.filter(Boolean);
|
|
505
|
-
const rest = result.filter(m => !recentSet.has(m.id) || this.pinnedIds.has(m.id));
|
|
506
|
-
result = [...recent, ...rest];
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
this.filteredModelsCache = {
|
|
510
|
-
modelsRef: this.models,
|
|
511
|
-
configuredProvidersKey,
|
|
512
|
-
pinnedIdsKey,
|
|
513
|
-
recentIdsKey,
|
|
514
|
-
query: this.query,
|
|
515
|
-
categoryFilter: this.categoryFilter,
|
|
516
|
-
capabilityFilter: this.capabilityFilter,
|
|
517
|
-
availableOnly: this.availableOnly,
|
|
518
|
-
benchmarkSort: this.benchmarkSort,
|
|
519
|
-
groupBy: this.groupBy,
|
|
520
|
-
result,
|
|
521
|
-
};
|
|
522
|
-
this.modelItemsCache = null;
|
|
403
|
+
const { result, cache } = buildFilteredModels(
|
|
404
|
+
{
|
|
405
|
+
models: this.models,
|
|
406
|
+
configuredProviders: this.configuredProviders,
|
|
407
|
+
pinnedIds: this.pinnedIds,
|
|
408
|
+
recentIds: this.recentIds,
|
|
409
|
+
query: this.query,
|
|
410
|
+
categoryFilter: this.categoryFilter,
|
|
411
|
+
capabilityFilter: this.capabilityFilter,
|
|
412
|
+
availableOnly: this.availableOnly,
|
|
413
|
+
benchmarkSort: this.benchmarkSort,
|
|
414
|
+
groupBy: this.groupBy,
|
|
415
|
+
benchmarkStore: this.benchmarkStore,
|
|
416
|
+
providerRegistry: this.providerRegistry,
|
|
417
|
+
},
|
|
418
|
+
this.filteredModelsCache,
|
|
419
|
+
);
|
|
420
|
+
this.filteredModelsCache = cache;
|
|
523
421
|
return result;
|
|
524
422
|
}
|
|
525
423
|
|
|
@@ -541,151 +439,37 @@ export class ModelPickerModal {
|
|
|
541
439
|
* - 'All Synthetic' — remaining synthetic models
|
|
542
440
|
*/
|
|
543
441
|
getModelGroupKey(model: ModelDefinition): string {
|
|
544
|
-
|
|
545
|
-
case 'provider':
|
|
546
|
-
if (model.provider === 'synthetic') {
|
|
547
|
-
return this._getSyntheticSubgroup(model) === 'top' ? 'Top Models' : 'All Synthetic';
|
|
548
|
-
}
|
|
549
|
-
return model.provider;
|
|
550
|
-
case 'family': return detectFamily(model);
|
|
551
|
-
case 'pricingTier': return tierToCategoryFilter(model.tier);
|
|
552
|
-
case 'qualityTier': {
|
|
553
|
-
if (model.provider === 'synthetic') {
|
|
554
|
-
const info = this.providerRegistry.getSyntheticModelInfoFromCatalog(model.id);
|
|
555
|
-
return info?.bestCompositeScore != null ? getQualityTierFromScore(info.bestCompositeScore) : 'C';
|
|
556
|
-
}
|
|
557
|
-
const b = this.benchmarkStore.getBenchmarks(model.id) ?? this.benchmarkStore.getBenchmarks(model.displayName);
|
|
558
|
-
return b ? getQualityTier(b.benchmarks) : 'C';
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Classify a synthetic model as 'top' or 'all' based on benchmark composite score.
|
|
565
|
-
* 'top': has benchmark data and score ≥ 0.65 (A-tier or S-tier)
|
|
566
|
-
* 'all': no benchmark data or score < 0.65
|
|
567
|
-
*/
|
|
568
|
-
private _getSyntheticSubgroup(model: ModelDefinition): 'top' | 'all' {
|
|
569
|
-
const info = this.providerRegistry.getSyntheticModelInfoFromCatalog(model.id);
|
|
570
|
-
const score = info?.bestCompositeScore ?? null;
|
|
571
|
-
return score !== null && score >= A_TIER_THRESHOLD ? 'top' : 'all';
|
|
442
|
+
return getModelGroupKey(model, this.groupBy, this.providerRegistry, this.benchmarkStore);
|
|
572
443
|
}
|
|
573
444
|
|
|
574
445
|
/** Get the items for the current mode as a unified list. */
|
|
575
446
|
getItems(): PickerItem[] {
|
|
576
447
|
if (this.mode === 'model') {
|
|
577
448
|
const filtered = this.getFilteredModels();
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
// Separate pinned and unpinned
|
|
590
|
-
const pinned = filtered.filter(m => this.pinnedIds.has(m.id));
|
|
591
|
-
const unpinned = filtered.filter(m => !this.pinnedIds.has(m.id));
|
|
592
|
-
|
|
593
|
-
const items: PickerItem[] = [];
|
|
594
|
-
|
|
595
|
-
// Pinned section header (only if pinned models are in the filtered list)
|
|
596
|
-
if (pinned.length > 0) {
|
|
597
|
-
items.push({ id: '__header__pinned', label: 'Favorites', isGroupHeader: true });
|
|
598
|
-
for (const m of pinned) {
|
|
599
|
-
items.push(this._modelToItem(m, true));
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// Grouped unpinned models
|
|
604
|
-
let lastGroupKey = '';
|
|
605
|
-
for (const m of unpinned) {
|
|
606
|
-
const groupKey = this.getModelGroupKey(m);
|
|
607
|
-
if (groupKey !== lastGroupKey) {
|
|
608
|
-
items.push({ id: `__header__${groupKey}`, label: groupKey, isGroupHeader: true });
|
|
609
|
-
lastGroupKey = groupKey;
|
|
610
|
-
}
|
|
611
|
-
items.push(this._modelToItem(m, false));
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
this.modelItemsCache = {
|
|
615
|
-
filteredModelsRef: filtered,
|
|
616
|
-
pinnedIdsKey,
|
|
617
|
-
groupBy: this.groupBy,
|
|
618
|
-
result: items,
|
|
619
|
-
};
|
|
620
|
-
return items;
|
|
449
|
+
const { result, cache } = buildModelItems(
|
|
450
|
+
filtered,
|
|
451
|
+
this.pinnedIds,
|
|
452
|
+
this.groupBy,
|
|
453
|
+
this.providerRegistry,
|
|
454
|
+
this.benchmarkStore,
|
|
455
|
+
this.modelItemsCache,
|
|
456
|
+
);
|
|
457
|
+
this.modelItemsCache = cache;
|
|
458
|
+
return result;
|
|
621
459
|
}
|
|
622
460
|
if (this.mode === 'provider') {
|
|
623
461
|
const filteredProviders = this.getFilteredProviders();
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
) {
|
|
633
|
-
return cached.result;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const providerItems: PickerItem[] = [];
|
|
637
|
-
let currentGroup: 'Popular' | 'All Providers' | null = null;
|
|
638
|
-
for (const p of filteredProviders) {
|
|
639
|
-
const group: 'Popular' | 'All Providers' = POPULAR_PROVIDERS.has(p.toLowerCase()) ? 'Popular' : 'All Providers';
|
|
640
|
-
if (group !== currentGroup) {
|
|
641
|
-
providerItems.push({ id: `__header__${group}`, label: group, isGroupHeader: true });
|
|
642
|
-
currentGroup = group;
|
|
643
|
-
}
|
|
644
|
-
providerItems.push({ id: p, label: p, isConfigured: this.configuredProviders.has(p), configuredVia: this.configuredViaMap.get(p) });
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
this.providerItemsCache = {
|
|
648
|
-
filteredProvidersRef: filteredProviders,
|
|
649
|
-
configuredProvidersKey,
|
|
650
|
-
configuredViaKey,
|
|
651
|
-
result: providerItems,
|
|
652
|
-
};
|
|
653
|
-
return providerItems;
|
|
462
|
+
const { result, cache } = buildProviderItems(
|
|
463
|
+
filteredProviders,
|
|
464
|
+
this.configuredProviders,
|
|
465
|
+
this.configuredViaMap,
|
|
466
|
+
this.providerItemsCache,
|
|
467
|
+
);
|
|
468
|
+
this.providerItemsCache = cache;
|
|
469
|
+
return result;
|
|
654
470
|
}
|
|
655
471
|
// effort mode
|
|
656
|
-
return this.effortLevels
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/** Build a PickerItem for a model, including quality tier and pin status. */
|
|
660
|
-
private _modelToItem(model: ModelDefinition, isPinned: boolean): PickerItem {
|
|
661
|
-
// For synthetic models, derive quality tier from cached bestCompositeScore
|
|
662
|
-
// (synthetic canonical slugs don't exist in ZeroEval benchmark data)
|
|
663
|
-
let qualityTier: string | undefined;
|
|
664
|
-
let detail: string;
|
|
665
|
-
if (model.provider === 'synthetic') {
|
|
666
|
-
const synthInfo = this.providerRegistry.getSyntheticModelInfoFromCatalog(model.id);
|
|
667
|
-
if (synthInfo?.bestCompositeScore != null) {
|
|
668
|
-
qualityTier = getQualityTierFromScore(synthInfo.bestCompositeScore);
|
|
669
|
-
}
|
|
670
|
-
// Reuse synthInfo for provider count detail
|
|
671
|
-
detail = synthInfo !== null
|
|
672
|
-
? `${model.provider} [${synthInfo.keyedBackendCount} provider${synthInfo.keyedBackendCount !== 1 ? 's' : ''}]`
|
|
673
|
-
: model.provider;
|
|
674
|
-
} else {
|
|
675
|
-
detail = model.provider;
|
|
676
|
-
const b = this.benchmarkStore.getBenchmarks(model.id) ?? this.benchmarkStore.getBenchmarks(model.displayName);
|
|
677
|
-
qualityTier = b ? getQualityTier(b.benchmarks) : undefined;
|
|
678
|
-
}
|
|
679
|
-
const isFree = tierToCategoryFilter(model.tier) === 'free';
|
|
680
|
-
|
|
681
|
-
return {
|
|
682
|
-
id: model.id,
|
|
683
|
-
label: model.displayName,
|
|
684
|
-
detail,
|
|
685
|
-
qualityTier,
|
|
686
|
-
isPinned,
|
|
687
|
-
isFree,
|
|
688
|
-
};
|
|
472
|
+
return buildEffortItems(this.effortLevels);
|
|
689
473
|
}
|
|
690
474
|
|
|
691
475
|
/** Get count of selectable (non-header) items in current mode. */
|
|
@@ -730,7 +514,7 @@ export class ModelPickerModal {
|
|
|
730
514
|
return filtered[this.selectedIndex] ?? null;
|
|
731
515
|
}
|
|
732
516
|
|
|
733
|
-
// ── Private helpers
|
|
517
|
+
// ── Private helpers ─────────────────────────────────────────────────────────────────────
|
|
734
518
|
|
|
735
519
|
private _clampSelection(): void {
|
|
736
520
|
const count = this.getItemCount();
|
|
@@ -778,20 +562,3 @@ export class ModelPickerModal {
|
|
|
778
562
|
this.clearFilteredCaches();
|
|
779
563
|
}
|
|
780
564
|
}
|
|
781
|
-
|
|
782
|
-
function setKey(values: ReadonlySet<string>): string {
|
|
783
|
-
if (values.size === 0) return '';
|
|
784
|
-
return [...values].sort().join('\u001f');
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function orderedListKey(values: readonly string[]): string {
|
|
788
|
-
return values.length === 0 ? '' : values.join('\u001f');
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function mapKey(values: ReadonlyMap<string, string | undefined>): string {
|
|
792
|
-
if (values.size === 0) return '';
|
|
793
|
-
return [...values.entries()]
|
|
794
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
795
|
-
.map(([key, value]) => `${key}\u001e${value ?? ''}`)
|
|
796
|
-
.join('\u001f');
|
|
797
|
-
}
|
|
@@ -15,6 +15,8 @@ type OnboardingRouteState = {
|
|
|
15
15
|
source?: 'settings' | 'onboarding',
|
|
16
16
|
) => boolean;
|
|
17
17
|
onAction?: (action: OnboardingWizardAction) => void;
|
|
18
|
+
/** Called after any step navigation so the handler can persist progress. */
|
|
19
|
+
onStepChange?: () => void;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
function activateSelection(state: OnboardingRouteState): void {
|
|
@@ -74,11 +76,13 @@ export function handleOnboardingWizardToken(state: OnboardingRouteState, token:
|
|
|
74
76
|
}
|
|
75
77
|
} else if (token.logicalName === 'left') {
|
|
76
78
|
state.onboardingWizard.prevStep();
|
|
79
|
+
state.onStepChange?.();
|
|
77
80
|
} else if (token.logicalName === 'right') {
|
|
78
81
|
state.onboardingWizard.nextStep();
|
|
82
|
+
state.onStepChange?.();
|
|
79
83
|
} else if (token.logicalName === 'tab') {
|
|
80
|
-
if (token.shift) state.onboardingWizard.prevStep();
|
|
81
|
-
else state.onboardingWizard.nextStep();
|
|
84
|
+
if (token.shift) { state.onboardingWizard.prevStep(); state.onStepChange?.(); }
|
|
85
|
+
else { state.onboardingWizard.nextStep(); state.onStepChange?.(); }
|
|
82
86
|
} else if (token.logicalName === 'up') {
|
|
83
87
|
state.onboardingWizard.moveSelection(-1, visibleFields);
|
|
84
88
|
} else if (token.logicalName === 'down') {
|
|
@@ -116,6 +120,7 @@ export function handleOnboardingWizardToken(state: OnboardingRouteState, token:
|
|
|
116
120
|
const stepIndex = Number(token.value) - 1;
|
|
117
121
|
if (stepIndex < state.onboardingWizard.steps.length) {
|
|
118
122
|
state.onboardingWizard.setStep(stepIndex);
|
|
123
|
+
state.onStepChange?.();
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { OnboardingVerificationItem } from '../../runtime/onboarding/index.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract an OAuth authorization code from a callback URL or raw code string.
|
|
5
|
+
* Returns the `code` query parameter if input is a URL, or the trimmed string
|
|
6
|
+
* itself if it looks like a raw code. Returns null for empty input.
|
|
7
|
+
*/
|
|
8
|
+
export function extractAuthorizationCode(input: string): string | null {
|
|
9
|
+
const trimmed = input.trim();
|
|
10
|
+
if (!trimmed) return null;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(trimmed);
|
|
14
|
+
return url.searchParams.get('code');
|
|
15
|
+
} catch {
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Return true if the value is a recognized loopback host (localhost, 127.x.x.x,
|
|
22
|
+
* ::1, or [::1]). Tolerates null / undefined / empty values by returning false.
|
|
23
|
+
*/
|
|
24
|
+
export function isLoopbackHostValue(value: string | null | undefined): boolean {
|
|
25
|
+
const normalized = (value ?? '').trim().toLowerCase();
|
|
26
|
+
if (normalized.length === 0) return false;
|
|
27
|
+
return normalized === 'localhost'
|
|
28
|
+
|| normalized === '::1'
|
|
29
|
+
|| normalized === '[::1]'
|
|
30
|
+
|| normalized === '0:0:0:0:0:0:0:1'
|
|
31
|
+
|| /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Priority rank used when de-duplicating verification items by id. */
|
|
35
|
+
function onboardingVerificationStatusRank(item: OnboardingVerificationItem): number {
|
|
36
|
+
if (item.status === 'fail') return 3;
|
|
37
|
+
if (item.status === 'warn') return 2;
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Collapse a list of OnboardingVerificationItem entries so that each unique id
|
|
43
|
+
* appears at most once, keeping the highest-severity status when duplicates exist.
|
|
44
|
+
*/
|
|
45
|
+
export function dedupeOnboardingVerificationItems(
|
|
46
|
+
items: readonly OnboardingVerificationItem[],
|
|
47
|
+
): OnboardingVerificationItem[] {
|
|
48
|
+
const order: string[] = [];
|
|
49
|
+
const byId = new Map<string, OnboardingVerificationItem>();
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
const existing = byId.get(item.id);
|
|
52
|
+
if (!existing) {
|
|
53
|
+
order.push(item.id);
|
|
54
|
+
byId.set(item.id, item);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (onboardingVerificationStatusRank(item) > onboardingVerificationStatusRank(existing)) {
|
|
58
|
+
byId.set(item.id, item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return order.map((id) => byId.get(id)).filter((item): item is OnboardingVerificationItem => Boolean(item));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format a human-readable summary of an onboarding apply operation given the
|
|
66
|
+
* list of verification items returned after the apply completed.
|
|
67
|
+
*/
|
|
68
|
+
export function formatOnboardingApplyCompletionMessage(items: readonly OnboardingVerificationItem[]): string {
|
|
69
|
+
const warnings = items.filter((item) => item.status === 'warn');
|
|
70
|
+
if (warnings.length === 0) return `Onboarding applied and verified ${items.length} item(s).`;
|
|
71
|
+
const passed = items.filter((item) => item.status === 'pass').length;
|
|
72
|
+
return [
|
|
73
|
+
`Onboarding settings applied. ${passed} verification item(s) passed; ${warnings.length} warning(s) need attention.`,
|
|
74
|
+
...warnings.map((warning) => ` warning ${warning.id}: ${warning.message}`),
|
|
75
|
+
].join('\n');
|
|
76
|
+
}
|
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
isExternalSurfaceSelectedByDefault,
|
|
17
17
|
} from './onboarding-wizard-external-surfaces.ts';
|
|
18
18
|
import { buildGoodVibesSecretKey, buildGoodVibesSecretRef, isLoopbackAddress, isSecretReferenceValue } from './onboarding-wizard-helpers.ts';
|
|
19
|
-
import type {
|
|
19
|
+
import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
|
|
20
20
|
|
|
21
|
-
export function buildOnboardingApplyRequest(controller:
|
|
21
|
+
export function buildOnboardingApplyRequest(controller: OnboardingWizardControllerLike): OnboardingApplyRequest {
|
|
22
22
|
const operations: OnboardingApplyOperation[] = [];
|
|
23
23
|
const hasServers = controller.hasServerCapabilitiesSelected();
|
|
24
24
|
const browserAccess = controller.shouldEnableBrowserSurface();
|
|
@@ -178,7 +178,7 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function addCloudflareOperations(
|
|
181
|
-
controller:
|
|
181
|
+
controller: OnboardingWizardControllerLike,
|
|
182
182
|
operations: OnboardingApplyOperation[],
|
|
183
183
|
setSecret: (key: string, value: string) => void,
|
|
184
184
|
): void {
|
|
@@ -251,7 +251,7 @@ function addCloudflareOperations(
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
export function addNetworkOperations(
|
|
254
|
-
controller:
|
|
254
|
+
controller: OnboardingWizardControllerLike,
|
|
255
255
|
operations: OnboardingApplyOperation[],
|
|
256
256
|
customNetwork: boolean,
|
|
257
257
|
enabled: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CLOUDFLARE_COMPONENT_IDS, DEFAULT_CLOUDFLARE_COMPONENT_SELECTION } from '../../runtime/cloudflare-control-plane.ts';
|
|
2
2
|
import { normalizeText } from './onboarding-wizard-helpers.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { OnboardingWizardControllerLike } from './onboarding-wizard-types.ts';
|
|
4
4
|
import type { OnboardingWizardFieldDefinition, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
|
|
5
5
|
import {
|
|
6
6
|
CLOUDFLARE_BATCH_MODE_OPTIONS,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getCloudflareSetupSource,
|
|
15
15
|
} from './onboarding-wizard-cloudflare.ts';
|
|
16
16
|
|
|
17
|
-
export function buildCloudflareStep(controller:
|
|
17
|
+
export function buildCloudflareStep(controller: OnboardingWizardControllerLike): OnboardingWizardStepDefinition {
|
|
18
18
|
const config = controller.runtimeSnapshot?.config.cloudflare;
|
|
19
19
|
const batch = controller.runtimeSnapshot?.config.batch;
|
|
20
20
|
const enabledDefault = controller.isCapabilitySelected('cloudflare-batch') || config?.enabled === true;
|