diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 48aea9f8c..b9d744f1c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,7 @@ - Added a `description` field for MCP servers (`pi mcp add --description`), shown with the server in the system prompt and used to rank its tools in tool search, and a `describeNamespace(name)` codemode helper that returns a namespace's instructions and tool names. `describeNamespace()` and `searchTools()` accept a namespace as `mcp__dev-radius`, `mcp__dev_radius`, `dev-radius`, or `dev_radius`. - Added an `oauth.clientName` setting for MCP servers (`pi mcp add --oauth-client-name`) to change the client name sent during OAuth client registration, for servers such as Figma that only accept known clients ([#10226](https://github.com/earendil-works/pi/issues/10226)). +- `/reload` now enables tools newly added to the `defaultTools` setting. Tools removed from it stay enabled, tools turned off during the session stay off unless newly added, and `--tools`, `--no-tools`, and `--no-builtin-tools` still override the setting ([#10245](https://github.com/earendil-works/pi/issues/10245)). ### Changed diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 5666a4da5..42c7871d7 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -53,6 +53,8 @@ A list of only `+name` and `-name` entries changes the inherited selection inste This replaces `bash` with `powershell` and enables `grep`: `["-bash", "+powershell", "+grep"]`. Project settings apply on top of user settings: a project list with only `+name` and `-name` entries changes the user's selection, and a project list with a plain name replaces it. In one list, plain names form the selection, and `+name` and `-name` then apply in order. +`/reload` enables tools newly added to `defaultTools`. It does not disable tools removed from it or re-enable unchanged tools you turned off. `--tools`, `--no-tools`, and `--no-builtin-tools` override `defaultTools`, also on reload. + CLI tool options override this setting for one invocation; `--tools` does not accept `+name` or `-name`. See [Command Line](cli.md#tools). ## Sessions and context diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 5c1d4b35d..c300e123c 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -129,7 +129,7 @@ import { SessionManager, type SessionProjection, } from "./session-manager.ts"; -import type { CacheWarmingMode, SettingsManager } from "./settings-manager.ts"; +import { type CacheWarmingMode, DEFAULT_TOOL_NAMES, type SettingsManager } from "./settings-manager.ts"; import type { SlashCommandInfo } from "./slash-commands.ts"; import { BUILTIN_PATH_PREFIX, createSyntheticSourceInfo, isSyntheticPath, type SourceInfo } from "./source-info.ts"; import { @@ -260,6 +260,11 @@ export interface AgentSessionConfig { cacheWarmer?: Pick; /** Initial active built-in tool names. Default: [read, bash, edit, write] */ initialActiveToolNames?: string[]; + /** + * Whether the initial tools come from the `defaultTools` setting. When true, reload activates + * tools newly added to the setting. Tools removed from it stay active. + */ + usesDefaultTools?: boolean; /** Optional allowlist of tool names. When provided, only these tool names are exposed. */ allowedToolNames?: string[]; /** Optional denylist of tool names. When provided, these tool names are not exposed. */ @@ -418,6 +423,7 @@ export class AgentSession { private _cwd: string; private _extensionRunnerRef?: { current?: ExtensionRunner }; private _initialActiveToolNames?: string[]; + private _usesDefaultTools: boolean; private _allowedToolNames?: Set; private _excludedToolNames?: Set; private _baseToolsOverride?: Record; @@ -462,6 +468,7 @@ export class AgentSession { } this._extensionRunnerRef = config.extensionRunnerRef; this._initialActiveToolNames = config.initialActiveToolNames; + this._usesDefaultTools = config.usesDefaultTools ?? false; this._allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined; this._excludedToolNames = config.excludedToolNames ? new Set(config.excludedToolNames) : undefined; this._baseToolsOverride = config.baseToolsOverride; @@ -3582,12 +3589,22 @@ export class AgentSession { const previousFlagValues = oldRunner.getFlagValues(); await emitSessionShutdownEvent(oldRunner, { type: "session_shutdown", reason: "reload" }); oldRunner.invalidate(); + const previousDefaultTools = new Set( + this._usesDefaultTools ? (this.settingsManager.getDefaultTools() ?? DEFAULT_TOOL_NAMES) : [], + ); await this.settingsManager.reload(); this.syncQueueModesFromSettings(); resetApiProviders(); await this._resourceLoader.reload(); + // Activate tools newly added to defaultTools. Removed ones stay active, and tools disabled + // during the session stay disabled unless the setting newly adds them. + const addedDefaultTools = this._usesDefaultTools + ? (this.settingsManager.getDefaultTools() ?? DEFAULT_TOOL_NAMES).filter( + (name) => !previousDefaultTools.has(name), + ) + : []; this._buildRuntime({ - activeToolNames: this.getActiveToolNames(), + activeToolNames: [...this.getActiveToolNames(), ...addedDefaultTools], flagValues: previousFlagValues, includeAllExtensionTools: true, }); diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 217c7b291..035304a2c 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -445,6 +445,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} modelRuntime, cacheWarmer, initialActiveToolNames, + usesDefaultTools: options.tools === undefined && !options.noTools, allowedToolNames, excludedToolNames, extensionRunnerRef, diff --git a/packages/coding-agent/test/default-tools-setting.test.ts b/packages/coding-agent/test/default-tools-setting.test.ts index c354d06c1..9030fb360 100644 --- a/packages/coding-agent/test/default-tools-setting.test.ts +++ b/packages/coding-agent/test/default-tools-setting.test.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getModel } from "@earendil-works/pi-ai/compat"; @@ -156,6 +156,84 @@ describe("defaultTools setting", () => { toolLessSession.dispose(); }); + describe("reload", () => { + const inactiveTool: InlineExtension = (pi) => { + pi.registerTool({ + name: "inactive_tool", + label: "Inactive Tool", + description: "Extension tool registered inactive", + parameters: Type.Object({}), + execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }), + defaultActive: false, + }); + }; + + const writeSettings = (settings: object) => + writeFileSync(join(agentDir, "settings.json"), JSON.stringify(settings)); + + async function createFileSession(options: ToolOptions = {}) { + const settingsManager = SettingsManager.create(tempDir, agentDir); + const resourceLoader = new DefaultResourceLoader({ + cwd: tempDir, + agentDir, + settingsManager, + extensionFactories: [inactiveTool], + }); + await resourceLoader.reload(); + return ( + await createAgentSession({ + cwd: tempDir, + agentDir, + model: getModel("anthropic", "claude-sonnet-4-5")!, + settingsManager, + sessionManager: SessionManager.inMemory(tempDir), + resourceLoader, + ...options, + }) + ).session; + } + + // #10245 + it("activates only tools newly added to defaultTools", async () => { + const session = await createFileSession(); + expect(session.getActiveToolNames()).toEqual(["read", "bash", "edit", "write"]); + session.setActiveToolsByName(["read", "edit", "write"]); + + writeSettings({ defaultTools: ["+inactive_tool", "+grep"] }); + await session.reload(); + // bash was disabled during the session and is not newly added, so it stays off. + expect(session.getActiveToolNames().sort()).toEqual(["edit", "grep", "inactive_tool", "read", "write"]); + + // Removing tools from the setting does not disable them. + writeSettings({ defaultTools: ["-read"] }); + await session.reload(); + expect(session.getActiveToolNames().sort()).toEqual(["edit", "grep", "inactive_tool", "read", "write"]); + session.dispose(); + }); + + it("keeps explicit tool options on reload", async () => { + const allowlisted = await createFileSession({ tools: ["read"] }); + writeSettings({ defaultTools: ["+grep"] }); + await allowlisted.reload(); + expect(allowlisted.getActiveToolNames()).toEqual(["read"]); + allowlisted.dispose(); + + writeSettings({}); + const builtinless = await createFileSession({ noTools: "builtin" }); + writeSettings({ defaultTools: ["+grep"] }); + await builtinless.reload(); + expect(builtinless.getActiveToolNames()).toEqual([]); + builtinless.dispose(); + + writeSettings({}); + const excluded = await createFileSession({ excludeTools: ["grep"] }); + writeSettings({ defaultTools: ["+grep", "+inactive_tool"] }); + await excluded.reload(); + expect(excluded.getActiveToolNames().sort()).toEqual(["bash", "edit", "inactive_tool", "read", "write"]); + excluded.dispose(); + }); + }); + it("applies through service-based session creation", async () => { const settingsManager = SettingsManager.inMemory({ defaultTools: ["ls"] }); const services = await createAgentSessionServices({ cwd: tempDir, agentDir, settingsManager });