]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
server, ui: only offer a working directory when a tool reads it (#26762)
authorPascal <redacted>
Sat, 8 Aug 2026 14:36:21 +0000 (16:36 +0200)
committerGitHub <redacted>
Sat, 8 Aug 2026 14:36:21 +0000 (16:36 +0200)
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.

tools/server/server-tools.cpp
tools/server/server-tools.h
tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte
tools/ui/src/lib/constants/chat-commands.ts
tools/ui/src/lib/hooks/use-chat-form-pickers.svelte.ts
tools/ui/src/lib/stores/tools.svelte.ts
tools/ui/src/lib/types/mcp.d.ts
tools/ui/tests/client/components/ChatFormPickersHarness.svelte

index eacfbf0f74ac84d4d14c242a1af54764d76d7adb..d5e696434ca716aaeb668757343eb622702297f8 100644 (file)
@@ -74,6 +74,7 @@ json server_tool::to_json() const {
         {"permissions", json{
             {"write", permission_write}
         }},
+        {"uses_cwd", uses_cwd},
         {"definition", get_definition()},
     };
 }
@@ -763,6 +764,7 @@ struct server_tool_read_file : server_tool {
     server_tool_read_file() {
         name = "read_file";
         display_name = "Read file";
+        uses_cwd = true;
         permission_write = false;
     }
 
@@ -851,6 +853,7 @@ struct server_tool_file_glob_search : server_tool {
     server_tool_file_glob_search() {
         name = "file_glob_search";
         display_name = "File search";
+        uses_cwd = true;
         permission_write = false;
     }
 
@@ -965,6 +968,7 @@ struct server_tool_grep_search : server_tool {
     server_tool_grep_search() {
         name = "grep_search";
         display_name = "Grep search";
+        uses_cwd = true;
         permission_write = false;
     }
 
@@ -1117,6 +1121,7 @@ struct server_tool_exec_shell_command : server_tool {
     server_tool_exec_shell_command() {
         name = "exec_shell_command";
         display_name = "Execute shell command";
+        uses_cwd = true;
         permission_write = true;
         support_stream = true;
     }
@@ -1195,6 +1200,7 @@ struct server_tool_write_file : server_tool {
     server_tool_write_file() {
         name = "write_file";
         display_name = "Write file";
+        uses_cwd = true;
         permission_write = true;
     }
 
@@ -1237,6 +1243,7 @@ struct server_tool_edit_file : server_tool {
     server_tool_edit_file() {
         name = "edit_file";
         display_name = "Edit file";
+        uses_cwd = true;
         permission_write = true;
     }
 
@@ -1625,6 +1632,7 @@ struct server_tool_get_info : server_tool {
     server_tool_get_info() {
         name = "get_info";
         display_name = "Get Runtime Info";
+        uses_cwd = true;
         permission_write = false;
     }
 
index 7f70e6767e4c5418aea1d1f62d1d1662f7673fb7..ede303181b636d1d84384091ef0a2fa73573585b 100644 (file)
@@ -14,6 +14,7 @@ struct server_tool {
     std::string display_name;
     bool permission_write = false;
     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 json get_definition() const = 0;
index 1df1257089e7389dff4621ef2336f32c65d80fa5..2d70c302cb64ad608833695dcc602bbe0ca55885 100644 (file)
                focusInput: refocusInput,
                getShowModelSelector: () => showModelSelector,
                hasPrompts: () => mcpStore.hasPromptsCapability(conversationsStore.getAllMcpServerOverrides()),
-               hasBuiltinTools: () => toolsStore.builtinTools.length > 0,
+               hasCwdTools: () => toolsStore.hasEnabledCwdTools,
                getCwd: () => cwd,
                getServerHome: () => toolsStore.serverHome ?? null,
                openModelSelector: () => chatFormActionsRef?.openModelSelector(),
 
        <ContextGaugePopup />
 
-       {#if toolsStore.builtinTools.length > 0}
+       {#if toolsStore.hasEnabledCwdTools}
                <ChatFormWorkingDirectory
                        directory={cwd}
                        isOpen={pickers.isWorkingDirectoryPickerOpen}
index bbe762c555e8dd8a6b5a2fad07328a2320a141cf..3cc09fe6d73a2f1c97a1a67c37fee7099180b8c4 100644 (file)
@@ -8,7 +8,7 @@ interface ChatCommandsOptions {
        /** Gates `/prompt`. */
        hasPrompts: () => boolean;
        /** Gates `/cwd`. */
-       hasBuiltinTools: () => boolean;
+       hasCwdTools: () => boolean;
 }
 
 /**
@@ -32,7 +32,7 @@ export function getChatCommands(options: ChatCommandsOptions): ChatFormCommand[]
                        description: SET_WORKING_DIRECTORY_LABEL,
                        keywords: ['current working directory'],
                        action: ChatFormCommandAction.CWD,
-                       disabled: !options.hasBuiltinTools()
+                       disabled: !options.hasCwdTools()
                },
                {
                        name: 'model',
index f3bc5f7320b7da14246101b3b5e8e65c8d1a72a6..4860f16fb2af0eca5c973aa8c43b877dadf2f74f 100644 (file)
@@ -24,7 +24,7 @@ export interface UseChatFormPickersOptions {
        /** Gates `/prompt`. */
        hasPrompts: () => boolean;
        /** Gates `/cwd`. */
-       hasBuiltinTools: () => boolean;
+       hasCwdTools: () => boolean;
        getCwd: () => string | null;
        /** Mention search fallback scope. */
        getServerHome: () => string | null;
@@ -63,7 +63,7 @@ export function useChatFormPickers(opts: UseChatFormPickersOptions) {
                getChatCommands({
                        showModelSelector: opts.getShowModelSelector(),
                        hasPrompts: opts.hasPrompts,
-                       hasBuiltinTools: opts.hasBuiltinTools
+                       hasCwdTools: opts.hasCwdTools
                })
        );
 
index 5136101e751c3b54057237b870fc3132ae740ed9..4114ef756646d828fa08f591101446a6378f627b 100644 (file)
@@ -27,6 +27,9 @@ class ToolsStore {
        private _loading = $state(false);
        private _error = $state<string | null>(null);
        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 _serverHome = $state<string | null | undefined>(undefined);
 
@@ -476,6 +479,21 @@ class ToolsStore {
                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> {
                if (this._loading) return;
 
@@ -486,6 +504,9 @@ class ToolsStore {
                try {
                        const toolInfos = await ToolsService.list();
                        this._builtinTools = toolInfos.map((info) => info.definition);
+                       this._cwdAwareTools = new SvelteSet(
+                               toolInfos.filter((info) => info.uses_cwd).map((info) => info.tool)
+                       );
                } catch (err) {
                        const errorMessage = err instanceof Error ? err.message : String(err);
                        this._error = errorMessage;
index b567c20c94490bed49ed07b6f495d6912fd29a9a..2b5937913d638be0c16b13ea6b63694167773b88 100644 (file)
@@ -292,6 +292,7 @@ export interface ServerBuiltinToolInfo {
        permissions: {
                write: boolean;
        };
+       uses_cwd: boolean;
        definition: OpenAIToolDefinition;
 }
 
index 76b2e9fe3d2f564241a98cedcbaf42d2712a4bdc..8e5e56c20b06d23c2cf190d06345b5594ec36c5e 100644 (file)
@@ -21,7 +21,7 @@
                focusInput: () => {},
                getShowModelSelector: () => true,
                hasPrompts: () => true,
-               hasBuiltinTools: () => true,
+               hasCwdTools: () => true,
                getCwd: () => null,
                getServerHome: () => null,
                openModelSelector: () => {