@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.3
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 +113 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +14 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-selector.d.ts +16 -7
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +0 -18
- package/dist/types/modes/controllers/event-controller.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/session-manager.d.ts +5 -2
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +13 -1
- package/dist/types/tools/read.d.ts +2 -1
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/renderers.d.ts +0 -15
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/write.d.ts +0 -2
- package/dist/types/tools/yield.d.ts +8 -0
- package/dist/types/tui/code-cell.d.ts +0 -2
- package/dist/types/tui/hyperlink.d.ts +5 -7
- package/dist/types/tui/output-block.d.ts +0 -18
- package/package.json +9 -9
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +2 -4
- package/src/cli/gallery-cli.ts +4 -0
- package/src/cli/gallery-fixtures/codeintel.ts +0 -1
- package/src/cli/gallery-fixtures/fs.ts +68 -1
- package/src/cli/gallery-fixtures/types.ts +8 -1
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +4 -22
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings.ts +86 -41
- package/src/debug/index.ts +8 -0
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/__tests__/llm-bridge.test.ts +20 -0
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/js/shared/prelude.txt +26 -10
- package/src/eval/llm-bridge.ts +14 -3
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +189 -61
- package/src/main.ts +144 -78
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +2 -2
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/custom-editor.ts +143 -111
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/plan-review-overlay.ts +26 -5
- package/src/modes/components/read-tool-group.ts +415 -35
- package/src/modes/components/session-selector.ts +89 -35
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/tool-execution.ts +7 -49
- package/src/modes/components/transcript-container.ts +108 -32
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/controllers/event-controller.ts +32 -1
- package/src/modes/controllers/input-controller.ts +56 -9
- package/src/modes/interactive-mode.ts +107 -20
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/types.ts +7 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/modes/workflow.ts +10 -10
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +85 -10
- package/src/session/agent-session.ts +42 -15
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +98 -25
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +72 -36
- package/src/task/render.ts +3 -4
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +7 -7
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/eval-render.ts +4 -23
- package/src/tools/eval.ts +13 -2
- package/src/tools/find.ts +148 -99
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/index.ts +32 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/path-utils.ts +47 -24
- package/src/tools/plan-mode-guard.ts +52 -7
- package/src/tools/read.ts +41 -20
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/search.ts +38 -3
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +5 -14
- package/src/tools/yield.ts +10 -1
- package/src/tui/code-cell.ts +1 -6
- package/src/tui/hyperlink.ts +13 -23
- package/src/tui/output-block.ts +2 -97
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/enhanced-paste.ts +30 -2
- package/src/web/search/providers/codex.ts +37 -8
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// biome-ignore-all lint/suspicious/noTemplateCurlyInString: sample source-code strings (read fixtures) intentionally contain literal ${...}.
|
|
2
2
|
// Gallery fixtures for the filesystem tools (read, write, find).
|
|
3
|
-
import
|
|
3
|
+
import { ReadToolGroupComponent } from "../../modes/components/read-tool-group";
|
|
4
|
+
import type { GalleryFixture, GalleryFixtureState, GalleryResult } from "./types";
|
|
4
5
|
|
|
5
6
|
const readSnippet = [
|
|
6
7
|
"export const findToolRenderer = {",
|
|
@@ -36,6 +37,64 @@ const writtenContent = [
|
|
|
36
37
|
"",
|
|
37
38
|
].join("\n");
|
|
38
39
|
|
|
40
|
+
const groupedReadTargets = [
|
|
41
|
+
"packages/coding-agent/test/streaming-preview-height.test.ts:301-409",
|
|
42
|
+
"packages/coding-agent/test/tool-live-region-scrollback.test.ts:143-310",
|
|
43
|
+
"packages/tui/test/streaming-scrollback-defer.test.ts:89-464",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const groupedReadDelimitedPath = groupedReadTargets.join(",");
|
|
47
|
+
const groupedReadRepeatedFile = "packages/coding-agent/src/task/render.ts";
|
|
48
|
+
const groupedReadRepeatedRanges = `${groupedReadRepeatedFile}:507-605,1070-1194,1210-1240,1270-1274`;
|
|
49
|
+
|
|
50
|
+
function textResult(text: string, details?: unknown, isError?: boolean): GalleryResult {
|
|
51
|
+
return { content: [{ type: "text", text }], details, isError };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function addGroupedReadArgs(component: ReadToolGroupComponent): void {
|
|
55
|
+
component.updateArgs({ path: groupedReadDelimitedPath }, "read-delimited");
|
|
56
|
+
component.updateArgs({ path: groupedReadRepeatedRanges }, "read-ranges");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderReadGroupFixtureState(state: GalleryFixtureState, width: number, expanded: boolean): string[] {
|
|
60
|
+
const component = new ReadToolGroupComponent();
|
|
61
|
+
component.setExpanded(expanded);
|
|
62
|
+
|
|
63
|
+
if (state === "streaming") {
|
|
64
|
+
component.updateArgs(
|
|
65
|
+
{
|
|
66
|
+
path: [
|
|
67
|
+
"packages/coding-agent/test/streaming-preview-height.test.ts:301-409",
|
|
68
|
+
"packages/coding-agent/test/tool-live-region-scrollback.test.ts:143-",
|
|
69
|
+
].join(","),
|
|
70
|
+
},
|
|
71
|
+
"read-delimited",
|
|
72
|
+
);
|
|
73
|
+
return component.render(width);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
addGroupedReadArgs(component);
|
|
77
|
+
if (state === "progress") return component.render(width);
|
|
78
|
+
|
|
79
|
+
component.updateResult(
|
|
80
|
+
textResult("Read three focused test ranges.", { displayReadTargets: groupedReadTargets }),
|
|
81
|
+
false,
|
|
82
|
+
"read-delimited",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (state === "error") {
|
|
86
|
+
component.updateResult(
|
|
87
|
+
textResult("Error: selector 1270-1274 is outside the file", undefined, true),
|
|
88
|
+
false,
|
|
89
|
+
"read-ranges",
|
|
90
|
+
);
|
|
91
|
+
return component.render(width);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
component.updateResult(textResult("Read four render.ts ranges."), false, "read-ranges");
|
|
95
|
+
return component.render(width);
|
|
96
|
+
}
|
|
97
|
+
|
|
39
98
|
export const fsFixtures: Record<string, GalleryFixture> = {
|
|
40
99
|
read: {
|
|
41
100
|
label: "Read",
|
|
@@ -81,6 +140,14 @@ export const fsFixtures: Record<string, GalleryFixture> = {
|
|
|
81
140
|
},
|
|
82
141
|
},
|
|
83
142
|
|
|
143
|
+
read_group: {
|
|
144
|
+
label: "Read Groups",
|
|
145
|
+
args: {},
|
|
146
|
+
result: textResult("Rendered grouped read calls."),
|
|
147
|
+
errorResult: textResult("Rendered grouped read errors.", undefined, true),
|
|
148
|
+
renderState: renderReadGroupFixtureState,
|
|
149
|
+
},
|
|
150
|
+
|
|
84
151
|
write: {
|
|
85
152
|
label: "Write",
|
|
86
153
|
// Streaming: path known, content still arriving (only the imports so far).
|
|
@@ -11,14 +11,21 @@ export interface GalleryResult {
|
|
|
11
11
|
isError?: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export type GalleryFixtureState = "streaming" | "progress" | "success" | "error";
|
|
15
|
+
|
|
14
16
|
export interface GalleryFixture {
|
|
15
17
|
/** Display label for the tool header (defaults to the tool name). */
|
|
16
18
|
label?: string;
|
|
17
19
|
/** Edit mode for edit-like tools so the streaming preview dispatches correctly. */
|
|
18
20
|
editMode?: EditMode;
|
|
21
|
+
/**
|
|
22
|
+
* Custom gallery-only renderer for fixtures that are not one ToolExecutionComponent
|
|
23
|
+
* (for example the read-group transcript component).
|
|
24
|
+
*/
|
|
25
|
+
renderState?: (state: GalleryFixtureState, width: number, expanded: boolean) => string[] | Promise<string[]>;
|
|
19
26
|
/**
|
|
20
27
|
* Set for tools whose real `AgentTool` attaches `renderCall`/`renderResult`
|
|
21
|
-
* directly on the instance (e.g. `
|
|
28
|
+
* directly on the instance (e.g. `task`). The harness then attaches
|
|
22
29
|
* the registry renderer onto the fake tool so the component routes through
|
|
23
30
|
* the custom-tool branch — the same path production takes — instead of the
|
|
24
31
|
* built-in registry branch. The two branches can diverge, so exercising the
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getProjectDir, normalizePathForComparison, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import type { Args } from "./args";
|
|
6
|
+
|
|
7
|
+
async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
8
|
+
if (parsed.allowHome || parsed.cwd) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
if (!home) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const normalizePath = normalizePathForComparison;
|
|
18
|
+
|
|
19
|
+
const cwd = normalizePath(getProjectDir());
|
|
20
|
+
const normalizedHome = normalizePath(home);
|
|
21
|
+
if (cwd !== normalizedHome) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isDirectory = async (p: string) => {
|
|
26
|
+
try {
|
|
27
|
+
const s = await fs.stat(p);
|
|
28
|
+
return s.isDirectory();
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
|
|
35
|
+
for (const candidate of candidates) {
|
|
36
|
+
try {
|
|
37
|
+
if (!(await isDirectory(candidate))) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
setProjectDir(candidate);
|
|
41
|
+
return;
|
|
42
|
+
} catch {
|
|
43
|
+
// Try next candidate.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const fallback = os.tmpdir();
|
|
49
|
+
if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
|
|
50
|
+
setProjectDir(fallback);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Ignore fallback errors.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function applyStartupCwd(parsed: Args): Promise<void> {
|
|
58
|
+
if (parsed.cwd) {
|
|
59
|
+
setProjectDir(parsed.cwd);
|
|
60
|
+
// setProjectDir resolves the (possibly relative) target against the launch
|
|
61
|
+
// cwd and chdirs into it. Re-sync parsed.cwd to the resolved absolute path
|
|
62
|
+
// so downstream consumers (buildSessionOptions, settings/discovery, session
|
|
63
|
+
// persistence) don't re-resolve a relative string against the new cwd.
|
|
64
|
+
parsed.cwd = getProjectDir();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await maybeAutoChdir(parsed);
|
|
68
|
+
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -49,6 +49,9 @@ export default class Index extends Command {
|
|
|
49
49
|
"allow-home": Flags.boolean({
|
|
50
50
|
description: "Allow starting in ~ without auto-switching to a temp dir",
|
|
51
51
|
}),
|
|
52
|
+
cwd: Flags.string({
|
|
53
|
+
description: "Directory to start in (overrides the launch cwd)",
|
|
54
|
+
}),
|
|
52
55
|
mode: Flags.string({
|
|
53
56
|
description: "Output mode: text (default), json, rpc, or rpc-ui",
|
|
54
57
|
options: ["text", "json", "rpc", "acp", "rpc-ui"],
|
|
@@ -3,6 +3,7 @@ import type { Api, ApiKey, Model } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import type { ApiKeyResolverRegistry } from "../config/api-key-resolver";
|
|
4
4
|
import { MODEL_ROLE_IDS } from "../config/model-registry";
|
|
5
5
|
import {
|
|
6
|
+
getModelMatchPreferences,
|
|
6
7
|
type ModelLookupRegistry,
|
|
7
8
|
parseModelPattern,
|
|
8
9
|
resolveModelRoleValue,
|
|
@@ -33,7 +34,7 @@ export async function resolvePrimaryModel(
|
|
|
33
34
|
modelRegistry: CommitModelRegistry,
|
|
34
35
|
): Promise<ResolvedCommitModel> {
|
|
35
36
|
const available = modelRegistry.getAvailable();
|
|
36
|
-
const matchPreferences =
|
|
37
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
37
38
|
const resolved = override
|
|
38
39
|
? resolveModelRoleValue(override, available, { settings, matchPreferences, modelRegistry })
|
|
39
40
|
: resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available, modelRegistry);
|
|
@@ -73,7 +74,7 @@ export async function resolveSmolModel(
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
const matchPreferences =
|
|
77
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
77
78
|
for (const pattern of MODEL_PRIO.smol) {
|
|
78
79
|
const candidate = parseModelPattern(pattern, available, matchPreferences, { modelRegistry }).model;
|
|
79
80
|
if (!candidate) continue;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const DEFAULT_MODEL_PROVIDER_ORDER = [
|
|
2
|
+
// First-party / native account providers. Prefer these over relays when the
|
|
3
|
+
// same upstream model is available in more than one place.
|
|
4
|
+
"openai-codex",
|
|
5
|
+
"anthropic",
|
|
6
|
+
"openai",
|
|
7
|
+
"google-gemini-cli",
|
|
8
|
+
"google",
|
|
9
|
+
"google-vertex",
|
|
10
|
+
"kimi-code",
|
|
11
|
+
"moonshot",
|
|
12
|
+
"qwen-portal",
|
|
13
|
+
"zai",
|
|
14
|
+
"xai-oauth",
|
|
15
|
+
"xai",
|
|
16
|
+
"mistral",
|
|
17
|
+
"deepseek",
|
|
18
|
+
"groq",
|
|
19
|
+
|
|
20
|
+
// High-quality aggregators / hosted inference providers.
|
|
21
|
+
"fireworks",
|
|
22
|
+
"cerebras",
|
|
23
|
+
"openrouter",
|
|
24
|
+
"together",
|
|
25
|
+
|
|
26
|
+
// Generic gateways and editor/proxy providers. These are useful when picked
|
|
27
|
+
// explicitly, but should not win ambiguous automatic role selection.
|
|
28
|
+
"alibaba-coding-plan",
|
|
29
|
+
"google-antigravity",
|
|
30
|
+
"opencode-zen",
|
|
31
|
+
"gitlab-duo",
|
|
32
|
+
"opencode-go",
|
|
33
|
+
"kilo",
|
|
34
|
+
"vercel-ai-gateway",
|
|
35
|
+
"cloudflare-ai-gateway",
|
|
36
|
+
"nanogpt",
|
|
37
|
+
"github-copilot",
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
function addProviderRank(rank: Map<string, number>, provider: string): void {
|
|
41
|
+
const normalized = provider.trim().toLowerCase();
|
|
42
|
+
if (!normalized || rank.has(normalized)) return;
|
|
43
|
+
rank.set(normalized, rank.size);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildModelProviderPriorityRank(configuredProviderOrder?: readonly string[]): Map<string, number> {
|
|
47
|
+
const rank = new Map<string, number>();
|
|
48
|
+
for (const provider of configuredProviderOrder ?? []) {
|
|
49
|
+
addProviderRank(rank, provider);
|
|
50
|
+
}
|
|
51
|
+
for (const provider of DEFAULT_MODEL_PROVIDER_ORDER) {
|
|
52
|
+
addProviderRank(rank, provider);
|
|
53
|
+
}
|
|
54
|
+
return rank;
|
|
55
|
+
}
|
|
@@ -118,6 +118,7 @@ import {
|
|
|
118
118
|
getModelLikeIdSegments,
|
|
119
119
|
stripBracketedModelIdAffixes,
|
|
120
120
|
} from "./model-id-affixes";
|
|
121
|
+
import { buildModelProviderPriorityRank } from "./model-provider-priority";
|
|
121
122
|
import {
|
|
122
123
|
type ModelOverride,
|
|
123
124
|
type ModelsConfig,
|
|
@@ -2208,27 +2209,8 @@ export class ModelRegistry {
|
|
|
2208
2209
|
});
|
|
2209
2210
|
}
|
|
2210
2211
|
|
|
2211
|
-
#providerRank(
|
|
2212
|
-
|
|
2213
|
-
const result = new Map<string, number>();
|
|
2214
|
-
let nextRank = 0;
|
|
2215
|
-
for (const provider of configuredProviders) {
|
|
2216
|
-
const normalized = provider.trim().toLowerCase();
|
|
2217
|
-
if (!normalized || result.has(normalized)) {
|
|
2218
|
-
continue;
|
|
2219
|
-
}
|
|
2220
|
-
result.set(normalized, nextRank);
|
|
2221
|
-
nextRank += 1;
|
|
2222
|
-
}
|
|
2223
|
-
for (const model of models) {
|
|
2224
|
-
const normalized = model.provider.toLowerCase();
|
|
2225
|
-
if (result.has(normalized)) {
|
|
2226
|
-
continue;
|
|
2227
|
-
}
|
|
2228
|
-
result.set(normalized, nextRank);
|
|
2229
|
-
nextRank += 1;
|
|
2230
|
-
}
|
|
2231
|
-
return result;
|
|
2212
|
+
#providerRank(): Map<string, number> {
|
|
2213
|
+
return buildModelProviderPriorityRank(getConfiguredProviderOrderFromSettings());
|
|
2232
2214
|
}
|
|
2233
2215
|
|
|
2234
2216
|
#resolveCanonicalVariant(
|
|
@@ -2238,7 +2220,7 @@ export class ModelRegistry {
|
|
|
2238
2220
|
if (variants.length === 0) {
|
|
2239
2221
|
return undefined;
|
|
2240
2222
|
}
|
|
2241
|
-
const providerRank = this.#providerRank(
|
|
2223
|
+
const providerRank = this.#providerRank();
|
|
2242
2224
|
const modelOrder = new Map<string, number>();
|
|
2243
2225
|
for (let index = 0; index < allCandidates.length; index += 1) {
|
|
2244
2226
|
modelOrder.set(formatCanonicalVariantSelector(allCandidates[index]!), index);
|
|
@@ -17,6 +17,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
19
19
|
import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
|
|
20
|
+
import { buildModelProviderPriorityRank } from "./model-provider-priority";
|
|
20
21
|
import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
|
|
21
22
|
import type { Settings } from "./settings";
|
|
22
23
|
|
|
@@ -179,7 +180,9 @@ export function resolveProviderModelReference(
|
|
|
179
180
|
export interface ModelMatchPreferences {
|
|
180
181
|
/** Most-recently-used model keys (provider/modelId) to prefer when ambiguous. */
|
|
181
182
|
usageOrder?: string[];
|
|
182
|
-
/**
|
|
183
|
+
/** Provider precedence used for ambiguous unqualified model patterns. */
|
|
184
|
+
providerOrder?: readonly string[];
|
|
185
|
+
/** Providers to deprioritize when no recent usage or provider priority is available. */
|
|
183
186
|
deprioritizeProviders?: string[];
|
|
184
187
|
}
|
|
185
188
|
|
|
@@ -194,6 +197,7 @@ type RestorableModelRegistry = Pick<ModelRegistry, "getAvailable" | "find" | "ge
|
|
|
194
197
|
interface ModelPreferenceContext {
|
|
195
198
|
modelUsageRank: Map<string, number>;
|
|
196
199
|
providerUsageRank: Map<string, number>;
|
|
200
|
+
providerPriorityRank: Map<string, number>;
|
|
197
201
|
deprioritizedProviders: Set<string>;
|
|
198
202
|
modelOrder: Map<string, number>;
|
|
199
203
|
}
|
|
@@ -215,14 +219,35 @@ function buildPreferenceContext(
|
|
|
215
219
|
providerUsageRank.set(parsed.provider, i);
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
|
-
|
|
219
|
-
const deprioritizedProviders = new Set(preferences?.deprioritizeProviders ?? [
|
|
222
|
+
const providerPriorityRank = buildModelProviderPriorityRank(preferences?.providerOrder);
|
|
223
|
+
const deprioritizedProviders = new Set(preferences?.deprioritizeProviders ?? []);
|
|
220
224
|
const modelOrder = new Map<string, number>();
|
|
221
225
|
for (let i = 0; i < availableModels.length; i += 1) {
|
|
222
226
|
modelOrder.set(formatModelString(availableModels[i]), i);
|
|
223
227
|
}
|
|
224
228
|
|
|
225
|
-
return { modelUsageRank, providerUsageRank, deprioritizedProviders, modelOrder };
|
|
229
|
+
return { modelUsageRank, providerUsageRank, providerPriorityRank, deprioritizedProviders, modelOrder };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getModelMatchPreferences(
|
|
233
|
+
settings?: Partial<Pick<Settings, "get" | "getStorage">>,
|
|
234
|
+
): ModelMatchPreferences {
|
|
235
|
+
return {
|
|
236
|
+
usageOrder: settings?.getStorage?.()?.getModelUsageOrder(),
|
|
237
|
+
providerOrder: settings?.get?.("modelProviderOrder"),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function mergeModelMatchPreferences(
|
|
242
|
+
settings: Settings | undefined,
|
|
243
|
+
preferences: ModelMatchPreferences | undefined,
|
|
244
|
+
): ModelMatchPreferences {
|
|
245
|
+
const settingsPreferences = getModelMatchPreferences(settings);
|
|
246
|
+
return {
|
|
247
|
+
usageOrder: preferences?.usageOrder ?? settingsPreferences.usageOrder,
|
|
248
|
+
providerOrder: preferences?.providerOrder ?? settingsPreferences.providerOrder,
|
|
249
|
+
deprioritizeProviders: preferences?.deprioritizeProviders,
|
|
250
|
+
};
|
|
226
251
|
}
|
|
227
252
|
|
|
228
253
|
function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceContext): Model<Api> {
|
|
@@ -236,6 +261,12 @@ function pickPreferredModel(candidates: Model<Api>[], context: ModelPreferenceCo
|
|
|
236
261
|
return (aUsage ?? Number.POSITIVE_INFINITY) - (bUsage ?? Number.POSITIVE_INFINITY);
|
|
237
262
|
}
|
|
238
263
|
|
|
264
|
+
const aProviderPriority = context.providerPriorityRank.get(a.provider.toLowerCase());
|
|
265
|
+
const bProviderPriority = context.providerPriorityRank.get(b.provider.toLowerCase());
|
|
266
|
+
if (aProviderPriority !== undefined || bProviderPriority !== undefined) {
|
|
267
|
+
return (aProviderPriority ?? Number.POSITIVE_INFINITY) - (bProviderPriority ?? Number.POSITIVE_INFINITY);
|
|
268
|
+
}
|
|
269
|
+
|
|
239
270
|
const aProviderUsage = context.providerUsageRank.get(a.provider);
|
|
240
271
|
const bProviderUsage = context.providerUsageRank.get(b.provider);
|
|
241
272
|
if (aProviderUsage !== undefined || bProviderUsage !== undefined) {
|
|
@@ -618,8 +649,9 @@ export function resolveModelRoleValue(
|
|
|
618
649
|
}
|
|
619
650
|
|
|
620
651
|
let warning: string | undefined;
|
|
652
|
+
const matchPreferences = mergeModelMatchPreferences(options?.settings, options?.matchPreferences);
|
|
621
653
|
for (const effectivePattern of effectivePatterns) {
|
|
622
|
-
const resolved = parseModelPattern(effectivePattern, availableModels,
|
|
654
|
+
const resolved = parseModelPattern(effectivePattern, availableModels, matchPreferences, {
|
|
623
655
|
modelRegistry: options?.modelRegistry,
|
|
624
656
|
});
|
|
625
657
|
if (resolved.model) {
|
|
@@ -720,7 +752,7 @@ export function resolveModelOverride(
|
|
|
720
752
|
): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
|
|
721
753
|
if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
|
|
722
754
|
const availableModels = modelRegistry.getAvailable();
|
|
723
|
-
const matchPreferences =
|
|
755
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
724
756
|
for (const pattern of modelPatterns) {
|
|
725
757
|
const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
|
|
726
758
|
settings,
|
|
@@ -800,7 +832,7 @@ export function resolveRoleSelection(
|
|
|
800
832
|
availableModels: Model<Api>[],
|
|
801
833
|
modelRegistry?: CanonicalModelRegistry,
|
|
802
834
|
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
803
|
-
const matchPreferences =
|
|
835
|
+
const matchPreferences = getModelMatchPreferences(settings);
|
|
804
836
|
for (const role of roles) {
|
|
805
837
|
const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
|
|
806
838
|
settings,
|
package/src/config/settings.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface SettingsOptions {
|
|
|
72
72
|
/**
|
|
73
73
|
* Get a nested value from an object by path segments.
|
|
74
74
|
*/
|
|
75
|
-
function getByPath(obj: RawSettings, segments: string[]): unknown {
|
|
75
|
+
function getByPath(obj: RawSettings, segments: readonly string[]): unknown {
|
|
76
76
|
let current: unknown = obj;
|
|
77
77
|
for (const segment of segments) {
|
|
78
78
|
if (current === null || current === undefined || typeof current !== "object") {
|
|
@@ -83,6 +83,10 @@ function getByPath(obj: RawSettings, segments: string[]): unknown {
|
|
|
83
83
|
return current;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const SETTING_PATH_SEGMENTS: Record<SettingPath, readonly string[]> = Object.fromEntries(
|
|
87
|
+
(Object.keys(SETTINGS_SCHEMA) as SettingPath[]).map(settingPath => [settingPath, settingPath.split(".")]),
|
|
88
|
+
) as unknown as Record<SettingPath, readonly string[]>;
|
|
89
|
+
|
|
86
90
|
/**
|
|
87
91
|
* Set a nested value in an object by path segments.
|
|
88
92
|
* Creates intermediate objects as needed.
|
|
@@ -196,6 +200,8 @@ export class Settings {
|
|
|
196
200
|
#overrides: RawSettings = {};
|
|
197
201
|
/** Merged view (global + project + overrides) */
|
|
198
202
|
#merged: RawSettings = {};
|
|
203
|
+
/** Cached resolved values from the merged view, including defaults/path scoping */
|
|
204
|
+
#resolvedCache = new Map<SettingPath, unknown>();
|
|
199
205
|
|
|
200
206
|
/** Paths modified during this session (for partial save) */
|
|
201
207
|
#modified = new Set<string>();
|
|
@@ -282,13 +288,15 @@ export class Settings {
|
|
|
282
288
|
* Returns the merged value from global + project + overrides, or the default.
|
|
283
289
|
*/
|
|
284
290
|
get<P extends SettingPath>(path: P): SettingValue<P> {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (value !== undefined) {
|
|
288
|
-
const pathScopedValue = resolvePathScopedStringArray(path, value, this.#cwd);
|
|
289
|
-
return (pathScopedValue ?? value) as SettingValue<P>;
|
|
291
|
+
if (this.#resolvedCache.has(path)) {
|
|
292
|
+
return this.#resolvedCache.get(path) as SettingValue<P>;
|
|
290
293
|
}
|
|
291
|
-
|
|
294
|
+
|
|
295
|
+
const value = getByPath(this.#merged, SETTING_PATH_SEGMENTS[path]);
|
|
296
|
+
const resolved =
|
|
297
|
+
value !== undefined ? (resolvePathScopedStringArray(path, value, this.#cwd) ?? value) : getDefault(path);
|
|
298
|
+
this.#resolvedCache.set(path, resolved);
|
|
299
|
+
return resolved as SettingValue<P>;
|
|
292
300
|
}
|
|
293
301
|
|
|
294
302
|
/**
|
|
@@ -302,6 +310,7 @@ export class Settings {
|
|
|
302
310
|
setByPath(this.#global, segments, value);
|
|
303
311
|
this.#modified.add(path);
|
|
304
312
|
this.#rebuildMerged();
|
|
313
|
+
const next = this.get(path);
|
|
305
314
|
this.#queueSave();
|
|
306
315
|
|
|
307
316
|
// Trigger hook if exists
|
|
@@ -309,21 +318,25 @@ export class Settings {
|
|
|
309
318
|
if (hook) {
|
|
310
319
|
hook(value, prev);
|
|
311
320
|
}
|
|
321
|
+
this.#fireEffectiveSettingChanged(path, next, prev);
|
|
312
322
|
}
|
|
313
323
|
|
|
314
324
|
/**
|
|
315
325
|
* Apply runtime overrides (not persisted).
|
|
316
326
|
*/
|
|
317
327
|
override<P extends SettingPath>(path: P, value: SettingValue<P>): void {
|
|
328
|
+
const prev = this.get(path);
|
|
318
329
|
const segments = path.split(".");
|
|
319
330
|
setByPath(this.#overrides, segments, value);
|
|
320
331
|
this.#rebuildMerged();
|
|
332
|
+
this.#fireEffectiveSettingChanged(path, this.get(path), prev);
|
|
321
333
|
}
|
|
322
334
|
|
|
323
335
|
/**
|
|
324
336
|
* Clear a runtime override.
|
|
325
337
|
*/
|
|
326
338
|
clearOverride(path: SettingPath): void {
|
|
339
|
+
const prev = this.get(path);
|
|
327
340
|
const segments = path.split(".");
|
|
328
341
|
let current = this.#overrides;
|
|
329
342
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
@@ -333,6 +346,14 @@ export class Settings {
|
|
|
333
346
|
}
|
|
334
347
|
delete current[segments[segments.length - 1]];
|
|
335
348
|
this.#rebuildMerged();
|
|
349
|
+
this.#fireEffectiveSettingChanged(path, this.get(path), prev);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#fireEffectiveSettingChanged(path: SettingPath, value: unknown, prev: unknown): void {
|
|
353
|
+
if (Object.is(value, prev)) return;
|
|
354
|
+
if (path === "statusLine.sessionAccent") {
|
|
355
|
+
statusLineSessionAccentSignal.fire();
|
|
356
|
+
}
|
|
336
357
|
}
|
|
337
358
|
|
|
338
359
|
/**
|
|
@@ -842,6 +863,7 @@ export class Settings {
|
|
|
842
863
|
#rebuildMerged(): void {
|
|
843
864
|
this.#merged = this.#deepMerge(this.#deepMerge({}, this.#global), this.#project);
|
|
844
865
|
this.#merged = this.#deepMerge(this.#merged, this.#overrides);
|
|
866
|
+
this.#resolvedCache.clear();
|
|
845
867
|
}
|
|
846
868
|
|
|
847
869
|
#fireAllHooks(): void {
|
|
@@ -885,6 +907,45 @@ export class Settings {
|
|
|
885
907
|
|
|
886
908
|
type SettingHook<P extends SettingPath> = (value: SettingValue<P>, prev: SettingValue<P>) => void;
|
|
887
909
|
|
|
910
|
+
/**
|
|
911
|
+
* Minimal change-notification primitive backing the exported `on*Changed`
|
|
912
|
+
* subscriptions. Holds a listener set, hands out unsubscribe closures, and
|
|
913
|
+
* isolates errors so a single throwing listener can't abort the rest or bubble
|
|
914
|
+
* out of `Settings.set()`.
|
|
915
|
+
*
|
|
916
|
+
* @typeParam A - argument tuple forwarded to each listener on `fire`.
|
|
917
|
+
*/
|
|
918
|
+
class SettingSignal<A extends unknown[] = []> {
|
|
919
|
+
#listeners = new Set<(...args: A) => void>();
|
|
920
|
+
|
|
921
|
+
constructor(private readonly label: string) {}
|
|
922
|
+
|
|
923
|
+
/** Subscribe `cb`; returns an unsubscribe function. */
|
|
924
|
+
on(cb: (...args: A) => void): () => void {
|
|
925
|
+
this.#listeners.add(cb);
|
|
926
|
+
return () => {
|
|
927
|
+
this.#listeners.delete(cb);
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Invoke every listener with `args`. Iterates a snapshot so a listener may
|
|
933
|
+
* (un)subscribe mid-fire without re-entrancy — the Hindsight backend
|
|
934
|
+
* re-registers the fresh state's listener on every rebuild — and wraps each
|
|
935
|
+
* call so a throwing listener is logged and skipped instead of aborting the
|
|
936
|
+
* rest.
|
|
937
|
+
*/
|
|
938
|
+
fire(...args: A): void {
|
|
939
|
+
for (const cb of [...this.#listeners]) {
|
|
940
|
+
try {
|
|
941
|
+
cb(...args);
|
|
942
|
+
} catch (err) {
|
|
943
|
+
logger.warn(`Settings: ${this.label} hook failed`, { error: String(err) });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
888
949
|
const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
|
|
889
950
|
"theme.dark": value => {
|
|
890
951
|
if (typeof value === "string") {
|
|
@@ -917,45 +978,34 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
|
|
|
917
978
|
},
|
|
918
979
|
"provider.appendOnlyContext": value => {
|
|
919
980
|
if (typeof value === "string") {
|
|
920
|
-
|
|
981
|
+
appendOnlyModeSignal.fire(value);
|
|
921
982
|
}
|
|
922
983
|
},
|
|
923
|
-
"hindsight.bankId": () =>
|
|
924
|
-
"hindsight.bankIdPrefix": () =>
|
|
925
|
-
"hindsight.scoping": () =>
|
|
984
|
+
"hindsight.bankId": () => hindsightScopeSignal.fire(),
|
|
985
|
+
"hindsight.bankIdPrefix": () => hindsightScopeSignal.fire(),
|
|
986
|
+
"hindsight.scoping": () => hindsightScopeSignal.fire(),
|
|
926
987
|
};
|
|
927
|
-
/**
|
|
928
|
-
const
|
|
988
|
+
/** Fires when `provider.appendOnlyContext` changes at runtime. */
|
|
989
|
+
const appendOnlyModeSignal = new SettingSignal<[value: string]>("provider.appendOnlyContext");
|
|
929
990
|
|
|
930
991
|
/**
|
|
931
992
|
* Subscribe to append-only mode setting changes.
|
|
932
993
|
* Returns an unsubscribe function. Multiple sessions (main + subagents)
|
|
933
994
|
* can register independently without overwriting each other.
|
|
934
995
|
*/
|
|
935
|
-
export
|
|
936
|
-
appendOnlyModeCallbacks.add(cb);
|
|
937
|
-
return () => {
|
|
938
|
-
appendOnlyModeCallbacks.delete(cb);
|
|
939
|
-
};
|
|
940
|
-
}
|
|
996
|
+
export const onAppendOnlyModeChanged = (cb: (value: string) => void) => appendOnlyModeSignal.on(cb);
|
|
941
997
|
|
|
942
|
-
/**
|
|
943
|
-
const
|
|
998
|
+
/** Fires when `statusLine.sessionAccent` changes at runtime. */
|
|
999
|
+
const statusLineSessionAccentSignal = new SettingSignal("statusLine.sessionAccent");
|
|
944
1000
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
cb();
|
|
954
|
-
} catch (err) {
|
|
955
|
-
logger.warn("Settings: hindsight scope hook failed", { error: String(err) });
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Subscribe to session-accent setting changes.
|
|
1003
|
+
* Returns an unsubscribe function. Callers should re-read settings in the callback.
|
|
1004
|
+
*/
|
|
1005
|
+
export const onStatusLineSessionAccentChanged = (cb: () => void) => statusLineSessionAccentSignal.on(cb);
|
|
1006
|
+
|
|
1007
|
+
/** Fires when any `hindsight.bankId` / `bankIdPrefix` / `scoping` value changes. */
|
|
1008
|
+
const hindsightScopeSignal = new SettingSignal("hindsight scope");
|
|
959
1009
|
|
|
960
1010
|
/**
|
|
961
1011
|
* Subscribe to changes in the Hindsight bank-scoping settings. Lets the
|
|
@@ -967,12 +1017,7 @@ function fireHindsightScopeChanged(): void {
|
|
|
967
1017
|
* Returns an unsubscribe function. The callback receives no arguments — the
|
|
968
1018
|
* caller is expected to re-read the relevant settings via `Settings.get`.
|
|
969
1019
|
*/
|
|
970
|
-
export
|
|
971
|
-
hindsightScopeCallbacks.add(cb);
|
|
972
|
-
return () => {
|
|
973
|
-
hindsightScopeCallbacks.delete(cb);
|
|
974
|
-
};
|
|
975
|
-
}
|
|
1020
|
+
export const onHindsightScopeChanged = (cb: () => void) => hindsightScopeSignal.on(cb);
|
|
976
1021
|
|
|
977
1022
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
978
1023
|
// Global Singleton
|