ui: CWD for agent (#26518)
* server : extend file_glob_search for UI pickers * ui : add per-conversation working directory with picker * ui : add path navigation and search scope to cwd picker Treat path-like queries (starting with / or ~) as directory navigation instead of glob-matching the whole query: search the parent for the last segment, and descend into an exactly-typed directory by listing its children. Show the effective search scope in the footer and auto-search on open so the current directory and its siblings appear immediately. Assisted-by: Claude * db : persist per-call tool cwd on tool result messages * ui : abbreviate tool paths under home with a tilde * ui : show the per-call cwd on exec shell rows * ui : clarify the synthetic cwd message for the model * ui : reuse the trailing cwd row on a repeated pick * ui : don't jump when a cwd row is injected mid-chat * chore: Formatting * refactor: Cleanup comments * ui : unify working directory naming and add a synthetic-message flag * ui : render synthetic cwd rows without a scroll jump * ui : decouple the working directory picker into utils and sub-components * ui : add get_info tool call block * chore: Formatting * refactor: Cleanup * refactor: Cleanup * refactor: Cleanup * fix: UI * server : harden file_glob_search listing (kind enum, timeout, symlink guard, absolute base) * ui : use persisted isSynthetic flag for cwd rows, drop legacy formats * ui : cache picker search, fail visibly on native resolve * ui : escape glob metacharacters in picker search glob * ui : simplify auto-scroll pin * chore: Format * fix: Use `SvelteMap` * refactor: Post-review fixes * ui: accept Windows roots in the working directory picker recognize a drive root (C:) and a UNC share (//host/share) as path navigation, alongside the POSIX root and ~, so a query like D:\repos lists that directory instead of glob-matching it under the home dir split below the root, so a bare drive resolves to its root rather than to a drive-relative prefix rewrite backslashes into forward slashes only when the query carries a Windows root, since a backslash is a legal POSIX filename character paths keep travelling with forward slashes, which is what the server returns and what Windows accepts --------- Co-authored-by: Pascal <admin@serveurperso.com>
This commit is contained in:
co-authored by
Pascal
parent
0713275082
commit
2f56fc3431
@@ -164,3 +164,101 @@ def test_tools_builtin_edit_file_rejects_overlapping_edits():
|
||||
finally:
|
||||
if os.path.exists(log_path):
|
||||
os.remove(log_path)
|
||||
|
||||
|
||||
def test_tools_builtin_file_glob_search_type_dir(tmp_path):
|
||||
global server
|
||||
server.start()
|
||||
|
||||
(tmp_path / "project-alpha" / "src").mkdir(parents=True)
|
||||
(tmp_path / "project-alpha" / "README.md").write_text("alpha")
|
||||
(tmp_path / "project-alpha" / "src" / "main.cpp").write_text("int main() {}")
|
||||
(tmp_path / "project-beta").mkdir()
|
||||
(tmp_path / "project-beta" / "notes.txt").write_text("beta")
|
||||
|
||||
res = call_tool("file_glob_search", {"path": str(tmp_path), "type": "dir"})
|
||||
text = res["plain_text_response"]
|
||||
assert "project-alpha/" in text
|
||||
assert "project-beta/" in text
|
||||
assert "project-alpha/src/" in text
|
||||
assert "README.md" not in text
|
||||
types = {e["path"]: e["type"] for e in res["entries"]}
|
||||
assert types["project-alpha"] == "dir"
|
||||
assert types["project-alpha/src"] == "dir"
|
||||
|
||||
res_all = call_tool("file_glob_search", {"path": str(tmp_path), "type": "all", "include": "*proj*"})
|
||||
paths = [e["path"] for e in res_all["entries"]]
|
||||
assert "project-alpha" in paths
|
||||
assert "project-beta" in paths
|
||||
|
||||
|
||||
def test_tools_builtin_file_glob_search_max_depth_and_limit(tmp_path):
|
||||
global server
|
||||
server.start()
|
||||
|
||||
(tmp_path / "a" / "b" / "c").mkdir(parents=True)
|
||||
(tmp_path / "top.txt").write_text("top")
|
||||
(tmp_path / "a" / "mid.txt").write_text("mid")
|
||||
(tmp_path / "a" / "b" / "deep.txt").write_text("deep")
|
||||
|
||||
res = call_tool("file_glob_search", {"path": str(tmp_path), "max_depth": 1})
|
||||
assert "top.txt" in res["plain_text_response"]
|
||||
assert "mid.txt" not in res["plain_text_response"]
|
||||
|
||||
res = call_tool("file_glob_search", {"path": str(tmp_path), "max_depth": 2})
|
||||
assert "mid.txt" in res["plain_text_response"]
|
||||
assert "deep.txt" not in res["plain_text_response"]
|
||||
|
||||
res = call_tool("file_glob_search", {"path": str(tmp_path), "limit": 1})
|
||||
assert len(res["entries"]) == 1
|
||||
assert "Total matches: 3" in res["plain_text_response"]
|
||||
|
||||
|
||||
def test_tools_builtin_file_glob_search_rejects_invalid_type(tmp_path):
|
||||
global server
|
||||
server.start()
|
||||
|
||||
err = call_tool_expect_error("file_glob_search", {"path": str(tmp_path), "type": "bogus"})
|
||||
assert "invalid type" in err
|
||||
|
||||
|
||||
def test_tools_builtin_cwd_header_overrides_model_param(tmp_path):
|
||||
global server
|
||||
server.start()
|
||||
|
||||
workdir = tmp_path / "workdir"
|
||||
workdir.mkdir()
|
||||
(workdir / "marker.txt").write_text("marker")
|
||||
|
||||
# a model-provided "cwd" in the params is overridden by the x-tool-cwd header
|
||||
res = call_tool("read_file", {"path": "marker.txt", "cwd": "/definitely/not/a/real/path"},
|
||||
headers={"x-tool-cwd": str(workdir)})
|
||||
assert "marker" in res["plain_text_response"]
|
||||
|
||||
|
||||
def test_tools_builtin_cwd_relative_paths(tmp_path):
|
||||
global server
|
||||
server.start()
|
||||
|
||||
workdir = tmp_path / "workdir"
|
||||
workdir.mkdir()
|
||||
(workdir / "rel.txt").write_text("relative-content")
|
||||
|
||||
headers = {"x-tool-cwd": str(workdir)}
|
||||
|
||||
# relative paths in file tools resolve against the header cwd
|
||||
res = call_tool("read_file", {"path": "rel.txt"}, headers=headers)
|
||||
assert "relative-content" in res["plain_text_response"]
|
||||
|
||||
res = call_tool("write_file", {"path": "sub/out.txt", "content": "written"}, headers=headers)
|
||||
assert (workdir / "sub" / "out.txt").read_text() == "written"
|
||||
|
||||
res = call_tool("file_glob_search", {"path": ".", "include": "*.txt"}, headers=headers)
|
||||
assert "rel.txt" in res["plain_text_response"]
|
||||
|
||||
# absolute paths are unaffected by the cwd
|
||||
other = tmp_path / "other"
|
||||
other.mkdir()
|
||||
(other / "abs.txt").write_text("absolute-content")
|
||||
res = call_tool("read_file", {"path": str(other / "abs.txt")}, headers=headers)
|
||||
assert "absolute-content" in res["plain_text_response"]
|
||||
|
||||
Reference in New Issue
Block a user