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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Vendored
+1
@@ -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: () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user