pi-vision-handoff 0.3.2 → 0.4.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/package.json +1 -1
- package/src/describer.ts +25 -3
- package/src/index.ts +36 -1
- package/src/vision-model-selector.ts +87 -5
- package/vision-handoff.ts +63 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vision-handoff",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Give text-only pi models vision — describe images with a vision model you pick via an interactive picker, then hand off the text description to non-vision models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Tom X Nguyen",
|
package/src/describer.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* abort listener.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type { Api, ImageContent, Message, Model, TextContent } from "@earendil-works/pi-ai";
|
|
21
|
+
import type { Api, ImageContent, Message, Model, TextContent, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
22
22
|
import { complete } from "@earendil-works/pi-ai/compat";
|
|
23
23
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
24
24
|
import {
|
|
@@ -76,6 +76,26 @@ export function resolveMaxTokens(
|
|
|
76
76
|
return Math.max(1, Math.min(requested, cap));
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Resolve the `reasoning` (thinking) level to pass to `complete()`, or
|
|
80
|
+
* `undefined` to leave thinking off.
|
|
81
|
+
*
|
|
82
|
+
* - Returns `undefined` when thinking is disabled in config.
|
|
83
|
+
* - Returns `undefined` when the vision model doesn't declare reasoning
|
|
84
|
+
* support (`model.reasoning === false`) — sending a `reasoning` level to a
|
|
85
|
+
* non-reasoning model would be ignored at best and rejected at worst.
|
|
86
|
+
* - Otherwise returns the configured {@link VisionHandoffConfig.thinkingLevel}.
|
|
87
|
+
*
|
|
88
|
+
* Mirrors pi core's `createSummarizationOptions` guard so the describer's
|
|
89
|
+
* thinking behaviour matches the rest of pi. */
|
|
90
|
+
export function resolveReasoning(
|
|
91
|
+
cfg: VisionHandoffConfig,
|
|
92
|
+
visionModel: Model<Api>,
|
|
93
|
+
): ThinkingLevel | undefined {
|
|
94
|
+
if (!cfg.thinking) return undefined;
|
|
95
|
+
if (!visionModel.reasoning) return undefined;
|
|
96
|
+
return cfg.thinkingLevel;
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
/** Dependencies the describer can't own itself (held by the engine). */
|
|
80
100
|
export interface DescriberDeps {
|
|
81
101
|
/** Report a usage+energy record for one real describer call (cache hits emit none). */
|
|
@@ -126,6 +146,7 @@ export async function runBatch(
|
|
|
126
146
|
|
|
127
147
|
const timeoutMs = describeTimeoutMs(misses.length);
|
|
128
148
|
const maxTokens = resolveMaxTokens(cfg, visionModel);
|
|
149
|
+
const reasoning = resolveReasoning(cfg, visionModel);
|
|
129
150
|
const controller = new AbortController();
|
|
130
151
|
let timedOut = false;
|
|
131
152
|
using fetchGuard = fetchInterceptorGuard();
|
|
@@ -141,7 +162,7 @@ export async function runBatch(
|
|
|
141
162
|
complete(
|
|
142
163
|
visionModel,
|
|
143
164
|
{ systemPrompt, messages: [userMessage] },
|
|
144
|
-
{ apiKey: auth.apiKey, headers: auth.headers, signal: controller.signal, maxTokens },
|
|
165
|
+
{ apiKey: auth.apiKey, headers: auth.headers, signal: controller.signal, maxTokens, reasoning },
|
|
145
166
|
),
|
|
146
167
|
);
|
|
147
168
|
const capture = await readCapture(describeCtx);
|
|
@@ -239,6 +260,7 @@ export async function describeSingle(
|
|
|
239
260
|
const userMessage: Message = { role: "user", content, timestamp: Date.now() };
|
|
240
261
|
const timeoutMs = describeTimeoutMs(1);
|
|
241
262
|
const maxTokens = resolveMaxTokens(cfg, visionModel);
|
|
263
|
+
const reasoning = resolveReasoning(cfg, visionModel);
|
|
242
264
|
const controller = new AbortController();
|
|
243
265
|
let timedOut = false;
|
|
244
266
|
using fetchGuard = fetchInterceptorGuard();
|
|
@@ -254,7 +276,7 @@ export async function describeSingle(
|
|
|
254
276
|
complete(
|
|
255
277
|
visionModel,
|
|
256
278
|
{ systemPrompt, messages: [userMessage] },
|
|
257
|
-
{ apiKey: auth.apiKey, headers: auth.headers, signal: controller.signal, maxTokens },
|
|
279
|
+
{ apiKey: auth.apiKey, headers: auth.headers, signal: controller.signal, maxTokens, reasoning },
|
|
258
280
|
),
|
|
259
281
|
);
|
|
260
282
|
const capture = await readCapture(describeCtx);
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* convention pi-model-sort uses for picker-backed extensions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
8
|
+
import type { ImageContent, TextContent, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
9
9
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
@@ -57,6 +57,26 @@ export function markDescriptionTruncated(text: string): string {
|
|
|
57
57
|
/** Default vision cache size (number of described images kept in memory per session). */
|
|
58
58
|
export const DEFAULT_CACHE_MAX = 50;
|
|
59
59
|
|
|
60
|
+
/** The pi thinking levels, ordered from lightest to deepest reasoning.
|
|
61
|
+
* Mirrors `@earendil-works/pi-ai`'s `ThinkingLevel`; "off" is handled
|
|
62
|
+
* separately via {@link VisionHandoffConfig.thinking} (a boolean on/off
|
|
63
|
+
* switch) so the level persists across toggles. */
|
|
64
|
+
export const THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
65
|
+
"minimal",
|
|
66
|
+
"low",
|
|
67
|
+
"medium",
|
|
68
|
+
"high",
|
|
69
|
+
"xhigh",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/** Default thinking effort for the vision describer when thinking is enabled. */
|
|
73
|
+
export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium";
|
|
74
|
+
|
|
75
|
+
/** Whether `level` is one of the supported {@link ThinkingLevel} values. */
|
|
76
|
+
export function isThinkingLevel(level: unknown): level is ThinkingLevel {
|
|
77
|
+
return typeof level === "string" && (THINKING_LEVELS as readonly string[]).includes(level);
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
/** Per-description request timeout. Generous because the describer generates an
|
|
61
81
|
* exhaustive multi-paragraph description per image; a single image commonly
|
|
62
82
|
* takes ~20s, and a batched call over websocket transport scales with image
|
|
@@ -135,6 +155,16 @@ export interface VisionHandoffConfig {
|
|
|
135
155
|
prompt?: string;
|
|
136
156
|
/** Override the user-prompt prefix (defaults to DEFAULT_USER_PROMPT_PREFIX). */
|
|
137
157
|
userPromptPrefix?: string;
|
|
158
|
+
/** Whether the vision describer should reason (think) before describing.
|
|
159
|
+
* Off by default — describing images is a perception task, not a reasoning
|
|
160
|
+
* one, and thinking adds latency + cost. When on, the level below is sent
|
|
161
|
+
* to the vision model via pi-ai's `reasoning` option (only honoured when
|
|
162
|
+
* the configured vision model declares `reasoning: true`). */
|
|
163
|
+
thinking: boolean;
|
|
164
|
+
/** Thinking effort for the describer when {@link thinking} is on. One of
|
|
165
|
+
* {@link THINKING_LEVELS}. Ignored when {@link thinking} is off or the
|
|
166
|
+
* vision model lacks reasoning support. */
|
|
167
|
+
thinkingLevel: ThinkingLevel;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
export const DEFAULT_CONFIG: VisionHandoffConfig = {
|
|
@@ -145,6 +175,8 @@ export const DEFAULT_CONFIG: VisionHandoffConfig = {
|
|
|
145
175
|
maxTokens: undefined,
|
|
146
176
|
cacheMax: DEFAULT_CACHE_MAX,
|
|
147
177
|
maxDescriptionLines: DEFAULT_MAX_DESCRIPTION_LINES,
|
|
178
|
+
thinking: false,
|
|
179
|
+
thinkingLevel: DEFAULT_THINKING_LEVEL,
|
|
148
180
|
};
|
|
149
181
|
|
|
150
182
|
/** Parse a "provider/id" reference. Returns null if malformed. */
|
|
@@ -208,6 +240,9 @@ export function normalizeConfig(raw: unknown): VisionHandoffConfig {
|
|
|
208
240
|
if (typeof obj.prompt === "string" && obj.prompt.trim()) base.prompt = obj.prompt;
|
|
209
241
|
if (typeof obj.userPromptPrefix === "string") base.userPromptPrefix = obj.userPromptPrefix;
|
|
210
242
|
|
|
243
|
+
if (typeof obj.thinking === "boolean") base.thinking = obj.thinking;
|
|
244
|
+
if (isThinkingLevel(obj.thinkingLevel)) base.thinkingLevel = obj.thinkingLevel;
|
|
245
|
+
|
|
211
246
|
return base;
|
|
212
247
|
}
|
|
213
248
|
|
|
@@ -23,8 +23,9 @@ import {
|
|
|
23
23
|
Text,
|
|
24
24
|
} from "@earendil-works/pi-tui";
|
|
25
25
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import type { ThinkingLevel } from "@earendil-works/pi-ai";
|
|
26
27
|
import { DynamicBorder, keyText } from "@earendil-works/pi-coding-agent";
|
|
27
|
-
import { formatModelRef, isVisionModel } from "./index.js";
|
|
28
|
+
import { formatModelRef, isVisionModel, THINKING_LEVELS } from "./index.js";
|
|
28
29
|
|
|
29
30
|
interface DisplayItem {
|
|
30
31
|
/** "provider/id", or null for the synthetic "None" row. */
|
|
@@ -33,6 +34,8 @@ interface DisplayItem {
|
|
|
33
34
|
modelId: string;
|
|
34
35
|
modelName: string;
|
|
35
36
|
vision: boolean;
|
|
37
|
+
/** Whether the model declares reasoning (thinking) support. */
|
|
38
|
+
reasoning: boolean;
|
|
36
39
|
none?: boolean;
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -41,6 +44,10 @@ export interface VisionModelSelectorResult {
|
|
|
41
44
|
ref: string | null;
|
|
42
45
|
/** True if the user cancelled (esc) — config should not change. */
|
|
43
46
|
cancelled: boolean;
|
|
47
|
+
/** Thinking on/off chosen in the picker. */
|
|
48
|
+
thinking: boolean;
|
|
49
|
+
/** Thinking effort chosen in the picker. */
|
|
50
|
+
thinkingLevel: ThinkingLevel;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export class VisionModelSelectorComponent implements Component {
|
|
@@ -56,6 +63,8 @@ export class VisionModelSelectorComponent implements Component {
|
|
|
56
63
|
private footerText: Text;
|
|
57
64
|
|
|
58
65
|
private currentRef: string | null;
|
|
66
|
+
private thinking: boolean;
|
|
67
|
+
private thinkingLevel: ThinkingLevel;
|
|
59
68
|
|
|
60
69
|
private _focused = false;
|
|
61
70
|
get focused(): boolean {
|
|
@@ -68,13 +77,23 @@ export class VisionModelSelectorComponent implements Component {
|
|
|
68
77
|
|
|
69
78
|
constructor(
|
|
70
79
|
theme: Theme,
|
|
71
|
-
allModels: Array<{
|
|
80
|
+
allModels: Array<{
|
|
81
|
+
provider: string;
|
|
82
|
+
id: string;
|
|
83
|
+
name: string;
|
|
84
|
+
input?: ("text" | "image")[];
|
|
85
|
+
reasoning?: boolean;
|
|
86
|
+
}>,
|
|
72
87
|
currentRef: string | null,
|
|
88
|
+
currentThinking: boolean,
|
|
89
|
+
currentThinkingLevel: ThinkingLevel,
|
|
73
90
|
done: (result: VisionModelSelectorResult) => void,
|
|
74
91
|
) {
|
|
75
92
|
this.theme = theme;
|
|
76
93
|
this.done = done;
|
|
77
94
|
this.currentRef = currentRef;
|
|
95
|
+
this.thinking = currentThinking;
|
|
96
|
+
this.thinkingLevel = currentThinkingLevel;
|
|
78
97
|
this.allItems = this.buildItems(allModels);
|
|
79
98
|
this.filteredItems = this.allItems;
|
|
80
99
|
|
|
@@ -161,6 +180,22 @@ export class VisionModelSelectorComponent implements Component {
|
|
|
161
180
|
return;
|
|
162
181
|
}
|
|
163
182
|
|
|
183
|
+
// Thinking controls — reuse pi's own app.thinking.* keybindings so the
|
|
184
|
+
// hints and behaviour match the rest of pi: ctrl+t toggles thinking
|
|
185
|
+
// on/off, shift+tab cycles the effort level. Intercepted before the
|
|
186
|
+
// search input so they never get swallowed as filter text.
|
|
187
|
+
if (kb.matches(data, "app.thinking.toggle")) {
|
|
188
|
+
this.thinking = !this.thinking;
|
|
189
|
+
this.updateList();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (kb.matches(data, "app.thinking.cycle")) {
|
|
194
|
+
this.cycleThinkingLevel();
|
|
195
|
+
this.updateList();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
164
199
|
this.searchInput.handleInput(data);
|
|
165
200
|
this.refresh();
|
|
166
201
|
}
|
|
@@ -174,7 +209,13 @@ export class VisionModelSelectorComponent implements Component {
|
|
|
174
209
|
// Internal helpers
|
|
175
210
|
|
|
176
211
|
private buildItems(
|
|
177
|
-
allModels: Array<{
|
|
212
|
+
allModels: Array<{
|
|
213
|
+
provider: string;
|
|
214
|
+
id: string;
|
|
215
|
+
name: string;
|
|
216
|
+
input?: ("text" | "image")[];
|
|
217
|
+
reasoning?: boolean;
|
|
218
|
+
}>,
|
|
178
219
|
): DisplayItem[] {
|
|
179
220
|
const items: DisplayItem[] = [
|
|
180
221
|
{
|
|
@@ -183,6 +224,7 @@ export class VisionModelSelectorComponent implements Component {
|
|
|
183
224
|
modelId: "none",
|
|
184
225
|
modelName: "None — disable vision handoff",
|
|
185
226
|
vision: false,
|
|
227
|
+
reasoning: false,
|
|
186
228
|
none: true,
|
|
187
229
|
},
|
|
188
230
|
];
|
|
@@ -192,12 +234,14 @@ export class VisionModelSelectorComponent implements Component {
|
|
|
192
234
|
id: string;
|
|
193
235
|
name: string;
|
|
194
236
|
input?: ("text" | "image")[];
|
|
237
|
+
reasoning?: boolean;
|
|
195
238
|
}): DisplayItem => ({
|
|
196
239
|
ref: formatModelRef(m.provider, m.id),
|
|
197
240
|
provider: m.provider,
|
|
198
241
|
modelId: m.id,
|
|
199
242
|
modelName: m.name || m.id,
|
|
200
243
|
vision: isVisionModel(m),
|
|
244
|
+
reasoning: !!m.reasoning,
|
|
201
245
|
});
|
|
202
246
|
|
|
203
247
|
// Vision-capable first (registry order), then the rest (registry order).
|
|
@@ -217,6 +261,8 @@ export class VisionModelSelectorComponent implements Component {
|
|
|
217
261
|
const parts: string[] = [
|
|
218
262
|
`${keyText("tui.select.confirm")} select`,
|
|
219
263
|
`ctrl+s done`,
|
|
264
|
+
`${keyText("app.thinking.toggle")} thinking`,
|
|
265
|
+
`${keyText("app.thinking.cycle")} effort`,
|
|
220
266
|
`esc cancel`,
|
|
221
267
|
this.searchInput.getValue() ? `${this.filteredItems.length - 1} match` : `${totalCount} models · ${visionCount} vision`,
|
|
222
268
|
];
|
|
@@ -318,16 +364,52 @@ export class VisionModelSelectorComponent implements Component {
|
|
|
318
364
|
),
|
|
319
365
|
);
|
|
320
366
|
}
|
|
367
|
+
this.renderThinkingDetail(selected);
|
|
321
368
|
}
|
|
322
369
|
|
|
323
370
|
this.footerText.setText(this.getFooterText());
|
|
324
371
|
}
|
|
325
372
|
|
|
373
|
+
/** Append the thinking on/off + effort line to the detail pane, with a
|
|
374
|
+
* warning when the highlighted model can't reason (so the setting would
|
|
375
|
+
* be silently ignored by the describer). */
|
|
376
|
+
private renderThinkingDetail(selected: DisplayItem): void {
|
|
377
|
+
const state = this.thinking
|
|
378
|
+
? this.theme.fg("success", `on (${this.thinkingLevel})`)
|
|
379
|
+
: this.theme.fg("muted", "off");
|
|
380
|
+
this.listContainer.addChild(
|
|
381
|
+
new Text(this.theme.fg("dim", ` Thinking: ${state}`), 0, 0),
|
|
382
|
+
);
|
|
383
|
+
if (this.thinking && !selected.none && !selected.reasoning) {
|
|
384
|
+
this.listContainer.addChild(
|
|
385
|
+
new Text(
|
|
386
|
+
this.theme.fg(
|
|
387
|
+
"warning",
|
|
388
|
+
` ⚠ ${selected.modelId} declares no reasoning — thinking will be ignored`,
|
|
389
|
+
),
|
|
390
|
+
0, 0,
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
326
396
|
private confirm(item: DisplayItem): void {
|
|
327
|
-
this.done({ ref: item.ref, cancelled: false });
|
|
397
|
+
this.done({ ref: item.ref, cancelled: false, thinking: this.thinking, thinkingLevel: this.thinkingLevel });
|
|
328
398
|
}
|
|
329
399
|
|
|
330
400
|
private finish(cancelled: boolean): void {
|
|
331
|
-
this.done({ ref: null, cancelled });
|
|
401
|
+
this.done({ ref: null, cancelled, thinking: this.thinking, thinkingLevel: this.thinkingLevel });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Cycle the thinking effort forward through {@link THINKING_LEVELS},
|
|
405
|
+
* wrapping from the last back to the first. Cycling implicitly turns
|
|
406
|
+
* thinking on (you don't usually cycle a switch you want off) — matching
|
|
407
|
+
* pi's own `app.thinking.cycle` behaviour, which is a no-op only when the
|
|
408
|
+
* active model has no reasoning. */
|
|
409
|
+
private cycleThinkingLevel(): void {
|
|
410
|
+
if (!this.thinking) this.thinking = true;
|
|
411
|
+
const idx = THINKING_LEVELS.indexOf(this.thinkingLevel);
|
|
412
|
+
const next = THINKING_LEVELS[(idx + 1) % THINKING_LEVELS.length]!;
|
|
413
|
+
this.thinkingLevel = next;
|
|
332
414
|
}
|
|
333
415
|
}
|
package/vision-handoff.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type { Api, ImageContent, Model, TextContent } from "@earendil-works/pi-a
|
|
|
33
33
|
import {
|
|
34
34
|
extractImageFromBlock,
|
|
35
35
|
formatModelRef,
|
|
36
|
+
isThinkingLevel,
|
|
36
37
|
isVisionModel,
|
|
37
38
|
NON_VISION_IMAGE_NOTE,
|
|
38
39
|
parseModelRef,
|
|
@@ -412,7 +413,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
412
413
|
pi.registerCommand("vision-handoff", {
|
|
413
414
|
description: HANDOFF_COMMAND_DESCRIPTION,
|
|
414
415
|
getArgumentCompletions(prefix: string) {
|
|
415
|
-
const subcommands = ["select", "model", "status", "enable", "disable", "auto", "add", "remove", "clear", "help"];
|
|
416
|
+
const subcommands = ["select", "model", "status", "enable", "disable", "auto", "thinking", "add", "remove", "clear", "help"];
|
|
416
417
|
const matches = subcommands.filter((s) => s.startsWith(prefix));
|
|
417
418
|
return matches.length > 0 ? matches.map((s) => ({ value: s, label: s })) : null;
|
|
418
419
|
},
|
|
@@ -444,6 +445,8 @@ async function handleHandoffCommand(ctx: ExtensionCommandContext, args: string):
|
|
|
444
445
|
" /vision-handoff enable Enable vision handoff",
|
|
445
446
|
" /vision-handoff disable Disable vision handoff (keeps configured model)",
|
|
446
447
|
" /vision-handoff auto <on|off> Toggle automatic handoff for all non-vision models",
|
|
448
|
+
" /vision-handoff thinking <off|minimal|low|medium|high|xhigh>",
|
|
449
|
+
" Set the vision describer's thinking effort (off = disabled)",
|
|
447
450
|
" /vision-handoff add <p/id> Force handoff for an extra model",
|
|
448
451
|
" /vision-handoff remove <p/id> Stop forcing handoff for a model",
|
|
449
452
|
" /vision-handoff clear Clear the configured vision model",
|
|
@@ -489,6 +492,11 @@ async function handleHandoffCommand(ctx: ExtensionCommandContext, args: string):
|
|
|
489
492
|
return;
|
|
490
493
|
}
|
|
491
494
|
|
|
495
|
+
if (subcommand === "thinking") {
|
|
496
|
+
handleThinkingSubcommand(ctx, rest);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
492
500
|
if (subcommand === "clear") {
|
|
493
501
|
updateConfig(
|
|
494
502
|
ctx,
|
|
@@ -582,6 +590,38 @@ function updateConfig(
|
|
|
582
590
|
ctx.ui.notify(`${message} (config: ${path})`, "info");
|
|
583
591
|
}
|
|
584
592
|
|
|
593
|
+
/** Resolve a `/vision-handoff thinking <level>` argument into a
|
|
594
|
+
* `(thinking, thinkingLevel)` pair. `off` disables thinking; any of the
|
|
595
|
+
* {@link THINKING_LEVELS} enables it at that effort. */
|
|
596
|
+
function handleThinkingSubcommand(ctx: ExtensionCommandContext, rest: string): void {
|
|
597
|
+
const arg = rest.trim().toLowerCase();
|
|
598
|
+
if (!arg) {
|
|
599
|
+
ctx.ui.notify(
|
|
600
|
+
`Thinking: ${config.thinking ? `on (${config.thinkingLevel})` : "off"}.\n` +
|
|
601
|
+
`Usage: /vision-handoff thinking <off|minimal|low|medium|high|xhigh>`,
|
|
602
|
+
"info",
|
|
603
|
+
);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (arg === "off") {
|
|
607
|
+
updateConfig(ctx, (c) => ({ ...c, thinking: false }), "Vision describer thinking off.");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (!isThinkingLevel(arg)) {
|
|
611
|
+
ctx.ui.notify(
|
|
612
|
+
`Unknown thinking level: "${arg}". Use off, minimal, low, medium, high, or xhigh.`,
|
|
613
|
+
"error",
|
|
614
|
+
);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const level = arg;
|
|
618
|
+
updateConfig(
|
|
619
|
+
ctx,
|
|
620
|
+
(c) => ({ ...c, thinking: true, thinkingLevel: level }),
|
|
621
|
+
`Vision describer thinking on (${level}).`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
585
625
|
async function showSelector(ctx: ExtensionCommandContext): Promise<void> {
|
|
586
626
|
if (!ctx.hasUI) {
|
|
587
627
|
ctx.ui.notify("/vision-handoff requires interactive mode.", "error");
|
|
@@ -590,10 +630,17 @@ async function showSelector(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
590
630
|
|
|
591
631
|
const allModels = ctx.modelRegistry
|
|
592
632
|
.getAll()
|
|
593
|
-
.map((m) => ({ provider: m.provider, id: m.id, name: m.name, input: m.input }));
|
|
633
|
+
.map((m) => ({ provider: m.provider, id: m.id, name: m.name, input: m.input, reasoning: m.reasoning }));
|
|
594
634
|
|
|
595
635
|
const result = await ctx.ui.custom<VisionModelSelectorResult>((tui, theme, _kb, done) => {
|
|
596
|
-
const selector = new VisionModelSelectorComponent(
|
|
636
|
+
const selector = new VisionModelSelectorComponent(
|
|
637
|
+
theme,
|
|
638
|
+
allModels,
|
|
639
|
+
config.visionModel,
|
|
640
|
+
config.thinking,
|
|
641
|
+
config.thinkingLevel,
|
|
642
|
+
(r) => done(r),
|
|
643
|
+
);
|
|
597
644
|
return {
|
|
598
645
|
render(width: number) {
|
|
599
646
|
return selector.render(width);
|
|
@@ -614,7 +661,18 @@ async function showSelector(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
614
661
|
}
|
|
615
662
|
|
|
616
663
|
const ref = result.ref;
|
|
617
|
-
|
|
664
|
+
const thinking = result.thinking;
|
|
665
|
+
const thinkingLevel = result.thinkingLevel;
|
|
666
|
+
updateConfig(
|
|
667
|
+
ctx,
|
|
668
|
+
(c) => ({ ...c, visionModel: ref, thinking, thinkingLevel }),
|
|
669
|
+
ref ? `Vision model set to ${ref}` : "Vision model cleared",
|
|
670
|
+
);
|
|
671
|
+
ctx.ui.notify(
|
|
672
|
+
`Thinking: ${thinking ? `on (${thinkingLevel})` : "off"}` +
|
|
673
|
+
(thinking && ref ? " — applies only if the vision model supports reasoning" : ""),
|
|
674
|
+
"info",
|
|
675
|
+
);
|
|
618
676
|
if (!ref) {
|
|
619
677
|
ctx.ui.notify("Handoff is inactive until you pick a vision model.", "warning");
|
|
620
678
|
}
|
|
@@ -626,6 +684,7 @@ function showStatus(ctx: ExtensionCommandContext): void {
|
|
|
626
684
|
lines.push(`Vision model: ${config.visionModel ?? "(none — pick one with /vision-handoff)"}`);
|
|
627
685
|
lines.push(`Auto handoff (non-vision models): ${config.autoHandoff ? "on" : "off"}`);
|
|
628
686
|
lines.push(`Handoff targets (explicit): ${config.handoffModels.length ? config.handoffModels.join(", ") : "(none)"}`);
|
|
687
|
+
lines.push(`Thinking: ${config.thinking ? `on (${config.thinkingLevel})` : "off"}`);
|
|
629
688
|
lines.push(`maxTokens: ${config.maxTokens ?? "unbounded"} · cacheMax: ${config.cacheMax} · maxDescriptionLines: ${config.maxDescriptionLines === 0 ? "unbounded" : config.maxDescriptionLines}`);
|
|
630
689
|
|
|
631
690
|
const model = ctx.model;
|