@oh-my-pi/pi-coding-agent 13.7.6 → 13.9.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/package.json +7 -7
  3. package/scripts/generate-docs-index.ts +3 -3
  4. package/src/capability/context-file.ts +6 -3
  5. package/src/capability/fs.ts +18 -0
  6. package/src/capability/index.ts +3 -2
  7. package/src/capability/rule.ts +0 -4
  8. package/src/capability/types.ts +2 -0
  9. package/src/cli/agents-cli.ts +1 -1
  10. package/src/cli/args.ts +7 -12
  11. package/src/commands/launch.ts +3 -2
  12. package/src/config/model-resolver.ts +118 -33
  13. package/src/config/settings-schema.ts +14 -2
  14. package/src/config/settings.ts +1 -17
  15. package/src/discovery/agents-md.ts +3 -4
  16. package/src/discovery/agents.ts +104 -84
  17. package/src/discovery/builtin.ts +28 -15
  18. package/src/discovery/claude.ts +27 -9
  19. package/src/discovery/helpers.ts +10 -17
  20. package/src/extensibility/extensions/loader.ts +1 -2
  21. package/src/extensibility/extensions/types.ts +2 -1
  22. package/src/extensibility/skills.ts +2 -2
  23. package/src/internal-urls/docs-index.generated.ts +1 -1
  24. package/src/main.ts +21 -10
  25. package/src/modes/components/agent-dashboard.ts +12 -13
  26. package/src/modes/components/model-selector.ts +157 -59
  27. package/src/modes/components/read-tool-group.ts +36 -2
  28. package/src/modes/components/settings-defs.ts +11 -8
  29. package/src/modes/components/settings-selector.ts +1 -1
  30. package/src/modes/components/thinking-selector.ts +3 -15
  31. package/src/modes/controllers/selector-controller.ts +6 -4
  32. package/src/modes/rpc/rpc-client.ts +2 -2
  33. package/src/modes/rpc/rpc-types.ts +2 -2
  34. package/src/modes/theme/theme.ts +2 -1
  35. package/src/patch/hashline.ts +113 -0
  36. package/src/patch/index.ts +27 -18
  37. package/src/prompts/tools/hashline.md +9 -10
  38. package/src/prompts/tools/read.md +2 -2
  39. package/src/sdk.ts +21 -25
  40. package/src/session/agent-session.ts +54 -59
  41. package/src/task/executor.ts +10 -8
  42. package/src/task/types.ts +1 -2
  43. package/src/tools/fetch.ts +152 -4
  44. package/src/tools/read.ts +88 -264
  45. package/src/utils/frontmatter.ts +25 -4
  46. package/src/web/scrapers/choosealicense.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.9.0] - 2026-03-05
6
+ ### Added
7
+
8
+ - Added `read.defaultLimit` setting to configure default number of lines returned by read tool when no limit is specified (default: 300 lines)
9
+ - Added preset options for read default limit (200, 300, 500, 1000, 5000 lines) in settings UI
10
+
11
+ ### Changed
12
+
13
+ - Updated read tool prompt to distinguish between default limit and maximum limit per call
14
+ - Moved `ThinkingLevel` type from `@oh-my-pi/pi-agent-core` to `@oh-my-pi/pi-ai` for centralized thinking level definitions
15
+ - Replaced local thinking level validation with `parseThinkingLevel()` and `ALL_THINKING_LEVELS` from `@oh-my-pi/pi-ai`
16
+ - Updated thinking level option providers to use `THINKING_MODE_DESCRIPTIONS` from `@oh-my-pi/pi-ai` for consistent descriptions
17
+ - Renamed `RoleThinkingMode` type to `ThinkingMode` and changed default value from `'default'` to `'inherit'` for clarity
18
+ - Replaced `formatThinkingEffortLabel()` utility with `formatThinking()` from `@oh-my-pi/pi-ai`
19
+ - Renamed `extractExplicitThinkingLevel()` to `extractExplicitThinkingSelector()` in model resolver
20
+ - Updated thinking level clamping to use `getAvailableThinkingLevel()` from `@oh-my-pi/pi-ai`
21
+
22
+ ### Removed
23
+
24
+ - Removed `thinking-effort-label.ts` utility file (functionality moved to `@oh-my-pi/pi-ai`)
25
+ - Removed local `VALID_THINKING_LEVELS` constant definitions across multiple files
26
+ - Removed `isValidThinkingLevel()` function (replaced by `parseThinkingLevel()` from `@oh-my-pi/pi-ai`)
27
+ - Removed `parseThinkingLevel()` helper from discovery module (now uses centralized version from `@oh-my-pi/pi-ai`)
28
+
29
+ ### Fixed
30
+
31
+ - Fixed provider session state not being cleared when branching or navigating tree history, preventing resource leaks with codex provider sessions
32
+
33
+ ## [13.8.0] - 2026-03-04
34
+ ### Added
35
+
36
+ - Added `buildCompactHashlineDiffPreview()` function to generate compact diff previews for model-visible tool responses, collapsing long unchanged runs and consecutive additions/removals to show edit shape without full file content
37
+ - Added project-level discovery for `.agent/` and `.agents/` directories, walking up from cwd to repo root (matching behavior of other providers like `.omp`, `.claude`, `.codex`). Applies to skills, rules, prompts, commands, context files (AGENTS.md), and system prompts (SYSTEM.md)
38
+
39
+ ### Changed
40
+
41
+ - Changed edit tool response to include diff summary with line counts (+added -removed) and a compact diff preview instead of warnings-only output
42
+ - Limited auto context promotion to models with explicit `contextPromotionTarget`; models without a configured target now compact on overflow instead of switching to arbitrary larger models ([#282](https://github.com/can1357/oh-my-pi/issues/282))
43
+
44
+ ### Fixed
45
+
46
+ - Fixed `:thinking` suffix in `modelRoles` config values silently breaking model resolution (e.g., `slow: anthropic/claude-opus-4-6:high`) and being stripped on Ctrl+P role cycling
47
+
5
48
  ## [13.7.6] - 2026-03-04
6
49
  ### Added
7
50
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.7.6",
4
+ "version": "13.9.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.7.6",
45
- "@oh-my-pi/pi-agent-core": "13.7.6",
46
- "@oh-my-pi/pi-ai": "13.7.6",
47
- "@oh-my-pi/pi-natives": "13.7.6",
48
- "@oh-my-pi/pi-tui": "13.7.6",
49
- "@oh-my-pi/pi-utils": "13.7.6",
44
+ "@oh-my-pi/omp-stats": "13.9.1",
45
+ "@oh-my-pi/pi-agent-core": "13.9.1",
46
+ "@oh-my-pi/pi-ai": "13.9.1",
47
+ "@oh-my-pi/pi-natives": "13.9.1",
48
+ "@oh-my-pi/pi-tui": "13.9.1",
49
+ "@oh-my-pi/pi-utils": "13.9.1",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { Glob } from "bun";
4
3
  import * as path from "node:path";
4
+ import { Glob } from "bun";
5
5
 
6
6
  const docsDir = path.resolve(import.meta.dir, "../../../docs");
7
7
  const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.ts");
@@ -14,10 +14,10 @@ for await (const relativePath of glob.scan(docsDir)) {
14
14
  entries.sort();
15
15
 
16
16
  const docsWithContent = await Promise.all(
17
- entries.map(async (relativePath) => ({
17
+ entries.map(async relativePath => ({
18
18
  relativePath,
19
19
  content: await Bun.file(path.join(docsDir, relativePath)).text(),
20
- }))
20
+ })),
21
21
  );
22
22
 
23
23
  const filenamesLiteral = JSON.stringify(entries);
@@ -27,9 +27,12 @@ export const contextFileCapability = defineCapability<ContextFile>({
27
27
  id: "context-files",
28
28
  displayName: "Context Files",
29
29
  description: "Persistent instruction files (CLAUDE.md, AGENTS.md, etc.) that guide agent behavior",
30
- // Deduplicate by level: one user-level file, one project-level file
31
- // Higher-priority providers shadow lower-priority ones at the same scope
32
- key: file => file.level,
30
+ // Deduplicate by scope: one user-level file, and one project-level file per directory depth.
31
+ // Within each depth level, higher-priority providers shadow lower-priority ones.
32
+ // This supports monorepo hierarchies where AGENTS.md exists at multiple ancestor levels.
33
+ // Clamp depth >= 0: files inside config subdirectories of an ancestor (e.g. .claude/, .github/)
34
+ // are same-scope as the ancestor itself.
35
+ key: file => (file.level === "user" ? "user" : `project:${Math.max(0, file.depth ?? 0)}`),
33
36
  validate: file => {
34
37
  if (!file.path) return "Missing path";
35
38
  if (file.content === undefined) return "Missing content";
@@ -66,6 +66,24 @@ export async function walkUp(
66
66
  }
67
67
  }
68
68
 
69
+ /**
70
+ * Walk up from startDir looking for a `.git` entry (file or directory).
71
+ * Returns the directory containing `.git` (the repo root), or null if not in a git repo.
72
+ * Results are based on the cached readDirEntries, so repeated calls are cheap.
73
+ */
74
+ export async function findRepoRoot(startDir: string): Promise<string | null> {
75
+ let current = resolvePath(startDir);
76
+ while (true) {
77
+ const entries = await readDirEntries(current);
78
+ if (entries.some(e => e.name === ".git")) {
79
+ return current;
80
+ }
81
+ const parent = path.dirname(current);
82
+ if (parent === current) return null;
83
+ current = parent;
84
+ }
85
+ }
86
+
69
87
  export function cacheStats(): { content: number; dir: number } {
70
88
  return {
71
89
  content: contentCache.size,
@@ -11,7 +11,7 @@ import * as path from "node:path";
11
11
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
12
12
 
13
13
  import type { Settings } from "../config/settings";
14
- import { clearCache as clearFsCache, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
14
+ import { clearCache as clearFsCache, findRepoRoot, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs";
15
15
  import type {
16
16
  Capability,
17
17
  CapabilityInfo,
@@ -220,7 +220,8 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
220
220
 
221
221
  const cwd = options.cwd ?? getProjectDir();
222
222
  const home = os.homedir();
223
- const ctx: LoadContext = { cwd, home };
223
+ const repoRoot = await findRepoRoot(cwd);
224
+ const ctx: LoadContext = { cwd, home, repoRoot };
224
225
  const providers = filterProviders(capability, options);
225
226
 
226
227
  return await loadImpl(capability, providers, ctx, options);
@@ -20,10 +20,6 @@ export interface RuleFrontmatter {
20
20
  condition?: string | string[];
21
21
  /** New key for TTSR stream scope. */
22
22
  scope?: string | string[];
23
- /** Legacy key accepted for backward compatibility with existing rules. */
24
- ttsr_trigger?: string | string[];
25
- /** Legacy camelCase key accepted for backward compatibility with existing rules. */
26
- ttsrTrigger?: string | string[];
27
23
  [key: string]: unknown;
28
24
  }
29
25
 
@@ -14,6 +14,8 @@ export interface LoadContext {
14
14
  cwd: string;
15
15
  /** User home directory */
16
16
  home: string;
17
+ /** Git repository root (directory containing .git), or null if not in a repo */
18
+ repoRoot: string | null;
17
19
  }
18
20
 
19
21
  /**
@@ -61,7 +61,7 @@ function toFrontmatter(agent: AgentDefinition): Record<string, unknown> {
61
61
  if (agent.tools && agent.tools.length > 0) frontmatter.tools = agent.tools;
62
62
  if (agent.spawns !== undefined) frontmatter.spawns = agent.spawns;
63
63
  if (agent.model && agent.model.length > 0) frontmatter.model = agent.model;
64
- if (agent.thinkingLevel) frontmatter["thinking-level"] = agent.thinkingLevel;
64
+ if (agent.thinkingLevel) frontmatter.thinkingLevel = agent.thinkingLevel;
65
65
  if (agent.output !== undefined) frontmatter.output = agent.output;
66
66
  if (agent.blocking) frontmatter.blocking = true;
67
67
 
package/src/cli/args.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI argument parsing and help display
3
3
  */
4
- import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
4
+ import { getAvailableThinkingLevels, parseThinkingLevel, type ThinkingLevel } from "@oh-my-pi/pi-ai";
5
5
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
6
6
  import chalk from "chalk";
7
7
  import { BUILTIN_TOOLS } from "../tools";
@@ -48,12 +48,6 @@ export interface Args {
48
48
  unknownFlags: Map<string, boolean | string>;
49
49
  }
50
50
 
51
- const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
52
-
53
- export function isValidThinkingLevel(level: string): level is ThinkingLevel {
54
- return VALID_THINKING_LEVELS.includes(level as ThinkingLevel);
55
- }
56
-
57
51
  export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
58
52
  const result: Args = {
59
53
  messages: [],
@@ -127,13 +121,14 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
127
121
  }
128
122
  result.tools = validTools;
129
123
  } else if (arg === "--thinking" && i + 1 < args.length) {
130
- const level = args[++i];
131
- if (isValidThinkingLevel(level)) {
132
- result.thinking = level;
124
+ const rawThinking = args[++i];
125
+ const thinking = parseThinkingLevel(rawThinking);
126
+ if (thinking !== undefined) {
127
+ result.thinking = thinking;
133
128
  } else {
134
129
  logger.warn("Invalid thinking level passed to --thinking", {
135
- level,
136
- validThinkingLevels: [...VALID_THINKING_LEVELS],
130
+ level: rawThinking,
131
+ validThinkingLevels: getAvailableThinkingLevels(),
137
132
  });
138
133
  }
139
134
  } else if (arg === "--print" || arg === "-p") {
@@ -2,6 +2,7 @@
2
2
  * Root command for the coding agent CLI.
3
3
  */
4
4
 
5
+ import { getAvailableThinkingLevels } from "@oh-my-pi/pi-ai";
5
6
  import { APP_NAME } from "@oh-my-pi/pi-utils";
6
7
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
7
8
  import { parseArgs } from "../cli/args";
@@ -85,8 +86,8 @@ export default class Index extends Command {
85
86
  description: "Comma-separated list of tools to enable (default: all)",
86
87
  }),
87
88
  thinking: Flags.string({
88
- description: "Set thinking level: off, minimal, low, medium, high, xhigh",
89
- options: ["off", "minimal", "low", "medium", "high", "xhigh"],
89
+ description: `Set thinking level: ${getAvailableThinkingLevels().join(", ")}`,
90
+ options: getAvailableThinkingLevels(),
90
91
  }),
91
92
  hook: Flags.string({
92
93
  description: "Load a hook/extension file (can be used multiple times)",
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * Model resolution, scoping, and initial selection
3
3
  */
4
- import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
- import { type Api, DEFAULT_MODEL_PER_PROVIDER, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
4
+ import {
5
+ type Api,
6
+ DEFAULT_MODEL_PER_PROVIDER,
7
+ type KnownProvider,
8
+ type Model,
9
+ modelsAreEqual,
10
+ parseThinkingLevel,
11
+ type ThinkingLevel,
12
+ } from "@oh-my-pi/pi-ai";
6
13
  import chalk from "chalk";
7
- import { isValidThinkingLevel } from "../cli/args";
8
14
  import MODEL_PRIO from "../priority.json" with { type: "json" };
9
15
  import { fuzzyMatch } from "../utils/fuzzy";
10
16
  import { MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
@@ -23,10 +29,23 @@ export interface ScopedModel {
23
29
  * Parse a model string in "provider/modelId" format.
24
30
  * Returns undefined if the format is invalid.
25
31
  */
26
- export function parseModelString(modelStr: string): { provider: string; id: string } | undefined {
32
+ export function parseModelString(
33
+ modelStr: string,
34
+ ): { provider: string; id: string; thinkingLevel?: ThinkingLevel } | undefined {
27
35
  const slashIdx = modelStr.indexOf("/");
28
36
  if (slashIdx <= 0) return undefined;
29
- return { provider: modelStr.slice(0, slashIdx), id: modelStr.slice(slashIdx + 1) };
37
+ const id = modelStr.slice(slashIdx + 1);
38
+ const provider = modelStr.slice(0, slashIdx);
39
+ // Strip valid thinking level suffix (e.g., "claude-sonnet-4-6:high" -> id "claude-sonnet-4-6", thinkingLevel "high")
40
+ const colonIdx = id.lastIndexOf(":");
41
+ if (colonIdx !== -1) {
42
+ const suffix = id.slice(colonIdx + 1);
43
+ const thinkingLevel = parseThinkingLevel(suffix);
44
+ if (thinkingLevel) {
45
+ return { provider, id: id.slice(0, colonIdx), thinkingLevel };
46
+ }
47
+ }
48
+ return { provider, id };
30
49
  }
31
50
 
32
51
  /**
@@ -230,7 +249,7 @@ function parseModelPatternWithContext(
230
249
  pattern: string,
231
250
  availableModels: Model<Api>[],
232
251
  context: ModelPreferenceContext,
233
- options?: { allowInvalidThinkingLevelFallback?: boolean },
252
+ options?: { allowInvalidThinkingSelectorFallback?: boolean },
234
253
  ): ParsedModelResult {
235
254
  // Try exact match first
236
255
  const exactMatch = tryMatchModel(pattern, availableModels, context);
@@ -248,7 +267,8 @@ function parseModelPatternWithContext(
248
267
  const prefix = pattern.substring(0, lastColonIndex);
249
268
  const suffix = pattern.substring(lastColonIndex + 1);
250
269
 
251
- if (isValidThinkingLevel(suffix)) {
270
+ const parsedThinkingLevel = parseThinkingLevel(suffix);
271
+ if (parsedThinkingLevel) {
252
272
  // Valid thinking level - recurse on prefix and use this level
253
273
  const result = parseModelPatternWithContext(prefix, availableModels, context, options);
254
274
  if (result.model) {
@@ -256,7 +276,7 @@ function parseModelPatternWithContext(
256
276
  const explicitThinkingLevel = !result.warning;
257
277
  return {
258
278
  model: result.model,
259
- thinkingLevel: explicitThinkingLevel ? suffix : undefined,
279
+ thinkingLevel: explicitThinkingLevel ? parsedThinkingLevel : undefined,
260
280
  warning: result.warning,
261
281
  explicitThinkingLevel,
262
282
  };
@@ -264,7 +284,7 @@ function parseModelPatternWithContext(
264
284
  return result;
265
285
  }
266
286
 
267
- const allowFallback = options?.allowInvalidThinkingLevelFallback ?? true;
287
+ const allowFallback = options?.allowInvalidThinkingSelectorFallback ?? true;
268
288
  if (!allowFallback) {
269
289
  return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
270
290
  }
@@ -286,7 +306,7 @@ export function parseModelPattern(
286
306
  pattern: string,
287
307
  availableModels: Model<Api>[],
288
308
  preferences?: ModelMatchPreferences,
289
- options?: { allowInvalidThinkingLevelFallback?: boolean },
309
+ options?: { allowInvalidThinkingSelectorFallback?: boolean },
290
310
  ): ParsedModelResult {
291
311
  const context = buildPreferenceContext(availableModels, preferences);
292
312
  return parseModelPatternWithContext(pattern, availableModels, context, options);
@@ -319,6 +339,74 @@ export function expandRoleAlias(value: string, settings?: Settings): string {
319
339
  return settings?.getModelRole(role) ?? value;
320
340
  }
321
341
 
342
+ /**
343
+ * Resolve a model role value into a concrete model and thinking metadata.
344
+ */
345
+ export interface ResolvedModelRoleValue {
346
+ model: Model<Api> | undefined;
347
+ thinkingLevel?: ThinkingLevel;
348
+ explicitThinkingLevel: boolean;
349
+ warning: string | undefined;
350
+ }
351
+
352
+ export function resolveModelRoleValue(
353
+ roleValue: string | undefined,
354
+ availableModels: Model<Api>[],
355
+ options?: { settings?: Settings; matchPreferences?: ModelMatchPreferences },
356
+ ): ResolvedModelRoleValue {
357
+ if (!roleValue) {
358
+ return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
359
+ }
360
+
361
+ const normalized = roleValue.trim();
362
+ if (!normalized || normalized === DEFAULT_MODEL_ROLE) {
363
+ return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
364
+ }
365
+
366
+ const lastColonIndex = normalized.lastIndexOf(":");
367
+ const hasThinkingSuffix =
368
+ lastColonIndex > PREFIX_MODEL_ROLE.length && parseThinkingLevel(normalized.slice(lastColonIndex + 1));
369
+ const aliasCandidate = hasThinkingSuffix ? normalized.slice(0, lastColonIndex) : normalized;
370
+ const effectivePattern = expandRoleAlias(aliasCandidate, options?.settings);
371
+ const patternWithSuffix = hasThinkingSuffix
372
+ ? `${effectivePattern}:${normalized.slice(lastColonIndex + 1)}`
373
+ : effectivePattern;
374
+ const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPattern(
375
+ patternWithSuffix,
376
+ availableModels,
377
+ options?.matchPreferences,
378
+ );
379
+
380
+ return { model, thinkingLevel, explicitThinkingLevel, warning };
381
+ }
382
+
383
+ export function extractExplicitThinkingSelector(
384
+ value: string | undefined,
385
+ settings?: Settings,
386
+ ): ThinkingLevel | undefined {
387
+ if (!value) return undefined;
388
+ const normalized = value.trim();
389
+ if (!normalized || normalized === DEFAULT_MODEL_ROLE) return undefined;
390
+
391
+ const visited = new Set<string>();
392
+ let current = normalized;
393
+ while (!visited.has(current)) {
394
+ visited.add(current);
395
+ const lastColonIndex = current.lastIndexOf(":");
396
+ const hasThinkingSuffix =
397
+ lastColonIndex > PREFIX_MODEL_ROLE.length && parseThinkingLevel(current.slice(lastColonIndex + 1));
398
+ if (hasThinkingSuffix) {
399
+ return current.slice(lastColonIndex + 1) as ThinkingLevel;
400
+ }
401
+ const expanded = expandRoleAlias(current, settings).trim();
402
+ if (!expanded || expanded === current) break;
403
+ if (expanded === DEFAULT_MODEL_ROLE) return undefined;
404
+ current = expanded;
405
+ }
406
+
407
+ return undefined;
408
+ }
409
+
322
410
  /**
323
411
  * Resolve a model identifier or pattern to a Model instance.
324
412
  */
@@ -329,7 +417,8 @@ export function resolveModelFromString(
329
417
  ): Model<Api> | undefined {
330
418
  const parsed = parseModelString(value);
331
419
  if (parsed) {
332
- return available.find(model => model.provider === parsed.provider && model.id === parsed.id);
420
+ const exact = available.find(model => model.provider === parsed.provider && model.id === parsed.id);
421
+ if (exact) return exact;
333
422
  }
334
423
  return parseModelPattern(value, available, matchPreferences).model;
335
424
  }
@@ -361,25 +450,20 @@ export function resolveModelOverride(
361
450
  modelPatterns: string[],
362
451
  modelRegistry: ModelRegistry,
363
452
  settings?: Settings,
364
- ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel } {
365
- if (modelPatterns.length === 0) return {};
453
+ ): { model?: Model<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
454
+ if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
455
+ const availableModels = modelRegistry.getAvailable();
366
456
  const matchPreferences = { usageOrder: settings?.getStorage()?.getModelUsageOrder() };
367
457
  for (const pattern of modelPatterns) {
368
- const normalized = pattern.trim();
369
- if (!normalized || isDefaultModelAlias(normalized)) {
370
- continue;
371
- }
372
- const effectivePattern = expandRoleAlias(pattern, settings);
373
- const { model, thinkingLevel } = parseModelPattern(
374
- effectivePattern,
375
- modelRegistry.getAvailable(),
458
+ const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(pattern, availableModels, {
459
+ settings,
376
460
  matchPreferences,
377
- );
461
+ });
378
462
  if (model) {
379
- return { model, thinkingLevel: thinkingLevel !== "off" ? thinkingLevel : undefined };
463
+ return { model, thinkingLevel, explicitThinkingLevel };
380
464
  }
381
465
  }
382
- return {};
466
+ return { explicitThinkingLevel: false };
383
467
  }
384
468
 
385
469
  /**
@@ -413,8 +497,9 @@ export async function resolveModelScope(
413
497
 
414
498
  if (colonIdx !== -1) {
415
499
  const suffix = pattern.substring(colonIdx + 1);
416
- if (isValidThinkingLevel(suffix)) {
417
- thinkingLevel = suffix;
500
+ const parsedThinkingLevel = parseThinkingLevel(suffix);
501
+ if (parsedThinkingLevel) {
502
+ thinkingLevel = parsedThinkingLevel;
418
503
  explicitThinkingLevel = true;
419
504
  globPattern = pattern.substring(0, colonIdx);
420
505
  }
@@ -541,7 +626,7 @@ export function resolveCliModel(options: {
541
626
 
542
627
  const candidates = provider ? availableModels.filter(model => model.provider === provider) : availableModels;
543
628
  const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, preferences, {
544
- allowInvalidThinkingLevelFallback: false,
629
+ allowInvalidThinkingSelectorFallback: false,
545
630
  });
546
631
 
547
632
  if (!model) {
@@ -578,7 +663,7 @@ export async function findInitialModel(options: {
578
663
  isContinuing: boolean;
579
664
  defaultProvider?: string;
580
665
  defaultModelId?: string;
581
- defaultThinkingLevel?: ThinkingLevel;
666
+ defaultThinkingSelector?: ThinkingLevel;
582
667
  modelRegistry: ModelRegistry;
583
668
  }): Promise<InitialModelResult> {
584
669
  const {
@@ -588,7 +673,7 @@ export async function findInitialModel(options: {
588
673
  isContinuing,
589
674
  defaultProvider,
590
675
  defaultModelId,
591
- defaultThinkingLevel,
676
+ defaultThinkingSelector,
592
677
  modelRegistry,
593
678
  } = options;
594
679
 
@@ -608,10 +693,10 @@ export async function findInitialModel(options: {
608
693
  // 2. Use first model from scoped models (skip if continuing/resuming)
609
694
  if (scopedModels.length > 0 && !isContinuing) {
610
695
  const scoped = scopedModels[0];
611
- const scopedThinkingLevel = scoped.thinkingLevel ?? defaultThinkingLevel ?? "off";
696
+ const scopedThinkingSelector = scoped.thinkingLevel ?? defaultThinkingSelector ?? "off";
612
697
  return {
613
698
  model: scoped.model,
614
- thinkingLevel: scopedThinkingLevel,
699
+ thinkingLevel: scopedThinkingSelector,
615
700
  fallbackMessage: undefined,
616
701
  };
617
702
  }
@@ -621,8 +706,8 @@ export async function findInitialModel(options: {
621
706
  const found = modelRegistry.find(defaultProvider, defaultModelId);
622
707
  if (found) {
623
708
  model = found;
624
- if (defaultThinkingLevel) {
625
- thinkingLevel = defaultThinkingLevel;
709
+ if (defaultThinkingSelector) {
710
+ thinkingLevel = defaultThinkingSelector;
626
711
  }
627
712
  return { model, thinkingLevel, fallbackMessage: undefined };
628
713
  }
@@ -1,4 +1,6 @@
1
- /**
1
+ import { getAvailableThinkingLevels } from "@oh-my-pi/pi-ai";
2
+
3
+ /** Unified settings schema - single source of truth for all settings.
2
4
  * Unified settings schema - single source of truth for all settings.
3
5
  *
4
6
  * Each setting is defined once here with:
@@ -190,7 +192,7 @@ export const SETTINGS_SCHEMA = {
190
192
  },
191
193
  defaultThinkingLevel: {
192
194
  type: "enum",
193
- values: ["off", "minimal", "low", "medium", "high", "xhigh"] as const,
195
+ values: getAvailableThinkingLevels(),
194
196
  default: "high",
195
197
  ui: {
196
198
  tab: "agent",
@@ -283,6 +285,16 @@ export const SETTINGS_SCHEMA = {
283
285
  description: "Include line hashes in read output for hashline edit mode (LINE#ID:content)",
284
286
  },
285
287
  },
288
+ "read.defaultLimit": {
289
+ type: "number",
290
+ default: 300,
291
+ ui: {
292
+ tab: "tools",
293
+ label: "Read default limit",
294
+ description: "Default number of lines returned when agent calls read without a limit",
295
+ submenu: true,
296
+ },
297
+ },
286
298
  showHardwareCursor: {
287
299
  type: "boolean",
288
300
  default: true, // will be computed based on platform if undefined
@@ -318,21 +318,6 @@ export class Settings {
318
318
  return result as unknown as GroupTypeMap[G];
319
319
  }
320
320
 
321
- /**
322
- * Get edit model variants (typed accessor for complex nested config).
323
- */
324
- getEditModelVariants(): Record<string, EditMode | null> {
325
- const variants = (this.#merged.edit as { modelVariants?: Record<string, string> })?.modelVariants ?? {};
326
- const result: Record<string, EditMode | null> = {};
327
- for (const pattern in variants) {
328
- const value = normalizeEditMode(variants[pattern]);
329
- if (value) {
330
- result[pattern] = value;
331
- }
332
- }
333
- return result;
334
- }
335
-
336
321
  /**
337
322
  * Get the edit variant for a specific model.
338
323
  * Returns "patch", "replace", "hashline", or null (use global default).
@@ -341,9 +326,8 @@ export class Settings {
341
326
  if (!model) return null;
342
327
  const variants = (this.#merged.edit as { modelVariants?: Record<string, string> })?.modelVariants;
343
328
  if (!variants) return null;
344
- const modelLower = model.toLowerCase();
345
329
  for (const pattern in variants) {
346
- if (modelLower.includes(pattern)) {
330
+ if (model.includes(pattern)) {
347
331
  const value = normalizeEditMode(variants[pattern]);
348
332
  if (value) {
349
333
  return value;
@@ -14,7 +14,6 @@ import { calculateDepth, createSourceMeta } from "./helpers";
14
14
 
15
15
  const PROVIDER_ID = "agents-md";
16
16
  const DISPLAY_NAME = "AGENTS.md";
17
- const MAX_DEPTH = 20; // Prevent walking up excessively far from cwd
18
17
 
19
18
  /**
20
19
  * Load standalone AGENTS.md files.
@@ -25,9 +24,8 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
25
24
 
26
25
  // Walk up from cwd looking for AGENTS.md files
27
26
  let current = ctx.cwd;
28
- let depth = 0;
29
27
 
30
- while (depth < MAX_DEPTH) {
28
+ while (true) {
31
29
  const candidate = path.join(current, "AGENTS.md");
32
30
  const content = await readFile(candidate);
33
31
 
@@ -49,11 +47,12 @@ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>>
49
47
  }
50
48
  }
51
49
 
50
+ if (current === (ctx.repoRoot ?? ctx.home)) break; // scanned repo root or home, stop
51
+
52
52
  // Move to parent directory
53
53
  const parent = path.dirname(current);
54
54
  if (parent === current) break; // Reached filesystem root
55
55
  current = parent;
56
- depth++;
57
56
  }
58
57
 
59
58
  return { items, warnings };