From d09ee592af6b8c5f33a6b850e2ee94ba2b8e1727 Mon Sep 17 00:00:00 2001 From: Zadak Date: Wed, 1 Jul 2026 15:22:05 +0200 Subject: [PATCH 1/5] fix(graph-ui): navigate Windows drive breadcrumbs to real paths The index file picker built breadcrumb targets as '/' + segments, so on a Windows drive path (C:/Users/rap) clicking a crumb browsed to '/C:/...', which the backend rejected as 'not a directory'. Only the '.. (up)' button worked. Build drive-aware crumb targets (C:/, C:/Users) and drop the bogus unified '/' root crumb on Windows drive paths; POSIX behavior is unchanged. Add a regression test for Windows breadcrumb navigation and cleanup() for test isolation. Signed-off-by: Zadak --- graph-ui/src/components/StatsTab.test.tsx | 36 ++++++++++++++++++++++- graph-ui/src/components/StatsTab.tsx | 18 ++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/graph-ui/src/components/StatsTab.test.tsx b/graph-ui/src/components/StatsTab.test.tsx index 1a3df5b6f..35ee94c5c 100644 --- a/graph-ui/src/components/StatsTab.test.tsx +++ b/graph-ui/src/components/StatsTab.test.tsx @@ -1,6 +1,6 @@ /* @vitest-environment jsdom */ import "@testing-library/jest-dom/vitest"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { StatsTab } from "./StatsTab"; @@ -42,6 +42,7 @@ function mockProjectsFetch(extra?: (url: string, init?: RequestInit) => Response describe("StatsTab index modal", () => { afterEach(() => { + cleanup(); vi.unstubAllGlobals(); }); @@ -92,4 +93,37 @@ describe("StatsTab index modal", () => { expect(screen.getByRole("button", { name: "Index beta" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Browse D:/" })).toBeInTheDocument(); }); + + it("navigates Windows breadcrumb segments to real drive paths", async () => { + const fetchMock = mockProjectsFetch((url) => { + if (url.startsWith("/api/browse")) { + return new Response(JSON.stringify({ + path: "C:/Users/rap", + parent: "C:/Users", + dirs: ["Documents", "Downloads"], + roots: ["C:/", "D:/"], + }), { status: 200, headers: { "Content-Type": "application/json" } }); + } + return undefined; + }); + + render( {}} />); + fireEvent.click(await screen.findByRole("button", { name: "Index your first repository" })); + + /* No bogus unified "/" root crumb on a Windows drive path. */ + await screen.findByRole("button", { name: "C:" }); + expect(screen.queryByRole("button", { name: "/" })).not.toBeInTheDocument(); + + /* Clicking the drive crumb browses to "C:/", not "/C:". */ + fireEvent.click(screen.getByRole("button", { name: "C:" })); + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith("/api/browse?path=C%3A%2F"); + }); + + /* Clicking a nested crumb browses to "C:/Users", not "/C:/Users". */ + fireEvent.click(screen.getByRole("button", { name: "Users" })); + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith("/api/browse?path=C%3A%2FUsers"); + }); + }); }); diff --git a/graph-ui/src/components/StatsTab.tsx b/graph-ui/src/components/StatsTab.tsx index 3c439dce4..f6bea46e7 100644 --- a/graph-ui/src/components/StatsTab.tsx +++ b/graph-ui/src/components/StatsTab.tsx @@ -239,6 +239,16 @@ function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreat /* Breadcrumb segments */ const displayPath = currentPath.replace(/\\/g, "/"); const segments = displayPath.split("/").filter(Boolean); + /* A Windows drive path ("C:/Users/rap") has no unified "/" root — its first + * segment is the drive letter. Build crumb targets accordingly so clicking a + * segment navigates to a real directory instead of a bogus "/C:/..." path + * that the backend rejects as "not a directory". */ + const isWinPath = /^[A-Za-z]:$/.test(segments[0] ?? ""); + const crumbPath = (i: number): string => { + const parts = segments.slice(0, i + 1); + if (isWinPath) return parts.length === 1 ? `${parts[0]}/` : parts.join("/"); + return "/" + parts.join("/"); + }; return (
@@ -297,12 +307,14 @@ function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreat {/* Breadcrumb */}
- + {!isWinPath && ( + + )} {segments.map((seg, i) => ( - / + {(i > 0 || !isWinPath) && /}
-
+
-
diff --git a/graph-ui/src/lib/i18n.ts b/graph-ui/src/lib/i18n.ts index 10c68c748..1030f4a48 100644 --- a/graph-ui/src/lib/i18n.ts +++ b/graph-ui/src/lib/i18n.ts @@ -43,8 +43,6 @@ export const messages = { selectRepositoryFolder: "Select Repository Folder", instructions: "Navigate to the project root and click \"Index This Folder\".", repositoryPath: "Repository path", - projectName: "Project name", - projectNamePlaceholder: "Optional display name", filterFolders: "Filter folders", noSubdirectories: "No subdirectories", indexThisFolder: "Index This Folder", @@ -112,8 +110,6 @@ export const messages = { selectRepositoryFolder: "选择仓库目录", instructions: "导航到项目根目录,然后点击“索引此目录”。", repositoryPath: "仓库路径", - projectName: "项目名称", - projectNamePlaceholder: "可选显示名称", filterFolders: "筛选目录", noSubdirectories: "没有子目录", indexThisFolder: "索引此目录", From a201f8b94539e25476f06b139502815b26dfd486 Mon Sep 17 00:00:00 2001 From: Zadak Date: Wed, 1 Jul 2026 15:59:33 +0200 Subject: [PATCH 4/5] fix(graph-ui): drop the empty '/' root on Windows, offer the drive instead On Windows the POSIX '/' quick-jump root is meaningless (browsing it returns an empty listing), yet the picker showed a '/' button whenever the backend did not enumerate drives (older builds return roots=['/']). Clicking it stranded the user on an empty view. Derive Windows-aware quick-jump roots: drop non-drive roots and always include the current drive (parsed from the browsed path), so the button lists the drive root. Other drives remain reachable by typing. Add a regression test. Signed-off-by: Zadak --- graph-ui/src/components/StatsTab.test.tsx | 29 +++++++++++++++++++++++ graph-ui/src/components/StatsTab.tsx | 16 ++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/graph-ui/src/components/StatsTab.test.tsx b/graph-ui/src/components/StatsTab.test.tsx index 3ad9896c2..5a2cb26de 100644 --- a/graph-ui/src/components/StatsTab.test.tsx +++ b/graph-ui/src/components/StatsTab.test.tsx @@ -151,4 +151,33 @@ describe("StatsTab index modal", () => { expect(await screen.findByText("projects")).toBeInTheDocument(); expect(screen.queryByText("Documents")).not.toBeInTheDocument(); }); + + it("replaces the meaningless '/' root with the drive on Windows", async () => { + const fetchMock = mockProjectsFetch((url) => { + if (url.startsWith("/api/browse")) { + const m = /[?&]path=([^&]*)/.exec(url); + const path = m ? decodeURIComponent(m[1]) : "C:/Users/rap"; + return new Response(JSON.stringify({ + path, + parent: "C:/", + dirs: ["Documents"], + roots: ["/"], // older backend: no drive enumeration + }), { status: 200, headers: { "Content-Type": "application/json" } }); + } + return undefined; + }); + + render( {}} />); + fireEvent.click(await screen.findByRole("button", { name: "Index your first repository" })); + + /* The bogus "/" quick-jump is gone; the current drive root is offered. */ + expect(await screen.findByRole("button", { name: "Browse C:/" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Browse /" })).not.toBeInTheDocument(); + + /* Clicking it browses to the drive root, not "/". */ + fireEvent.click(screen.getByRole("button", { name: "Browse C:/" })); + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledWith("/api/browse?path=C%3A%2F"); + }); + }); }); diff --git a/graph-ui/src/components/StatsTab.tsx b/graph-ui/src/components/StatsTab.tsx index 6d69629d1..f058f081c 100644 --- a/graph-ui/src/components/StatsTab.tsx +++ b/graph-ui/src/components/StatsTab.tsx @@ -266,6 +266,20 @@ function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreat return "/" + parts.join("/"); }; + /* Root/drive quick-jump buttons. On Windows the POSIX "/" root is meaningless + * — browsing it returns an empty listing — so drop it and offer drive roots + * instead. An older backend may not enumerate drives, so always include the + * current drive; other drives stay reachable by typing a path. */ + const displayRoots = (() => { + if (!isWinPath) return roots; + const drives = Array.from(new Set( + roots.filter((r) => /^[A-Za-z]:[\\/]?$/.test(r)).map((r) => `${r[0].toUpperCase()}:/`), + )); + const curRoot = `${displayPath[0].toUpperCase()}:/`; + if (!drives.includes(curRoot)) drives.unshift(curRoot); + return drives; + })(); + return (
@@ -299,7 +313,7 @@ function CreateIndexModal({ onClose, onCreated }: { onClose: () => void; onCreat className="flex-1 bg-white/[0.04] border border-white/[0.06] rounded-lg px-3 py-2 text-[12px] text-foreground outline-none focus:border-primary/40 placeholder:text-foreground/20" />
- {roots.map((root) => ( + {displayRoots.map((root) => (