@oh-my-pi/pi-coding-agent 13.10.1 → 13.11.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 (76) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +7 -7
  3. package/src/commit/agentic/agent.ts +3 -1
  4. package/src/commit/agentic/index.ts +7 -1
  5. package/src/commit/analysis/conventional.ts +5 -1
  6. package/src/commit/analysis/summary.ts +5 -1
  7. package/src/commit/changelog/generate.ts +5 -1
  8. package/src/commit/changelog/index.ts +4 -0
  9. package/src/commit/map-reduce/index.ts +5 -0
  10. package/src/commit/map-reduce/map-phase.ts +17 -2
  11. package/src/commit/map-reduce/reduce-phase.ts +5 -1
  12. package/src/commit/model-selection.ts +38 -26
  13. package/src/commit/pipeline.ts +22 -11
  14. package/src/config/model-registry.ts +98 -17
  15. package/src/config/settings-schema.ts +31 -12
  16. package/src/config.ts +10 -3
  17. package/src/discovery/helpers.ts +10 -3
  18. package/src/exa/index.ts +1 -11
  19. package/src/exa/search.ts +1 -122
  20. package/src/internal-urls/docs-index.generated.ts +2 -2
  21. package/src/lsp/config.ts +1 -0
  22. package/src/lsp/defaults.json +3 -3
  23. package/src/lsp/index.ts +4 -4
  24. package/src/lsp/utils.ts +81 -0
  25. package/src/modes/components/settings-defs.ts +5 -0
  26. package/src/modes/components/todo-reminder.ts +8 -1
  27. package/src/modes/controllers/command-controller.ts +77 -3
  28. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  29. package/src/modes/controllers/input-controller.ts +2 -3
  30. package/src/modes/controllers/selector-controller.ts +18 -17
  31. package/src/modes/interactive-mode.ts +11 -7
  32. package/src/modes/theme/theme.ts +30 -27
  33. package/src/modes/types.ts +2 -1
  34. package/src/patch/hashline.ts +123 -22
  35. package/src/prompts/system/eager-todo.md +13 -0
  36. package/src/prompts/tools/ast-edit.md +1 -1
  37. package/src/prompts/tools/ast-grep.md +1 -1
  38. package/src/prompts/tools/code-search.md +45 -0
  39. package/src/prompts/tools/find.md +1 -0
  40. package/src/prompts/tools/grep.md +1 -0
  41. package/src/prompts/tools/hashline.md +26 -111
  42. package/src/prompts/tools/read.md +2 -2
  43. package/src/prompts/tools/todo-write.md +11 -1
  44. package/src/sdk.ts +20 -16
  45. package/src/session/agent-session.ts +85 -7
  46. package/src/session/streaming-output.ts +17 -54
  47. package/src/slash-commands/builtin-registry.ts +10 -2
  48. package/src/task/executor.ts +10 -19
  49. package/src/task/index.ts +8 -4
  50. package/src/task/render.ts +5 -10
  51. package/src/task/template.ts +4 -1
  52. package/src/task/types.ts +2 -0
  53. package/src/tools/ast-edit.ts +26 -7
  54. package/src/tools/ast-grep.ts +26 -9
  55. package/src/tools/exit-plan-mode.ts +6 -0
  56. package/src/tools/fetch.ts +37 -6
  57. package/src/tools/find.ts +13 -64
  58. package/src/tools/grep.ts +27 -10
  59. package/src/tools/output-meta.ts +10 -7
  60. package/src/tools/path-utils.ts +348 -0
  61. package/src/tools/read.ts +13 -26
  62. package/src/tools/todo-write.ts +27 -4
  63. package/src/utils/commit-message-generator.ts +27 -22
  64. package/src/utils/image-input.ts +1 -1
  65. package/src/utils/image-resize.ts +4 -4
  66. package/src/utils/title-generator.ts +36 -23
  67. package/src/utils/tool-choice.ts +28 -0
  68. package/src/web/parallel.ts +346 -0
  69. package/src/web/scrapers/youtube.ts +29 -0
  70. package/src/web/search/code-search.ts +385 -0
  71. package/src/web/search/index.ts +25 -280
  72. package/src/web/search/provider.ts +4 -1
  73. package/src/web/search/providers/parallel.ts +63 -0
  74. package/src/web/search/types.ts +29 -0
  75. package/src/exa/company.ts +0 -26
  76. package/src/exa/linkedin.ts +0 -26
package/CHANGELOG.md CHANGED
@@ -2,6 +2,75 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.11.1] - 2026-03-13
6
+
7
+ ### Added
8
+
9
+ - Added `llama.cpp` as local provider
10
+ - Added `code_search` tool supporting both Exa and grep.app providers for code snippet and documentation search
11
+ - Added `providers.codeSearch` setting to configure code search provider (exa or grep)
12
+ - Added grep.app integration for public code search with result ranking by context relevance
13
+
14
+ ### Changed
15
+
16
+ - Updated compact diff preview to include line hashes for visibility and integrity verification of unchanged and added lines
17
+ - Modified compact diff preview to track line number synchronization between old and new files when processing insertions and deletions
18
+ - Simplified web search tools: removed `web_search_deep`, `web_search_crawl`, `web_search_linkedin`, and `web_search_company` tools
19
+ - Removed `exa.enableLinkedin` and `exa.enableCompany` settings; LinkedIn and company research are no longer available
20
+ - Refactored code search to use pluggable provider system instead of Exa-only implementation
21
+
22
+ ### Removed
23
+
24
+ - Removed Exa LinkedIn search tool (`exa_linkedin`)
25
+ - Removed Exa company research tool (`exa_company`)
26
+ - Removed Exa deep search tool (`exa_search_deep`)
27
+ - Removed Exa URL crawl tool (`exa_crawl`)
28
+
29
+ ### Fixed
30
+
31
+ - Fixed line number parsing in compact diff preview to handle variable-width line number fields with leading whitespace
32
+
33
+ ## [13.11.0] - 2026-03-12
34
+ ### Added
35
+
36
+ - Added Parallel as a web search provider with support for fast and research modes
37
+ - Added Parallel extract API integration for URL content fetching and YouTube video extraction
38
+ - Added `providers.parallelFetch` setting to enable/disable Parallel extract for URL fetching
39
+ - Added `/login parallel` command support for Parallel API authentication
40
+ - Added subcommands to `/copy` command: `code` (copy last code block), `all` (copy all code blocks), `cmd` (copy last bash/python command), and `last` (copy full message)
41
+ - Added support for copying last executed bash or python command via `/copy cmd` subcommand
42
+ - Added `assignment` field to task progress and result objects to track the raw per-task assignment text separately from the full templated task
43
+ - Added `details` field to todo items for storing implementation specifics, file paths, and edge cases (shown only when task is active)
44
+ - Added support for multi-line details in todo items with automatic indentation in interactive and reminder displays
45
+ - Added `todo.eager` setting to automatically create a comprehensive todo list after the first user message
46
+ - Added `buildNamedToolChoice` utility function to build provider-aware tool choice constraints for named tools
47
+ - Support for comma/space-separated path lists in `find`, `grep`, `ast_grep`, and `ast_edit` tools (e.g., `apps/,packages/,phases/` or `apps/ packages/ phases/`)
48
+ - New `resolveMultiSearchPath` and `resolveMultiFindPattern` functions to handle multi-path search inputs with automatic common base path detection
49
+
50
+ ### Changed
51
+
52
+ - Updated HTML-to-text rendering to prefer Parallel extract when credentials are available, before falling back to jina, trafilatura, or lynx
53
+ - Updated YouTube scraper to prefer Parallel extract when credentials are available, before falling back to yt-dlp
54
+ - Updated web search provider priority order to include Parallel between Exa and Kagi
55
+ - Updated hashline tool documentation with explicit guidance on `replace` operation semantics, clarifying that `lines` must not extend past `end` to avoid unintended line duplication
56
+ - Improved diagnostic message formatting to group errors by file path with indented details for better readability
57
+ - Modified eager todo prelude to use hidden custom message type instead of visible developer message, preventing duplicate prompt text in session history
58
+ - Updated eager todo prompt to remove dynamic user request injection, simplifying the template and preventing request repetition in displayed messages
59
+ - Modified eager todo enforcement to prepend the todo reminder to the first user turn instead of executing it as a separate synthetic turn, reducing unnecessary prompt calls
60
+ - Updated task rendering to display assignment text instead of full task template when available, reducing noise in progress and result displays
61
+ - Modified task section rendering to show trimmed assignment text without stripping context blocks, simplifying the display logic
62
+ - Updated todo item display to show `details` field indented below active tasks in both interactive mode and todo reminder component
63
+ - Modified tool choice resolution to support per-turn tool choice overrides via `consumeNextToolChoiceOverride()`
64
+ - Updated tool documentation to clarify that `path` parameter accepts files, directories, glob patterns, or comma/space-separated path lists
65
+ - Refactored path resolution logic in `find`, `grep`, `ast_grep`, and `ast_edit` tools to use unified multi-path handling
66
+
67
+ ### Fixed
68
+
69
+ - Fixed hashline line normalization to trim trailing whitespace and strip carriage returns instead of removing all whitespace, preserving intentional spacing in code
70
+ - Fixed noop detection in hashline replace operations to check array length equality before comparing lines, preventing false noop classification when single-line replacements expand to multiple lines
71
+ - Fixed path resolution to accept bare directory names without trailing slashes in comma/space-separated path lists (e.g., `apps packages phases`)
72
+ - Per-role `modelRoles` thinking selectors now propagate through commit/title helper model selection, legacy commit analysis, and agentic commit sessions while preserving default thinking inheritance when no role override is configured
73
+
5
74
  ## [13.10.1] - 2026-03-10
6
75
  ### Added
7
76
 
@@ -10,6 +79,8 @@
10
79
  - Added reactive 401/403 retry with automatic token refresh on HTTP MCP transports
11
80
  - Added `refreshMCPOAuthToken()` for standard OAuth 2.0 refresh_token grants
12
81
  - Persisted `tokenUrl`, `clientId`, and `clientSecret` in MCP auth config for cross-session token refresh
82
+ ### Fixed
83
+ - Respected `PI_CONFIG_DIR` when discovering native user config paths for slash commands and related config directories ([#349](https://github.com/can1357/oh-my-pi/issues/349))
13
84
 
14
85
  ## [13.10.0] - 2026-03-10
15
86
  ### Fixed
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.10.1",
4
+ "version": "13.11.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.10.1",
45
- "@oh-my-pi/pi-agent-core": "13.10.1",
46
- "@oh-my-pi/pi-ai": "13.10.1",
47
- "@oh-my-pi/pi-natives": "13.10.1",
48
- "@oh-my-pi/pi-tui": "13.10.1",
49
- "@oh-my-pi/pi-utils": "13.10.1",
44
+ "@oh-my-pi/omp-stats": "13.11.1",
45
+ "@oh-my-pi/pi-agent-core": "13.11.1",
46
+ "@oh-my-pi/pi-ai": "13.11.1",
47
+ "@oh-my-pi/pi-natives": "13.11.1",
48
+ "@oh-my-pi/pi-tui": "13.11.1",
49
+ "@oh-my-pi/pi-utils": "13.11.1",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -1,4 +1,4 @@
1
- import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
1
+ import { INTENT_FIELD, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import { Markdown } from "@oh-my-pi/pi-tui";
4
4
  import chalk from "chalk";
@@ -20,6 +20,7 @@ export interface CommitAgentInput {
20
20
  cwd: string;
21
21
  git: ControlledGit;
22
22
  model: Model<Api>;
23
+ thinkingLevel?: ThinkingLevel;
23
24
  settings: Settings;
24
25
  modelRegistry: ModelRegistry;
25
26
  authStorage: AuthStorage;
@@ -61,6 +62,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
61
62
  modelRegistry: input.modelRegistry,
62
63
  settings: input.settings,
63
64
  model: input.model,
65
+ thinkingLevel: input.thinkingLevel,
64
66
  systemPrompt,
65
67
  customTools: tools,
66
68
  enableLsp: false,
@@ -48,7 +48,12 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
48
48
  const { model: primaryModel, apiKey: primaryApiKey } = primaryModelResult;
49
49
  process.stdout.write(` └─ ${primaryModel.name}\n`);
50
50
 
51
- const { model: agentModel } = await resolveSmolModel(settings, modelRegistry, primaryModel, primaryApiKey);
51
+ const { model: agentModel, thinkingLevel: agentThinkingLevel } = await resolveSmolModel(
52
+ settings,
53
+ modelRegistry,
54
+ primaryModel,
55
+ primaryApiKey,
56
+ );
52
57
 
53
58
  if (stagedFiles.length === 0) {
54
59
  process.stderr.write("No changes to commit.\n");
@@ -126,6 +131,7 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
126
131
  cwd,
127
132
  git,
128
133
  model: agentModel,
134
+ thinkingLevel: agentThinkingLevel,
129
135
  settings,
130
136
  modelRegistry,
131
137
  authStorage,
@@ -1,3 +1,4 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
3
4
  import { Type } from "@sinclair/typebox";
@@ -5,6 +6,7 @@ import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with
5
6
  import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
6
7
  import type { ChangelogCategory, ConventionalAnalysis } from "../../commit/types";
7
8
  import { renderPromptTemplate } from "../../config/prompt-templates";
9
+ import { toReasoningEffort } from "../../thinking";
8
10
  import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
9
11
 
10
12
  const ConventionalAnalysisTool = {
@@ -49,6 +51,7 @@ const ConventionalAnalysisTool = {
49
51
  export interface ConventionalAnalysisInput {
50
52
  model: Model<Api>;
51
53
  apiKey: string;
54
+ thinkingLevel?: ThinkingLevel;
52
55
  contextFiles?: Array<{ path: string; content: string }>;
53
56
  userContext?: string;
54
57
  typesDescription?: string;
@@ -64,6 +67,7 @@ export interface ConventionalAnalysisInput {
64
67
  export async function generateConventionalAnalysis({
65
68
  model,
66
69
  apiKey,
70
+ thinkingLevel,
67
71
  contextFiles,
68
72
  userContext,
69
73
  typesDescription,
@@ -89,7 +93,7 @@ export async function generateConventionalAnalysis({
89
93
  messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
90
94
  tools: [ConventionalAnalysisTool],
91
95
  },
92
- { apiKey, maxTokens: 2400 },
96
+ { apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
93
97
  );
94
98
 
95
99
  return parseAnalysisFromResponse(response);
@@ -1,3 +1,4 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
3
4
  import { Type } from "@sinclair/typebox";
@@ -5,6 +6,7 @@ import summarySystemPrompt from "../../commit/prompts/summary-system.md" with {
5
6
  import summaryUserPrompt from "../../commit/prompts/summary-user.md" with { type: "text" };
6
7
  import type { CommitSummary } from "../../commit/types";
7
8
  import { renderPromptTemplate } from "../../config/prompt-templates";
9
+ import { toReasoningEffort } from "../../thinking";
8
10
  import { extractTextContent, extractToolCall } from "../utils";
9
11
 
10
12
  const SummaryTool = {
@@ -18,6 +20,7 @@ const SummaryTool = {
18
20
  export interface SummaryInput {
19
21
  model: Model<Api>;
20
22
  apiKey: string;
23
+ thinkingLevel?: ThinkingLevel;
21
24
  commitType: string;
22
25
  scope: string | null;
23
26
  details: string[];
@@ -32,6 +35,7 @@ export interface SummaryInput {
32
35
  export async function generateSummary({
33
36
  model,
34
37
  apiKey,
38
+ thinkingLevel,
35
39
  commitType,
36
40
  scope,
37
41
  details,
@@ -53,7 +57,7 @@ export async function generateSummary({
53
57
  messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }],
54
58
  tools: [SummaryTool],
55
59
  },
56
- { apiKey, maxTokens: 200 },
60
+ { apiKey, maxTokens: 200, reasoning: toReasoningEffort(thinkingLevel) },
57
61
  );
58
62
 
59
63
  return parseSummaryFromResponse(response, commitType, scope);
@@ -1,3 +1,4 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
3
4
  import { type TSchema, Type } from "@sinclair/typebox";
@@ -5,6 +6,7 @@ import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" wit
5
6
  import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
6
7
  import { CHANGELOG_CATEGORIES, type ChangelogCategory, type ChangelogGenerationResult } from "../../commit/types";
7
8
  import { renderPromptTemplate } from "../../config/prompt-templates";
9
+ import { toReasoningEffort } from "../../thinking";
8
10
  import { extractTextContent, extractToolCall, parseJsonPayload } from "../utils";
9
11
 
10
12
  const changelogEntryProperties = CHANGELOG_CATEGORIES.reduce<Record<ChangelogCategory, TSchema>>(
@@ -28,6 +30,7 @@ export const changelogTool = {
28
30
  export interface ChangelogPromptInput {
29
31
  model: Model<Api>;
30
32
  apiKey: string;
33
+ thinkingLevel?: ThinkingLevel;
31
34
  changelogPath: string;
32
35
  isPackageChangelog: boolean;
33
36
  existingEntries?: string;
@@ -38,6 +41,7 @@ export interface ChangelogPromptInput {
38
41
  export async function generateChangelogEntries({
39
42
  model,
40
43
  apiKey,
44
+ thinkingLevel,
41
45
  changelogPath,
42
46
  isPackageChangelog,
43
47
  existingEntries,
@@ -58,7 +62,7 @@ export async function generateChangelogEntries({
58
62
  messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
59
63
  tools: [changelogTool],
60
64
  },
61
- { apiKey, maxTokens: 1200 },
65
+ { apiKey, maxTokens: 1200, reasoning: toReasoningEffort(thinkingLevel) },
62
66
  );
63
67
 
64
68
  const parsed = parseChangelogResponse(response);
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
3
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
4
  import { logger } from "@oh-my-pi/pi-utils";
4
5
  import type { ControlledGit } from "../../commit/git";
@@ -16,6 +17,7 @@ export interface ChangelogFlowInput {
16
17
  cwd: string;
17
18
  model: Model<Api>;
18
19
  apiKey: string;
20
+ thinkingLevel?: ThinkingLevel;
19
21
  stagedFiles: string[];
20
22
  dryRun: boolean;
21
23
  maxDiffChars?: number;
@@ -42,6 +44,7 @@ export async function runChangelogFlow({
42
44
  cwd,
43
45
  model,
44
46
  apiKey,
47
+ thinkingLevel,
45
48
  stagedFiles,
46
49
  dryRun,
47
50
  maxDiffChars,
@@ -72,6 +75,7 @@ export async function runChangelogFlow({
72
75
  const generated = await generateChangelogEntries({
73
76
  model,
74
77
  apiKey,
78
+ thinkingLevel,
75
79
  changelogPath: boundary.changelogPath,
76
80
  isPackageChangelog,
77
81
  existingEntries: existingEntries || undefined,
@@ -1,3 +1,4 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
2
3
  import { $env } from "@oh-my-pi/pi-utils";
3
4
  import { parseFileDiffs } from "../../commit/git/diff";
@@ -21,8 +22,10 @@ export interface MapReduceSettings {
21
22
  export interface MapReduceInput {
22
23
  model: Model<Api>;
23
24
  apiKey: string;
25
+ thinkingLevel?: ThinkingLevel;
24
26
  smolModel: Model<Api>;
25
27
  smolApiKey: string;
28
+ smolThinkingLevel?: ThinkingLevel;
26
29
  diff: string;
27
30
  stat: string;
28
31
  scopeCandidates: string;
@@ -50,12 +53,14 @@ export async function runMapReduceAnalysis(input: MapReduceInput): Promise<Conve
50
53
  const observations = await runMapPhase({
51
54
  model: input.smolModel,
52
55
  apiKey: input.smolApiKey,
56
+ thinkingLevel: input.smolThinkingLevel,
53
57
  files: fileDiffs,
54
58
  config: input.settings,
55
59
  });
56
60
  return runReducePhase({
57
61
  model: input.model,
58
62
  apiKey: input.apiKey,
63
+ thinkingLevel: input.thinkingLevel,
59
64
  observations,
60
65
  stat: input.stat,
61
66
  scopeCandidates: input.scopeCandidates,
@@ -1,3 +1,4 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
2
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
3
4
  import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
@@ -5,6 +6,7 @@ import fileObserverUserPrompt from "../../commit/prompts/file-observer-user.md"
5
6
  import type { FileDiff, FileObservation } from "../../commit/types";
6
7
  import { isExcludedFile } from "../../commit/utils/exclusions";
7
8
  import { renderPromptTemplate } from "../../config/prompt-templates";
9
+ import { toReasoningEffort } from "../../thinking";
8
10
  import { truncateToTokenLimit } from "./utils";
9
11
 
10
12
  const MAX_FILE_TOKENS = 50_000;
@@ -17,6 +19,7 @@ const RETRY_BACKOFF_MS = 1000;
17
19
  export interface MapPhaseInput {
18
20
  model: Model<Api>;
19
21
  apiKey: string;
22
+ thinkingLevel?: ThinkingLevel;
20
23
  files: FileDiff[];
21
24
  config?: {
22
25
  maxFileTokens?: number;
@@ -27,7 +30,13 @@ export interface MapPhaseInput {
27
30
  };
28
31
  }
29
32
 
30
- export async function runMapPhase({ model, apiKey, files, config }: MapPhaseInput): Promise<FileObservation[]> {
33
+ export async function runMapPhase({
34
+ model,
35
+ apiKey,
36
+ thinkingLevel,
37
+ files,
38
+ config,
39
+ }: MapPhaseInput): Promise<FileObservation[]> {
31
40
  const filtered = files.filter(file => !isExcludedFile(file.filename));
32
41
  const systemPrompt = renderPromptTemplate(fileObserverSystemPrompt);
33
42
  const maxFileTokens = config?.maxFileTokens ?? MAX_FILE_TOKENS;
@@ -58,7 +67,13 @@ export async function runMapPhase({ model, apiKey, files, config }: MapPhaseInpu
58
67
  };
59
68
 
60
69
  const response = await withRetry(
61
- () => completeSimple(model, request, { apiKey, maxTokens: 400, signal: AbortSignal.timeout(timeoutMs) }),
70
+ () =>
71
+ completeSimple(model, request, {
72
+ apiKey,
73
+ maxTokens: 400,
74
+ reasoning: toReasoningEffort(thinkingLevel),
75
+ signal: AbortSignal.timeout(timeoutMs),
76
+ }),
62
77
  maxRetries,
63
78
  retryBackoffMs,
64
79
  );
@@ -1,3 +1,4 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
2
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
3
4
  import { Type } from "@sinclair/typebox";
@@ -5,6 +6,7 @@ import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { ty
5
6
  import reduceUserPrompt from "../../commit/prompts/reduce-user.md" with { type: "text" };
6
7
  import type { ChangelogCategory, ConventionalAnalysis, FileObservation } from "../../commit/types";
7
8
  import { renderPromptTemplate } from "../../config/prompt-templates";
9
+ import { toReasoningEffort } from "../../thinking";
8
10
  import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
9
11
 
10
12
  const ReduceTool = {
@@ -49,6 +51,7 @@ const ReduceTool = {
49
51
  export interface ReducePhaseInput {
50
52
  model: Model<Api>;
51
53
  apiKey: string;
54
+ thinkingLevel?: ThinkingLevel;
52
55
  observations: FileObservation[];
53
56
  stat: string;
54
57
  scopeCandidates: string;
@@ -58,6 +61,7 @@ export interface ReducePhaseInput {
58
61
  export async function runReducePhase({
59
62
  model,
60
63
  apiKey,
64
+ thinkingLevel,
61
65
  observations,
62
66
  stat,
63
67
  scopeCandidates,
@@ -76,7 +80,7 @@ export async function runReducePhase({
76
80
  messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
77
81
  tools: [ReduceTool],
78
82
  },
79
- { apiKey, maxTokens: 2400 },
83
+ { apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
80
84
  );
81
85
 
82
86
  return parseAnalysisResponse(response);
@@ -1,14 +1,34 @@
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
2
3
  import { MODEL_ROLE_IDS } from "../config/model-registry";
3
- import {
4
- expandRoleAlias,
5
- parseModelPattern,
6
- resolveModelFromSettings,
7
- resolveModelFromString,
8
- } from "../config/model-resolver";
4
+ import { parseModelPattern, resolveModelRoleValue } from "../config/model-resolver";
9
5
  import type { Settings } from "../config/settings";
10
6
  import MODEL_PRIO from "../priority.json" with { type: "json" };
11
7
 
8
+ export interface ResolvedCommitModel {
9
+ model: Model<Api>;
10
+ apiKey: string;
11
+ thinkingLevel?: ThinkingLevel;
12
+ }
13
+
14
+ function resolveRoleSelection(
15
+ roles: readonly string[],
16
+ settings: Settings,
17
+ availableModels: Model<Api>[],
18
+ ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
19
+ const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
20
+ for (const role of roles) {
21
+ const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
22
+ settings,
23
+ matchPreferences,
24
+ });
25
+ if (resolved.model) {
26
+ return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
27
+ }
28
+ }
29
+ return undefined;
30
+ }
31
+
12
32
  export async function resolvePrimaryModel(
13
33
  override: string | undefined,
14
34
  settings: Settings,
@@ -16,18 +36,13 @@ export async function resolvePrimaryModel(
16
36
  getAvailable: () => Model<Api>[];
17
37
  getApiKey: (model: Model<Api>) => Promise<string | undefined>;
18
38
  },
19
- ): Promise<{ model: Model<Api>; apiKey: string }> {
39
+ ): Promise<ResolvedCommitModel> {
20
40
  const available = modelRegistry.getAvailable();
21
41
  const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
22
- const roleOrder = ["commit", "smol", ...MODEL_ROLE_IDS] as const;
23
- const model = override
24
- ? resolveModelFromString(expandRoleAlias(override, settings), available, matchPreferences)
25
- : resolveModelFromSettings({
26
- settings,
27
- availableModels: available,
28
- matchPreferences,
29
- roleOrder,
30
- });
42
+ const resolved = override
43
+ ? resolveModelRoleValue(override, available, { settings, matchPreferences })
44
+ : resolveRoleSelection(["commit", "smol", ...MODEL_ROLE_IDS], settings, available);
45
+ const model = resolved?.model;
31
46
  if (!model) {
32
47
  throw new Error("No model available for commit generation");
33
48
  }
@@ -35,7 +50,7 @@ export async function resolvePrimaryModel(
35
50
  if (!apiKey) {
36
51
  throw new Error(`No API key available for model ${model.provider}/${model.id}`);
37
52
  }
38
- return { model, apiKey };
53
+ return { model, apiKey, thinkingLevel: resolved?.thinkingLevel };
39
54
  }
40
55
 
41
56
  export async function resolveSmolModel(
@@ -46,18 +61,15 @@ export async function resolveSmolModel(
46
61
  },
47
62
  fallbackModel: Model<Api>,
48
63
  fallbackApiKey: string,
49
- ): Promise<{ model: Model<Api>; apiKey: string }> {
64
+ ): Promise<ResolvedCommitModel> {
50
65
  const available = modelRegistry.getAvailable();
51
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
52
- const role = settings.getModelRole("smol");
53
- const roleModel = role
54
- ? resolveModelFromString(expandRoleAlias(role, settings), available, matchPreferences)
55
- : undefined;
56
- if (roleModel) {
57
- const apiKey = await modelRegistry.getApiKey(roleModel);
58
- if (apiKey) return { model: roleModel, apiKey };
66
+ const resolvedSmol = resolveRoleSelection(["smol"], settings, available);
67
+ if (resolvedSmol?.model) {
68
+ const apiKey = await modelRegistry.getApiKey(resolvedSmol.model);
69
+ if (apiKey) return { model: resolvedSmol.model, apiKey, thinkingLevel: resolvedSmol.thinkingLevel };
59
70
  }
60
71
 
72
+ const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
61
73
  for (const pattern of MODEL_PRIO.smol) {
62
74
  const candidate = parseModelPattern(pattern, available, matchPreferences).model;
63
75
  if (!candidate) continue;
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
3
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
4
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
4
5
  import { ModelRegistry } from "../config/model-registry";
@@ -45,17 +46,16 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
45
46
  const modelRegistry = new ModelRegistry(authStorage);
46
47
  await modelRegistry.refresh();
47
48
 
48
- const { model: primaryModel, apiKey: primaryApiKey } = await resolvePrimaryModel(
49
- args.model,
50
- settings,
51
- modelRegistry,
52
- );
53
- const { model: smolModel, apiKey: smolApiKey } = await resolveSmolModel(
54
- settings,
55
- modelRegistry,
56
- primaryModel,
57
- primaryApiKey,
58
- );
49
+ const {
50
+ model: primaryModel,
51
+ apiKey: primaryApiKey,
52
+ thinkingLevel: primaryThinkingLevel,
53
+ } = await resolvePrimaryModel(args.model, settings, modelRegistry);
54
+ const {
55
+ model: smolModel,
56
+ apiKey: smolApiKey,
57
+ thinkingLevel: smolThinkingLevel,
58
+ } = await resolveSmolModel(settings, modelRegistry, primaryModel, primaryApiKey);
59
59
 
60
60
  const git = new ControlledGit(cwd);
61
61
  let stagedFiles = await git.getStagedFiles();
@@ -75,6 +75,7 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
75
75
  cwd,
76
76
  model: primaryModel,
77
77
  apiKey: primaryApiKey,
78
+ thinkingLevel: primaryThinkingLevel,
78
79
  stagedFiles,
79
80
  dryRun: args.dryRun,
80
81
  maxDiffChars: commitSettings.changelogMaxDiffChars,
@@ -101,8 +102,10 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
101
102
  userContext: args.context,
102
103
  primaryModel,
103
104
  primaryApiKey,
105
+ primaryThinkingLevel,
104
106
  smolModel,
105
107
  smolApiKey,
108
+ smolThinkingLevel,
106
109
  commitSettings,
107
110
  });
108
111
 
@@ -116,6 +119,7 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
116
119
  stat,
117
120
  model: primaryModel,
118
121
  apiKey: primaryApiKey,
122
+ thinkingLevel: primaryThinkingLevel,
119
123
  userContext: args.context,
120
124
  });
121
125
 
@@ -144,8 +148,10 @@ async function generateAnalysis(input: {
144
148
  userContext?: string;
145
149
  primaryModel: Model<Api>;
146
150
  primaryApiKey: string;
151
+ primaryThinkingLevel?: ThinkingLevel;
147
152
  smolModel: Model<Api>;
148
153
  smolApiKey: string;
154
+ smolThinkingLevel?: ThinkingLevel;
149
155
  commitSettings: {
150
156
  mapReduceEnabled: boolean;
151
157
  mapReduceMinFiles: number;
@@ -166,8 +172,10 @@ async function generateAnalysis(input: {
166
172
  return runMapReduceAnalysis({
167
173
  model: input.primaryModel,
168
174
  apiKey: input.primaryApiKey,
175
+ thinkingLevel: input.primaryThinkingLevel,
169
176
  smolModel: input.smolModel,
170
177
  smolApiKey: input.smolApiKey,
178
+ smolThinkingLevel: input.smolThinkingLevel,
171
179
  diff: input.diff,
172
180
  stat: input.stat,
173
181
  scopeCandidates: input.scopeCandidates,
@@ -185,6 +193,7 @@ async function generateAnalysis(input: {
185
193
  return generateConventionalAnalysis({
186
194
  model: input.primaryModel,
187
195
  apiKey: input.primaryApiKey,
196
+ thinkingLevel: input.primaryThinkingLevel,
188
197
  contextFiles: input.contextFiles,
189
198
  userContext: input.userContext,
190
199
  typesDescription: TYPES_DESCRIPTION,
@@ -200,6 +209,7 @@ async function generateSummaryWithRetry(input: {
200
209
  stat: string;
201
210
  model: Model<Api>;
202
211
  apiKey: string;
212
+ thinkingLevel?: ThinkingLevel;
203
213
  userContext?: string;
204
214
  }): Promise<{ summary: string }> {
205
215
  let context = input.userContext;
@@ -207,6 +217,7 @@ async function generateSummaryWithRetry(input: {
207
217
  const result = await generateSummary({
208
218
  model: input.model,
209
219
  apiKey: input.apiKey,
220
+ thinkingLevel: input.thinkingLevel,
210
221
  commitType: input.analysis.type,
211
222
  scope: input.analysis.scope,
212
223
  details: input.analysis.details.map(detail => detail.text),