server, ui: only offer a working directory when a tool reads it (#26762)

The working directory chip showed up as soon as the server exposed any
builtin tool, so a server started with just get_datetime, or a user who
turned every filesystem tool off in the settings, still got a control
that nothing would read.

Tools now declare whether they resolve their paths and run against the
working directory, next to the write permission they already publish in
the /tools listing. The WebUI shows the chip and enables the /cwd
command only when at least one such tool is both served and left
enabled.
This commit is contained in:
Pascal
2026-08-08 16:36:21 +02:00
committed by GitHub
parent dd2c7c4471
commit 18f7ad7fc9
8 changed files with 38 additions and 7 deletions
+8
View File
@@ -74,6 +74,7 @@ json server_tool::to_json() const {
{"permissions", json{ {"permissions", json{
{"write", permission_write} {"write", permission_write}
}}, }},
{"uses_cwd", uses_cwd},
{"definition", get_definition()}, {"definition", get_definition()},
}; };
} }
@@ -763,6 +764,7 @@ struct server_tool_read_file : server_tool {
server_tool_read_file() { server_tool_read_file() {
name = "read_file"; name = "read_file";
display_name = "Read file"; display_name = "Read file";
uses_cwd = true;
permission_write = false; permission_write = false;
} }
@@ -851,6 +853,7 @@ struct server_tool_file_glob_search : server_tool {
server_tool_file_glob_search() { server_tool_file_glob_search() {
name = "file_glob_search"; name = "file_glob_search";
display_name = "File search"; display_name = "File search";
uses_cwd = true;
permission_write = false; permission_write = false;
} }
@@ -965,6 +968,7 @@ struct server_tool_grep_search : server_tool {
server_tool_grep_search() { server_tool_grep_search() {
name = "grep_search"; name = "grep_search";
display_name = "Grep search"; display_name = "Grep search";
uses_cwd = true;
permission_write = false; permission_write = false;
} }
@@ -1117,6 +1121,7 @@ struct server_tool_exec_shell_command : server_tool {
server_tool_exec_shell_command() { server_tool_exec_shell_command() {
name = "exec_shell_command"; name = "exec_shell_command";
display_name = "Execute shell command"; display_name = "Execute shell command";
uses_cwd = true;
permission_write = true; permission_write = true;
support_stream = true; support_stream = true;
} }
@@ -1195,6 +1200,7 @@ struct server_tool_write_file : server_tool {
server_tool_write_file() { server_tool_write_file() {
name = "write_file"; name = "write_file";
display_name = "Write file"; display_name = "Write file";
uses_cwd = true;
permission_write = true; permission_write = true;
} }
@@ -1237,6 +1243,7 @@ struct server_tool_edit_file : server_tool {
server_tool_edit_file() { server_tool_edit_file() {
name = "edit_file"; name = "edit_file";
display_name = "Edit file"; display_name = "Edit file";
uses_cwd = true;
permission_write = true; permission_write = true;
} }
@@ -1625,6 +1632,7 @@ struct server_tool_get_info : server_tool {
server_tool_get_info() { server_tool_get_info() {
name = "get_info"; name = "get_info";
display_name = "Get Runtime Info"; display_name = "Get Runtime Info";
uses_cwd = true;
permission_write = false; permission_write = false;
} }
+1
View File
@@ -14,6 +14,7 @@ struct server_tool {
std::string display_name; std::string display_name;
bool permission_write = false; bool permission_write = false;
bool support_stream = false; // if true, output can be streamed bool support_stream = false; // if true, output can be streamed
bool uses_cwd = false; // if true, the tool resolves paths and runs against the working directory
virtual ~server_tool() = default; virtual ~server_tool() = default;
virtual json get_definition() const = 0; virtual json get_definition() const = 0;
@@ -156,7 +156,7 @@
focusInput: refocusInput, focusInput: refocusInput,
getShowModelSelector: () => showModelSelector, getShowModelSelector: () => showModelSelector,
hasPrompts: () => mcpStore.hasPromptsCapability(conversationsStore.getAllMcpServerOverrides()), hasPrompts: () => mcpStore.hasPromptsCapability(conversationsStore.getAllMcpServerOverrides()),
hasBuiltinTools: () => toolsStore.builtinTools.length > 0, hasCwdTools: () => toolsStore.hasEnabledCwdTools,
getCwd: () => cwd, getCwd: () => cwd,
getServerHome: () => toolsStore.serverHome ?? null, getServerHome: () => toolsStore.serverHome ?? null,
openModelSelector: () => chatFormActionsRef?.openModelSelector(), openModelSelector: () => chatFormActionsRef?.openModelSelector(),
@@ -651,7 +651,7 @@
<ContextGaugePopup /> <ContextGaugePopup />
{#if toolsStore.builtinTools.length > 0} {#if toolsStore.hasEnabledCwdTools}
<ChatFormWorkingDirectory <ChatFormWorkingDirectory
directory={cwd} directory={cwd}
isOpen={pickers.isWorkingDirectoryPickerOpen} isOpen={pickers.isWorkingDirectoryPickerOpen}
+2 -2
View File
@@ -8,7 +8,7 @@ interface ChatCommandsOptions {
/** Gates `/prompt`. */ /** Gates `/prompt`. */
hasPrompts: () => boolean; hasPrompts: () => boolean;
/** Gates `/cwd`. */ /** Gates `/cwd`. */
hasBuiltinTools: () => boolean; hasCwdTools: () => boolean;
} }
/** /**
@@ -32,7 +32,7 @@ export function getChatCommands(options: ChatCommandsOptions): ChatFormCommand[]
description: SET_WORKING_DIRECTORY_LABEL, description: SET_WORKING_DIRECTORY_LABEL,
keywords: ['current working directory'], keywords: ['current working directory'],
action: ChatFormCommandAction.CWD, action: ChatFormCommandAction.CWD,
disabled: !options.hasBuiltinTools() disabled: !options.hasCwdTools()
}, },
{ {
name: 'model', name: 'model',
@@ -24,7 +24,7 @@ export interface UseChatFormPickersOptions {
/** Gates `/prompt`. */ /** Gates `/prompt`. */
hasPrompts: () => boolean; hasPrompts: () => boolean;
/** Gates `/cwd`. */ /** Gates `/cwd`. */
hasBuiltinTools: () => boolean; hasCwdTools: () => boolean;
getCwd: () => string | null; getCwd: () => string | null;
/** Mention search fallback scope. */ /** Mention search fallback scope. */
getServerHome: () => string | null; getServerHome: () => string | null;
@@ -63,7 +63,7 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
getChatCommands({ getChatCommands({
showModelSelector: opts.getShowModelSelector(), showModelSelector: opts.getShowModelSelector(),
hasPrompts: opts.hasPrompts, hasPrompts: opts.hasPrompts,
hasBuiltinTools: opts.hasBuiltinTools hasCwdTools: opts.hasCwdTools
}) })
); );
+21
View File
@@ -27,6 +27,9 @@ class ToolsStore {
private _loading = $state(false); private _loading = $state(false);
private _error = $state<string | null>(null); private _error = $state<string | null>(null);
private _disabledTools = $state(new SvelteSet<string>()); private _disabledTools = $state(new SvelteSet<string>());
// builtin tools that resolve their paths against the working directory,
// as declared by the server in its `/tools` listing
private _cwdAwareTools = $state(new SvelteSet<string>());
private _toolsEndpointUnreachable = $state(false); private _toolsEndpointUnreachable = $state(false);
private _serverHome = $state<string | null | undefined>(undefined); private _serverHome = $state<string | null | undefined>(undefined);
@@ -476,6 +479,21 @@ class ToolsStore {
return this.getEnabledToolsForLLM().length > 0; return this.getEnabledToolsForLLM().length > 0;
} }
/**
* Check if a working directory is worth setting: at least one builtin tool
* that reads it is both served and left enabled by the user.
*/
get hasEnabledCwdTools(): boolean {
return this._builtinTools.some((def) => {
const name = def.function.name;
return (
this._cwdAwareTools.has(name) &&
!this._disabledTools.has(this.toolKey(ToolSource.BUILTIN, name))
);
});
}
async fetchBuiltinTools(): Promise<void> { async fetchBuiltinTools(): Promise<void> {
if (this._loading) return; if (this._loading) return;
@@ -486,6 +504,9 @@ class ToolsStore {
try { try {
const toolInfos = await ToolsService.list(); const toolInfos = await ToolsService.list();
this._builtinTools = toolInfos.map((info) => info.definition); this._builtinTools = toolInfos.map((info) => info.definition);
this._cwdAwareTools = new SvelteSet(
toolInfos.filter((info) => info.uses_cwd).map((info) => info.tool)
);
} catch (err) { } catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err); const errorMessage = err instanceof Error ? err.message : String(err);
this._error = errorMessage; this._error = errorMessage;
+1
View File
@@ -292,6 +292,7 @@ export interface ServerBuiltinToolInfo {
permissions: { permissions: {
write: boolean; write: boolean;
}; };
uses_cwd: boolean;
definition: OpenAIToolDefinition; definition: OpenAIToolDefinition;
} }
@@ -21,7 +21,7 @@
focusInput: () => {}, focusInput: () => {},
getShowModelSelector: () => true, getShowModelSelector: () => true,
hasPrompts: () => true, hasPrompts: () => true,
hasBuiltinTools: () => true, hasCwdTools: () => true,
getCwd: () => null, getCwd: () => null,
getServerHome: () => null, getServerHome: () => null,
openModelSelector: () => { openModelSelector: () => {