@pellux/goodvibes-tui 1.20.1 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +106 -22
- package/package.json +7 -7
- package/src/cli/doctor.ts +7 -1
- package/src/cli/package-verification.ts +92 -7
- package/src/cli/status.ts +16 -0
- package/src/config/read-versioned.ts +115 -2
- package/src/core/conversation-event-navigation.ts +42 -0
- package/src/core/conversation-search-expansion.ts +67 -0
- package/src/core/conversation-splash-state.ts +58 -0
- package/src/core/conversation.ts +23 -23
- package/src/core/model-routing-chip.ts +38 -18
- package/src/core/rewind-turn-anchors.ts +155 -3
- package/src/core/stream-event-wiring.ts +141 -20
- package/src/core/transcript-journal.ts +145 -7
- package/src/daemon/handlers/inbox/cursor-store.ts +159 -3
- package/src/daemon/handlers/inbox/index.ts +14 -1
- package/src/daemon/handlers/sqlite-store.ts +160 -6
- package/src/input/command-registry.ts +8 -0
- package/src/input/commands/local-runtime.ts +6 -1
- package/src/input/commands/schedule-runtime.ts +15 -2
- package/src/input/commands/shell-core.ts +87 -38
- package/src/input/composer-capture.ts +47 -0
- package/src/input/handler-command-route.ts +4 -0
- package/src/input/handler-content-actions.ts +9 -0
- package/src/input/handler-feed-routes.ts +1 -0
- package/src/input/handler-modal-routes.ts +12 -1
- package/src/input/handler-picker-routes.ts +43 -5
- package/src/input/handler-shortcuts.ts +1 -1
- package/src/input/handler-ui-state.ts +8 -3
- package/src/input/model-picker-items.ts +17 -2
- package/src/input/model-picker.ts +19 -2
- package/src/input/search.ts +183 -89
- package/src/input/settings-modal-types.ts +2 -1
- package/src/main.ts +12 -20
- package/src/providers/reasoning-effort-surface.ts +399 -0
- package/src/renderer/compositor.ts +68 -18
- package/src/renderer/markdown-inline.ts +143 -0
- package/src/renderer/markdown-table.ts +459 -0
- package/src/renderer/markdown.ts +6 -335
- package/src/renderer/model-workspace.ts +48 -5
- package/src/renderer/settings-modal-helpers.ts +53 -1
- package/src/renderer/settings-modal.ts +1 -50
- package/src/runtime/bootstrap-command-parts.ts +36 -4
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/durability-housekeeping.ts +183 -0
- package/src/runtime/durability-services.ts +68 -11
- package/src/runtime/legacy-daemon-reconcile.ts +16 -7
- package/src/runtime/provider-fallback.ts +16 -2
- package/src/runtime/recovery-decisions.ts +180 -11
- package/src/runtime/recovery-prompt.ts +11 -1
- package/src/runtime/services.ts +9 -4
- package/src/runtime/session-liveness-marker.ts +142 -5
- package/src/runtime/workstream-draft-store.ts +198 -25
- package/src/runtime/workstream-services.ts +17 -2
- package/src/shell/ui-openers.ts +1 -1
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +334 -16
|
@@ -14,6 +14,53 @@
|
|
|
14
14
|
* written and where.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
/** The provider-registry surface the `@model:` directive needs. */
|
|
18
|
+
export interface AtModelRegistry {
|
|
19
|
+
setCurrentModel(reference: string): void;
|
|
20
|
+
getCurrentModel(): { id: string; provider: string; registryKey: string; displayName: string };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AtModelDeps {
|
|
24
|
+
readonly providerRegistry: AtModelRegistry;
|
|
25
|
+
/** Session runtime whose model/provider labels follow an explicit switch. */
|
|
26
|
+
readonly runtime: { model: string; provider: string };
|
|
27
|
+
readonly configManager: { set(key: string, value: unknown): void };
|
|
28
|
+
/** Surfaces the switch confirmation / unknown-model message. */
|
|
29
|
+
readonly notify: (message: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AT_MODEL_PATTERN = /@model:([^\s]+)/g;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Apply every `@model:<id>` directive in a composer submission and return the
|
|
36
|
+
* text with the directives stripped.
|
|
37
|
+
*
|
|
38
|
+
* The switch is a real, persisted selection (config `provider.model`), not a
|
|
39
|
+
* per-turn override, so the runtime labels are updated alongside it and each
|
|
40
|
+
* one is announced. An unknown id is reported as unknown rather than silently
|
|
41
|
+
* dropped — the user asked for a specific backend and did not get it.
|
|
42
|
+
*/
|
|
43
|
+
export function applyAtModelDirective(input: string, deps: AtModelDeps): string {
|
|
44
|
+
let text = input;
|
|
45
|
+
let match: RegExpExecArray | null;
|
|
46
|
+
const pattern = new RegExp(AT_MODEL_PATTERN.source, 'g');
|
|
47
|
+
while ((match = pattern.exec(input)) !== null) {
|
|
48
|
+
const modelId = match[1]!;
|
|
49
|
+
try {
|
|
50
|
+
deps.providerRegistry.setCurrentModel(modelId);
|
|
51
|
+
const def = deps.providerRegistry.getCurrentModel();
|
|
52
|
+
deps.runtime.model = def.id;
|
|
53
|
+
deps.runtime.provider = def.provider;
|
|
54
|
+
deps.configManager.set('provider.model', def.registryKey);
|
|
55
|
+
deps.notify(`[Model] Switched to ${def.displayName} (${def.provider}) via @model:`);
|
|
56
|
+
} catch {
|
|
57
|
+
deps.notify(`[Model] Unknown model: ${modelId}`);
|
|
58
|
+
}
|
|
59
|
+
text = text.replace(match[0], '').trim();
|
|
60
|
+
}
|
|
61
|
+
return text;
|
|
62
|
+
}
|
|
63
|
+
|
|
17
64
|
export interface ComposerCaptureDeps {
|
|
18
65
|
readonly sessionMemoryStore: { add(text: string): string };
|
|
19
66
|
/** Surfaces the confirmation / usage message (a system message the shell renders). */
|
|
@@ -86,6 +86,10 @@ export function handleCommandModeToken(state: CommandModeRouteState, token: Inpu
|
|
|
86
86
|
const raw = selectedCmd ? `/${selectedCmd.name}` : state.prompt.trim();
|
|
87
87
|
if (raw.startsWith('/') && state.commandRegistry && state.commandContext) {
|
|
88
88
|
closeCommandMode();
|
|
89
|
+
// Submitting a command is a submission: the splash yields to command
|
|
90
|
+
// input exactly as it does to text input (owner rule), whether or not
|
|
91
|
+
// the command puts anything in the transcript.
|
|
92
|
+
state.conversationManager?.dismissSplash();
|
|
89
93
|
const parts = raw.slice(1).trim().split(/\s+/);
|
|
90
94
|
const name = parts[0];
|
|
91
95
|
const args = parts.slice(1);
|
|
@@ -303,6 +303,11 @@ export function handleBlockCopy(
|
|
|
303
303
|
}
|
|
304
304
|
copyToClipboard(nearest.rawContent);
|
|
305
305
|
onCopied();
|
|
306
|
+
// An explicit action on this block permanently exempts it from the
|
|
307
|
+
// search-close auto-re-collapse (see ConversationManager.noteUserTouch) —
|
|
308
|
+
// copying content the user just had search reveal is a deliberate choice
|
|
309
|
+
// to keep it visible, not an accident of typing a query.
|
|
310
|
+
conversationManager.searchExpansion.noteUserTouch(nearest.collapseKey);
|
|
306
311
|
conversationManager.log(`[Copied ${describeBlockForReceipt(nearest)}]`, { fg: '#22c55e' });
|
|
307
312
|
requestRender();
|
|
308
313
|
setTimeout(() => requestRender(), 2005);
|
|
@@ -324,6 +329,8 @@ export function handleBookmark(
|
|
|
324
329
|
}
|
|
325
330
|
const label = `${nearest.type}: ${nearest.rawContent.slice(0, 40).replace(/\n/g, ' ')}`;
|
|
326
331
|
const added = bookmarkManager.toggle(nearest.collapseKey, label);
|
|
332
|
+
// See handleBlockCopy's note — bookmarking is an explicit block action too.
|
|
333
|
+
conversationManager.searchExpansion.noteUserTouch(nearest.collapseKey);
|
|
327
334
|
const target = describeBlockForReceipt(nearest);
|
|
328
335
|
const msg = added
|
|
329
336
|
? `[Bookmarked: ${target}]`
|
|
@@ -346,6 +353,8 @@ export function handleBlockSave(
|
|
|
346
353
|
requestRender();
|
|
347
354
|
return;
|
|
348
355
|
}
|
|
356
|
+
// See handleBlockCopy's note — saving to a file is an explicit block action too.
|
|
357
|
+
conversationManager.searchExpansion.noteUserTouch(nearest.collapseKey);
|
|
349
358
|
const target = describeBlockForReceipt(nearest);
|
|
350
359
|
try {
|
|
351
360
|
const filePath = bookmarkManager.saveToFile(nearest.rawContent, nearest.type);
|
|
@@ -468,6 +468,7 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
|
|
|
468
468
|
prompt = '';
|
|
469
469
|
cursorPos = 0;
|
|
470
470
|
if (name) {
|
|
471
|
+
state.conversationManager?.dismissSplash(); // command input retires the splash, same as text input
|
|
471
472
|
const executeCommand = state.commandContext.executeCommand;
|
|
472
473
|
void executeCommand(name, args).then((handled) => {
|
|
473
474
|
if (!handled) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
2
|
+
import { servingEffortForLevel, toEffortModel } from '../providers/reasoning-effort-surface.ts';
|
|
2
3
|
import type { SelectionResult, SelectionAction } from './selection-modal.ts';
|
|
3
4
|
import type { CommandContext } from './command-registry.ts';
|
|
4
5
|
import type { ConfigModal } from './config-modal.ts';
|
|
@@ -286,7 +287,17 @@ type SettingsRouteState = {
|
|
|
286
287
|
function syncRuntimeAfterSettingReset(ctx: CommandContext | undefined, key: string, value: unknown): void {
|
|
287
288
|
if (!ctx) return;
|
|
288
289
|
if (key === 'provider.model') ctx.session.runtime.model = String(value);
|
|
289
|
-
if (key === 'provider.reasoningEffort')
|
|
290
|
+
if (key === 'provider.reasoningEffort') {
|
|
291
|
+
// config holds the REQUESTED level; the session holds the EFFECTIVE one for
|
|
292
|
+
// whichever model is serving, so the reset value is resolved rather than
|
|
293
|
+
// copied straight across. A context with no reachable provider registry
|
|
294
|
+
// (a surface that routes only through the provider API) stores the reset
|
|
295
|
+
// value unresolved rather than failing the whole reset.
|
|
296
|
+
const serving = ctx.provider?.providerRegistry?.getCurrentModel?.();
|
|
297
|
+
ctx.session.runtime.reasoningEffort = serving
|
|
298
|
+
? servingEffortForLevel(String(value), toEffortModel(serving)).effective ?? ''
|
|
299
|
+
: String(value);
|
|
300
|
+
}
|
|
290
301
|
}
|
|
291
302
|
|
|
292
303
|
function consumeSettingsPickerRequest(state: SettingsRouteState): void {
|
|
@@ -7,6 +7,7 @@ import { ensureProviderKeyThenSelect } from './provider-key-intake.ts';
|
|
|
7
7
|
import { resolveAndValidatePath } from '@pellux/goodvibes-sdk/platform/utils';
|
|
8
8
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
9
|
import type { BlockActionId } from '../renderer/block-actions.ts';
|
|
10
|
+
import { offersConfigurableEffort, requestedEffortLevel, servingEffortForLevel, toEffortModel } from '../providers/reasoning-effort-surface.ts';
|
|
10
11
|
|
|
11
12
|
type ModelPickerRouteState = {
|
|
12
13
|
modelPicker: ModelPickerModal;
|
|
@@ -18,6 +19,31 @@ type ModelPickerRouteState = {
|
|
|
18
19
|
onModelPickerCommit?: () => boolean;
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The user's REQUESTED reasoning level for this session, or undefined when the
|
|
24
|
+
* command context is not attached. Read from config through the shared helper —
|
|
25
|
+
* never from `session.runtime.reasoningEffort`, which holds the EFFECTIVE level
|
|
26
|
+
* for whichever model is serving and would re-seed a resolution with an already
|
|
27
|
+
* snapped-down value.
|
|
28
|
+
*/
|
|
29
|
+
function readRequestedEffort(state: ModelPickerRouteState): string | undefined {
|
|
30
|
+
const configManager = state.commandContext?.platform?.configManager;
|
|
31
|
+
if (!configManager) return undefined;
|
|
32
|
+
return requestedEffortLevel(configManager) || undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The level the effort step should open on for a model about to be selected:
|
|
37
|
+
* the requested level SNAPPED to that model. Opening on the raw requested level
|
|
38
|
+
* would miss the list entirely whenever the target model caps lower, and
|
|
39
|
+
* showEffortPicker falls back to index 0 — landing the cursor on the LOWEST
|
|
40
|
+
* level rather than on what pressing Enter would actually give you.
|
|
41
|
+
*/
|
|
42
|
+
function effortStepPreselect(state: ModelPickerRouteState, model: { id: string; provider?: string; displayName?: string }): string {
|
|
43
|
+
const requested = readRequestedEffort(state) ?? 'medium';
|
|
44
|
+
return servingEffortForLevel(requested, toEffortModel(model as never)).effective ?? requested;
|
|
45
|
+
}
|
|
46
|
+
|
|
21
47
|
export function handleModelPickerToken(state: ModelPickerRouteState, token: InputToken): boolean {
|
|
22
48
|
if (!state.modelPicker.active) return false;
|
|
23
49
|
|
|
@@ -61,9 +87,17 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
61
87
|
if (mode === 'model') {
|
|
62
88
|
const selected = state.modelPicker.getSelected();
|
|
63
89
|
if (selected) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
// Preselect the REQUESTED level, not the effective one: the effort step
|
|
91
|
+
// re-chooses the preference, so it must open on what the user asked for
|
|
92
|
+
// even while a model that caps lower is serving.
|
|
93
|
+
const currentEffort = readRequestedEffort(state) ?? 'medium';
|
|
94
|
+
// Whether an effort step appears at all is now this model's own
|
|
95
|
+
// resolved spec: a model with no configurable reasoning level — or
|
|
96
|
+
// one whose levels are only a labelled best guess the adapters will
|
|
97
|
+
// discard — skips straight to commit instead of showing a list it
|
|
98
|
+
// does not honour.
|
|
99
|
+
if (state.modelPicker.target === 'main' && offersConfigurableEffort(toEffortModel(selected))) {
|
|
100
|
+
state.modelPicker.showEffortPicker(selected, effortStepPreselect(state, selected));
|
|
67
101
|
} else {
|
|
68
102
|
const target = state.modelPicker.target;
|
|
69
103
|
const handled = state.onModelPickerCommit?.() ?? false;
|
|
@@ -75,6 +109,9 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
75
109
|
// (concealed prompt → secrets manager → live re-register) and
|
|
76
110
|
// only then completes the original selection.
|
|
77
111
|
ensureProviderKeyThenSelect(ctx, selected.provider, () => {
|
|
112
|
+
// No effort step ran, so `currentEffort` is carried over, not
|
|
113
|
+
// chosen: the commit path re-resolves from the stored
|
|
114
|
+
// preference instead of treating this as a new choice.
|
|
78
115
|
ctx.completeModelSelection?.({ model: selected, effort: currentEffort, target });
|
|
79
116
|
});
|
|
80
117
|
}
|
|
@@ -98,7 +135,8 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
98
135
|
const ctx = state.commandContext;
|
|
99
136
|
if (model && effort && !handled && ctx) {
|
|
100
137
|
ensureProviderKeyThenSelect(ctx, model.provider, () => {
|
|
101
|
-
|
|
138
|
+
// The effort STEP: the level below is one the user just picked.
|
|
139
|
+
ctx.completeModelSelection?.({ model, effort, target, effortChosenByUser: true });
|
|
102
140
|
});
|
|
103
141
|
}
|
|
104
142
|
} else if (mode === 'embeddingProvider') {
|
|
@@ -119,7 +157,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
119
157
|
} else {
|
|
120
158
|
state.modelPicker.contextCapError = null;
|
|
121
159
|
const validCap = parsedCap !== null && parsedCap > 0 && parsedCap <= 10_000_000 ? parsedCap : null;
|
|
122
|
-
const effort = state
|
|
160
|
+
const effort = readRequestedEffort(state) ?? 'medium';
|
|
123
161
|
const target = state.modelPicker.target;
|
|
124
162
|
const handled = state.onModelPickerCommit?.() ?? false;
|
|
125
163
|
state.modelPicker.close();
|
|
@@ -256,7 +256,7 @@ export function handleGlobalShortcutToken(
|
|
|
256
256
|
return true;
|
|
257
257
|
|
|
258
258
|
case 'search':
|
|
259
|
-
if (state.searchManager.active) state.searchManager.close();
|
|
259
|
+
if (state.searchManager.active) state.searchManager.close(state.conversationManager);
|
|
260
260
|
else state.searchManager.open();
|
|
261
261
|
state.requestRender();
|
|
262
262
|
return true;
|
|
@@ -223,10 +223,11 @@ export function handleSearchModeToken(
|
|
|
223
223
|
searchManager.search(newQuery, history, state.conversationManager);
|
|
224
224
|
} else if (token.type === 'key') {
|
|
225
225
|
if (token.logicalName === 'escape') {
|
|
226
|
-
searchManager.close();
|
|
226
|
+
searchManager.close(state.conversationManager);
|
|
227
227
|
} else if (token.logicalName === 'enter' || token.logicalName === 'tab') {
|
|
228
228
|
if (searchManager.query.length > 0) {
|
|
229
229
|
searchManager.lock();
|
|
230
|
+
searchManager.revealCurrentMatch(history, state.conversationManager);
|
|
230
231
|
const matchLine = searchManager.getCurrentMatchLine();
|
|
231
232
|
if (matchLine >= 0) {
|
|
232
233
|
state.scroll(matchLine - state.getScrollTop() - Math.floor(state.getViewportHeight() / 2));
|
|
@@ -236,21 +237,23 @@ export function handleSearchModeToken(
|
|
|
236
237
|
const newQuery = searchManager.query.slice(0, -1);
|
|
237
238
|
searchManager.search(newQuery, history, state.conversationManager);
|
|
238
239
|
} else if (matchesSearchShortcut) {
|
|
239
|
-
searchManager.close();
|
|
240
|
+
searchManager.close(state.conversationManager);
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
} else {
|
|
243
244
|
if (token.type === 'key') {
|
|
244
245
|
if (token.logicalName === 'escape' || matchesSearchShortcut) {
|
|
245
|
-
searchManager.close();
|
|
246
|
+
searchManager.close(state.conversationManager);
|
|
246
247
|
} else if (token.logicalName === 'right' || token.logicalName === 'down') {
|
|
247
248
|
searchManager.nextMatch();
|
|
249
|
+
searchManager.revealCurrentMatch(history, state.conversationManager);
|
|
248
250
|
const matchLine = searchManager.getCurrentMatchLine();
|
|
249
251
|
if (matchLine >= 0) {
|
|
250
252
|
state.scroll(matchLine - state.getScrollTop() - Math.floor(state.getViewportHeight() / 2));
|
|
251
253
|
}
|
|
252
254
|
} else if (token.logicalName === 'left' || token.logicalName === 'up') {
|
|
253
255
|
searchManager.prevMatch();
|
|
256
|
+
searchManager.revealCurrentMatch(history, state.conversationManager);
|
|
254
257
|
const matchLine = searchManager.getCurrentMatchLine();
|
|
255
258
|
if (matchLine >= 0) {
|
|
256
259
|
state.scroll(matchLine - state.getScrollTop() - Math.floor(state.getViewportHeight() / 2));
|
|
@@ -261,12 +264,14 @@ export function handleSearchModeToken(
|
|
|
261
264
|
} else if (token.type === 'text') {
|
|
262
265
|
if (token.value === 'n' || token.value === 'j' || token.value === 'l') {
|
|
263
266
|
searchManager.nextMatch();
|
|
267
|
+
searchManager.revealCurrentMatch(history, state.conversationManager);
|
|
264
268
|
const matchLine = searchManager.getCurrentMatchLine();
|
|
265
269
|
if (matchLine >= 0) {
|
|
266
270
|
state.scroll(matchLine - state.getScrollTop() - Math.floor(state.getViewportHeight() / 2));
|
|
267
271
|
}
|
|
268
272
|
} else if (token.value === 'N' || token.value === 'k' || token.value === 'h') {
|
|
269
273
|
searchManager.prevMatch();
|
|
274
|
+
searchManager.revealCurrentMatch(history, state.conversationManager);
|
|
270
275
|
const matchLine = searchManager.getCurrentMatchLine();
|
|
271
276
|
if (matchLine >= 0) {
|
|
272
277
|
state.scroll(matchLine - state.getScrollTop() - Math.floor(state.getViewportHeight() / 2));
|
|
@@ -203,6 +203,21 @@ export function buildProviderItems(
|
|
|
203
203
|
// buildEffortItems — effort levels -> PickerItem[]
|
|
204
204
|
// ---------------------------------------------------------------------------
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Effort levels as picker items.
|
|
208
|
+
*
|
|
209
|
+
* `choices` carries the per-spec descriptions (a budget-typed model shows the
|
|
210
|
+
* thinking-token budget each level sends, a toggle says on/off), so it is used
|
|
211
|
+
* when the caller has a resolved presentation. The bare level table is only the
|
|
212
|
+
* fallback for a caller that has levels but no spec.
|
|
213
|
+
*/
|
|
214
|
+
export function buildEffortItems(
|
|
215
|
+
effortLevels: readonly string[],
|
|
216
|
+
choices?: readonly { readonly level: string; readonly description: string }[],
|
|
217
|
+
): PickerItem[] {
|
|
218
|
+
return effortLevels.map((e) => ({
|
|
219
|
+
id: e,
|
|
220
|
+
label: e,
|
|
221
|
+
detail: choices?.find((choice) => choice.level === e)?.description ?? EFFORT_DESCRIPTIONS[e] ?? '',
|
|
222
|
+
}));
|
|
208
223
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
|
|
2
|
+
import {
|
|
3
|
+
effortPresentationForModel,
|
|
4
|
+
toEffortModel,
|
|
5
|
+
type EffortPresentation,
|
|
6
|
+
} from '../providers/reasoning-effort-surface.ts';
|
|
2
7
|
import type { FavoritesStore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
3
8
|
import type { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
4
9
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
@@ -87,7 +92,14 @@ export class ModelPickerModal {
|
|
|
87
92
|
public scrollOffset = 0;
|
|
88
93
|
public models: ModelDefinition[] = [];
|
|
89
94
|
public providers: string[] = [];
|
|
95
|
+
/** Levels the model chosen in model-mode actually offers (see showEffortPicker). */
|
|
90
96
|
public effortLevels: string[] = [];
|
|
97
|
+
/**
|
|
98
|
+
* How to present those levels honestly: the spec kind decides the headline
|
|
99
|
+
* (named levels vs a token budget vs an on/off toggle), and a best-guess
|
|
100
|
+
* spec carries a caveat. Null outside effort mode.
|
|
101
|
+
*/
|
|
102
|
+
public effortPresentation: EffortPresentation | null = null;
|
|
91
103
|
/** Entries for PickerMode 'embeddingProvider' — the 'embeddings' target's own tiny item list. */
|
|
92
104
|
public embeddingProviders: EmbeddingProviderPickerEntry[] = [];
|
|
93
105
|
/** Mode to restore when navigating the targets rail away from 'embeddings'. */
|
|
@@ -356,7 +368,11 @@ export class ModelPickerModal {
|
|
|
356
368
|
this.previousMode = 'model';
|
|
357
369
|
this.pendingModel = model;
|
|
358
370
|
this.searchFocused = false;
|
|
359
|
-
this
|
|
371
|
+
// Only this model's real levels: the picker used to read a raw list off
|
|
372
|
+
// the definition, which was the same four values for every model.
|
|
373
|
+
const presentation = effortPresentationForModel(toEffortModel(model));
|
|
374
|
+
this.effortPresentation = presentation;
|
|
375
|
+
this.effortLevels = presentation.choices.map((choice) => choice.level);
|
|
360
376
|
this.mode = 'effort';
|
|
361
377
|
const idx = this.effortLevels.indexOf(currentEffort);
|
|
362
378
|
this.selectedIndex = idx >= 0 ? idx : 0;
|
|
@@ -376,6 +392,7 @@ export class ModelPickerModal {
|
|
|
376
392
|
this.embeddingProviders = [];
|
|
377
393
|
this.nonEmbeddingMode = 'model';
|
|
378
394
|
this.pendingModel = null;
|
|
395
|
+
this.effortPresentation = null;
|
|
379
396
|
this.contextCapPendingModel = null;
|
|
380
397
|
this.contextCapQuery = '';
|
|
381
398
|
this.contextCapError = null;
|
|
@@ -548,7 +565,7 @@ export class ModelPickerModal {
|
|
|
548
565
|
}));
|
|
549
566
|
}
|
|
550
567
|
// effort mode
|
|
551
|
-
return buildEffortItems(this.effortLevels);
|
|
568
|
+
return buildEffortItems(this.effortLevels, this.effortPresentation?.choices);
|
|
552
569
|
}
|
|
553
570
|
|
|
554
571
|
/** Get count of selectable (non-header) items in current mode. */
|