@oh-my-pi/pi-coding-agent 16.4.6 → 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 +88 -0
- package/dist/cli.js +3312 -3254
- 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 +28 -3
- 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/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/welcome.d.ts +4 -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/eval/js/shared/rewrite-imports.ts +31 -13
- 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 +152 -49
- package/src/modes/components/model-hub.ts +55 -142
- package/src/modes/components/model-picker.ts +233 -0
- package/src/modes/components/plan-review-overlay.ts +7 -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/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +13 -14
- 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 +20 -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/perplexity.ts +18 -2
- 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,9 +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;
|
|
361
|
+
/** Whether the host pane owns arrow keys; drives cursor strength and the selected-row band. */
|
|
362
|
+
#focused = true;
|
|
363
|
+
/** `provider/id` of the session's active model; marked in rows and detail. */
|
|
364
|
+
#currentSelector: string | undefined;
|
|
296
365
|
|
|
297
366
|
/** Enter or click-on-selected. */
|
|
298
367
|
onActivate?: (item: ModelBrowserItem) => void;
|
|
@@ -308,9 +377,11 @@ export class ModelBrowser implements Component {
|
|
|
308
377
|
this.#currentContextTokens = Number.isFinite(tokens) && tokens > 0 ? Math.floor(tokens) : 0;
|
|
309
378
|
this.#disableOverContext = options.disableOverContext ?? false;
|
|
310
379
|
this.#emptyText = options.emptyText;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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;
|
|
314
385
|
}
|
|
315
386
|
|
|
316
387
|
/** Replace the scope's base items; the live query re-applies and selection is pinned by selector. */
|
|
@@ -345,6 +416,18 @@ export class ModelBrowser implements Component {
|
|
|
345
416
|
setShowProvider(show: boolean): void {
|
|
346
417
|
this.#showProvider = show;
|
|
347
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
|
+
}
|
|
427
|
+
/** Focused: accent cursor + selected-row background band. Unfocused: dim cursor, no band. */
|
|
428
|
+
setFocused(focused: boolean): void {
|
|
429
|
+
this.#focused = focused;
|
|
430
|
+
}
|
|
348
431
|
|
|
349
432
|
/** Total rendered height for the current `maxVisible` (host layout budgeting). */
|
|
350
433
|
get renderedRows(): number {
|
|
@@ -416,18 +499,27 @@ export class ModelBrowser implements Component {
|
|
|
416
499
|
this.#windowStart = this.#clampWindowStart(this.#windowStart);
|
|
417
500
|
}
|
|
418
501
|
|
|
419
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Move the selection by `delta` rows, skipping disabled rows. Single steps
|
|
504
|
+
* wrap at the ends; `wrap: false` (page/home/end jumps) clamps instead.
|
|
505
|
+
*/
|
|
506
|
+
moveSelection(delta: number, options: { wrap?: boolean } = {}): void {
|
|
420
507
|
const count = this.#visibleItems.length;
|
|
421
508
|
if (count === 0) return;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
this.#
|
|
428
|
-
|
|
509
|
+
if (options.wrap ?? true) {
|
|
510
|
+
let index = this.#selectedIndex;
|
|
511
|
+
for (let step = 0; step < count; step++) {
|
|
512
|
+
index = (index + delta + count) % count;
|
|
513
|
+
const item = this.#visibleItems[index];
|
|
514
|
+
if (item && !this.#isDisabled(item)) {
|
|
515
|
+
this.#setSelectedIndex(index);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
429
518
|
}
|
|
519
|
+
return;
|
|
430
520
|
}
|
|
521
|
+
const target = Math.max(0, Math.min(this.#selectedIndex + delta, count - 1));
|
|
522
|
+
this.#setSelectedIndex(this.#coerceSelectedIndex(target));
|
|
431
523
|
}
|
|
432
524
|
|
|
433
525
|
#setSelectedIndex(index: number): void {
|
|
@@ -480,17 +572,21 @@ export class ModelBrowser implements Component {
|
|
|
480
572
|
// queries all flow through the same fuzzy matcher.
|
|
481
573
|
const ranked = fuzzyRank(this.#baseItems, query, ({ provider, id }) => `${provider}/${id}`);
|
|
482
574
|
const matches = ranked.map(result => result.item);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
+
}
|
|
494
590
|
} else {
|
|
495
591
|
items = this.#baseItems;
|
|
496
592
|
}
|
|
@@ -514,11 +610,19 @@ export class ModelBrowser implements Component {
|
|
|
514
610
|
return;
|
|
515
611
|
}
|
|
516
612
|
if (matchesSelectPageUp(data)) {
|
|
517
|
-
this.moveSelection(-this.#maxVisible);
|
|
613
|
+
this.moveSelection(-this.#maxVisible, { wrap: false });
|
|
518
614
|
return;
|
|
519
615
|
}
|
|
520
616
|
if (matchesSelectPageDown(data)) {
|
|
521
|
-
this.moveSelection(this.#maxVisible);
|
|
617
|
+
this.moveSelection(this.#maxVisible, { wrap: false });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (matchesKey(data, "home")) {
|
|
621
|
+
this.moveSelection(-this.#visibleItems.length, { wrap: false });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (matchesKey(data, "end")) {
|
|
625
|
+
this.moveSelection(this.#visibleItems.length, { wrap: false });
|
|
522
626
|
return;
|
|
523
627
|
}
|
|
524
628
|
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
@@ -574,6 +678,10 @@ export class ModelBrowser implements Component {
|
|
|
574
678
|
this.#setSelectedIndex(index);
|
|
575
679
|
}
|
|
576
680
|
}
|
|
681
|
+
/** Drop the hover band. Hosts call this when the pointer leaves the browser pane. */
|
|
682
|
+
clearHover(): void {
|
|
683
|
+
this.#hoveredIndex = null;
|
|
684
|
+
}
|
|
577
685
|
|
|
578
686
|
/** List index under a frame-local row, or null when off-list or on a disabled row. */
|
|
579
687
|
#hoverIndexAt(line: number): number | null {
|
|
@@ -585,22 +693,6 @@ export class ModelBrowser implements Component {
|
|
|
585
693
|
return index;
|
|
586
694
|
}
|
|
587
695
|
|
|
588
|
-
#chipsFor(model: Model): string {
|
|
589
|
-
const parts: string[] = [];
|
|
590
|
-
const seen = new Set<string>();
|
|
591
|
-
const pushChip = (role: string) => {
|
|
592
|
-
if (seen.has(role)) return;
|
|
593
|
-
seen.add(role);
|
|
594
|
-
const assignment = this.#roles[role];
|
|
595
|
-
if (!assignment || !modelsAreEqual(assignment.model, model)) return;
|
|
596
|
-
if (getRoleInfo(role, this.#settings).hidden) return;
|
|
597
|
-
parts.push(formatRoleChip(role, assignment, this.#settings));
|
|
598
|
-
};
|
|
599
|
-
for (const role of MODEL_ROLE_IDS) pushChip(role);
|
|
600
|
-
for (const role in this.#roles) pushChip(role);
|
|
601
|
-
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
602
|
-
}
|
|
603
|
-
|
|
604
696
|
/** `0.9s 118t/s` measured-perf cell for the row's meta block; empty when unmeasured or the column is off. */
|
|
605
697
|
#perfCell(item: ModelBrowserItem, mode: PerfMode): string {
|
|
606
698
|
if (mode === "off") return "";
|
|
@@ -627,13 +719,19 @@ export class ModelBrowser implements Component {
|
|
|
627
719
|
return ` ${line} `;
|
|
628
720
|
}
|
|
629
721
|
const disabled = this.#isDisabled(item);
|
|
630
|
-
const prefix = selected ? `${theme.fg("accent", theme.nav.cursor)} ` : " ";
|
|
722
|
+
const prefix = selected && this.#focused ? `${theme.fg("accent", theme.nav.cursor)} ` : " ";
|
|
631
723
|
const providerPrefix = this.#showProvider ? theme.fg("dim", `${item.provider}/`) : "";
|
|
632
|
-
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)}` : "";
|
|
633
731
|
const overLimit = disabled
|
|
634
732
|
? ` ${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`
|
|
635
733
|
: "";
|
|
636
|
-
let left = `${prefix}${providerPrefix}${name}${
|
|
734
|
+
let left = `${prefix}${providerPrefix}${name}${currentMark}${overLimit}`;
|
|
637
735
|
|
|
638
736
|
// Perf column collapses entirely when no visible row has measurements.
|
|
639
737
|
const perfCol =
|
|
@@ -648,7 +746,9 @@ export class ModelBrowser implements Component {
|
|
|
648
746
|
if (disabled) {
|
|
649
747
|
line = theme.fg("dim", Bun.stripANSI(line));
|
|
650
748
|
}
|
|
651
|
-
|
|
749
|
+
// The bg band is reserved for the mouse: it marks hover, nothing else.
|
|
750
|
+
// Keyboard selection is the cursor glyph + accent name.
|
|
751
|
+
if (hovered && !disabled) {
|
|
652
752
|
line = theme.bg("selectedBg", line);
|
|
653
753
|
}
|
|
654
754
|
return line;
|
|
@@ -678,6 +778,9 @@ export class ModelBrowser implements Component {
|
|
|
678
778
|
}
|
|
679
779
|
|
|
680
780
|
const chips: string[] = [];
|
|
781
|
+
if (selected.selector === this.#currentSelector) {
|
|
782
|
+
chips.push(theme.fg("success", `${theme.status.enabled} current`));
|
|
783
|
+
}
|
|
681
784
|
const seen = new Set<string>();
|
|
682
785
|
const pushRole = (role: string) => {
|
|
683
786
|
if (seen.has(role)) return;
|