@oh-my-pi/pi-coding-agent 16.4.8 → 16.5.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 +63 -0
- package/dist/cli.js +3502 -3444
- package/dist/types/cli/args.d.ts +5 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +15 -0
- package/dist/types/config/model-resolver.d.ts +4 -3
- package/dist/types/config/model-roles.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +46 -14
- package/dist/types/extensibility/extensions/types.d.ts +1 -1
- package/dist/types/launch/broker.d.ts +2 -0
- package/dist/types/launch/client.d.ts +22 -0
- package/dist/types/launch/paths.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +8 -0
- package/dist/types/launch/protocol.d.ts +170 -0
- package/dist/types/launch/terminal-output.d.ts +7 -0
- package/dist/types/modes/components/agent-hub.d.ts +1 -1
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-browser.d.ts +17 -2
- package/dist/types/modes/components/model-hub.d.ts +0 -10
- package/dist/types/modes/components/model-picker.d.ts +44 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/print-mode.d.ts +14 -8
- package/dist/types/modes/print-mode.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/sdk.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +42 -0
- package/dist/types/session/session-context.d.ts +9 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/thinking.d.ts +2 -2
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +28 -2
- package/dist/types/tools/browser/tab-protocol.d.ts +6 -0
- package/dist/types/tools/browser/tab-worker.d.ts +6 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/launch.d.ts +121 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/terminal-output.d.ts +5 -0
- package/dist/types/vibe/runtime.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +0 -8
- package/package.json +20 -20
- package/src/cli/args.ts +11 -0
- package/src/cli/flag-tables.ts +9 -0
- package/src/cli/gallery-fixtures/shell.ts +82 -1
- package/src/cli.ts +10 -0
- package/src/commands/launch.ts +17 -0
- package/src/config/model-resolver.ts +110 -31
- package/src/config/model-roles.ts +14 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +2 -2
- package/src/eval/__tests__/completion-bridge.test.ts +1 -1
- package/src/eval/completion-bridge.ts +4 -4
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/extensions/model-api.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/internal-urls/docs-index.ts +4 -4
- package/src/launch/broker.ts +1017 -0
- package/src/launch/client.ts +344 -0
- package/src/launch/paths.ts +17 -0
- package/src/launch/presence.ts +82 -0
- package/src/launch/protocol.ts +386 -0
- package/src/launch/terminal-output.ts +46 -0
- package/src/main.ts +50 -1
- package/src/modes/acp/acp-agent.ts +8 -1
- package/src/modes/components/agent-hub.ts +101 -31
- package/src/modes/components/compaction-summary-message.ts +8 -2
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/model-browser.ts +111 -21
- package/src/modes/components/model-hub.ts +23 -116
- package/src/modes/components/model-picker.ts +233 -0
- package/src/modes/components/snapcompact-shape-preview-doc.md +7 -11
- package/src/modes/components/status-line/component.test.ts +41 -2
- package/src/modes/components/status-line/component.ts +4 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/controllers/command-controller.ts +9 -1
- package/src/modes/controllers/event-controller.ts +31 -32
- package/src/modes/controllers/selector-controller.ts +96 -22
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/print-mode.test.ts +71 -0
- package/src/modes/print-mode.ts +51 -2
- package/src/modes/theme/theme.ts +9 -0
- package/src/modes/utils/ui-helpers.ts +25 -4
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/librarian.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/scout.md +1 -1
- package/src/prompts/system/plan-yolo-handoff.md +5 -0
- package/src/prompts/system/prewalk-checklist.md +7 -0
- package/src/prompts/system/prewalk-continue.md +1 -0
- package/src/prompts/system/prewalk-plan.md +13 -0
- package/src/prompts/system/system-prompt.md +9 -7
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/browser.md +4 -4
- package/src/prompts/tools/launch.md +25 -0
- package/src/sdk.ts +8 -2
- package/src/session/agent-session.ts +550 -87
- package/src/session/session-context.test.ts +10 -5
- package/src/session/session-context.ts +25 -8
- package/src/session/session-entries.ts +6 -0
- package/src/slash-commands/builtin-registry.ts +25 -0
- package/src/task/agents.ts +2 -2
- package/src/thinking.ts +10 -3
- package/src/tiny/models.ts +7 -7
- package/src/tools/bash-interactive.ts +5 -8
- package/src/tools/bash.ts +38 -24
- package/src/tools/browser/cmux/cmux-tab.ts +17 -2
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +66 -6
- package/src/tools/browser/tab-protocol.ts +6 -0
- package/src/tools/browser/tab-supervisor.ts +17 -1
- package/src/tools/browser/tab-worker.ts +140 -21
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/index.ts +5 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/launch.ts +643 -0
- package/src/tools/render-utils.ts +3 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/terminal-output.ts +141 -0
- package/src/tts/speech-enhancer.ts +2 -2
- package/src/utils/image-vision-fallback.ts +3 -3
- package/src/vibe/runtime.ts +2 -2
- package/src/web/search/provider.ts +0 -10
- package/src/web/search/providers/public.ts +2 -4
- package/src/web/search/types.ts +0 -10
- package/dist/types/web/search/providers/bing.d.ts +0 -14
- package/dist/types/web/search/providers/yahoo.d.ts +0 -14
- package/src/web/search/providers/bing.ts +0 -197
- package/src/web/search/providers/yahoo.ts +0 -179
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as path from "node:path";
|
|
18
|
-
import type
|
|
19
|
-
import { Container, Ellipsis, matchesKey, type OverlayHandle, type TUI } from "@oh-my-pi/pi-tui";
|
|
18
|
+
import { type AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
19
|
+
import { Container, Ellipsis, matchesKey, type OverlayHandle, padding, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
20
20
|
import { formatAge, getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
21
21
|
import { ADVISOR_TRANSCRIPT_FILENAME, isAdvisorTranscriptName } from "../../advisor";
|
|
22
22
|
import type { KeyId } from "../../config/keybindings";
|
|
@@ -25,6 +25,7 @@ import { IrcBus } from "../../irc/bus";
|
|
|
25
25
|
import { AgentLifecycleManager } from "../../registry/agent-lifecycle";
|
|
26
26
|
import { type AgentRef, AgentRegistry, type AgentStatus, MAIN_AGENT_ID } from "../../registry/agent-registry";
|
|
27
27
|
import { USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
28
|
+
import { parseThinkingLevel } from "../../thinking";
|
|
28
29
|
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
29
30
|
import type { ObservableSession, SessionObserverRegistry } from "../session-observer-registry";
|
|
30
31
|
import { theme } from "../theme/theme";
|
|
@@ -55,20 +56,50 @@ function clampHubLine(line: string, width: number): string {
|
|
|
55
56
|
|
|
56
57
|
const STATUS_ORDER: Record<AgentStatus, number> = { running: 0, idle: 1, parked: 2, aborted: 3 };
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
-
function
|
|
59
|
+
/** Status glyph, colored per theme status conventions. The title-line counts spell out the words. */
|
|
60
|
+
function statusGlyph(status: AgentStatus): string {
|
|
60
61
|
switch (status) {
|
|
61
62
|
case "running":
|
|
62
|
-
return theme.fg("accent",
|
|
63
|
+
return theme.fg("accent", theme.status.running);
|
|
63
64
|
case "idle":
|
|
64
|
-
return theme.fg("success",
|
|
65
|
+
return theme.fg("success", theme.status.enabled);
|
|
65
66
|
case "parked":
|
|
66
|
-
return theme.fg("muted",
|
|
67
|
+
return theme.fg("muted", theme.status.shadowed);
|
|
67
68
|
case "aborted":
|
|
68
|
-
return theme.fg("error",
|
|
69
|
+
return theme.fg("error", theme.status.aborted);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/** Model id + thinking level (`sonnet-4-6 ◒ high`), level colored per theme. */
|
|
74
|
+
function formatModelBadge(modelId: string, level: ThinkingLevel | undefined): string {
|
|
75
|
+
const model = theme.fg("muted", replaceTabs(modelId));
|
|
76
|
+
if (!level || level === ThinkingLevel.Off || level === ThinkingLevel.Inherit) return model;
|
|
77
|
+
const display = theme.thinking[level as keyof typeof theme.thinking] ?? level;
|
|
78
|
+
return `${model} ${theme.getThinkingBorderColor(level)(display)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Active model + reasoning level for a hub row: live session state when the
|
|
83
|
+
* agent is attached, else the executor-reported `resolvedModel` selector
|
|
84
|
+
* (`provider/id`, optionally `:<level>`). Undefined when neither is known
|
|
85
|
+
* (e.g. a parked historical agent restored from disk).
|
|
86
|
+
*/
|
|
87
|
+
function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): string | undefined {
|
|
88
|
+
const model = ref.session?.model;
|
|
89
|
+
if (model) {
|
|
90
|
+
const level = model.thinking ? ref.session?.thinkingLevel : undefined;
|
|
91
|
+
return formatModelBadge(model.id, level);
|
|
92
|
+
}
|
|
93
|
+
const resolved = observed?.progress?.resolvedModel;
|
|
94
|
+
if (!resolved) return undefined;
|
|
95
|
+
// Model ids may themselves contain colons (`qwen3:14b`), so only treat the
|
|
96
|
+
// suffix as a thinking level when it parses as one.
|
|
97
|
+
const colon = resolved.lastIndexOf(":");
|
|
98
|
+
const level = colon >= 0 ? parseThinkingLevel(resolved.slice(colon + 1)) : undefined;
|
|
99
|
+
const selector = level !== undefined ? resolved.slice(0, colon) : resolved;
|
|
100
|
+
return formatModelBadge(selector.slice(selector.indexOf("/") + 1), level);
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
async function registerPersistedSubagents(
|
|
73
104
|
registry: AgentRegistry,
|
|
74
105
|
sessionFile: string | null | undefined,
|
|
@@ -427,17 +458,31 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
427
458
|
} else {
|
|
428
459
|
const termHeight = process.stdout.rows || 40;
|
|
429
460
|
// Chrome: 2 borders + title + notice? + blank + hints + border
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
461
|
+
const budget = Math.max(4, termHeight - 7 - (this.#notice ? 1 : 0));
|
|
462
|
+
const entries = this.#rows.map((ref, i) => this.#renderEntry(ref, i === this.#selectedRow, width));
|
|
463
|
+
// Entries are 1-2 lines tall; grow a window around the selection until
|
|
464
|
+
// the line budget is spent, so the selected entry stays centered.
|
|
465
|
+
let start = this.#selectedRow;
|
|
466
|
+
let end = this.#selectedRow + 1;
|
|
467
|
+
let used = entries[start]?.length ?? 0;
|
|
468
|
+
for (let grew = true; grew; ) {
|
|
469
|
+
grew = false;
|
|
470
|
+
if (end < entries.length && used + entries[end].length <= budget) {
|
|
471
|
+
used += entries[end].length;
|
|
472
|
+
end++;
|
|
473
|
+
grew = true;
|
|
474
|
+
}
|
|
475
|
+
if (start > 0 && used + entries[start - 1].length <= budget) {
|
|
476
|
+
start--;
|
|
477
|
+
used += entries[start].length;
|
|
478
|
+
grew = true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (start > 0) {
|
|
482
|
+
lines.push(` ${theme.fg("dim", `… ${start} more`)}`);
|
|
437
483
|
}
|
|
438
|
-
const end = Math.min(start + maxVisible, this.#rows.length);
|
|
439
484
|
for (let i = start; i < end; i++) {
|
|
440
|
-
lines.push(
|
|
485
|
+
lines.push(...entries[i]);
|
|
441
486
|
}
|
|
442
487
|
if (end < this.#rows.length) {
|
|
443
488
|
lines.push(` ${theme.fg("dim", `… ${this.#rows.length - end} more`)}`);
|
|
@@ -466,26 +511,51 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
466
511
|
return parts.join(theme.sep.dot);
|
|
467
512
|
}
|
|
468
513
|
|
|
469
|
-
|
|
514
|
+
/**
|
|
515
|
+
* One agent entry, 1-2 lines:
|
|
516
|
+
* `❯ ⟳ Name type ↳ parent ⧉ 2 ········ model ◒ level · age` — identity
|
|
517
|
+
* left, metadata right-aligned (inlined when the terminal is too narrow) —
|
|
518
|
+
* plus an indented dim task line when the agent's work is known.
|
|
519
|
+
*/
|
|
520
|
+
#renderEntry(ref: AgentRef, selected: boolean, width: number): string[] {
|
|
521
|
+
const max = Math.max(1, width - 2);
|
|
470
522
|
const cursor = selected ? theme.fg("accent", theme.nav.cursor) : " ";
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (ref.kind === "advisor") {
|
|
475
|
-
parts.push(theme.fg("warning", "read-only"));
|
|
523
|
+
const fields: string[] = [`${cursor} ${statusGlyph(ref.status)} ${theme.bold(replaceTabs(ref.id))}`];
|
|
524
|
+
if (ref.displayName && ref.displayName !== ref.id) {
|
|
525
|
+
fields.push(theme.fg("dim", replaceTabs(ref.displayName)));
|
|
476
526
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
527
|
+
if (ref.parentId && ref.parentId !== MAIN_AGENT_ID) {
|
|
528
|
+
fields.push(theme.fg("dim", `↳ ${replaceTabs(ref.parentId)}`));
|
|
529
|
+
}
|
|
530
|
+
if (ref.kind === "advisor") {
|
|
531
|
+
fields.push(theme.fg("warning", "read-only"));
|
|
481
532
|
}
|
|
482
533
|
const unread = this.#irc.unreadCount(ref.id);
|
|
483
534
|
if (unread > 0) {
|
|
484
|
-
|
|
535
|
+
fields.push(theme.fg("warning", `⧉ ${unread}`));
|
|
536
|
+
}
|
|
537
|
+
const left = ` ${fields.join(" ")}`;
|
|
538
|
+
|
|
539
|
+
const observed = this.#observableFor(ref.id);
|
|
540
|
+
const meta: string[] = [];
|
|
541
|
+
const badge = modelBadge(ref, observed);
|
|
542
|
+
if (badge) meta.push(badge);
|
|
543
|
+
meta.push(theme.fg("dim", formatAge(Math.max(1, Math.round((Date.now() - ref.lastActivity) / 1000)))));
|
|
544
|
+
const right = meta.join(theme.sep.dot);
|
|
545
|
+
|
|
546
|
+
const leftWidth = visibleWidth(left);
|
|
547
|
+
const rightWidth = visibleWidth(right);
|
|
548
|
+
const line =
|
|
549
|
+
leftWidth + 2 + rightWidth <= max
|
|
550
|
+
? left + padding(max - leftWidth - rightWidth) + right
|
|
551
|
+
: truncateToWidth(`${left} ${right}`.replace(/[\r\n]+/g, " "), max);
|
|
552
|
+
const entry = [line];
|
|
553
|
+
|
|
554
|
+
const task = observed?.description ?? observed?.progress?.task ?? ref.activity;
|
|
555
|
+
if (task) {
|
|
556
|
+
entry.push(` ${theme.fg("muted", sanitizeLine(task, Math.max(10, max - 5)))}`);
|
|
485
557
|
}
|
|
486
|
-
|
|
487
|
-
const rawLine = ` ${cursor} ${parts.join(theme.sep.dot)}`;
|
|
488
|
-
return truncateToWidth(rawLine.replace(/[\r\n]+/g, " "), Math.max(1, width - 1));
|
|
558
|
+
return entry;
|
|
489
559
|
}
|
|
490
560
|
|
|
491
561
|
#handleTableInput(keyData: string): void {
|
|
@@ -87,7 +87,12 @@ export class CompactionSummaryMessageComponent implements Component {
|
|
|
87
87
|
|
|
88
88
|
constructor(private readonly message: CompactionSummaryMessage) {
|
|
89
89
|
this.#divider = new SummaryDividerComponent({
|
|
90
|
-
|
|
90
|
+
// A dead-end warning stamped by the progress guard badges the bar;
|
|
91
|
+
// the full text lives in the ctrl+o detail block below.
|
|
92
|
+
label: () =>
|
|
93
|
+
this.message.warning
|
|
94
|
+
? `${theme.icon.camera} compacted ${theme.fg("warning", theme.icon.warning)}`
|
|
95
|
+
: `${theme.icon.camera} compacted`,
|
|
91
96
|
detailMarkdown: () => this.#detailMarkdown(),
|
|
92
97
|
});
|
|
93
98
|
}
|
|
@@ -109,7 +114,8 @@ export class CompactionSummaryMessageComponent implements Component {
|
|
|
109
114
|
const frameCount = this.message.images?.length ?? 0;
|
|
110
115
|
const frameNote =
|
|
111
116
|
frameCount > 0 ? `\n\n_${frameCount} snapcompact frame${frameCount === 1 ? "" : "s"} attached_` : "";
|
|
112
|
-
|
|
117
|
+
const warningNote = this.message.warning ? `\n\n${theme.icon.warning} **Warning:** ${this.message.warning}` : "";
|
|
118
|
+
return `**Compacted from ${tokenStr} tokens**${warningNote}\n\n${this.message.summary}${frameNote}`;
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
|
|
@@ -20,6 +20,7 @@ export * from "./login-dialog";
|
|
|
20
20
|
export * from "./logout-account-selector";
|
|
21
21
|
export * from "./model-browser";
|
|
22
22
|
export * from "./model-hub";
|
|
23
|
+
export * from "./model-picker";
|
|
23
24
|
export * from "./oauth-selector";
|
|
24
25
|
export * from "./queue-mode-selector";
|
|
25
26
|
export * from "./read-tool-group";
|
|
@@ -22,11 +22,12 @@ import {
|
|
|
22
22
|
visibleWidth,
|
|
23
23
|
} from "@oh-my-pi/pi-tui";
|
|
24
24
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
25
|
-
import {
|
|
25
|
+
import { getModelMatchPreferences, resolveModelRoleValue } from "../../config/model-resolver";
|
|
26
|
+
import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS } from "../../config/model-roles";
|
|
26
27
|
import type { Settings } from "../../config/settings";
|
|
27
28
|
import type { ModelPerfStats } from "../../session/agent-storage";
|
|
28
|
-
import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
|
|
29
|
-
import { theme } from "../theme/theme";
|
|
29
|
+
import { AUTO_THINKING, type ConfiguredThinkingLevel, parseConfiguredThinkingLevel } from "../../thinking";
|
|
30
|
+
import { type ThemeColor, theme } from "../theme/theme";
|
|
30
31
|
import {
|
|
31
32
|
matchesSelectCancel,
|
|
32
33
|
matchesSelectDown,
|
|
@@ -35,12 +36,14 @@ import {
|
|
|
35
36
|
matchesSelectUp,
|
|
36
37
|
} from "../utils/keybinding-matchers";
|
|
37
38
|
|
|
38
|
-
/** One selectable
|
|
39
|
+
/** One selectable row. `selector` is a canonical model key or host-specific virtual key. */
|
|
39
40
|
export interface ModelBrowserItem {
|
|
40
41
|
provider: string;
|
|
41
42
|
id: string;
|
|
42
43
|
model: Model;
|
|
43
44
|
selector: string;
|
|
45
|
+
/** Optional foreground color for the row label. */
|
|
46
|
+
labelColor?: ThemeColor;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/** Resolved role assignment as displayed by the browser and the hub. */
|
|
@@ -54,6 +57,67 @@ export interface RoleAssignment {
|
|
|
54
57
|
/** Map of role id to its resolved assignment (absent roles are unresolved). */
|
|
55
58
|
export type RoleAssignments = Record<string, RoleAssignment | undefined>;
|
|
56
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Resolve every known role to its display assignment: configured role values
|
|
62
|
+
* resolve against `allModels`; unconfigured roles fall back to auto-selection
|
|
63
|
+
* over `autoCandidates` (skipped when empty). Shared by the /models hub and
|
|
64
|
+
* the alt+p session picker.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveRoleAssignments(
|
|
67
|
+
settings: Settings,
|
|
68
|
+
allModels: ReadonlyArray<Model>,
|
|
69
|
+
autoCandidates: ReadonlyArray<Model>,
|
|
70
|
+
): RoleAssignments {
|
|
71
|
+
const resolvedThinkingLevel = (
|
|
72
|
+
role: string,
|
|
73
|
+
resolved: { explicitThinkingLevel: boolean; thinkingLevel?: ConfiguredThinkingLevel },
|
|
74
|
+
): ConfiguredThinkingLevel => {
|
|
75
|
+
if (resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined) {
|
|
76
|
+
return resolved.thinkingLevel;
|
|
77
|
+
}
|
|
78
|
+
if (role === "default") {
|
|
79
|
+
return parseConfiguredThinkingLevel(settings.get("defaultThinkingLevel")) ?? ThinkingLevel.Inherit;
|
|
80
|
+
}
|
|
81
|
+
return ThinkingLevel.Inherit;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const roles: RoleAssignments = {};
|
|
85
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
86
|
+
const knownRoles = getKnownRoleIds(settings);
|
|
87
|
+
const configuredRoles = new Set<string>();
|
|
88
|
+
const catalog = [...allModels];
|
|
89
|
+
|
|
90
|
+
for (const role of knownRoles) {
|
|
91
|
+
const roleValue = settings.getModelRole(role);
|
|
92
|
+
if (!roleValue) continue;
|
|
93
|
+
configuredRoles.add(role);
|
|
94
|
+
const resolved = resolveModelRoleValue(roleValue, catalog, { settings, matchPreferences });
|
|
95
|
+
if (resolved.model) {
|
|
96
|
+
roles[role] = {
|
|
97
|
+
model: resolved.model,
|
|
98
|
+
thinkingLevel: resolvedThinkingLevel(role, resolved),
|
|
99
|
+
autoSelected: false,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (autoCandidates.length > 0) {
|
|
105
|
+
const candidates = [...autoCandidates];
|
|
106
|
+
for (const role of knownRoles) {
|
|
107
|
+
if (configuredRoles.has(role)) continue;
|
|
108
|
+
const resolved = resolveModelRoleValue(`pi/${role}`, candidates, { settings, matchPreferences });
|
|
109
|
+
if (!resolved.model) continue;
|
|
110
|
+
roles[role] = {
|
|
111
|
+
model: resolved.model,
|
|
112
|
+
thinkingLevel: resolvedThinkingLevel(role, resolved),
|
|
113
|
+
autoSelected: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return roles;
|
|
119
|
+
}
|
|
120
|
+
|
|
57
121
|
/** Wrap raw models into browser items. */
|
|
58
122
|
export function buildBrowserItems(models: ReadonlyArray<Model>): ModelBrowserItem[] {
|
|
59
123
|
return models.map(model => ({
|
|
@@ -256,7 +320,6 @@ export interface ModelBrowserOptions {
|
|
|
256
320
|
disableOverContext?: boolean;
|
|
257
321
|
/** Host-provided empty-state text (e.g. provider discovery status). */
|
|
258
322
|
emptyText?: () => string | undefined;
|
|
259
|
-
initialQuery?: string;
|
|
260
323
|
}
|
|
261
324
|
|
|
262
325
|
/** Rendered rows before the list window: search row + blank. */
|
|
@@ -290,11 +353,15 @@ export class ModelBrowser implements Component {
|
|
|
290
353
|
#currentContextTokens: number;
|
|
291
354
|
#disableOverContext: boolean;
|
|
292
355
|
#emptyText?: () => string | undefined;
|
|
356
|
+
/** Keep role-like virtual rows in their host-defined order during search. */
|
|
357
|
+
#preserveQueryOrder = false;
|
|
293
358
|
/** First visible list row; panned by the wheel, snapped to the selection on keyboard navigation. */
|
|
294
359
|
#windowStart = 0;
|
|
295
360
|
#windowCount = 0;
|
|
296
361
|
/** Whether the host pane owns arrow keys; drives cursor strength and the selected-row band. */
|
|
297
362
|
#focused = true;
|
|
363
|
+
/** `provider/id` of the session's active model; marked in rows and detail. */
|
|
364
|
+
#currentSelector: string | undefined;
|
|
298
365
|
|
|
299
366
|
/** Enter or click-on-selected. */
|
|
300
367
|
onActivate?: (item: ModelBrowserItem) => void;
|
|
@@ -310,9 +377,11 @@ export class ModelBrowser implements Component {
|
|
|
310
377
|
this.#currentContextTokens = Number.isFinite(tokens) && tokens > 0 ? Math.floor(tokens) : 0;
|
|
311
378
|
this.#disableOverContext = options.disableOverContext ?? false;
|
|
312
379
|
this.#emptyText = options.emptyText;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Mark `selector` as the session's active model (undefined clears the mark). */
|
|
383
|
+
setCurrentSelector(selector: string | undefined): void {
|
|
384
|
+
this.#currentSelector = selector;
|
|
316
385
|
}
|
|
317
386
|
|
|
318
387
|
/** Replace the scope's base items; the live query re-applies and selection is pinned by selector. */
|
|
@@ -347,6 +416,14 @@ export class ModelBrowser implements Component {
|
|
|
347
416
|
setShowProvider(show: boolean): void {
|
|
348
417
|
this.#showProvider = show;
|
|
349
418
|
}
|
|
419
|
+
/** Keep the source order after fuzzy filtering instead of applying model-specific ranking. */
|
|
420
|
+
setPreserveQueryOrder(preserve: boolean): void {
|
|
421
|
+
this.#preserveQueryOrder = preserve;
|
|
422
|
+
}
|
|
423
|
+
/** Allow hosts to toggle context-window eligibility between browser modes. */
|
|
424
|
+
setDisableOverContext(disable: boolean): void {
|
|
425
|
+
this.#disableOverContext = disable;
|
|
426
|
+
}
|
|
350
427
|
/** Focused: accent cursor + selected-row background band. Unfocused: dim cursor, no band. */
|
|
351
428
|
setFocused(focused: boolean): void {
|
|
352
429
|
this.#focused = focused;
|
|
@@ -495,17 +572,21 @@ export class ModelBrowser implements Component {
|
|
|
495
572
|
// queries all flow through the same fuzzy matcher.
|
|
496
573
|
const ranked = fuzzyRank(this.#baseItems, query, ({ provider, id }) => `${provider}/${id}`);
|
|
497
574
|
const matches = ranked.map(result => result.item);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
575
|
+
if (this.#preserveQueryOrder) {
|
|
576
|
+
items = matches;
|
|
577
|
+
} else {
|
|
578
|
+
// Match quality is the primary key while searching: an exact
|
|
579
|
+
// "gpt-5.5" must beat the MRU (or role-assigned) "gpt-5.6", so
|
|
580
|
+
// role rank is skipped and MRU only breaks ties. Scores are
|
|
581
|
+
// bucketed so sub-point position noise (provider-name length)
|
|
582
|
+
// can't split equally good matches; within a bucket the stable
|
|
583
|
+
// sort keeps sortModelItems' MRU/version order.
|
|
584
|
+
sortModelItems(matches, { roles: this.#roles, mruOrder: this.#mruOrder, skipRoleRank: true });
|
|
585
|
+
const buckets = new Map<ModelBrowserItem, number>();
|
|
586
|
+
for (const result of ranked) buckets.set(result.item, Math.round(result.score / 10));
|
|
587
|
+
matches.sort((a, b) => (buckets.get(a) ?? 0) - (buckets.get(b) ?? 0));
|
|
588
|
+
items = matches;
|
|
589
|
+
}
|
|
509
590
|
} else {
|
|
510
591
|
items = this.#baseItems;
|
|
511
592
|
}
|
|
@@ -640,11 +721,17 @@ export class ModelBrowser implements Component {
|
|
|
640
721
|
const disabled = this.#isDisabled(item);
|
|
641
722
|
const prefix = selected && this.#focused ? `${theme.fg("accent", theme.nav.cursor)} ` : " ";
|
|
642
723
|
const providerPrefix = this.#showProvider ? theme.fg("dim", `${item.provider}/`) : "";
|
|
643
|
-
const name =
|
|
724
|
+
const name = item.labelColor
|
|
725
|
+
? theme.fg(item.labelColor, item.id)
|
|
726
|
+
: selected
|
|
727
|
+
? theme.fg("accent", item.id)
|
|
728
|
+
: item.id;
|
|
729
|
+
const currentMark =
|
|
730
|
+
item.selector === this.#currentSelector ? ` ${theme.fg("success", theme.status.enabled)}` : "";
|
|
644
731
|
const overLimit = disabled
|
|
645
732
|
? ` ${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`
|
|
646
733
|
: "";
|
|
647
|
-
let left = `${prefix}${providerPrefix}${name}${overLimit}`;
|
|
734
|
+
let left = `${prefix}${providerPrefix}${name}${currentMark}${overLimit}`;
|
|
648
735
|
|
|
649
736
|
// Perf column collapses entirely when no visible row has measurements.
|
|
650
737
|
const perfCol =
|
|
@@ -691,6 +778,9 @@ export class ModelBrowser implements Component {
|
|
|
691
778
|
}
|
|
692
779
|
|
|
693
780
|
const chips: string[] = [];
|
|
781
|
+
if (selected.selector === this.#currentSelector) {
|
|
782
|
+
chips.push(theme.fg("success", `${theme.status.enabled} current`));
|
|
783
|
+
}
|
|
694
784
|
const seen = new Set<string>();
|
|
695
785
|
const pushRole = (role: string) => {
|
|
696
786
|
if (seen.has(role)) return;
|