@oh-my-pi/pi-coding-agent 13.10.0 → 13.11.0

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 (69) hide show
  1. package/CHANGELOG.md +52 -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/settings-schema.ts +20 -0
  15. package/src/config.ts +10 -3
  16. package/src/discovery/helpers.ts +7 -3
  17. package/src/internal-urls/docs-index.generated.ts +1 -1
  18. package/src/lsp/index.ts +4 -4
  19. package/src/lsp/utils.ts +81 -0
  20. package/src/main.ts +25 -14
  21. package/src/mcp/manager.ts +40 -2
  22. package/src/mcp/oauth-flow.ts +41 -0
  23. package/src/mcp/transports/http.ts +23 -0
  24. package/src/mcp/types.ts +6 -0
  25. package/src/modes/components/mcp-add-wizard.ts +12 -0
  26. package/src/modes/components/settings-defs.ts +2 -1
  27. package/src/modes/components/todo-reminder.ts +8 -1
  28. package/src/modes/controllers/command-controller.ts +75 -3
  29. package/src/modes/controllers/input-controller.ts +2 -3
  30. package/src/modes/controllers/mcp-command-controller.ts +9 -1
  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 +3 -6
  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/find.md +1 -0
  39. package/src/prompts/tools/grep.md +1 -0
  40. package/src/prompts/tools/hashline.md +23 -111
  41. package/src/prompts/tools/todo-write.md +11 -1
  42. package/src/sdk.ts +1 -1
  43. package/src/session/agent-session.ts +85 -7
  44. package/src/session/session-manager.ts +5 -9
  45. package/src/slash-commands/builtin-registry.ts +10 -2
  46. package/src/task/executor.ts +9 -18
  47. package/src/task/index.ts +8 -4
  48. package/src/task/render.ts +5 -10
  49. package/src/task/template.ts +4 -1
  50. package/src/task/types.ts +2 -0
  51. package/src/tools/ast-edit.ts +26 -7
  52. package/src/tools/ast-grep.ts +26 -9
  53. package/src/tools/fetch.ts +36 -5
  54. package/src/tools/find.ts +13 -64
  55. package/src/tools/grep.ts +27 -10
  56. package/src/tools/json-tree.ts +1 -1
  57. package/src/tools/output-meta.ts +2 -1
  58. package/src/tools/path-utils.ts +348 -0
  59. package/src/tools/todo-write.ts +27 -4
  60. package/src/utils/commit-message-generator.ts +27 -22
  61. package/src/utils/image-input.ts +1 -1
  62. package/src/utils/image-resize.ts +4 -4
  63. package/src/utils/title-generator.ts +36 -23
  64. package/src/utils/tool-choice.ts +28 -0
  65. package/src/web/parallel.ts +346 -0
  66. package/src/web/scrapers/youtube.ts +29 -0
  67. package/src/web/search/provider.ts +4 -1
  68. package/src/web/search/providers/parallel.ts +63 -0
  69. package/src/web/search/types.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,58 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.11.0] - 2026-03-12
6
+ ### Added
7
+
8
+ - Added Parallel as a web search provider with support for fast and research modes
9
+ - Added Parallel extract API integration for URL content fetching and YouTube video extraction
10
+ - Added `providers.parallelFetch` setting to enable/disable Parallel extract for URL fetching
11
+ - Added `/login parallel` command support for Parallel API authentication
12
+ - 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)
13
+ - Added support for copying last executed bash or python command via `/copy cmd` subcommand
14
+ - Added `assignment` field to task progress and result objects to track the raw per-task assignment text separately from the full templated task
15
+ - Added `details` field to todo items for storing implementation specifics, file paths, and edge cases (shown only when task is active)
16
+ - Added support for multi-line details in todo items with automatic indentation in interactive and reminder displays
17
+ - Added `todo.eager` setting to automatically create a comprehensive todo list after the first user message
18
+ - Added `buildNamedToolChoice` utility function to build provider-aware tool choice constraints for named tools
19
+ - 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/`)
20
+ - New `resolveMultiSearchPath` and `resolveMultiFindPattern` functions to handle multi-path search inputs with automatic common base path detection
21
+
22
+ ### Changed
23
+
24
+ - Updated HTML-to-text rendering to prefer Parallel extract when credentials are available, before falling back to jina, trafilatura, or lynx
25
+ - Updated YouTube scraper to prefer Parallel extract when credentials are available, before falling back to yt-dlp
26
+ - Updated web search provider priority order to include Parallel between Exa and Kagi
27
+ - Updated hashline tool documentation with explicit guidance on `replace` operation semantics, clarifying that `lines` must not extend past `end` to avoid unintended line duplication
28
+ - Improved diagnostic message formatting to group errors by file path with indented details for better readability
29
+ - Modified eager todo prelude to use hidden custom message type instead of visible developer message, preventing duplicate prompt text in session history
30
+ - Updated eager todo prompt to remove dynamic user request injection, simplifying the template and preventing request repetition in displayed messages
31
+ - 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
32
+ - Updated task rendering to display assignment text instead of full task template when available, reducing noise in progress and result displays
33
+ - Modified task section rendering to show trimmed assignment text without stripping context blocks, simplifying the display logic
34
+ - Updated todo item display to show `details` field indented below active tasks in both interactive mode and todo reminder component
35
+ - Modified tool choice resolution to support per-turn tool choice overrides via `consumeNextToolChoiceOverride()`
36
+ - Updated tool documentation to clarify that `path` parameter accepts files, directories, glob patterns, or comma/space-separated path lists
37
+ - Refactored path resolution logic in `find`, `grep`, `ast_grep`, and `ast_edit` tools to use unified multi-path handling
38
+
39
+ ### Fixed
40
+
41
+ - Fixed hashline line normalization to trim trailing whitespace and strip carriage returns instead of removing all whitespace, preserving intentional spacing in code
42
+ - 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
43
+ - Fixed path resolution to accept bare directory names without trailing slashes in comma/space-separated path lists (e.g., `apps packages phases`)
44
+ - 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
45
+
46
+ ## [13.10.1] - 2026-03-10
47
+ ### Added
48
+
49
+ - Exported `submitInteractiveInput()` function for programmatic submission of user input in interactive mode
50
+ - Added proactive OAuth token refresh for MCP server connections with 5-minute expiry buffer
51
+ - Added reactive 401/403 retry with automatic token refresh on HTTP MCP transports
52
+ - Added `refreshMCPOAuthToken()` for standard OAuth 2.0 refresh_token grants
53
+ - Persisted `tokenUrl`, `clientId`, and `clientSecret` in MCP auth config for cross-session token refresh
54
+ ### Fixed
55
+ - 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))
56
+
5
57
  ## [13.10.0] - 2026-03-10
6
58
  ### Fixed
7
59
 
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.0",
4
+ "version": "13.11.0",
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.0",
45
- "@oh-my-pi/pi-agent-core": "13.10.0",
46
- "@oh-my-pi/pi-ai": "13.10.0",
47
- "@oh-my-pi/pi-natives": "13.10.0",
48
- "@oh-my-pi/pi-tui": "13.10.0",
49
- "@oh-my-pi/pi-utils": "13.10.0",
44
+ "@oh-my-pi/omp-stats": "13.11.0",
45
+ "@oh-my-pi/pi-agent-core": "13.11.0",
46
+ "@oh-my-pi/pi-ai": "13.11.0",
47
+ "@oh-my-pi/pi-natives": "13.11.0",
48
+ "@oh-my-pi/pi-tui": "13.11.0",
49
+ "@oh-my-pi/pi-utils": "13.11.0",
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),
@@ -470,6 +470,15 @@ export const SETTINGS_SCHEMA = {
470
470
  submenu: true,
471
471
  },
472
472
  },
473
+ "todo.eager": {
474
+ type: "boolean",
475
+ default: false,
476
+ ui: {
477
+ tab: "agent",
478
+ label: "Eager todos",
479
+ description: "Automatically create a comprehensive todo list after the first message",
480
+ },
481
+ },
473
482
 
474
483
  // ─────────────────────────────────────────────────────────────────────────
475
484
  // Optional tools
@@ -833,6 +842,7 @@ export const SETTINGS_SCHEMA = {
833
842
  "tavily",
834
843
  "kagi",
835
844
  "synthetic",
845
+ "parallel",
836
846
  ] as const,
837
847
  default: "auto",
838
848
  ui: { tab: "services", label: "Web search provider", description: "Provider for web search tool", submenu: true },
@@ -871,6 +881,16 @@ export const SETTINGS_SCHEMA = {
871
881
  },
872
882
  },
873
883
 
884
+ "providers.parallelFetch": {
885
+ type: "boolean",
886
+ default: true,
887
+ ui: {
888
+ tab: "services",
889
+ label: "Parallel fetch",
890
+ description: "Use Parallel extract API for URL fetching when credentials are available",
891
+ },
892
+ },
893
+
874
894
  // ─────────────────────────────────────────────────────────────────────────
875
895
  // Exa settings
876
896
  // ─────────────────────────────────────────────────────────────────────────
package/src/config.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { CONFIG_DIR_NAME, getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
4
+ import {
5
+ CONFIG_DIR_NAME,
6
+ getAgentDir,
7
+ getConfigAgentDirName,
8
+ getProjectDir,
9
+ isEnoent,
10
+ logger,
11
+ } from "@oh-my-pi/pi-utils";
5
12
  import type { TSchema } from "@sinclair/typebox";
6
13
  import { Value } from "@sinclair/typebox/value";
7
14
  import { Ajv, type ErrorObject, type ValidateFunction } from "ajv";
@@ -9,7 +16,7 @@ import { JSONC, YAML } from "bun";
9
16
  import { expandTilde } from "./tools/path-utils";
10
17
 
11
18
  const priorityList = [
12
- { dir: CONFIG_DIR_NAME, globalAgentDir: `${CONFIG_DIR_NAME}/agent` },
19
+ { dir: CONFIG_DIR_NAME, globalAgentDir: getConfigAgentDirName },
13
20
  { dir: ".claude" },
14
21
  { dir: ".codex" },
15
22
  { dir: ".gemini" },
@@ -251,7 +258,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
251
258
  * Project-level: .omp, .claude, .codex, .gemini
252
259
  */
253
260
  const USER_CONFIG_BASES = priorityList.map(({ dir, globalAgentDir }) => ({
254
- base: () => path.join(os.homedir(), globalAgentDir ?? dir),
261
+ base: () => path.join(os.homedir(), globalAgentDir ? globalAgentDir() : dir),
255
262
  name: dir,
256
263
  }));
257
264