@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.1
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 +41 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +71 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +5 -6
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +79 -45
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +14 -87
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +102 -114
- package/src/prompts/tools/read.md +1 -0
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +27 -4
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +3 -1
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +7 -6
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +516 -233
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +44 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
5
|
+
import { Value } from "@sinclair/typebox/value";
|
|
6
|
+
import type { ErrorObject } from "ajv";
|
|
7
|
+
import { JSONC, YAML } from "bun";
|
|
8
|
+
|
|
9
|
+
function migrateJsonToYml(jsonPath: string, ymlPath: string) {
|
|
10
|
+
try {
|
|
11
|
+
if (fs.existsSync(ymlPath)) return;
|
|
12
|
+
if (!fs.existsSync(jsonPath)) return;
|
|
13
|
+
|
|
14
|
+
const content = fs.readFileSync(jsonPath, "utf-8");
|
|
15
|
+
const parsed = JSON.parse(content);
|
|
16
|
+
if (!parsed) {
|
|
17
|
+
logger.warn("migrateJsonToYml: invalid json structure", { path: jsonPath });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
fs.writeFileSync(ymlPath, YAML.stringify(parsed, null, 2));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
logger.warn("migrateJsonToYml: migration failed", { error: String(error) });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface IConfigFile<T> {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly schema: TSchema;
|
|
29
|
+
path?(): string;
|
|
30
|
+
load(): T | null;
|
|
31
|
+
invalidate?(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class ConfigError extends Error {
|
|
35
|
+
readonly #message: string;
|
|
36
|
+
constructor(
|
|
37
|
+
public readonly id: string,
|
|
38
|
+
public readonly schemaErrors: ErrorObject[] | null | undefined,
|
|
39
|
+
public readonly other?: { err: unknown; stage: string },
|
|
40
|
+
) {
|
|
41
|
+
let messages: string[] | undefined;
|
|
42
|
+
let cause: Error | undefined;
|
|
43
|
+
let klass: string;
|
|
44
|
+
|
|
45
|
+
if (schemaErrors) {
|
|
46
|
+
klass = "Schema";
|
|
47
|
+
messages = schemaErrors.map(e => `${e.instancePath || "root"}: ${e.message}`);
|
|
48
|
+
} else if (other) {
|
|
49
|
+
klass = other.stage;
|
|
50
|
+
if (other.err instanceof Error) {
|
|
51
|
+
messages = [other.err.message];
|
|
52
|
+
cause = other.err;
|
|
53
|
+
} else {
|
|
54
|
+
messages = [String(other.err)];
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
klass = "Unknown";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const title = `Failed to load config file ${id}, ${klass} error:`;
|
|
61
|
+
let message: string;
|
|
62
|
+
switch (messages?.length ?? 0) {
|
|
63
|
+
case 0:
|
|
64
|
+
message = title.slice(0, -1);
|
|
65
|
+
break;
|
|
66
|
+
case 1:
|
|
67
|
+
message = `${title} ${messages![0]}`;
|
|
68
|
+
break;
|
|
69
|
+
default:
|
|
70
|
+
message = `${title}\n${messages!.map(m => ` - ${m}`).join("\n")}`;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
super(message, { cause });
|
|
75
|
+
this.name = "LoadError";
|
|
76
|
+
this.#message = message;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get message(): string {
|
|
80
|
+
return this.#message;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toString(): string {
|
|
84
|
+
return this.message;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type LoadStatus = "ok" | "error" | "not-found";
|
|
89
|
+
|
|
90
|
+
export type LoadResult<T> =
|
|
91
|
+
| { value?: null; error: ConfigError; status: "error" }
|
|
92
|
+
| { value: T; error?: undefined; status: "ok" }
|
|
93
|
+
| { value?: null; error?: unknown; status: "not-found" };
|
|
94
|
+
|
|
95
|
+
export class ConfigFile<T> implements IConfigFile<T> {
|
|
96
|
+
readonly #basePath: string;
|
|
97
|
+
#cache?: LoadResult<T>;
|
|
98
|
+
#auxValidate?: (value: T) => void;
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
readonly id: string,
|
|
102
|
+
readonly schema: TSchema,
|
|
103
|
+
configPath: string = path.join(getAgentDir(), `${id}.yml`),
|
|
104
|
+
) {
|
|
105
|
+
this.#basePath = configPath;
|
|
106
|
+
if (configPath.endsWith(".yml")) {
|
|
107
|
+
const jsonPath = `${configPath.slice(0, -4)}.json`;
|
|
108
|
+
migrateJsonToYml(jsonPath, configPath);
|
|
109
|
+
} else if (configPath.endsWith(".yaml")) {
|
|
110
|
+
const jsonPath = `${configPath.slice(0, -5)}.json`;
|
|
111
|
+
migrateJsonToYml(jsonPath, configPath);
|
|
112
|
+
} else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
|
|
113
|
+
// JSON configs are still supported without migration.
|
|
114
|
+
} else {
|
|
115
|
+
throw new Error(`Invalid config file path: ${configPath}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
relocate(configPath?: string): ConfigFile<T> {
|
|
120
|
+
if (!configPath || configPath === this.#basePath) return this;
|
|
121
|
+
const result = new ConfigFile<T>(this.id, this.schema, configPath);
|
|
122
|
+
result.#auxValidate = this.#auxValidate;
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getMtimeMs(): number | null {
|
|
127
|
+
try {
|
|
128
|
+
return fs.statSync(this.path()).mtimeMs;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (isEnoent(err)) return null;
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
withValidation(name: string, validate: (value: T) => void): this {
|
|
136
|
+
const prev = this.#auxValidate;
|
|
137
|
+
this.#auxValidate = (value: T) => {
|
|
138
|
+
prev?.(value);
|
|
139
|
+
try {
|
|
140
|
+
validate(value);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new ConfigError(this.id, undefined, { err: error, stage: `Validate(${name})` });
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
createDefault(): T {
|
|
149
|
+
return Value.Default(this.schema, [], undefined) as T;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#storeCache(result: LoadResult<T>): LoadResult<T> {
|
|
153
|
+
this.#cache = result;
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
tryLoad(): LoadResult<T> {
|
|
158
|
+
if (this.#cache) return this.#cache;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const content = fs.readFileSync(this.path(), "utf-8").trim();
|
|
162
|
+
|
|
163
|
+
let parsed: unknown;
|
|
164
|
+
if (this.#basePath.endsWith(".json") || this.#basePath.endsWith(".jsonc")) {
|
|
165
|
+
parsed = JSONC.parse(content);
|
|
166
|
+
} else if (this.#basePath.endsWith(".yml") || this.#basePath.endsWith(".yaml")) {
|
|
167
|
+
parsed = YAML.parse(content);
|
|
168
|
+
} else {
|
|
169
|
+
throw new Error(`Invalid config file path: ${this.#basePath}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!Value.Check(this.schema, parsed)) {
|
|
173
|
+
const schemaErrors: ErrorObject[] = [];
|
|
174
|
+
for (const err of Value.Errors(this.schema, parsed)) {
|
|
175
|
+
schemaErrors.push({ instancePath: err.path, message: err.message } as ErrorObject);
|
|
176
|
+
if (schemaErrors.length >= 50) break;
|
|
177
|
+
}
|
|
178
|
+
const error = new ConfigError(this.id, schemaErrors);
|
|
179
|
+
logger.warn("Failed to parse config file", { path: this.path(), error });
|
|
180
|
+
return this.#storeCache({ error, status: "error" });
|
|
181
|
+
}
|
|
182
|
+
return this.#storeCache({ value: parsed as T, status: "ok" });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (isEnoent(error)) {
|
|
185
|
+
return this.#storeCache({ status: "not-found" });
|
|
186
|
+
}
|
|
187
|
+
logger.warn("Failed to parse config file", { path: this.path(), error });
|
|
188
|
+
return this.#storeCache({
|
|
189
|
+
error: new ConfigError(this.id, undefined, { err: error, stage: "Unexpected" }),
|
|
190
|
+
status: "error",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
load(): T | null {
|
|
196
|
+
return this.tryLoad().value ?? null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
loadOrDefault(): T {
|
|
200
|
+
return this.tryLoad().value ?? this.createDefault();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
path(): string {
|
|
204
|
+
return this.#basePath;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
invalidate() {
|
|
208
|
+
this.#cache = undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -72,15 +72,12 @@ const TRAILING_MARKER_SUFFIXES: readonly string[] = (() => {
|
|
|
72
72
|
})();
|
|
73
73
|
const WRAPPER_PREFIXES = ["duo-chat-"] as const;
|
|
74
74
|
|
|
75
|
-
let
|
|
75
|
+
let referenceDataCache: CanonicalReferenceData | undefined;
|
|
76
76
|
const EMPTY_COMPILED_EQUIVALENCE: CompiledEquivalenceConfig = {
|
|
77
77
|
overrides: new Map<string, string>(),
|
|
78
78
|
exclude: new Set<string>(),
|
|
79
79
|
};
|
|
80
|
-
const
|
|
81
|
-
CompiledEquivalenceConfig,
|
|
82
|
-
WeakMap<Model<Api>, ResolvedCanonicalModel>
|
|
83
|
-
> = new WeakMap();
|
|
80
|
+
const resolutionCache: WeakMap<CompiledEquivalenceConfig, WeakMap<Model<Api>, ResolvedCanonicalModel>> = new WeakMap();
|
|
84
81
|
const FAMILY_EXTRACTION_PATTERNS = [
|
|
85
82
|
/(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+)(?::|$)/i,
|
|
86
83
|
/(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+(?:[-_/][a-z0-9.]+)*)(?::|$)/i,
|
|
@@ -98,8 +95,8 @@ function shouldReplaceReference(existing: Model<Api> | undefined, candidate: Mod
|
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
function createCanonicalReferenceData(): CanonicalReferenceData {
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
98
|
+
if (referenceDataCache) {
|
|
99
|
+
return referenceDataCache;
|
|
103
100
|
}
|
|
104
101
|
const references = new Map<string, Model<Api>>();
|
|
105
102
|
for (const provider of getBundledProviders()) {
|
|
@@ -112,11 +109,11 @@ function createCanonicalReferenceData(): CanonicalReferenceData {
|
|
|
112
109
|
}
|
|
113
110
|
}
|
|
114
111
|
const officialIds = new Set(references.keys());
|
|
115
|
-
|
|
112
|
+
referenceDataCache = {
|
|
116
113
|
references: Object.freeze(references) as Map<string, Model<Api>>,
|
|
117
114
|
officialIds: Object.freeze(officialIds) as Set<string>,
|
|
118
115
|
};
|
|
119
|
-
return
|
|
116
|
+
return referenceDataCache;
|
|
120
117
|
}
|
|
121
118
|
|
|
122
119
|
function normalizeSelectorKey(selector: string): string {
|
|
@@ -668,10 +665,10 @@ export function buildCanonicalModelIndex(
|
|
|
668
665
|
const byId = new Map<string, CanonicalModelRecord>();
|
|
669
666
|
const bySelector = new Map<string, string>();
|
|
670
667
|
|
|
671
|
-
let modelCache =
|
|
668
|
+
let modelCache = resolutionCache.get(compiledEquivalence);
|
|
672
669
|
if (!modelCache) {
|
|
673
670
|
modelCache = new WeakMap<Model<Api>, ResolvedCanonicalModel>();
|
|
674
|
-
|
|
671
|
+
resolutionCache.set(compiledEquivalence, modelCache);
|
|
675
672
|
}
|
|
676
673
|
|
|
677
674
|
for (const model of models) {
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
registerCustomApi,
|
|
19
19
|
type SimpleStreamOptions,
|
|
20
20
|
type ThinkingConfig,
|
|
21
|
+
UNK_CONTEXT_WINDOW,
|
|
22
|
+
UNK_MAX_TOKENS,
|
|
21
23
|
unregisterCustomApis,
|
|
22
24
|
} from "@oh-my-pi/pi-ai";
|
|
23
25
|
|
|
@@ -29,10 +31,10 @@ import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai
|
|
|
29
31
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
30
32
|
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
31
33
|
import { type Static, Type } from "@sinclair/typebox";
|
|
32
|
-
import { type ConfigError, ConfigFile } from "../config";
|
|
33
34
|
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
34
35
|
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
35
36
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
37
|
+
import { type ConfigError, ConfigFile } from "./config-file";
|
|
36
38
|
import {
|
|
37
39
|
buildCanonicalModelIndex,
|
|
38
40
|
type CanonicalModelIndex,
|
|
@@ -1053,7 +1055,16 @@ export class ModelRegistry {
|
|
|
1053
1055
|
const key = `${replacementModel.provider}\u0000${replacementModel.id}`;
|
|
1054
1056
|
const existingIndex = indexByKey.get(key);
|
|
1055
1057
|
if (existingIndex !== undefined) {
|
|
1056
|
-
merged[existingIndex]
|
|
1058
|
+
const existing = merged[existingIndex];
|
|
1059
|
+
merged[existingIndex] = {
|
|
1060
|
+
...replacementModel,
|
|
1061
|
+
contextWindow:
|
|
1062
|
+
replacementModel.contextWindow === UNK_CONTEXT_WINDOW
|
|
1063
|
+
? existing.contextWindow
|
|
1064
|
+
: replacementModel.contextWindow,
|
|
1065
|
+
maxTokens:
|
|
1066
|
+
replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
|
|
1067
|
+
};
|
|
1057
1068
|
} else {
|
|
1058
1069
|
merged.push(replacementModel);
|
|
1059
1070
|
indexByKey.set(key, merged.length - 1);
|
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
type Model,
|
|
13
13
|
modelsAreEqual,
|
|
14
14
|
} from "@oh-my-pi/pi-ai";
|
|
15
|
+
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
15
16
|
import chalk from "chalk";
|
|
16
17
|
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
17
18
|
import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
|
|
18
|
-
import { fuzzyMatch } from "../utils/fuzzy";
|
|
19
19
|
import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
|
|
20
20
|
import type { Settings } from "./settings";
|
|
21
21
|
|
|
@@ -607,9 +607,6 @@ export function resolveModelRoleValue(
|
|
|
607
607
|
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
-
const lastColonIndex = normalized.lastIndexOf(":");
|
|
611
|
-
const _thinkingSelector =
|
|
612
|
-
lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
|
|
613
610
|
const effectivePatterns = resolveConfiguredRolePattern(normalized, options?.settings);
|
|
614
611
|
if (!effectivePatterns || effectivePatterns.length === 0) {
|
|
615
612
|
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
@@ -460,6 +460,46 @@ export const SETTINGS_SCHEMA = {
|
|
|
460
460
|
],
|
|
461
461
|
},
|
|
462
462
|
},
|
|
463
|
+
"tools.artifactHeadBytes": {
|
|
464
|
+
type: "number",
|
|
465
|
+
default: 20,
|
|
466
|
+
ui: {
|
|
467
|
+
tab: "tools",
|
|
468
|
+
label: "Artifact head size (KB)",
|
|
469
|
+
description:
|
|
470
|
+
"Amount of head content kept inline alongside the tail when output spills to artifact (middle elision). 0 disables — keep tail only.",
|
|
471
|
+
options: [
|
|
472
|
+
{ value: "0", label: "0 KB", description: "Disabled; tail-only truncation" },
|
|
473
|
+
{ value: "1", label: "1 KB", description: "~250 tokens" },
|
|
474
|
+
{ value: "2.5", label: "2.5 KB", description: "~625 tokens" },
|
|
475
|
+
{ value: "5", label: "5 KB", description: "~1.25K tokens" },
|
|
476
|
+
{ value: "10", label: "10 KB", description: "~2.5K tokens" },
|
|
477
|
+
{ value: "20", label: "20 KB", description: "Default; ~5K tokens" },
|
|
478
|
+
{ value: "50", label: "50 KB", description: "~12.5K tokens" },
|
|
479
|
+
{ value: "100", label: "100 KB", description: "~25K tokens" },
|
|
480
|
+
{ value: "200", label: "200 KB", description: "~50K tokens" },
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
"tools.outputMaxColumns": {
|
|
485
|
+
type: "number",
|
|
486
|
+
default: 768,
|
|
487
|
+
ui: {
|
|
488
|
+
tab: "tools",
|
|
489
|
+
label: "Output column cap",
|
|
490
|
+
description:
|
|
491
|
+
"Per-line byte cap for streaming tool outputs (bash, ssh, python, js eval) and `read`. Lines wider than this are ellipsis-truncated; remaining bytes up to the next newline are dropped. 0 disables.",
|
|
492
|
+
options: [
|
|
493
|
+
{ value: "0", label: "Off", description: "No per-line cap" },
|
|
494
|
+
{ value: "256", label: "256", description: "Tight" },
|
|
495
|
+
{ value: "512", label: "512" },
|
|
496
|
+
{ value: "768", label: "768", description: "Default" },
|
|
497
|
+
{ value: "1024", label: "1024" },
|
|
498
|
+
{ value: "2048", label: "2048" },
|
|
499
|
+
{ value: "4096", label: "4096", description: "Loose" },
|
|
500
|
+
],
|
|
501
|
+
},
|
|
502
|
+
},
|
|
463
503
|
"tools.artifactTailLines": {
|
|
464
504
|
type: "number",
|
|
465
505
|
default: 500,
|
|
@@ -1498,7 +1538,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1498
1538
|
|
|
1499
1539
|
"read.defaultLimit": {
|
|
1500
1540
|
type: "number",
|
|
1501
|
-
default:
|
|
1541
|
+
default: 300,
|
|
1502
1542
|
ui: {
|
|
1503
1543
|
tab: "editing",
|
|
1504
1544
|
label: "Default Read Limit",
|
|
@@ -2095,6 +2135,36 @@ export const SETTINGS_SCHEMA = {
|
|
|
2095
2135
|
},
|
|
2096
2136
|
},
|
|
2097
2137
|
|
|
2138
|
+
"goal.enabled": {
|
|
2139
|
+
type: "boolean",
|
|
2140
|
+
default: true,
|
|
2141
|
+
ui: {
|
|
2142
|
+
tab: "tasks",
|
|
2143
|
+
label: "Goal Mode",
|
|
2144
|
+
description: "Enable per-session goal mode and the hidden goal tool",
|
|
2145
|
+
},
|
|
2146
|
+
},
|
|
2147
|
+
|
|
2148
|
+
"goal.statusInFooter": {
|
|
2149
|
+
type: "boolean",
|
|
2150
|
+
default: true,
|
|
2151
|
+
ui: {
|
|
2152
|
+
tab: "tasks",
|
|
2153
|
+
label: "Goal Status In Footer",
|
|
2154
|
+
description: "Show token budget alongside the goal indicator in the status line",
|
|
2155
|
+
},
|
|
2156
|
+
},
|
|
2157
|
+
|
|
2158
|
+
"goal.continuationModes": {
|
|
2159
|
+
type: "array",
|
|
2160
|
+
default: ["interactive"],
|
|
2161
|
+
ui: {
|
|
2162
|
+
tab: "tasks",
|
|
2163
|
+
label: "Goal Continuation Modes",
|
|
2164
|
+
description: "Run modes where active goals may auto-continue between turns",
|
|
2165
|
+
},
|
|
2166
|
+
},
|
|
2167
|
+
|
|
2098
2168
|
// Delegation
|
|
2099
2169
|
"task.isolation.mode": {
|
|
2100
2170
|
type: "enum",
|
package/src/config/settings.ts
CHANGED
|
@@ -850,7 +850,7 @@ export function isSettingsInitialized(): boolean {
|
|
|
850
850
|
* Reset the global singleton for testing.
|
|
851
851
|
* @internal
|
|
852
852
|
*/
|
|
853
|
-
export function
|
|
853
|
+
export function resetSettingsForTest(): void {
|
|
854
854
|
globalInstance = null;
|
|
855
855
|
globalInstancePromise = null;
|
|
856
856
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
CONFIG_DIR_NAME,
|
|
6
|
-
getAgentDir,
|
|
7
|
-
getConfigAgentDirName,
|
|
8
|
-
getProjectDir,
|
|
9
|
-
isEnoent,
|
|
10
|
-
logger,
|
|
11
|
-
} from "@oh-my-pi/pi-utils";
|
|
12
|
-
import type { TSchema } from "@sinclair/typebox";
|
|
13
|
-
import { Value } from "@sinclair/typebox/value";
|
|
14
|
-
import type { ErrorObject } from "ajv";
|
|
15
|
-
import { JSONC, YAML } from "bun";
|
|
4
|
+
import { CONFIG_DIR_NAME, getConfigAgentDirName, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
16
5
|
import { expandTilde } from "./tools/path-utils";
|
|
17
6
|
|
|
7
|
+
export * from "./config/config-file";
|
|
8
|
+
|
|
18
9
|
const priorityList = [
|
|
19
10
|
{ dir: CONFIG_DIR_NAME, globalAgentDir: getConfigAgentDirName },
|
|
20
11
|
{ dir: ".claude" },
|
|
@@ -53,213 +44,6 @@ export function getChangelogPath(): string {
|
|
|
53
44
|
return path.resolve(path.join(getPackageDir(), "CHANGELOG.md"));
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
// =============================================================================
|
|
57
|
-
// User Config Paths (~/.omp/agent/*)
|
|
58
|
-
// =============================================================================
|
|
59
|
-
|
|
60
|
-
function migrateJsonToYml(jsonPath: string, ymlPath: string) {
|
|
61
|
-
try {
|
|
62
|
-
if (fs.existsSync(ymlPath)) return;
|
|
63
|
-
if (!fs.existsSync(jsonPath)) return;
|
|
64
|
-
|
|
65
|
-
const content = fs.readFileSync(jsonPath, "utf-8");
|
|
66
|
-
const parsed = JSON.parse(content);
|
|
67
|
-
if (!parsed) {
|
|
68
|
-
logger.warn("migrateJsonToYml: invalid json structure", { path: jsonPath });
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
fs.writeFileSync(ymlPath, YAML.stringify(parsed, null, 2));
|
|
72
|
-
} catch (error) {
|
|
73
|
-
logger.warn("migrateJsonToYml: migration failed", { error: String(error) });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface IConfigFile<T> {
|
|
78
|
-
readonly id: string;
|
|
79
|
-
readonly schema: TSchema;
|
|
80
|
-
path?(): string;
|
|
81
|
-
load(): T | null;
|
|
82
|
-
invalidate?(): void;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export class ConfigError extends Error {
|
|
86
|
-
readonly #message: string;
|
|
87
|
-
constructor(
|
|
88
|
-
public readonly id: string,
|
|
89
|
-
public readonly schemaErrors: ErrorObject[] | null | undefined,
|
|
90
|
-
public readonly other?: { err: unknown; stage: string },
|
|
91
|
-
) {
|
|
92
|
-
let messages: string[] | undefined;
|
|
93
|
-
let cause: any | undefined;
|
|
94
|
-
let klass: string;
|
|
95
|
-
|
|
96
|
-
if (schemaErrors) {
|
|
97
|
-
klass = "Schema";
|
|
98
|
-
messages = schemaErrors.map(e => `${e.instancePath || "root"}: ${e.message}`);
|
|
99
|
-
} else if (other) {
|
|
100
|
-
klass = other.stage;
|
|
101
|
-
if (other.err instanceof Error) {
|
|
102
|
-
messages = [other.err.message];
|
|
103
|
-
cause = other.err;
|
|
104
|
-
} else {
|
|
105
|
-
messages = [String(other.err)];
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
klass = "Unknown";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const title = `Failed to load config file ${id}, ${klass} error:`;
|
|
112
|
-
let message: string;
|
|
113
|
-
switch (messages?.length ?? 0) {
|
|
114
|
-
case 0:
|
|
115
|
-
message = title.slice(0, -1);
|
|
116
|
-
break;
|
|
117
|
-
case 1:
|
|
118
|
-
message = `${title} ${messages![0]}`;
|
|
119
|
-
break;
|
|
120
|
-
default:
|
|
121
|
-
message = `${title}\n${messages!.map(m => ` - ${m}`).join("\n")}`;
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
super(message, { cause });
|
|
126
|
-
this.name = "LoadError";
|
|
127
|
-
this.#message = message;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
get message(): string {
|
|
131
|
-
return this.#message;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
toString(): string {
|
|
135
|
-
return this.message;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export type LoadStatus = "ok" | "error" | "not-found";
|
|
140
|
-
|
|
141
|
-
export type LoadResult<T> =
|
|
142
|
-
| { value?: null; error: ConfigError; status: "error" }
|
|
143
|
-
| { value: T; error?: undefined; status: "ok" }
|
|
144
|
-
| { value?: null; error?: unknown; status: "not-found" };
|
|
145
|
-
|
|
146
|
-
export class ConfigFile<T> implements IConfigFile<T> {
|
|
147
|
-
readonly #basePath: string;
|
|
148
|
-
#cache?: LoadResult<T>;
|
|
149
|
-
#auxValidate?: (value: T) => void;
|
|
150
|
-
|
|
151
|
-
constructor(
|
|
152
|
-
readonly id: string,
|
|
153
|
-
readonly schema: TSchema,
|
|
154
|
-
configPath: string = path.join(getAgentDir(), `${id}.yml`),
|
|
155
|
-
) {
|
|
156
|
-
this.#basePath = configPath;
|
|
157
|
-
if (configPath.endsWith(".yml")) {
|
|
158
|
-
const jsonPath = `${configPath.slice(0, -4)}.json`;
|
|
159
|
-
migrateJsonToYml(jsonPath, configPath);
|
|
160
|
-
} else if (configPath.endsWith(".yaml")) {
|
|
161
|
-
const jsonPath = `${configPath.slice(0, -5)}.json`;
|
|
162
|
-
migrateJsonToYml(jsonPath, configPath);
|
|
163
|
-
} else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
|
|
164
|
-
// JSON configs are still supported without migration.
|
|
165
|
-
} else {
|
|
166
|
-
throw new Error(`Invalid config file path: ${configPath}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
relocate(path?: string): ConfigFile<T> {
|
|
171
|
-
if (!path || path === this.#basePath) return this;
|
|
172
|
-
const result = new ConfigFile<T>(this.id, this.schema, path);
|
|
173
|
-
result.#auxValidate = this.#auxValidate;
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
getMtimeMs(): number | null {
|
|
178
|
-
try {
|
|
179
|
-
return fs.statSync(this.path()).mtimeMs;
|
|
180
|
-
} catch (err) {
|
|
181
|
-
if (isEnoent(err)) return null;
|
|
182
|
-
throw err;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
withValidation(name: string, validate: (value: T) => void): this {
|
|
187
|
-
const prev = this.#auxValidate;
|
|
188
|
-
this.#auxValidate = (value: T) => {
|
|
189
|
-
prev?.(value);
|
|
190
|
-
try {
|
|
191
|
-
validate(value);
|
|
192
|
-
} catch (error) {
|
|
193
|
-
throw new ConfigError(this.id, undefined, { err: error, stage: `Validate(${name})` });
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
createDefault() {
|
|
200
|
-
return Value.Default(this.schema, [], undefined) as T;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
#storeCache(result: LoadResult<T>): LoadResult<T> {
|
|
204
|
-
this.#cache = result;
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
tryLoad(): LoadResult<T> {
|
|
209
|
-
if (this.#cache) return this.#cache;
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const content = fs.readFileSync(this.path(), "utf-8").trim();
|
|
213
|
-
|
|
214
|
-
let parsed: unknown;
|
|
215
|
-
if (this.#basePath.endsWith(".json") || this.#basePath.endsWith(".jsonc")) {
|
|
216
|
-
parsed = JSONC.parse(content);
|
|
217
|
-
} else if (this.#basePath.endsWith(".yml") || this.#basePath.endsWith(".yaml")) {
|
|
218
|
-
parsed = YAML.parse(content);
|
|
219
|
-
} else {
|
|
220
|
-
throw new Error(`Invalid config file path: ${this.#basePath}`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!Value.Check(this.schema, parsed)) {
|
|
224
|
-
const schemaErrors: ErrorObject[] = [];
|
|
225
|
-
for (const err of Value.Errors(this.schema, parsed)) {
|
|
226
|
-
schemaErrors.push({ instancePath: err.path, message: err.message } as ErrorObject);
|
|
227
|
-
if (schemaErrors.length >= 50) break;
|
|
228
|
-
}
|
|
229
|
-
const error = new ConfigError(this.id, schemaErrors);
|
|
230
|
-
logger.warn("Failed to parse config file", { path: this.path(), error });
|
|
231
|
-
return this.#storeCache({ error, status: "error" });
|
|
232
|
-
}
|
|
233
|
-
return this.#storeCache({ value: parsed as T, status: "ok" });
|
|
234
|
-
} catch (error) {
|
|
235
|
-
if (isEnoent(error)) {
|
|
236
|
-
return this.#storeCache({ status: "not-found" });
|
|
237
|
-
}
|
|
238
|
-
logger.warn("Failed to parse config file", { path: this.path(), error });
|
|
239
|
-
return this.#storeCache({
|
|
240
|
-
error: new ConfigError(this.id, undefined, { err: error, stage: "Unexpected" }),
|
|
241
|
-
status: "error",
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
load(): T | null {
|
|
247
|
-
return this.tryLoad().value ?? null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
loadOrDefault(): T {
|
|
251
|
-
return this.tryLoad().value ?? this.createDefault();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
path(): string {
|
|
255
|
-
return this.#basePath;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
invalidate() {
|
|
259
|
-
this.#cache = undefined;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
47
|
// =============================================================================
|
|
264
48
|
// Multi-Config Directory Helpers
|
|
265
49
|
// =============================================================================
|
package/src/edit/renderer.ts
CHANGED
|
@@ -340,7 +340,13 @@ function normalizeHashlineInputPreviewPath(rawPath: string): string {
|
|
|
340
340
|
|
|
341
341
|
function parseHashlineInputPreviewHeader(line: string): string | null {
|
|
342
342
|
if (!line.startsWith(HL_INPUT_HEADER_PREFIX)) return null;
|
|
343
|
-
|
|
343
|
+
// The real parser (`parseHashlineHeaderLine` in `hashline/input.ts`) strips
|
|
344
|
+
// every leading "@" before resolving the path so canonical "@@ PATH" headers
|
|
345
|
+
// (and stray "@ PATH" / "@@@ PATH" runs) all route to the same file. Mirror
|
|
346
|
+
// that here so the renderer doesn't surface a literal "@ " in the title.
|
|
347
|
+
let prefixEnd = 0;
|
|
348
|
+
while (prefixEnd < line.length && line[prefixEnd] === HL_INPUT_HEADER_PREFIX) prefixEnd++;
|
|
349
|
+
const body = line.slice(prefixEnd).trim();
|
|
344
350
|
const previewPath = normalizeHashlineInputPreviewPath(body);
|
|
345
351
|
return previewPath.length > 0 ? previewPath : null;
|
|
346
352
|
}
|
package/src/eval/js/executor.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_MAX_BYTES, OutputSink } from "../../session/streaming-output";
|
|
2
2
|
import type { ToolSession } from "../../tools";
|
|
3
|
+
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
|
|
3
4
|
import { executeInVmContext, type JsDisplayOutput } from "./context-manager";
|
|
4
5
|
|
|
5
6
|
export interface JsExecutorOptions {
|
|
@@ -49,6 +50,8 @@ export async function executeJs(code: string, options: JsExecutorOptions): Promi
|
|
|
49
50
|
artifactPath: options.artifactPath,
|
|
50
51
|
artifactId: options.artifactId,
|
|
51
52
|
spillThreshold: DEFAULT_MAX_BYTES,
|
|
53
|
+
headBytes: resolveOutputSinkHeadBytes(options.session.settings),
|
|
54
|
+
maxColumns: resolveOutputMaxColumns(options.session.settings),
|
|
52
55
|
onChunk: chunk => options.onChunk?.(chunk),
|
|
53
56
|
});
|
|
54
57
|
const timeoutMs = getExecutionTimeoutMs(options);
|