@pellux/goodvibes-tui 0.19.32 → 0.19.34
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 +23 -0
- package/README.md +4 -2
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/audio/spoken-turn-model-routing.ts +117 -0
- package/src/input/command-registry.ts +2 -0
- package/src/input/commands/cloudflare-runtime.ts +343 -0
- package/src/input/commands/tts-runtime.ts +288 -7
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +1 -0
- package/src/input/handler-feed.ts +6 -0
- package/src/input/handler-modal-routes.ts +23 -10
- package/src/input/handler-modal-token-routes.ts +9 -0
- package/src/input/handler-onboarding-cloudflare.ts +391 -0
- package/src/input/handler-onboarding.ts +33 -0
- package/src/input/handler-picker-routes.ts +1 -1
- package/src/input/handler.ts +4 -1
- package/src/input/model-picker-types.ts +125 -0
- package/src/input/model-picker.ts +144 -134
- package/src/input/onboarding/onboarding-wizard-apply.ts +81 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +449 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +199 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +7 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +30 -8
- package/src/main.ts +12 -1
- package/src/renderer/buffer.ts +40 -2
- package/src/renderer/compositor.ts +25 -17
- package/src/renderer/model-picker-overlay.ts +70 -0
- package/src/renderer/settings-modal-helpers.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -0
- package/src/runtime/cloudflare-control-plane.ts +328 -0
- package/src/runtime/onboarding/derivation.ts +25 -0
- package/src/runtime/onboarding/snapshot.ts +2 -0
- package/src/runtime/onboarding/types.ts +5 -1
- package/src/shell/ui-openers.ts +21 -2
- package/src/version.ts +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { NETWORK_MODE_OPTIONS, REASONING_OPTIONS, HITL_MODE_OPTIONS, GUIDANCE_MODE_OPTIONS, PERMISSION_MODE_OPTIONS, SECRET_POLICY_OPTIONS } from './onboarding-wizard-constants.ts';
|
|
2
|
+
import { shouldShowCloudflareStep } from './onboarding-wizard-cloudflare.ts';
|
|
3
|
+
import { buildCloudflareStep } from './onboarding-wizard-cloudflare-step.ts';
|
|
2
4
|
import {
|
|
3
5
|
EXTERNAL_SURFACE_SPECS,
|
|
4
6
|
getExternalSurfaceAutoStartDefaultValue,
|
|
@@ -19,27 +21,25 @@ export function buildOnboardingWizardSteps(controller: OnboardingWizardControlle
|
|
|
19
21
|
const steps: OnboardingWizardStepDefinition[] = [
|
|
20
22
|
buildCapabilitiesStep(controller),
|
|
21
23
|
];
|
|
22
|
-
|
|
23
24
|
if (hasServers) {
|
|
24
25
|
steps.push(buildNetworkStep(controller));
|
|
25
26
|
}
|
|
26
|
-
|
|
27
27
|
if (hasServers || controller.hasExistingAccessState()) {
|
|
28
28
|
steps.push(buildAccessStep(controller));
|
|
29
29
|
}
|
|
30
|
-
|
|
31
30
|
if (wantsExternalServices) {
|
|
32
31
|
steps.push(buildExternalServicesStep(controller));
|
|
33
32
|
for (const surface of getSelectedExternalSurfaceSpecs(controller)) {
|
|
34
33
|
steps.push(buildExternalSurfaceStep(controller, surface));
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
|
-
|
|
36
|
+
if (shouldShowCloudflareStep(controller)) {
|
|
37
|
+
steps.push(buildCloudflareStep(controller));
|
|
38
|
+
}
|
|
38
39
|
steps.push(buildProviderAccessStep(controller));
|
|
39
40
|
steps.push(buildDefaultModelStep(controller));
|
|
40
41
|
steps.push(buildExperienceStep(controller));
|
|
41
42
|
steps.push(buildReviewStep(controller));
|
|
42
|
-
|
|
43
43
|
return steps.map(addApplyAndContinueAction);
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -49,7 +49,7 @@ function buildApplyAndContinueAction(step: OnboardingWizardStepDefinition): Onbo
|
|
|
49
49
|
id: `${step.id}.apply-and-continue`,
|
|
50
50
|
action: 'apply-and-continue',
|
|
51
51
|
label: 'Apply & Continue To Next Section',
|
|
52
|
-
hint: '
|
|
52
|
+
hint: 'Save the current wizard selections in this onboarding session and move to the next section. Settings are persisted on the final Review apply.',
|
|
53
53
|
defaultValue: 'Apply & next',
|
|
54
54
|
spacerBeforeRows: 2,
|
|
55
55
|
};
|
|
@@ -25,6 +25,7 @@ export type OnboardingWizardStepId =
|
|
|
25
25
|
| 'access'
|
|
26
26
|
| 'external-services'
|
|
27
27
|
| OnboardingWizardExternalSurfaceStepId
|
|
28
|
+
| 'cloudflare'
|
|
28
29
|
| 'provider-access'
|
|
29
30
|
| 'default-model'
|
|
30
31
|
| 'experience'
|
|
@@ -47,6 +48,13 @@ export type OnboardingWizardAction =
|
|
|
47
48
|
| 'clear-capabilities'
|
|
48
49
|
| 'select-all-external-surfaces'
|
|
49
50
|
| 'clear-external-surfaces'
|
|
51
|
+
| 'cloudflare-token-requirements'
|
|
52
|
+
| 'cloudflare-create-operational-token'
|
|
53
|
+
| 'cloudflare-discover'
|
|
54
|
+
| 'cloudflare-validate'
|
|
55
|
+
| 'cloudflare-provision'
|
|
56
|
+
| 'cloudflare-verify'
|
|
57
|
+
| 'cloudflare-disable'
|
|
50
58
|
| 'start-openai-subscription'
|
|
51
59
|
| 'finish-openai-subscription';
|
|
52
60
|
|
|
@@ -2,7 +2,7 @@ import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config/schema
|
|
|
2
2
|
import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
|
|
3
3
|
import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
|
|
4
4
|
|
|
5
|
-
export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'surfaces' | 'danger' | 'tools' | 'flags' | 'network';
|
|
5
|
+
export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'surfaces' | 'cloudflare' | 'danger' | 'tools' | 'flags' | 'network';
|
|
6
6
|
|
|
7
7
|
export const SETTINGS_CATEGORIES: SettingsCategory[] = [
|
|
8
8
|
'display',
|
|
@@ -15,6 +15,7 @@ export const SETTINGS_CATEGORIES: SettingsCategory[] = [
|
|
|
15
15
|
'mcp',
|
|
16
16
|
'sandbox',
|
|
17
17
|
'surfaces',
|
|
18
|
+
'cloudflare',
|
|
18
19
|
'danger',
|
|
19
20
|
'tools',
|
|
20
21
|
'flags',
|
|
@@ -41,13 +41,21 @@ export {
|
|
|
41
41
|
type SubscriptionEntry,
|
|
42
42
|
} from './settings-modal-types.ts';
|
|
43
43
|
|
|
44
|
+
type ModelPickerLaunch =
|
|
45
|
+
| { readonly flow: 'providerModel'; readonly target: ModelPickerTarget }
|
|
46
|
+
| { readonly flow: 'model'; readonly target: ModelPickerTarget };
|
|
47
|
+
|
|
44
48
|
/**
|
|
45
|
-
* Map
|
|
46
|
-
*
|
|
49
|
+
* Map config keys to the shared provider/model picker flows. Provider rows open
|
|
50
|
+
* provider first; model rows open directly to models for the same target.
|
|
47
51
|
*/
|
|
48
|
-
function
|
|
49
|
-
if (key === 'helper.globalProvider'
|
|
50
|
-
if (key === '
|
|
52
|
+
function _modelPickerLaunchForKey(key: string): ModelPickerLaunch | null {
|
|
53
|
+
if (key === 'helper.globalProvider') return { flow: 'providerModel', target: 'helper' };
|
|
54
|
+
if (key === 'helper.globalModel') return { flow: 'model', target: 'helper' };
|
|
55
|
+
if (key === 'tools.llmProvider') return { flow: 'providerModel', target: 'tool' };
|
|
56
|
+
if (key === 'tools.llmModel') return { flow: 'model', target: 'tool' };
|
|
57
|
+
if (key === 'tts.llmProvider') return { flow: 'providerModel', target: 'tts' };
|
|
58
|
+
if (key === 'tts.llmModel') return { flow: 'model', target: 'tts' };
|
|
51
59
|
return null;
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -94,6 +102,8 @@ export class SettingsModal {
|
|
|
94
102
|
* Consumed and cleared by the route handler after each Enter/Space action.
|
|
95
103
|
*/
|
|
96
104
|
public pendingModelPickerTarget: ModelPickerTarget | null = null;
|
|
105
|
+
/** Set when the highlighted setting should open provider selection before model selection. */
|
|
106
|
+
public pendingProviderModelPickerTarget: ModelPickerTarget | null = null;
|
|
97
107
|
/** Provider awaiting explicit logout confirmation, if any. */
|
|
98
108
|
public subscriptionLogoutConfirmationTarget: string | null = null;
|
|
99
109
|
|
|
@@ -146,6 +156,8 @@ export class SettingsModal {
|
|
|
146
156
|
this.selectedIndex = 0;
|
|
147
157
|
this.editingMode = false;
|
|
148
158
|
this.editBuffer = '';
|
|
159
|
+
this.pendingModelPickerTarget = null;
|
|
160
|
+
this.pendingProviderModelPickerTarget = null;
|
|
149
161
|
this.mcpAllowAllConfirmationTarget = null;
|
|
150
162
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
151
163
|
this.lastSaveTriggeredRestart = null;
|
|
@@ -156,6 +168,8 @@ export class SettingsModal {
|
|
|
156
168
|
this.active = false;
|
|
157
169
|
this.editingMode = false;
|
|
158
170
|
this.editBuffer = '';
|
|
171
|
+
this.pendingModelPickerTarget = null;
|
|
172
|
+
this.pendingProviderModelPickerTarget = null;
|
|
159
173
|
this.mcpAllowAllConfirmationTarget = null;
|
|
160
174
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
161
175
|
this.lastSaveTriggeredRestart = null;
|
|
@@ -290,9 +304,13 @@ export class SettingsModal {
|
|
|
290
304
|
const { setting } = entry;
|
|
291
305
|
|
|
292
306
|
// Delegate provider/model picker settings to the model picker UI
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
|
|
307
|
+
const pickerLaunch = _modelPickerLaunchForKey(setting.key);
|
|
308
|
+
if (pickerLaunch !== null) {
|
|
309
|
+
if (pickerLaunch.flow === 'providerModel') {
|
|
310
|
+
this.pendingProviderModelPickerTarget = pickerLaunch.target;
|
|
311
|
+
} else {
|
|
312
|
+
this.pendingModelPickerTarget = pickerLaunch.target;
|
|
313
|
+
}
|
|
296
314
|
return;
|
|
297
315
|
}
|
|
298
316
|
|
|
@@ -521,6 +539,8 @@ export class SettingsModal {
|
|
|
521
539
|
cat = 'network';
|
|
522
540
|
} else if (rawCat === 'surfaces') {
|
|
523
541
|
cat = 'surfaces';
|
|
542
|
+
} else if (rawCat === 'cloudflare' || rawCat === 'batch') {
|
|
543
|
+
cat = 'cloudflare';
|
|
524
544
|
} else {
|
|
525
545
|
cat = rawCat as SettingsCategory;
|
|
526
546
|
}
|
|
@@ -750,6 +770,8 @@ export class SettingsModal {
|
|
|
750
770
|
}
|
|
751
771
|
} else if (rawCat === 'surfaces') {
|
|
752
772
|
cat = 'surfaces';
|
|
773
|
+
} else if (rawCat === 'cloudflare' || rawCat === 'batch') {
|
|
774
|
+
cat = 'cloudflare';
|
|
753
775
|
} else {
|
|
754
776
|
cat = rawCat as SettingsCategory;
|
|
755
777
|
}
|
package/src/main.ts
CHANGED
|
@@ -52,6 +52,10 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-displ
|
|
|
52
52
|
import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
53
53
|
import { applyInitialTuiCliState } from './cli/tui-startup.ts';
|
|
54
54
|
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
55
|
+
import {
|
|
56
|
+
attachSpokenTurnModelRouting,
|
|
57
|
+
createSpokenTurnInputOptions,
|
|
58
|
+
} from './audio/spoken-turn-model-routing.ts';
|
|
55
59
|
|
|
56
60
|
const ALT_SCREEN_ENTER = '\x1b[?1049h';
|
|
57
61
|
const ALT_SCREEN_EXIT = '\x1b[?1049l';
|
|
@@ -251,6 +255,12 @@ async function main() {
|
|
|
251
255
|
});
|
|
252
256
|
stopSpokenOutputForExit = () => spokenTurns.stop();
|
|
253
257
|
unsubs.push(...spokenTurns.unsubs);
|
|
258
|
+
unsubs.push(attachSpokenTurnModelRouting({
|
|
259
|
+
orchestrator,
|
|
260
|
+
providerRegistry,
|
|
261
|
+
configManager,
|
|
262
|
+
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
263
|
+
}));
|
|
254
264
|
|
|
255
265
|
const submitInput = (text: string, content?: ContentPart[], options: { readonly spokenOutput?: boolean } = {}) => {
|
|
256
266
|
input.clearModalStack();
|
|
@@ -289,7 +299,8 @@ async function main() {
|
|
|
289
299
|
if (options.spokenOutput && processedText) {
|
|
290
300
|
spokenTurns.submitNextTurn(processedText);
|
|
291
301
|
}
|
|
292
|
-
|
|
302
|
+
const inputOptions = options.spokenOutput ? createSpokenTurnInputOptions() : undefined;
|
|
303
|
+
orchestrator.handleUserInput(processedText, content, inputOptions).catch((err: unknown) => {
|
|
293
304
|
logger.debug('handleUserInput safety catch (already handled by runTurn)', { error: summarizeError(err) });
|
|
294
305
|
});
|
|
295
306
|
} else {
|
package/src/renderer/buffer.ts
CHANGED
|
@@ -39,7 +39,13 @@ export class TerminalBuffer {
|
|
|
39
39
|
|
|
40
40
|
public blitLine(row: number, line: Line): void {
|
|
41
41
|
if (row >= 0 && row < this.height) {
|
|
42
|
-
this.cells[row]
|
|
42
|
+
const current = this.cells[row]!;
|
|
43
|
+
const next = createEmptyLine(this.width);
|
|
44
|
+
for (let x = 0; x < Math.min(line.length, this.width); x++) {
|
|
45
|
+
next[x] = { ...line[x]! };
|
|
46
|
+
}
|
|
47
|
+
if (linesEqual(current, next, this.width)) return;
|
|
48
|
+
this.cells[row] = next;
|
|
43
49
|
this.dirtyRows[row] = true;
|
|
44
50
|
}
|
|
45
51
|
}
|
|
@@ -56,12 +62,17 @@ export class TerminalBuffer {
|
|
|
56
62
|
* If dimensions changed, reallocates cells array.
|
|
57
63
|
* Always clears the dirty bitmap.
|
|
58
64
|
*/
|
|
59
|
-
public reset(width: number, height: number): void {
|
|
65
|
+
public reset(width: number, height: number, source?: TerminalBuffer | null): void {
|
|
60
66
|
if (width !== this.width || height !== this.height) {
|
|
61
67
|
this.width = width;
|
|
62
68
|
this.height = height;
|
|
63
69
|
this.cells = Array.from({ length: height }, () => createEmptyLine(width));
|
|
64
70
|
this.dirtyRows = new Array(height).fill(false);
|
|
71
|
+
} else if (source && source.width === width && source.height === height) {
|
|
72
|
+
for (let y = 0; y < this.height; y++) {
|
|
73
|
+
this.cells[y] = source.cells[y]!.map(cell => ({ ...cell }));
|
|
74
|
+
this.dirtyRows[y] = false;
|
|
75
|
+
}
|
|
65
76
|
} else {
|
|
66
77
|
for (let y = 0; y < this.height; y++) {
|
|
67
78
|
const row = this.cells[y]!;
|
|
@@ -72,4 +83,31 @@ export class TerminalBuffer {
|
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
}
|
|
86
|
+
|
|
87
|
+
public clearDirty(): void {
|
|
88
|
+
this.dirtyRows.fill(false);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function linesEqual(left: Line, right: Line, width: number): boolean {
|
|
93
|
+
for (let x = 0; x < width; x++) {
|
|
94
|
+
const a = left[x];
|
|
95
|
+
const b = right[x];
|
|
96
|
+
if (!a && !b) continue;
|
|
97
|
+
if (!a || !b) return false;
|
|
98
|
+
if (
|
|
99
|
+
a.char !== b.char
|
|
100
|
+
|| a.fg !== b.fg
|
|
101
|
+
|| a.bg !== b.bg
|
|
102
|
+
|| a.bold !== b.bold
|
|
103
|
+
|| a.dim !== b.dim
|
|
104
|
+
|| a.underline !== b.underline
|
|
105
|
+
|| a.italic !== b.italic
|
|
106
|
+
|| a.strikethrough !== b.strikethrough
|
|
107
|
+
|| (a.link ?? '') !== (b.link ?? '')
|
|
108
|
+
) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
75
113
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TerminalBuffer } from './buffer.ts';
|
|
2
2
|
import { DiffEngine } from './diff.ts';
|
|
3
|
-
import { type Line, createStyledCell } from '../types/grid.ts';
|
|
3
|
+
import { type Line, createEmptyCell, createStyledCell } from '../types/grid.ts';
|
|
4
4
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
5
5
|
import type { SearchManager } from '../input/search.ts';
|
|
6
6
|
|
|
@@ -78,7 +78,7 @@ export class Compositor {
|
|
|
78
78
|
if (!this.backBuffer) {
|
|
79
79
|
this.backBuffer = new TerminalBuffer(width, height);
|
|
80
80
|
} else {
|
|
81
|
-
this.backBuffer.reset(width, height);
|
|
81
|
+
this.backBuffer.reset(width, height, this.frontBuffer);
|
|
82
82
|
}
|
|
83
83
|
const newBuffer = this.backBuffer;
|
|
84
84
|
|
|
@@ -154,19 +154,31 @@ export class Compositor {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
const panelStartX = sepX + 1;
|
|
157
|
+
const clearPanelRemainder = (fromX = 0) => {
|
|
158
|
+
for (let x = Math.max(0, fromX); x < panelWidth; x++) {
|
|
159
|
+
newBuffer.setCell(panelStartX + x, screenY, createEmptyCell());
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const drawPanelLine = (panelLine: Line | undefined) => {
|
|
163
|
+
if (panelLine === undefined) {
|
|
164
|
+
clearPanelRemainder();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const limit = Math.min(panelLine.length, panelWidth);
|
|
168
|
+
for (let x = 0; x < limit; x++) {
|
|
169
|
+
const cell = panelLine[x];
|
|
170
|
+
if (cell !== undefined) {
|
|
171
|
+
newBuffer.setCell(panelStartX + x, screenY, cell);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
clearPanelRemainder(limit);
|
|
175
|
+
};
|
|
157
176
|
|
|
158
177
|
if (!hasBottomPane) {
|
|
159
178
|
// --- Single pane mode ---
|
|
160
179
|
// viewport row 0 → workspace bar, viewport rows 1+ → panel content
|
|
161
180
|
const panelLine = i === 0 ? p.workspaceBar : p.topContent[i - 1];
|
|
162
|
-
|
|
163
|
-
for (let x = 0; x < panelWidth; x++) {
|
|
164
|
-
const cell = panelLine[x];
|
|
165
|
-
if (cell !== undefined) {
|
|
166
|
-
newBuffer.setCell(panelStartX + x, screenY, cell);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
181
|
+
drawPanelLine(panelLine);
|
|
170
182
|
} else {
|
|
171
183
|
// --- Two pane mode ---
|
|
172
184
|
// Row layout (by viewport row i):
|
|
@@ -203,13 +215,8 @@ export class Compositor {
|
|
|
203
215
|
panelLine = p.bottomContent?.[i - (hSepRow + 2)];
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
const cell = panelLine[x];
|
|
209
|
-
if (cell !== undefined) {
|
|
210
|
-
newBuffer.setCell(panelStartX + x, screenY, cell);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
218
|
+
if (i !== hSepRow) {
|
|
219
|
+
drawPanelLine(panelLine);
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
222
|
}
|
|
@@ -269,6 +276,7 @@ export class Compositor {
|
|
|
269
276
|
// Swap: back (just written) becomes the new front reference; old front becomes the next back
|
|
270
277
|
const swap = this.frontBuffer;
|
|
271
278
|
this.frontBuffer = this.backBuffer;
|
|
279
|
+
this.frontBuffer.clearDirty();
|
|
272
280
|
this.backBuffer = swap;
|
|
273
281
|
}
|
|
274
282
|
}
|
|
@@ -34,6 +34,10 @@ const MODE_TITLES: Record<string, string> = {
|
|
|
34
34
|
*/
|
|
35
35
|
export const MODEL_PICKER_CHROME_LINES = 7;
|
|
36
36
|
|
|
37
|
+
const renderCache = new WeakMap<ModelPickerModal, { key: string; lines: Line[] }>();
|
|
38
|
+
const objectIds = new WeakMap<object, number>();
|
|
39
|
+
let nextObjectId = 1;
|
|
40
|
+
|
|
37
41
|
function putRowText(line: Line, startX: number, maxWidth: number, text: string, fg: string, bg = '', bold = false, dim = false): void {
|
|
38
42
|
putOverlayText(line, startX, maxWidth, text, { fg, bg, bold, dim });
|
|
39
43
|
}
|
|
@@ -51,6 +55,10 @@ export function renderModelPickerOverlay(
|
|
|
51
55
|
maxVisible = 20,
|
|
52
56
|
viewportHeight?: number,
|
|
53
57
|
): Line[] {
|
|
58
|
+
const cacheKey = getRenderCacheKey(picker, width, maxVisible, viewportHeight);
|
|
59
|
+
const cached = renderCache.get(picker);
|
|
60
|
+
if (cached?.key === cacheKey) return cached.lines;
|
|
61
|
+
|
|
54
62
|
const lines: Line[] = [];
|
|
55
63
|
const metrics = getOverlaySurfaceMetrics(width, viewportHeight ?? 24, {
|
|
56
64
|
chromeRows: MODEL_PICKER_CHROME_LINES,
|
|
@@ -399,5 +407,67 @@ export function renderModelPickerOverlay(
|
|
|
399
407
|
putRowText(footerLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(hints, contentW), contentW), mutedFg, '', false, true);
|
|
400
408
|
lines.push(footerLine);
|
|
401
409
|
|
|
410
|
+
renderCache.set(picker, { key: cacheKey, lines });
|
|
402
411
|
return lines;
|
|
403
412
|
}
|
|
413
|
+
|
|
414
|
+
function getRenderCacheKey(
|
|
415
|
+
picker: ModelPickerModal,
|
|
416
|
+
width: number,
|
|
417
|
+
maxVisible: number,
|
|
418
|
+
viewportHeight: number | undefined,
|
|
419
|
+
): string {
|
|
420
|
+
const base = [
|
|
421
|
+
width,
|
|
422
|
+
maxVisible,
|
|
423
|
+
viewportHeight ?? '',
|
|
424
|
+
picker.mode,
|
|
425
|
+
picker.target,
|
|
426
|
+
picker.query,
|
|
427
|
+
picker.searchFocused ? 1 : 0,
|
|
428
|
+
picker.selectedIndex,
|
|
429
|
+
picker.scrollOffset,
|
|
430
|
+
picker.categoryFilter,
|
|
431
|
+
picker.capabilityFilter,
|
|
432
|
+
picker.availableOnly ? 1 : 0,
|
|
433
|
+
picker.benchmarkSort,
|
|
434
|
+
picker.groupBy,
|
|
435
|
+
keyForSet(picker.pinnedIds),
|
|
436
|
+
keyForSet(picker.configuredProviders),
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
if (picker.mode === 'model') {
|
|
440
|
+
const filtered = picker.getFilteredModels();
|
|
441
|
+
const selected = filtered[picker.selectedIndex];
|
|
442
|
+
base.push(objectId(picker.models), objectId(filtered), filtered.length, selected?.registryKey ?? selected?.id ?? '');
|
|
443
|
+
} else if (picker.mode === 'provider') {
|
|
444
|
+
const filteredProviders = picker.getFilteredProviders();
|
|
445
|
+
base.push(objectId(picker.providers), objectId(filteredProviders), filteredProviders.length, keyForMap(picker.configuredViaMap));
|
|
446
|
+
} else if (picker.mode === 'effort') {
|
|
447
|
+
base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
|
|
448
|
+
} else if (picker.mode === 'contextCap') {
|
|
449
|
+
base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return base.join('\u001e');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function objectId(value: object): number {
|
|
456
|
+
const existing = objectIds.get(value);
|
|
457
|
+
if (existing !== undefined) return existing;
|
|
458
|
+
const next = nextObjectId++;
|
|
459
|
+
objectIds.set(value, next);
|
|
460
|
+
return next;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function keyForSet(values: ReadonlySet<string>): string {
|
|
464
|
+
return values.size === 0 ? '' : [...values].sort().join('\u001f');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function keyForMap(values: ReadonlyMap<string, string | undefined>): string {
|
|
468
|
+
if (values.size === 0) return '';
|
|
469
|
+
return [...values.entries()]
|
|
470
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
471
|
+
.map(([key, value]) => `${key}\u001d${value ?? ''}`)
|
|
472
|
+
.join('\u001f');
|
|
473
|
+
}
|
|
@@ -203,6 +203,10 @@ export function createBootstrapCommandActions(
|
|
|
203
203
|
configManager.set('tools.llmModel', key);
|
|
204
204
|
configManager.setDynamic('tools.llmEnabled' as never, true);
|
|
205
205
|
conversation.log(`Tool LLM set to: ${def.displayName} (${def.provider})`, { fg: '135' });
|
|
206
|
+
} else if (resolvedTarget === 'tts') {
|
|
207
|
+
configManager.set('tts.llmProvider', def.provider);
|
|
208
|
+
configManager.set('tts.llmModel', key);
|
|
209
|
+
conversation.log(`TTS LLM set to: ${def.displayName} (${def.provider})`, { fg: '135' });
|
|
206
210
|
} else {
|
|
207
211
|
// Default: main provider/model
|
|
208
212
|
if (contextCap != null && contextCap > 0) {
|