Implement per-tool timeout configuration in settings

- Added support for per-tool timeout overrides in seconds, allowing users to customize tool execution limits.
- Introduced `setToolTimeoutOverrides` function to apply user-defined timeout settings.
- Updated the `FeatureConfig` interface and default settings to include `toolTimeoutsSec`.
- Enhanced the settings UI to display and manage tool timeout values, ensuring a better user experience for configuring tool behavior.
This commit is contained in:
Pawan Osman
2026-07-15 15:25:24 +03:00
parent 3b246ec787
commit 205c6cd1b7
6 changed files with 88 additions and 2 deletions
+2
View File
@@ -28,6 +28,8 @@ export {
disposeShellSession,
toolTimeoutMs,
withToolTimeout,
setToolTimeoutOverrides,
DEFAULT_TOOL_TIMEOUTS_SEC,
type TodoItem,
} from "./shared";
+23 -1
View File
@@ -52,6 +52,26 @@ export const DEFAULT_TOOL_TIMEOUT_MS = 60_000;
/** Tools that manage their own lifetime (user wait / nested agent). */
export const NO_TOOL_TIMEOUT = new Set(["Task", "AskQuestion"]);
/** Built-in defaults in seconds (for settings UI). */
export const DEFAULT_TOOL_TIMEOUTS_SEC: Record<string, number> = Object.fromEntries(
Object.entries(TOOL_TIMEOUT_MS).map(([k, v]) => [k, Math.round(v / 1000)]),
);
/** User overrides from settings (tool name → seconds). Empty/missing = built-in default. */
let toolTimeoutOverridesSec: Record<string, number> = {};
/** Apply settings overrides (seconds). Call whenever feature config loads/changes. */
export function setToolTimeoutOverrides(sec: Record<string, number> | undefined): void {
const next: Record<string, number> = {};
if (sec) {
for (const [k, v] of Object.entries(sec)) {
const n = Number(v);
if (Number.isFinite(n) && n > 0) next[k] = Math.floor(n);
}
}
toolTimeoutOverridesSec = next;
}
/**
* Race a tool promise against a hard timeout. On timeout rejects with an Error
* whose message starts with "timeout:" so the loop can surface it cleanly.
@@ -84,9 +104,11 @@ export function withToolTimeout<T>(p: Promise<T>, ms: number, label: string): Pr
});
}
/** Resolve the hard timeout for a tool name (0 = none). */
/** Resolve the hard timeout for a tool name (0 = none). Honors settings overrides. */
export function toolTimeoutMs(name: string): number {
if (NO_TOOL_TIMEOUT.has(name)) return 0;
const overrideSec = toolTimeoutOverridesSec[name];
if (overrideSec != null && overrideSec > 0) return overrideSec * 1000;
return TOOL_TIMEOUT_MS[name] ?? DEFAULT_TOOL_TIMEOUT_MS;
}
+4
View File
@@ -13,6 +13,7 @@ import { SidebarProvider } from './ui/sidebarProvider';
import { registerInlineReview } from './ui/inlineReview';
import { SettingsPanel } from './ui/settingsPanel';
import { FeatureStore } from './stores/featureStore';
import { setToolTimeoutOverrides } from './agent/tools/shared';
import { mcpManager } from './integrations/mcpClient';
import { setIndexStorageDir } from './agent/semanticIndex';
import { setDocsStorageDir, setDocSourcesProvider } from './agent/docsIndex';
@@ -32,6 +33,9 @@ export function activate(context: vscode.ExtensionContext) {
const settingsManager = new SettingsManager(context);
const featureStore = new FeatureStore(context);
const syncToolTimeouts = () => setToolTimeoutOverrides(featureStore.get().toolTimeoutsSec);
syncToolTimeouts();
context.subscriptions.push(featureStore.onDidChange(syncToolTimeouts));
initOAuth(context);
initUsage(context);
// Prefetch the provider-grouped model list so every UI (settings, pickers)
+3
View File
@@ -232,6 +232,8 @@ export interface FeatureConfig {
maxTabCount: number;
/** Max agent steps per run before pausing (0 = default 50). */
maxAgentSteps: number;
/** Per-tool hard timeout overrides in seconds (empty = built-in defaults). */
toolTimeoutsSec: Record<string, number>;
/** Automatically continue when the step limit is reached. */
autoContinue: boolean;
/** Play a sound when the agent finishes responding. */
@@ -281,6 +283,7 @@ const DEFAULTS: FeatureConfig = {
submitWithCtrlEnter: false,
maxTabCount: 0,
maxAgentSteps: 50,
toolTimeoutsSec: {},
autoContinue: false,
completionSound: false,
webSearchEnabled: true,
+53 -1
View File
@@ -67,11 +67,38 @@ const NAV: { id: Section; label: string; icon: IconName; sep?: boolean }[] = [
{ id: "about", label: "About", icon: "book" },
];
/** Built-in tool hard timeouts (seconds). Keep in sync with src/agent/tools/shared.ts. */
const TOOL_TIMEOUT_DEFAULTS: { name: string; sec: number }[] = [
{ name: "Shell", sec: 120 },
{ name: "AwaitShell", sec: 150 },
{ name: "Grep", sec: 30 },
{ name: "Glob", sec: 30 },
{ name: "FileSearch", sec: 20 },
{ name: "SemanticSearch", sec: 45 },
{ name: "SearchDocs", sec: 30 },
{ name: "ListDir", sec: 15 },
{ name: "Read", sec: 30 },
{ name: "ReadLints", sec: 20 },
{ name: "WebSearch", sec: 25 },
{ name: "WebFetch", sec: 30 },
{ name: "StrReplace", sec: 30 },
{ name: "Write", sec: 30 },
{ name: "Delete", sec: 15 },
{ name: "EditNotebook", sec: 30 },
{ name: "CallMcpTool", sec: 60 },
{ name: "FetchMcpResource", sec: 45 },
{ name: "ListMcpResources", sec: 20 },
{ name: "TodoWrite", sec: 5 },
{ name: "TodoRead", sec: 5 },
{ name: "WritePlan", sec: 15 },
{ name: "SwitchMode", sec: 5 },
];
/** Search terms per section so the nav filter finds settings inside pages too. */
const SECTION_KEYWORDS: Partial<Record<Section, string>> = {
general: "editor settings keyboard shortcuts notifications privacy chat titles auto judge model completion sound reset",
usage: "tokens quota limits plan usage oauth account rate limit",
agents: "text size submit ctrl enter max tab count web search fetch context conversation",
agents: "text size submit ctrl enter max tab count web search fetch context conversation tool timeout shell grep",
models: "enable disable model catalog reasoning effort thinking context",
providers: "api key openai anthropic google openrouter oauth custom base url connect",
behavior: "workspace context file reading terminal tools auto edits approval allow deny ask review policy allowlist denylist commands mcp web",
@@ -824,6 +851,31 @@ export function App() {
</Row>
</Group>
<div className="section-label">Tool Timeouts</div>
<p className="panel-hint">Hard timeout per tool (seconds). Empty = built-in default. Prevents a hung tool from blocking the agent forever.</p>
<Group>
{TOOL_TIMEOUT_DEFAULTS.map(({ name, sec }) => {
const overrides = features.toolTimeoutsSec || {};
const val = overrides[name];
return (
<Row key={name} title={name} desc={`Default ${sec}s`}>
<NumInput
value={val != null && val > 0 ? val : null}
step="1"
min="1"
placeholder={String(sec)}
onChange={(v) => {
const next = { ...(features.toolTimeoutsSec || {}) };
if (v == null || v <= 0) delete next[name];
else next[name] = Math.floor(v);
setFeatures({ toolTimeoutsSec: next });
}}
/>
</Row>
);
})}
</Group>
<div className="section-label">Subagents</div>
<Group>
<Row title="Subagent Model" desc="Default model for subagents launched via the Task tool.">
+3
View File
@@ -260,6 +260,8 @@ export interface FeatureConfig {
submitWithCtrlEnter: boolean;
maxTabCount: number;
maxAgentSteps: number;
/** Per-tool hard timeout overrides in seconds (empty = built-in defaults). */
toolTimeoutsSec: Record<string, number>;
autoContinue: boolean;
completionSound: boolean;
webSearchEnabled: boolean;
@@ -346,6 +348,7 @@ export const EMPTY_FEATURES: FeatureConfig = {
submitWithCtrlEnter: false,
maxTabCount: 0,
maxAgentSteps: 50,
toolTimeoutsSec: {},
autoContinue: false,
completionSound: false,
webSearchEnabled: true,