@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +10 -10
  4. package/scripts/build-binary.ts +5 -0
  5. package/src/autoresearch/helpers.ts +17 -0
  6. package/src/autoresearch/tools/log-experiment.ts +9 -17
  7. package/src/autoresearch/tools/run-experiment.ts +2 -17
  8. package/src/capability/skill.ts +7 -0
  9. package/src/cli/list-models.ts +1 -1
  10. package/src/cli/shell-cli.ts +3 -13
  11. package/src/cli/update-cli.ts +1 -1
  12. package/src/cli.ts +10 -29
  13. package/src/commands/commit.ts +10 -0
  14. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  15. package/src/commit/analysis/conventional.ts +8 -66
  16. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  17. package/src/commit/pipeline.ts +2 -2
  18. package/src/commit/shared-llm.ts +89 -0
  19. package/src/config/config-file.ts +210 -0
  20. package/src/config/model-equivalence.ts +8 -11
  21. package/src/config/model-registry.ts +44 -3
  22. package/src/config/model-resolver.ts +1 -4
  23. package/src/config/settings-schema.ts +82 -1
  24. package/src/config/settings.ts +1 -1
  25. package/src/config.ts +3 -219
  26. package/src/discovery/claude-plugins.ts +19 -7
  27. package/src/edit/renderer.ts +7 -1
  28. package/src/eval/js/executor.ts +3 -0
  29. package/src/eval/js/shared/rewrite-imports.ts +2 -2
  30. package/src/eval/py/executor.ts +5 -0
  31. package/src/eval/py/runner.py +42 -11
  32. package/src/eval/py/runtime.ts +1 -0
  33. package/src/exa/factory.ts +2 -2
  34. package/src/exa/mcp-client.ts +74 -1
  35. package/src/exec/bash-executor.ts +5 -1
  36. package/src/export/html/template.generated.ts +1 -1
  37. package/src/export/html/template.js +0 -11
  38. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  39. package/src/extensibility/extensions/runner.ts +1 -1
  40. package/src/extensibility/extensions/types.ts +89 -223
  41. package/src/extensibility/hooks/types.ts +89 -314
  42. package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
  43. package/src/extensibility/shared-events.ts +343 -0
  44. package/src/extensibility/skills.ts +9 -0
  45. package/src/goals/index.ts +3 -0
  46. package/src/goals/runtime.ts +500 -0
  47. package/src/goals/state.ts +37 -0
  48. package/src/goals/tools/goal-tool.ts +237 -0
  49. package/src/hashline/anchors.ts +2 -2
  50. package/src/hashline/input.ts +2 -1
  51. package/src/hashline/parser.ts +27 -3
  52. package/src/hindsight/mental-models.ts +1 -1
  53. package/src/internal-urls/agent-protocol.ts +1 -20
  54. package/src/internal-urls/artifact-protocol.ts +1 -19
  55. package/src/internal-urls/docs-index.generated.ts +11 -12
  56. package/src/internal-urls/registry-helpers.ts +25 -0
  57. package/src/internal-urls/router.ts +8 -0
  58. package/src/internal-urls/types.ts +21 -0
  59. package/src/lsp/config.ts +15 -6
  60. package/src/lsp/defaults.json +6 -2
  61. package/src/main.ts +11 -2
  62. package/src/mcp/oauth-flow.ts +20 -0
  63. package/src/modes/acp/acp-agent.ts +327 -95
  64. package/src/modes/components/assistant-message.ts +14 -8
  65. package/src/modes/components/bash-execution.ts +24 -63
  66. package/src/modes/components/custom-message.ts +14 -40
  67. package/src/modes/components/eval-execution.ts +27 -57
  68. package/src/modes/components/execution-shared.ts +102 -0
  69. package/src/modes/components/hook-message.ts +17 -49
  70. package/src/modes/components/mcp-add-wizard.ts +26 -5
  71. package/src/modes/components/message-frame.ts +88 -0
  72. package/src/modes/components/model-selector.ts +1 -1
  73. package/src/modes/components/session-observer-overlay.ts +6 -2
  74. package/src/modes/components/session-selector.ts +1 -1
  75. package/src/modes/components/status-line/segments.ts +93 -8
  76. package/src/modes/components/status-line/types.ts +4 -0
  77. package/src/modes/components/status-line.ts +28 -10
  78. package/src/modes/components/tool-execution.ts +7 -8
  79. package/src/modes/controllers/command-controller-shared.ts +108 -0
  80. package/src/modes/controllers/command-controller.ts +13 -4
  81. package/src/modes/controllers/event-controller.ts +36 -7
  82. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  83. package/src/modes/controllers/input-controller.ts +13 -0
  84. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  85. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  86. package/src/modes/interactive-mode.ts +624 -52
  87. package/src/modes/print-mode.ts +16 -86
  88. package/src/modes/rpc/host-uris.ts +235 -0
  89. package/src/modes/rpc/rpc-mode.ts +41 -88
  90. package/src/modes/rpc/rpc-types.ts +57 -0
  91. package/src/modes/runtime-init.ts +116 -0
  92. package/src/modes/theme/defaults/dark-poimandres.json +3 -0
  93. package/src/modes/theme/defaults/light-poimandres.json +3 -0
  94. package/src/modes/theme/theme.ts +24 -6
  95. package/src/modes/types.ts +14 -3
  96. package/src/modes/utils/context-usage.ts +13 -13
  97. package/src/modes/utils/ui-helpers.ts +10 -3
  98. package/src/plan-mode/approved-plan.ts +35 -1
  99. package/src/prompts/goals/goal-budget-limit.md +16 -0
  100. package/src/prompts/goals/goal-continuation.md +28 -0
  101. package/src/prompts/goals/goal-mode-active.md +23 -0
  102. package/src/prompts/system/plan-mode-active.md +5 -5
  103. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  104. package/src/prompts/tools/bash.md +6 -0
  105. package/src/prompts/tools/github.md +4 -4
  106. package/src/prompts/tools/goal.md +13 -0
  107. package/src/prompts/tools/hashline.md +101 -117
  108. package/src/prompts/tools/read.md +55 -36
  109. package/src/prompts/tools/resolve.md +6 -5
  110. package/src/sdk.ts +12 -5
  111. package/src/session/agent-session.ts +428 -106
  112. package/src/session/blob-store.ts +36 -3
  113. package/src/session/messages.ts +67 -2
  114. package/src/session/session-manager.ts +131 -12
  115. package/src/session/session-storage.ts +33 -15
  116. package/src/session/streaming-output.ts +309 -13
  117. package/src/slash-commands/builtin-registry.ts +18 -0
  118. package/src/ssh/ssh-executor.ts +5 -0
  119. package/src/system-prompt.ts +4 -2
  120. package/src/task/discovery.ts +5 -2
  121. package/src/task/executor.ts +19 -8
  122. package/src/task/index.ts +3 -0
  123. package/src/task/render.ts +21 -15
  124. package/src/task/types.ts +4 -0
  125. package/src/tools/ast-edit.ts +21 -120
  126. package/src/tools/ast-grep.ts +21 -119
  127. package/src/tools/bash-command-fixup.ts +47 -0
  128. package/src/tools/bash-interactive.ts +9 -1
  129. package/src/tools/bash.ts +66 -19
  130. package/src/tools/browser/attach.ts +3 -3
  131. package/src/tools/browser/launch.ts +81 -18
  132. package/src/tools/browser/registry.ts +1 -5
  133. package/src/tools/browser/render.ts +2 -2
  134. package/src/tools/browser/tab-supervisor.ts +51 -14
  135. package/src/tools/conflict-detect.ts +15 -4
  136. package/src/tools/eval.ts +12 -2
  137. package/src/tools/find.ts +20 -38
  138. package/src/tools/gh.ts +44 -10
  139. package/src/tools/index.ts +22 -11
  140. package/src/tools/inspect-image.ts +3 -10
  141. package/src/tools/job.ts +16 -7
  142. package/src/tools/output-meta.ts +202 -37
  143. package/src/tools/path-utils.ts +125 -2
  144. package/src/tools/read.ts +548 -237
  145. package/src/tools/render-utils.ts +92 -0
  146. package/src/tools/renderers.ts +2 -0
  147. package/src/tools/resolve.ts +72 -44
  148. package/src/tools/search.ts +120 -186
  149. package/src/tools/ssh.ts +3 -2
  150. package/src/tools/write.ts +64 -9
  151. package/src/utils/file-mentions.ts +1 -1
  152. package/src/utils/image-loading.ts +7 -3
  153. package/src/utils/image-resize.ts +32 -43
  154. package/src/vim/parser.ts +0 -17
  155. package/src/vim/render.ts +1 -1
  156. package/src/vim/types.ts +1 -1
  157. package/src/web/search/providers/anthropic.ts +5 -0
  158. package/src/web/search/providers/exa.ts +3 -0
  159. package/src/web/search/providers/gemini.ts +40 -95
  160. package/src/web/search/providers/jina.ts +5 -2
  161. package/src/web/search/providers/zai.ts +5 -2
  162. package/src/prompts/tools/exit-plan-mode.md +0 -6
  163. package/src/tools/exit-plan-mode.ts +0 -97
  164. package/src/utils/fuzzy.ts +0 -108
  165. package/src/utils/image-convert.ts +0 -27
@@ -0,0 +1,89 @@
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
+ import { validateToolCall } from "@oh-my-pi/pi-ai";
3
+ import { Type } from "@sinclair/typebox";
4
+ import type { ChangelogCategory, ConventionalAnalysis } from "./types";
5
+ import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "./utils";
6
+
7
+ /**
8
+ * Shared TypeBox schema for the `create_conventional_analysis` tool used by
9
+ * both the single-pass analysis call and the map-reduce reduce phase. Schemas
10
+ * are identical across phases — only the surrounding tool `description`
11
+ * differs to reflect the input the phase is summarizing.
12
+ */
13
+ export const conventionalAnalysisParameters = Type.Object({
14
+ type: Type.Union([
15
+ Type.Literal("feat"),
16
+ Type.Literal("fix"),
17
+ Type.Literal("refactor"),
18
+ Type.Literal("docs"),
19
+ Type.Literal("test"),
20
+ Type.Literal("chore"),
21
+ Type.Literal("style"),
22
+ Type.Literal("perf"),
23
+ Type.Literal("build"),
24
+ Type.Literal("ci"),
25
+ Type.Literal("revert"),
26
+ ]),
27
+ scope: Type.Union([Type.String(), Type.Null()]),
28
+ details: Type.Array(
29
+ Type.Object({
30
+ text: Type.String(),
31
+ changelog_category: Type.Optional(
32
+ Type.Union([
33
+ Type.Literal("Added"),
34
+ Type.Literal("Changed"),
35
+ Type.Literal("Fixed"),
36
+ Type.Literal("Deprecated"),
37
+ Type.Literal("Removed"),
38
+ Type.Literal("Security"),
39
+ Type.Literal("Breaking Changes"),
40
+ ]),
41
+ ),
42
+ user_visible: Type.Optional(Type.Boolean()),
43
+ }),
44
+ ),
45
+ issue_refs: Type.Array(Type.String()),
46
+ });
47
+
48
+ export interface ConventionalAnalysisTool {
49
+ name: "create_conventional_analysis";
50
+ description: string;
51
+ parameters: typeof conventionalAnalysisParameters;
52
+ }
53
+
54
+ /**
55
+ * Build a `create_conventional_analysis` tool descriptor. Phase-specific
56
+ * `description` text is the only thing that varies between callers.
57
+ */
58
+ export function createConventionalAnalysisTool(description: string): ConventionalAnalysisTool {
59
+ return {
60
+ name: "create_conventional_analysis",
61
+ description,
62
+ parameters: conventionalAnalysisParameters,
63
+ };
64
+ }
65
+
66
+ interface ParsedConventionalAnalysis {
67
+ type: ConventionalAnalysis["type"];
68
+ scope: string | null;
69
+ details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
70
+ issue_refs: string[];
71
+ }
72
+
73
+ /**
74
+ * Extract a {@link ConventionalAnalysis} from an assistant response, preferring
75
+ * a structured tool call and falling back to JSON embedded in text content.
76
+ */
77
+ export function parseConventionalAnalysisResponse(
78
+ message: AssistantMessage,
79
+ tool: ConventionalAnalysisTool,
80
+ ): ConventionalAnalysis {
81
+ const toolCall = extractToolCall(message, tool.name);
82
+ if (toolCall) {
83
+ const parsed = validateToolCall([tool], toolCall) as ParsedConventionalAnalysis;
84
+ return normalizeAnalysis(parsed);
85
+ }
86
+ const text = extractTextContent(message);
87
+ const parsed = parseJsonPayload(text) as ParsedConventionalAnalysis;
88
+ return normalizeAnalysis(parsed);
89
+ }
@@ -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 __referenceDataCache: CanonicalReferenceData | undefined;
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 __resolutionCache: WeakMap<
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 (__referenceDataCache) {
102
- return __referenceDataCache;
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
- __referenceDataCache = {
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 __referenceDataCache;
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 = __resolutionCache.get(compiledEquivalence);
668
+ let modelCache = resolutionCache.get(compiledEquivalence);
672
669
  if (!modelCache) {
673
670
  modelCache = new WeakMap<Model<Api>, ResolvedCanonicalModel>();
674
- __resolutionCache.set(compiledEquivalence, modelCache);
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,
@@ -1013,8 +1015,12 @@ export class ModelRegistry {
1013
1015
 
1014
1016
  this.#addImplicitDiscoverableProviders(configuredProviders);
1015
1017
  const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
1018
+ const cachedStandardModels = this.#applyHardcodedModelPolicies(this.#loadCachedStandardProviderModels());
1016
1019
  const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
1017
- const resolvedDefaults = this.#mergeResolvedModels(builtInModels, cachedDiscoveries);
1020
+ const resolvedDefaults = this.#mergeResolvedModels(
1021
+ this.#mergeResolvedModels(builtInModels, cachedStandardModels),
1022
+ cachedDiscoveries,
1023
+ );
1018
1024
  const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
1019
1025
  // Merge runtime extension models so they survive refresh() cycles
1020
1026
  const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
@@ -1053,7 +1059,16 @@ export class ModelRegistry {
1053
1059
  const key = `${replacementModel.provider}\u0000${replacementModel.id}`;
1054
1060
  const existingIndex = indexByKey.get(key);
1055
1061
  if (existingIndex !== undefined) {
1056
- merged[existingIndex] = replacementModel;
1062
+ const existing = merged[existingIndex];
1063
+ merged[existingIndex] = {
1064
+ ...replacementModel,
1065
+ contextWindow:
1066
+ replacementModel.contextWindow === UNK_CONTEXT_WINDOW
1067
+ ? existing.contextWindow
1068
+ : replacementModel.contextWindow,
1069
+ maxTokens:
1070
+ replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
1071
+ };
1057
1072
  } else {
1058
1073
  merged.push(replacementModel);
1059
1074
  indexByKey.set(key, merged.length - 1);
@@ -1104,6 +1119,32 @@ export class ModelRegistry {
1104
1119
  return merged;
1105
1120
  }
1106
1121
 
1122
+ #loadCachedStandardProviderModels(): Model<Api>[] {
1123
+ const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
1124
+ const cachedModels: Model<Api>[] = [];
1125
+ for (const descriptor of PROVIDER_DESCRIPTORS) {
1126
+ if (configuredDiscoveryProviders.has(descriptor.providerId)) {
1127
+ continue;
1128
+ }
1129
+ const cache = readModelCache<Api>(descriptor.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
1130
+ if (!cache) {
1131
+ continue;
1132
+ }
1133
+ const models = cache.models.map(model =>
1134
+ model.provider === descriptor.providerId ? model : { ...model, provider: descriptor.providerId },
1135
+ );
1136
+ const providerOverride = this.#providerOverrides.get(descriptor.providerId);
1137
+ const withTransport = providerOverride
1138
+ ? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
1139
+ : models;
1140
+ const withCompat = providerOverride?.compat
1141
+ ? withTransport.map(model => ({ ...model, compat: mergeCompat(model.compat, providerOverride.compat) }))
1142
+ : withTransport;
1143
+ cachedModels.push(...this.#applyProviderModelOverrides(descriptor.providerId, withCompat));
1144
+ }
1145
+ return cachedModels;
1146
+ }
1147
+
1107
1148
  #loadCachedDiscoverableModels(): Model<Api>[] {
1108
1149
  const cachedModels: Model<Api>[] = [];
1109
1150
  for (const providerConfig of this.#discoverableProviders) {
@@ -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: 500,
1541
+ default: 300,
1502
1542
  ui: {
1503
1543
  tab: "editing",
1504
1544
  label: "Default Read Limit",
@@ -1608,6 +1648,17 @@ export const SETTINGS_SCHEMA = {
1608
1648
  },
1609
1649
  "bashInterceptor.patterns": { type: "array", default: DEFAULT_BASH_INTERCEPTOR_RULES },
1610
1650
 
1651
+ "bash.stripTrailingHeadTail": {
1652
+ type: "boolean",
1653
+ default: true,
1654
+ ui: {
1655
+ tab: "editing",
1656
+ label: "Strip Trailing head/tail",
1657
+ description:
1658
+ "Silently drop trailing `| head`/`| tail` pipes from single-line bash commands. Output is already truncated automatically.",
1659
+ },
1660
+ },
1661
+
1611
1662
  // Shell output minimizer
1612
1663
  "shellMinimizer.enabled": {
1613
1664
  type: "boolean",
@@ -2095,6 +2146,36 @@ export const SETTINGS_SCHEMA = {
2095
2146
  },
2096
2147
  },
2097
2148
 
2149
+ "goal.enabled": {
2150
+ type: "boolean",
2151
+ default: true,
2152
+ ui: {
2153
+ tab: "tasks",
2154
+ label: "Goal Mode",
2155
+ description: "Enable per-session goal mode and the hidden goal tool",
2156
+ },
2157
+ },
2158
+
2159
+ "goal.statusInFooter": {
2160
+ type: "boolean",
2161
+ default: true,
2162
+ ui: {
2163
+ tab: "tasks",
2164
+ label: "Goal Status In Footer",
2165
+ description: "Show token budget alongside the goal indicator in the status line",
2166
+ },
2167
+ },
2168
+
2169
+ "goal.continuationModes": {
2170
+ type: "array",
2171
+ default: ["interactive"],
2172
+ ui: {
2173
+ tab: "tasks",
2174
+ label: "Goal Continuation Modes",
2175
+ description: "Run modes where active goals may auto-continue between turns",
2176
+ },
2177
+ },
2178
+
2098
2179
  // Delegation
2099
2180
  "task.isolation.mode": {
2100
2181
  type: "enum",
@@ -850,7 +850,7 @@ export function isSettingsInitialized(): boolean {
850
850
  * Reset the global singleton for testing.
851
851
  * @internal
852
852
  */
853
- export function _resetSettingsForTest(): void {
853
+ export function resetSettingsForTest(): void {
854
854
  globalInstance = null;
855
855
  globalInstancePromise = null;
856
856
  }