From 044cf46a0dd23cdcbd754f905354f91a89a1c7fb Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Jul 2026 21:07:49 -0500 Subject: [PATCH] fix(gateway): treat a leading `@/` as a separator, not just an absolute path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@/Desktop` returned nothing while `@Desktop` worked. The leading slash was always read literally, so the lookup went to the absolute `/Desktop` — which doesn't exist — and dead-ended instead of finding the folder one level down. The `@` has already announced "this is a path", so the slash people type next reads as a separator out of habit rather than a filesystem root. Take the absolute meaning only when it resolves: the parent directory has to exist, and a partially-typed segment has to match something in it. Otherwise drop the slash and resolve from the cwd. Real absolute paths are unaffected — `/usr`, `/etc/hos`, `/Users/...` all pass the existence probe and keep their current behaviour. The guard matters: stripping the slash unconditionally would let a repo-local `etc/` shadow the real `/etc`. --- tests/gateway/test_complete_path_at_filter.py | 62 +++++++++++++++++++ tui_gateway/server.py | 40 ++++++++++++ 2 files changed, 102 insertions(+) diff --git a/tests/gateway/test_complete_path_at_filter.py b/tests/gateway/test_complete_path_at_filter.py index f46631dea271..def6b922e5d1 100644 --- a/tests/gateway/test_complete_path_at_filter.py +++ b/tests/gateway/test_complete_path_at_filter.py @@ -353,3 +353,65 @@ def test_fuzzy_finds_top_level_entries_outside_a_git_repo(tmp_path, monkeypatch) (tmp_path / "Desktop" / "note.txt").write_text("x") assert "@folder:Desktop/" in [t for t, _, _ in _items("@Desktop")] + + +# ── Leading slash is a separator, not necessarily an absolute path ─────── +# `@/Desktop` used to dead-end: it was read as the absolute `/Desktop`, +# which doesn't exist. People type the slash out of habit — the `@` already +# announced "this is a path" — so it should mean the same as `@Desktop` +# unless a real absolute path is there. + + +def test_leading_slash_matches_the_bare_form(tmp_path, monkeypatch): + """`@/foo` and `@foo` return the same thing when `/foo` doesn't exist.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "Desktop").mkdir() + (tmp_path / "Desktop" / "note.txt").write_text("x") + + server._fuzzy_cache.clear() + bare = [t for t, _, _ in _items("@Desktop")] + server._fuzzy_cache.clear() + slashed = [t for t, _, _ in _items("@/Desktop")] + + assert "@folder:Desktop/" in bare + assert slashed == bare + + +def test_leading_slash_navigates_into_subfolders(tmp_path, monkeypatch): + """The fallback survives deeper paths, not just a single segment.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "apps" / "desktop").mkdir(parents=True) + (tmp_path / "apps" / "desktop" / "main.tsx").write_text("x") + + assert "@folder:apps/desktop/" in [t for t, _, _ in _items("@/apps/desktop")] + + server._fuzzy_cache.clear() + assert any("main.tsx" in t for t, _, _ in _items("@/apps/desktop/")) + + +def test_leading_slash_prefers_a_real_absolute_path(tmp_path, monkeypatch): + """When the absolute reading resolves, it wins — no silent rewrite. + + A cwd-relative `usr/` must not shadow the real `/usr`, or typing an + absolute path in a repo that happens to mirror those names breaks. + """ + monkeypatch.chdir(tmp_path) + # A decoy that would win if the slash were stripped unconditionally. + (tmp_path / "etc").mkdir() + (tmp_path / "etc" / "decoy.conf").write_text("x") + + texts = [t for t, _, _ in _items("@/etc/")] + + # `/etc` exists on any POSIX box, so the absolute reading must hold. + assert not any("decoy.conf" in t for t in texts), texts + + +def test_leading_slash_falls_back_only_when_absolute_is_missing(tmp_path, monkeypatch): + """A nonexistent absolute prefix falls back; an existing one doesn't.""" + monkeypatch.chdir(tmp_path) + (tmp_path / "nonexistent-at-root").mkdir() + (tmp_path / "nonexistent-at-root" / "f.txt").write_text("x") + + assert "@folder:nonexistent-at-root/" in [ + t for t, _, _ in _items("@/nonexistent-at-root") + ] diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 5ab0dd446760..1e15d788b0a3 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -16136,6 +16136,31 @@ def _fuzzy_basename_rank(name: str, query: str) -> tuple[int, int] | None: return None +def _abs_completion_prefix_exists(path_part: str) -> bool: + """True when ``path_part`` reads sensibly as an absolute path. + + A leading `/` is only meant literally if something is actually there: + the parent directory has to exist, and a partially-typed final segment + has to match at least one of its entries. Used to decide whether + `@/foo` is the absolute `/foo` or shorthand for `foo` under the cwd. + """ + expanded = _normalize_completion_path(path_part) + parent = os.path.dirname(expanded.rstrip("/")) or "/" + tail = os.path.basename(expanded.rstrip("/")) + + if not os.path.isdir(parent): + return False + + if not tail or expanded.endswith("/"): + return os.path.isdir(expanded) or expanded == "/" + + try: + tail_lower = tail.lower() + return any(e.lower().startswith(tail_lower) for e in os.listdir(parent)) + except OSError: + return False + + @method("complete.path") def _(rid, params: dict) -> dict: word = params.get("word", "") @@ -16171,6 +16196,21 @@ def _(rid, params: dict) -> dict: prefix_tag = "" path_part = query if is_context else query + # `@/foo` almost always means "foo, from here" rather than the absolute + # `/foo`: the `@` already says "this is a path", so the slash reads as a + # separator people type out of habit. Take the absolute reading only + # when something is actually there, else drop the slash and resolve + # relative to the cwd — otherwise `@/Desktop` dead-ends on a directory + # that exists one level down. Real absolute paths (`@/usr/local`, + # `@/etc/hosts`) still resolve, since those prefixes do exist. + if ( + is_context + and path_part.startswith("/") + and not path_part.startswith("//") + and not _abs_completion_prefix_exists(path_part) + ): + path_part = path_part.lstrip("/") + # Fuzzy basename search across the repo when the user types a bare # name with no path separator — `@appChrome` surfaces every file # whose basename matches, regardless of directory depth. Matches what