diff --git a/packages/coding-agent/src/cli/bench-cli.ts b/packages/coding-agent/src/cli/bench-cli.ts index 085271829..fd635e43f 100644 --- a/packages/coding-agent/src/cli/bench-cli.ts +++ b/packages/coding-agent/src/cli/bench-cli.ts @@ -30,7 +30,12 @@ import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSetting import { Settings } from "../config/settings"; import benchPrompt from "../prompts/bench.md" with { type: "text" }; import { discoverAuthStorage, loadCliExtensionProviders } from "../sdk"; -import { AUTO_THINKING, resolveThinkingLevelForModel, shouldDisableReasoning, toReasoningEffort } from "../thinking"; +import { + concreteThinkingLevel, + resolveThinkingLevelForModel, + shouldDisableReasoning, + toReasoningEffort, +} from "../thinking"; const DEFAULT_RUNS = 10; const DEFAULT_PAR = 4; @@ -477,10 +482,7 @@ function resolveBenchModels( resolved.push({ selector, model, - thinking: resolveThinkingLevelForModel( - model, - result.thinkingLevel === AUTO_THINKING ? undefined : result.thinkingLevel, - ), + thinking: resolveThinkingLevelForModel(model, concreteThinkingLevel(result.thinkingLevel)), }); } if (errors.length > 0) { diff --git a/packages/coding-agent/src/commit/model-selection.ts b/packages/coding-agent/src/commit/model-selection.ts index d4b499f44..b3f69c2ff 100644 --- a/packages/coding-agent/src/commit/model-selection.ts +++ b/packages/coding-agent/src/commit/model-selection.ts @@ -11,7 +11,7 @@ import { import { MODEL_ROLE_IDS } from "../config/model-roles"; import type { Settings } from "../config/settings"; import MODEL_PRIO from "../priority.json" with { type: "json" }; -import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../thinking"; +import { concreteThinkingLevel } from "../thinking"; export interface ResolvedCommitModel { model: Model; @@ -21,6 +21,11 @@ export interface ResolvedCommitModel { * central force-refresh + account-rotation policy. */ apiKey: ApiKey; + /** + * Commit-time inference is stateless: session-level auto classification + * isn't available, so an explicit `:auto` selector collapses to "no + * override" and the model's own default level fills in. + */ thinkingLevel?: ThinkingLevel; } @@ -29,13 +34,6 @@ type CommitModelRegistry = ModelLookupRegistry & getApiKey: (model: Model) => Promise; }; -// Commit-time inference is stateless: session-level auto classification isn't -// available, so an explicit `:auto` selector collapses to "no override" and -// the model's own default level fills in. -function coerceCommitThinkingLevel(level: ConfiguredThinkingLevel | undefined): ThinkingLevel | undefined { - return level === AUTO_THINKING ? undefined : level; -} - export async function resolvePrimaryModel( override: string | undefined, settings: Settings, @@ -57,7 +55,7 @@ export async function resolvePrimaryModel( return { model, apiKey: modelRegistry.resolver(model), - thinkingLevel: coerceCommitThinkingLevel(resolved?.thinkingLevel), + thinkingLevel: concreteThinkingLevel(resolved?.thinkingLevel), }; } @@ -75,7 +73,7 @@ export async function resolveSmolModel( return { model: resolvedSmol.model, apiKey: modelRegistry.resolver(resolvedSmol.model), - thinkingLevel: coerceCommitThinkingLevel(resolvedSmol.thinkingLevel), + thinkingLevel: concreteThinkingLevel(resolvedSmol.thinkingLevel), }; } } diff --git a/packages/coding-agent/src/config/model-resolver.ts b/packages/coding-agent/src/config/model-resolver.ts index d2930bc80..3daf4fa56 100644 --- a/packages/coding-agent/src/config/model-resolver.ts +++ b/packages/coding-agent/src/config/model-resolver.ts @@ -32,6 +32,7 @@ import MODEL_PRIO from "../priority.json" with { type: "json" }; import { AUTO_THINKING, type ConfiguredThinkingLevel, + concreteThinkingLevel, parseThinkingLevel, resolveThinkingLevelForModel, } from "../thinking"; @@ -136,12 +137,9 @@ function resolveGlobScopePattern( // Coerce the `auto` sentinel to a concrete-only view so scope callers stay // typed on `ThinkingLevel` and `enabledModels: [\"openai/*:auto\"]` doesn't // pin a stray per-model level. - const concrete = (level: ConfiguredThinkingLevel | undefined): ThinkingLevel | undefined => - level === AUTO_THINKING ? undefined : level; - const strictSuffix = splitThinkingSuffix(pattern); if (strictSuffix.level !== undefined) { - const thinkingLevel = concrete(strictSuffix.level); + const thinkingLevel = concreteThinkingLevel(strictSuffix.level); return { models: matchingGlobModels(strictSuffix.base, availableModels), thinkingLevel, @@ -155,7 +153,7 @@ function resolveGlobScopePattern( if (literalMatches.length > 0) { return { models: literalMatches, thinkingLevel: undefined, explicitThinkingLevel: false }; } - const thinkingLevel = concrete(maxSuffix.level); + const thinkingLevel = concreteThinkingLevel(maxSuffix.level); return { models: matchingGlobModels(maxSuffix.base, availableModels), thinkingLevel, diff --git a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts index a25dcee2b..4fbe42c17 100644 --- a/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts +++ b/packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts @@ -18,7 +18,7 @@ import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agen import type { TSchema } from "@oh-my-pi/pi-ai"; import { Text } from "@oh-my-pi/pi-tui"; import { parseFrontmatter as parseOmpFrontmatter } from "@oh-my-pi/pi-utils"; -import { Settings } from "../config/settings"; +import { type SettingPath, Settings } from "../config/settings"; import { EditTool } from "../edit"; import { DEFAULT_MAX_BYTES, @@ -46,6 +46,8 @@ type LegacyCodingToolName = (typeof LEGACY_CODING_TOOL_NAMES)[number]; type LegacyRegistryToolName = LegacyCodingToolName | "grep" | "glob"; type LegacyBuiltinToolDefinition = ToolDefinition & { [LEGACY_BUILTIN_TOOL_MARKER]: true }; +type LegacySettingOverrides = Partial>; + interface LegacyThemeLike { fg(color: string, text: string): string; bold(text: string): string; @@ -79,10 +81,18 @@ export interface BashToolOptions { } export interface ReadToolOptions { + /** Auto-resize large images; maps onto the `images.autoResize` setting. Default: true. */ autoResizeImages?: boolean; } export interface GrepToolOptions { + /** + * Unsupported. The historical grep operations seam (isDirectory/readFile for + * context lines) never delegated the search itself — ripgrep always ran + * locally — and the built-in native grep tool exposes no filesystem seam at + * all. Supplying operations throws at tool creation instead of silently + * searching the local filesystem. + */ operations?: unknown; } @@ -123,7 +133,6 @@ const legacyGrepSchema = Type.Object({ ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive search" })), literal: Type.Optional(Type.Boolean({ description: "Treat pattern as a literal string" })), context: Type.Optional(Type.Number({ description: "Context lines" })), - limit: Type.Optional(Type.Number({ description: "Maximum matches" })), }); const legacyFindSchema = Type.Object({ @@ -149,18 +158,22 @@ function markToolDefinition( return tool; } -function legacyToolSession(cwd: string): ToolSession { +function legacyToolSession(cwd: string, settingOverrides?: LegacySettingOverrides): ToolSession { return { cwd, hasUI: false, getSessionFile: () => null, getSessionSpawns: () => null, - settings: Settings.isolated(), + settings: Settings.isolated(settingOverrides), }; } -function createRegistryTool(cwd: string, name: LegacyRegistryToolName): Tool { - const session = legacyToolSession(cwd); +function createRegistryTool( + cwd: string, + name: LegacyRegistryToolName, + settingOverrides?: LegacySettingOverrides, +): Tool { + const session = legacyToolSession(cwd, settingOverrides); switch (name) { case "bash": return new BashTool(session); @@ -337,10 +350,6 @@ async function executeLegacyBashOperations( } } -function createLegacyTool(_cwd: string, definition: ToolDefinition): ToolDefinition { - return definition; -} - /** Parse frontmatter using the historical Pi package-root helper. */ export interface ParsedFrontmatter = Record> { frontmatter: T; @@ -368,8 +377,12 @@ export function defineTool 0) { // `auto` is a session-level concept only; per-scoped-model (Ctrl+P) thinking // overrides stay concrete, so coerce the auto default to "unset" here. - const defaultThinkingLevelSetting = parseConfiguredThinkingLevel(activeSettings.get("defaultThinkingLevel")); - const defaultThinkingLevel = - defaultThinkingLevelSetting === AUTO_THINKING ? undefined : defaultThinkingLevelSetting; + const defaultThinkingLevel = concreteThinkingLevel( + parseConfiguredThinkingLevel(activeSettings.get("defaultThinkingLevel")), + ); options.scopedModels = scopedModels.map(scopedModel => ({ model: scopedModel.model, thinkingLevel: scopedModel.explicitThinkingLevel diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index a09d8a1c6..ec061edae 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -157,7 +157,6 @@ export class SelectorController { const result = await previewTheme(themeName); if (result.success) { this.ctx.statusLine.invalidate(); - this.ctx.ui.requestRender(); this.ctx.ui.invalidate(); this.ctx.ui.requestRender(); } diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 9a9021eb2..5384bf7d5 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -2664,7 +2664,6 @@ export class InteractiveMode implements InteractiveModeContext { // plan-approved prompt is the source of the reference injection. this.session.markPlanReferenceSent(); const planModePrompt = prompt.render(planModeApprovedPrompt, { - planContent, planFilePath: options.planFilePath, contextPreserved: options.preserveContext === true, }); diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 063cb9357..002c95a8b 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -135,6 +135,7 @@ import { wrapStreamFnWithProviderConcurrency } from "./task/provider-concurrency import { AUTO_THINKING, type ConfiguredThinkingLevel, + concreteThinkingLevel, parseConfiguredThinkingLevel, parseThinkingLevel, resolveProvisionalAutoLevel, @@ -1343,7 +1344,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // Concrete level the agent/session start with. With `auto` this is the // provisional level shown until the first per-turn classification resolves; // `auto` itself stays a session-only concept handled by AgentSession. - let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel; + let effectiveThinkingLevel: ThinkingLevel | undefined = concreteThinkingLevel(thinkingLevel); if (model) { const resolvedModel = model; effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () => @@ -1941,7 +1942,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // `thinking.defaultLevel` must not become sticky. thinkingLevel = pickInitialThinkingLevel(restoredModel); autoThinking = thinkingLevel === AUTO_THINKING; - effectiveThinkingLevel = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel; + effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel); effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () => autoThinking ? resolveProvisionalAutoLevel(restoredModel) @@ -1997,7 +1998,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // so the role's explicit selector (e.g. `:max`) now applies. thinkingLevel = pickInitialThinkingLevel(resolvedDefaultModel); autoThinking = thinkingLevel === AUTO_THINKING; - effectiveThinkingLevel = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel; + effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel); effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () => autoThinking ? resolveProvisionalAutoLevel(resolvedDefaultModel) diff --git a/packages/coding-agent/src/slash-commands/builtin-registry.ts b/packages/coding-agent/src/slash-commands/builtin-registry.ts index 34b5eaaf9..7b4cb5d92 100644 --- a/packages/coding-agent/src/slash-commands/builtin-registry.ts +++ b/packages/coding-agent/src/slash-commands/builtin-registry.ts @@ -72,7 +72,6 @@ export interface TuiBuiltinSlashCommand extends BuiltinSlashCommand { function refreshStatusLine(ctx: InteractiveModeContext): void { ctx.statusLine.invalidate(); ctx.ui.requestRender(); - ctx.ui.requestRender(); } /** `/fast status` label for the active model: "on" when its family is priority, else "off". */ diff --git a/packages/coding-agent/src/thinking.ts b/packages/coding-agent/src/thinking.ts index 4853e49a1..7a6e83a8b 100644 --- a/packages/coding-agent/src/thinking.ts +++ b/packages/coding-agent/src/thinking.ts @@ -124,6 +124,11 @@ export const AUTO_THINKING = "auto" as const; /** A thinking selector as configured by the user — a concrete level or `auto`. */ export type ConfiguredThinkingLevel = ThinkingLevel | typeof AUTO_THINKING; +/** Maps the session-level `auto` sentinel to `undefined`; concrete levels pass through. */ +export function concreteThinkingLevel(level: ConfiguredThinkingLevel | undefined): ThinkingLevel | undefined { + return level === AUTO_THINKING ? undefined : level; +} + /** Metadata used to render the `auto` selector value alongside concrete levels. */ export interface ConfiguredThinkingLevelMetadata { value: ConfiguredThinkingLevel; diff --git a/packages/coding-agent/src/utils/commit-message-generator.ts b/packages/coding-agent/src/utils/commit-message-generator.ts index f78a26b63..8800e499e 100644 --- a/packages/coding-agent/src/utils/commit-message-generator.ts +++ b/packages/coding-agent/src/utils/commit-message-generator.ts @@ -12,7 +12,7 @@ import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model import type { Settings } from "../config/settings"; import MODEL_PRIO from "../priority.json" with { type: "json" }; import commitSystemPrompt from "../prompts/system/commit-message-system.md" with { type: "text" }; -import { AUTO_THINKING, toReasoningEffort } from "../thinking"; +import { concreteThinkingLevel, toReasoningEffort } from "../thinking"; const COMMIT_SYSTEM_PROMPT = prompt.render(commitSystemPrompt); const MAX_DIFF_CHARS = 4000; @@ -56,10 +56,7 @@ function getSmolModelCandidates( settings, matchPreferences, }); - addCandidate( - configuredSmol.model, - configuredSmol.thinkingLevel === AUTO_THINKING ? undefined : configuredSmol.thinkingLevel, - ); + addCandidate(configuredSmol.model, concreteThinkingLevel(configuredSmol.thinkingLevel)); for (const pattern of MODEL_PRIO.smol) { const needle = pattern.toLowerCase(); diff --git a/packages/coding-agent/test/tools/gh.test.ts b/packages/coding-agent/test/tools/gh.test.ts index 4fc28ac12..ddb885130 100644 --- a/packages/coding-agent/test/tools/gh.test.ts +++ b/packages/coding-agent/test/tools/gh.test.ts @@ -855,6 +855,8 @@ describe("github tool", () => { expect(runGit(remoteFixture.repoRoot, ["remote", "get-url", "forksrc"])).toBe(remoteFixture.forkBare); }); it("does not depend on localized git remote-add stderr for existing remotes", async () => { + // The shim is a bash script resolved via `which`; neither exists on Windows. + if (process.platform === "win32") return; const originalPath = process.env.PATH; const fakeBin = await fs.mkdtemp(path.join(os.tmpdir(), "omp-fake-git-")); const realGitResult = Bun.spawnSync(["which", "git"], { stdout: "pipe", stderr: "pipe" }); diff --git a/packages/coding-agent/test/tools/web-search-duckduckgo.test.ts b/packages/coding-agent/test/tools/web-search-duckduckgo.test.ts index 6fe92eadd..0c513a884 100644 --- a/packages/coding-agent/test/tools/web-search-duckduckgo.test.ts +++ b/packages/coding-agent/test/tools/web-search-duckduckgo.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai"; import { searchDuckDuckGo } from "@oh-my-pi/pi-coding-agent/web/search/providers/duckduckgo"; -import { SEARCH_PROVIDER_OPTIONS, SearchProviderError } from "@oh-my-pi/pi-coding-agent/web/search/types"; +import { SearchProviderError } from "@oh-my-pi/pi-coding-agent/web/search/types"; import { formatSearchProviderFailures } from "../../src/web/search/provider"; const fakeAuthStorage = { @@ -265,11 +265,4 @@ describe("DuckDuckGo web search provider", () => { expect(message).toContain("configure a credentialed provider"); expect(message).not.toContain("codex: 401 unauthorized"); }); - - it("documents DuckDuckGo as a best-effort datacenter-sensitive fallback", () => { - const option = SEARCH_PROVIDER_OPTIONS.find(item => item.value === "duckduckgo"); - - expect(option?.description).toContain("Credential-free best-effort fallback"); - expect(option?.description).toContain("datacenter/shared-egress IPs"); - }); });