refactor(web)!: restructure code browser to GitHub-style URLs

Quentin Gliech and Claude Opus 4.6 (1M context) created

replace search-param-based code browser with proper URL segments:
  /$repo/tree/$ref/...path   → directory view
  /$repo/blob/$ref/...path   → file view
  /$repo/commits/$ref        → commit history
  /$repo/                    → redirect to tree/{defaultRef}

add pathless _code layout route that:
- preloads refs (branches/tags) in beforeLoad
- renders shared header: breadcrumb, ref selector, history toggle
- child routes (tree, blob, commits) render inside Outlet

convert CodeBreadcrumb segments from onClick handlers to typed Links

update Header "Code" NavLink and repo picker to use new URL structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/src/components/code/CodeBreadcrumb.tsx  |  28 
webui2/src/components/code/FileViewer.tsx      |  18 
webui2/src/components/layout/Header.tsx        |   7 
webui2/src/routeTree.gen.ts                    |  89 +++++
webui2/src/routes/$repo/_code.tsx              | 136 +++++++++
webui2/src/routes/$repo/_code/blob/$ref/$.tsx  |  43 ++
webui2/src/routes/$repo/_code/commits/$ref.tsx |  16 +
webui2/src/routes/$repo/_code/tree/$ref/$.tsx  | 150 +++++++++
webui2/src/routes/$repo/index.tsx              | 301 +------------------
webui2/src/routes/index.tsx                    |   2 
10 files changed, 483 insertions(+), 307 deletions(-)

Detailed changes

webui2/src/components/code/CodeBreadcrumb.tsx 🔗

@@ -1,26 +1,27 @@
+import { Link } from "@tanstack/react-router";
 import { ChevronRight } from "lucide-react";
 
 interface CodeBreadcrumbProps {
   repoName: string;
-  ref: string;
+  currentRef: string;
   path: string;
-  // called when user clicks a breadcrumb segment — returns new path
-  onNavigate: (path: string) => void;
+  repo: string;
 }
 
-// Path breadcrumb for the code browser: repo name / ref / path segments.
-// Each segment is clickable to navigate up the tree.
-export function CodeBreadcrumb({ repoName, ref, path, onNavigate }: CodeBreadcrumbProps) {
+// Path breadcrumb for the code browser: repo name / path segments.
+// Each segment is a Link to the corresponding tree path.
+export function CodeBreadcrumb({ repoName, currentRef, path, repo }: CodeBreadcrumbProps) {
   const parts = path ? path.split("/").filter(Boolean) : [];
 
   return (
     <div className="flex flex-wrap items-center gap-1 font-mono text-sm">
-      <button
-        onClick={() => onNavigate("")}
+      <Link
+        to="/$repo/tree/$ref/$"
+        params={{ repo, ref: currentRef, _splat: "" }}
         className="text-foreground font-medium hover:underline"
       >
         {repoName}
-      </button>
+      </Link>
 
       {parts.map((part, i) => {
         const partPath = parts.slice(0, i + 1).join("/");
@@ -31,18 +32,19 @@ export function CodeBreadcrumb({ repoName, ref, path, onNavigate }: CodeBreadcru
             {isLast ? (
               <span className="text-foreground font-medium">{part}</span>
             ) : (
-              <button
-                onClick={() => onNavigate(partPath)}
+              <Link
+                to="/$repo/tree/$ref/$"
+                params={{ repo, ref: currentRef, _splat: partPath }}
                 className="text-muted-foreground hover:text-foreground hover:underline"
               >
                 {part}
-              </button>
+              </Link>
             )}
           </span>
         );
       })}
 
-      <span className="text-muted-foreground ml-2 text-xs">@ {ref}</span>
+      <span className="text-muted-foreground ml-2 text-xs">@ {currentRef}</span>
     </div>
   );
 }

webui2/src/components/code/FileViewer.tsx 🔗

@@ -9,11 +9,23 @@ import { Button } from "@/components/ui/button";
 import { Skeleton } from "@/components/ui/skeleton";
 
 interface FileViewerProps {
-  blob: GitBlob;
+  blob: GitBlob | null;
   loading?: boolean;
 }
 
-export function FileViewer({ blob, loading }: FileViewerProps) {
+export function FileViewer({ blob, loading = false }: FileViewerProps) {
+  if (loading || !blob) {
+    return (
+      <div className="divide-border border-border divide-y rounded-md border">
+        <div className="flex items-center gap-2 px-4 py-2">
+          <Skeleton className="h-4 w-48" />
+        </div>
+        <div className="p-4">
+          <Skeleton className="h-64 w-full" />
+        </div>
+      </div>
+    );
+  }
   const [highlighted, setHighlighted] = useState<{ html: string; lineCount: number } | null>(null);
 
   useEffect(() => {
@@ -43,7 +55,7 @@ export function FileViewer({ blob, loading }: FileViewerProps) {
   const { html, lineCount } = highlighted;
 
   function copyToClipboard() {
-    if (blob.text) void navigator.clipboard.writeText(blob.text);
+    if (blob?.text) void navigator.clipboard.writeText(blob.text);
   }
 
   return (

webui2/src/components/layout/Header.tsx 🔗

@@ -54,12 +54,7 @@ export function Header() {
         {/* Repo-scoped nav links — only shown when inside a repo */}
         {effectiveRepo && (
           <nav className="flex items-center gap-1">
-            <NavLink
-              to="/$repo"
-              params={{ repo: effectiveRepo }}
-              search={{ ref: "", path: "", type: "tree" as const }}
-              activeOptions={{ exact: true }}
-            >
+            <NavLink to="/$repo" params={{ repo: effectiveRepo }} activeOptions={{ exact: true }}>
               Code
             </NavLink>
             <NavLink

webui2/src/routeTree.gen.ts 🔗

@@ -14,11 +14,15 @@ import { Route as IndexRouteImport } from './routes/index'
 import { Route as RepoIndexRouteImport } from './routes/$repo/index'
 import { Route as AuthSelectIdentityRouteImport } from './routes/auth/select-identity'
 import { Route as RepoIssuesRouteImport } from './routes/$repo/_issues'
+import { Route as RepoCodeRouteImport } from './routes/$repo/_code'
 import { Route as RepoCommitHashRouteImport } from './routes/$repo/commit/$hash'
 import { Route as RepoIssuesIssuesIndexRouteImport } from './routes/$repo/_issues/issues/index'
 import { Route as RepoIssuesUserIdRouteImport } from './routes/$repo/_issues/user/$id'
 import { Route as RepoIssuesIssuesNewRouteImport } from './routes/$repo/_issues/issues/new'
 import { Route as RepoIssuesIssuesIdRouteImport } from './routes/$repo/_issues/issues/$id'
+import { Route as RepoCodeCommitsRefRouteImport } from './routes/$repo/_code/commits/$ref'
+import { Route as RepoCodeTreeRefSplatRouteImport } from './routes/$repo/_code/tree/$ref/$'
+import { Route as RepoCodeBlobRefSplatRouteImport } from './routes/$repo/_code/blob/$ref/$'
 
 const RepoRoute = RepoRouteImport.update({
   id: '/$repo',
@@ -44,6 +48,10 @@ const RepoIssuesRoute = RepoIssuesRouteImport.update({
   id: '/_issues',
   getParentRoute: () => RepoRoute,
 } as any)
+const RepoCodeRoute = RepoCodeRouteImport.update({
+  id: '/_code',
+  getParentRoute: () => RepoRoute,
+} as any)
 const RepoCommitHashRoute = RepoCommitHashRouteImport.update({
   id: '/commit/$hash',
   path: '/commit/$hash',
@@ -69,6 +77,21 @@ const RepoIssuesIssuesIdRoute = RepoIssuesIssuesIdRouteImport.update({
   path: '/issues/$id',
   getParentRoute: () => RepoIssuesRoute,
 } as any)
+const RepoCodeCommitsRefRoute = RepoCodeCommitsRefRouteImport.update({
+  id: '/commits/$ref',
+  path: '/commits/$ref',
+  getParentRoute: () => RepoCodeRoute,
+} as any)
+const RepoCodeTreeRefSplatRoute = RepoCodeTreeRefSplatRouteImport.update({
+  id: '/tree/$ref/$',
+  path: '/tree/$ref/$',
+  getParentRoute: () => RepoCodeRoute,
+} as any)
+const RepoCodeBlobRefSplatRoute = RepoCodeBlobRefSplatRouteImport.update({
+  id: '/blob/$ref/$',
+  path: '/blob/$ref/$',
+  getParentRoute: () => RepoCodeRoute,
+} as any)
 
 export interface FileRoutesByFullPath {
   '/': typeof IndexRoute
@@ -76,33 +99,43 @@ export interface FileRoutesByFullPath {
   '/auth/select-identity': typeof AuthSelectIdentityRoute
   '/$repo/': typeof RepoIndexRoute
   '/$repo/commit/$hash': typeof RepoCommitHashRoute
+  '/$repo/commits/$ref': typeof RepoCodeCommitsRefRoute
   '/$repo/issues/$id': typeof RepoIssuesIssuesIdRoute
   '/$repo/issues/new': typeof RepoIssuesIssuesNewRoute
   '/$repo/user/$id': typeof RepoIssuesUserIdRoute
   '/$repo/issues/': typeof RepoIssuesIssuesIndexRoute
+  '/$repo/blob/$ref/$': typeof RepoCodeBlobRefSplatRoute
+  '/$repo/tree/$ref/$': typeof RepoCodeTreeRefSplatRoute
 }
 export interface FileRoutesByTo {
   '/': typeof IndexRoute
   '/$repo': typeof RepoIndexRoute
   '/auth/select-identity': typeof AuthSelectIdentityRoute
   '/$repo/commit/$hash': typeof RepoCommitHashRoute
+  '/$repo/commits/$ref': typeof RepoCodeCommitsRefRoute
   '/$repo/issues/$id': typeof RepoIssuesIssuesIdRoute
   '/$repo/issues/new': typeof RepoIssuesIssuesNewRoute
   '/$repo/user/$id': typeof RepoIssuesUserIdRoute
   '/$repo/issues': typeof RepoIssuesIssuesIndexRoute
+  '/$repo/blob/$ref/$': typeof RepoCodeBlobRefSplatRoute
+  '/$repo/tree/$ref/$': typeof RepoCodeTreeRefSplatRoute
 }
 export interface FileRoutesById {
   __root__: typeof rootRouteImport
   '/': typeof IndexRoute
   '/$repo': typeof RepoRouteWithChildren
+  '/$repo/_code': typeof RepoCodeRouteWithChildren
   '/$repo/_issues': typeof RepoIssuesRouteWithChildren
   '/auth/select-identity': typeof AuthSelectIdentityRoute
   '/$repo/': typeof RepoIndexRoute
   '/$repo/commit/$hash': typeof RepoCommitHashRoute
+  '/$repo/_code/commits/$ref': typeof RepoCodeCommitsRefRoute
   '/$repo/_issues/issues/$id': typeof RepoIssuesIssuesIdRoute
   '/$repo/_issues/issues/new': typeof RepoIssuesIssuesNewRoute
   '/$repo/_issues/user/$id': typeof RepoIssuesUserIdRoute
   '/$repo/_issues/issues/': typeof RepoIssuesIssuesIndexRoute
+  '/$repo/_code/blob/$ref/$': typeof RepoCodeBlobRefSplatRoute
+  '/$repo/_code/tree/$ref/$': typeof RepoCodeTreeRefSplatRoute
 }
 export interface FileRouteTypes {
   fileRoutesByFullPath: FileRoutesByFullPath
@@ -112,32 +145,42 @@ export interface FileRouteTypes {
     | '/auth/select-identity'
     | '/$repo/'
     | '/$repo/commit/$hash'
+    | '/$repo/commits/$ref'
     | '/$repo/issues/$id'
     | '/$repo/issues/new'
     | '/$repo/user/$id'
     | '/$repo/issues/'
+    | '/$repo/blob/$ref/$'
+    | '/$repo/tree/$ref/$'
   fileRoutesByTo: FileRoutesByTo
   to:
     | '/'
     | '/$repo'
     | '/auth/select-identity'
     | '/$repo/commit/$hash'
+    | '/$repo/commits/$ref'
     | '/$repo/issues/$id'
     | '/$repo/issues/new'
     | '/$repo/user/$id'
     | '/$repo/issues'
+    | '/$repo/blob/$ref/$'
+    | '/$repo/tree/$ref/$'
   id:
     | '__root__'
     | '/'
     | '/$repo'
+    | '/$repo/_code'
     | '/$repo/_issues'
     | '/auth/select-identity'
     | '/$repo/'
     | '/$repo/commit/$hash'
+    | '/$repo/_code/commits/$ref'
     | '/$repo/_issues/issues/$id'
     | '/$repo/_issues/issues/new'
     | '/$repo/_issues/user/$id'
     | '/$repo/_issues/issues/'
+    | '/$repo/_code/blob/$ref/$'
+    | '/$repo/_code/tree/$ref/$'
   fileRoutesById: FileRoutesById
 }
 export interface RootRouteChildren {
@@ -183,6 +226,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof RepoIssuesRouteImport
       parentRoute: typeof RepoRoute
     }
+    '/$repo/_code': {
+      id: '/$repo/_code'
+      path: ''
+      fullPath: '/$repo'
+      preLoaderRoute: typeof RepoCodeRouteImport
+      parentRoute: typeof RepoRoute
+    }
     '/$repo/commit/$hash': {
       id: '/$repo/commit/$hash'
       path: '/commit/$hash'
@@ -218,9 +268,46 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof RepoIssuesIssuesIdRouteImport
       parentRoute: typeof RepoIssuesRoute
     }
+    '/$repo/_code/commits/$ref': {
+      id: '/$repo/_code/commits/$ref'
+      path: '/commits/$ref'
+      fullPath: '/$repo/commits/$ref'
+      preLoaderRoute: typeof RepoCodeCommitsRefRouteImport
+      parentRoute: typeof RepoCodeRoute
+    }
+    '/$repo/_code/tree/$ref/$': {
+      id: '/$repo/_code/tree/$ref/$'
+      path: '/tree/$ref/$'
+      fullPath: '/$repo/tree/$ref/$'
+      preLoaderRoute: typeof RepoCodeTreeRefSplatRouteImport
+      parentRoute: typeof RepoCodeRoute
+    }
+    '/$repo/_code/blob/$ref/$': {
+      id: '/$repo/_code/blob/$ref/$'
+      path: '/blob/$ref/$'
+      fullPath: '/$repo/blob/$ref/$'
+      preLoaderRoute: typeof RepoCodeBlobRefSplatRouteImport
+      parentRoute: typeof RepoCodeRoute
+    }
   }
 }
 
+interface RepoCodeRouteChildren {
+  RepoCodeCommitsRefRoute: typeof RepoCodeCommitsRefRoute
+  RepoCodeBlobRefSplatRoute: typeof RepoCodeBlobRefSplatRoute
+  RepoCodeTreeRefSplatRoute: typeof RepoCodeTreeRefSplatRoute
+}
+
+const RepoCodeRouteChildren: RepoCodeRouteChildren = {
+  RepoCodeCommitsRefRoute: RepoCodeCommitsRefRoute,
+  RepoCodeBlobRefSplatRoute: RepoCodeBlobRefSplatRoute,
+  RepoCodeTreeRefSplatRoute: RepoCodeTreeRefSplatRoute,
+}
+
+const RepoCodeRouteWithChildren = RepoCodeRoute._addFileChildren(
+  RepoCodeRouteChildren,
+)
+
 interface RepoIssuesRouteChildren {
   RepoIssuesIssuesIdRoute: typeof RepoIssuesIssuesIdRoute
   RepoIssuesIssuesNewRoute: typeof RepoIssuesIssuesNewRoute
@@ -240,12 +327,14 @@ const RepoIssuesRouteWithChildren = RepoIssuesRoute._addFileChildren(
 )
 
 interface RepoRouteChildren {
+  RepoCodeRoute: typeof RepoCodeRouteWithChildren
   RepoIssuesRoute: typeof RepoIssuesRouteWithChildren
   RepoIndexRoute: typeof RepoIndexRoute
   RepoCommitHashRoute: typeof RepoCommitHashRoute
 }
 
 const RepoRouteChildren: RepoRouteChildren = {
+  RepoCodeRoute: RepoCodeRouteWithChildren,
   RepoIssuesRoute: RepoIssuesRouteWithChildren,
   RepoIndexRoute: RepoIndexRoute,
   RepoCommitHashRoute: RepoCommitHashRoute,

webui2/src/routes/$repo/_code.tsx 🔗

@@ -0,0 +1,136 @@
+// Pathless layout for the code browser. Preloads refs (branches/tags)
+// and renders the shared header (breadcrumb + ref selector + history toggle).
+// Child routes (tree, blob, commits) render inside the Outlet.
+
+import { gql } from "@apollo/client";
+import { useReadQuery } from "@apollo/client/react";
+import { createFileRoute, Outlet, useMatchRoute, useParams } from "@tanstack/react-router";
+import { GitCommit } from "lucide-react";
+
+import type { GitRef } from "@/__generated__/graphql";
+import { CodeBreadcrumb } from "@/components/code/CodeBreadcrumb";
+import { RefSelector } from "@/components/code/RefSelector";
+import { ButtonLink } from "@/components/ui/button-link";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export const REFS_QUERY = gql`
+  query CodePageRefs($repo: String) {
+    repository(ref: $repo) {
+      name
+      refs {
+        nodes {
+          name
+          shortName
+          type
+          hash
+          isDefault
+        }
+      }
+    }
+  }
+`;
+
+export interface RefsQueryData {
+  repository: {
+    name: string;
+    refs: { nodes: GitRef[] } | null;
+  } | null;
+}
+
+export const Route = createFileRoute("/$repo/_code")({
+  component: CodeLayout,
+  pendingComponent: CodeLayoutSkeleton,
+  beforeLoad: ({ context: { preloadQuery, ref } }) => {
+    const refsRef = preloadQuery<RefsQueryData>(REFS_QUERY, {
+      variables: { repo: ref },
+    });
+    return { refsRef };
+  },
+});
+
+function CodeLayout() {
+  const { repo } = Route.useParams();
+  const { ref: repoRef } = Route.useRouteContext();
+  const { refsRef } = Route.useRouteContext();
+  const { data: refsData } = useReadQuery(refsRef);
+  const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? [];
+  const repoName = refsData?.repository?.name ?? repoRef ?? "default-repo";
+
+  // Read child route params (ref and splat path) via loose useParams
+  const allParams = useParams({ strict: false }) as {
+    ref?: string;
+    _splat?: string;
+  };
+  const currentRef = allParams.ref ?? "";
+  const currentPath = allParams._splat ?? "";
+
+  const matchRoute = useMatchRoute();
+  const isCommitsView = !!matchRoute({
+    to: "/$repo/commits/$ref",
+    params: { repo, ref: currentRef },
+    fuzzy: true,
+  });
+
+  function handleRefSelect(ref: GitRef) {
+    // When switching refs, always go to tree root
+    window.location.href = `/${repo}/tree/${ref.shortName}`;
+  }
+
+  return (
+    <div className="space-y-4">
+      <div className="flex flex-wrap items-center justify-between gap-3">
+        <CodeBreadcrumb
+          repoName={repoName}
+          currentRef={currentRef}
+          path={currentPath}
+          repo={repo}
+        />
+        <div className="flex items-center gap-2">
+          {isCommitsView ? (
+            <ButtonLink
+              to="/$repo/tree/$ref/$"
+              params={{ repo, ref: currentRef, _splat: currentPath }}
+              variant="secondary"
+              size="sm"
+            >
+              <GitCommit className="size-3.5" />
+              History
+            </ButtonLink>
+          ) : (
+            <ButtonLink
+              to="/$repo/commits/$ref"
+              params={{ repo, ref: currentRef }}
+              variant="outline"
+              size="sm"
+            >
+              <GitCommit className="size-3.5" />
+              History
+            </ButtonLink>
+          )}
+          <RefSelector refs={refs} currentRef={currentRef} onSelect={handleRefSelect} />
+        </div>
+      </div>
+
+      <Outlet />
+    </div>
+  );
+}
+
+function CodeLayoutSkeleton() {
+  return (
+    <div className="space-y-4">
+      <div className="flex items-center justify-between">
+        <Skeleton className="h-5 w-48" />
+        <Skeleton className="h-8 w-28" />
+      </div>
+      <div className="divide-border border-border divide-y rounded-md border">
+        {Array.from({ length: 8 }).map((_, i) => (
+          <div key={i} className="flex items-center gap-3 px-4 py-2">
+            <Skeleton className="size-4 rounded-sm" />
+            <Skeleton className="h-4 w-32" />
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}

webui2/src/routes/$repo/_code/blob/$ref/$.tsx 🔗

@@ -0,0 +1,43 @@
+// Blob (file) view: /$repo/blob/$ref/...path
+
+import { gql } from "@apollo/client";
+import { useQuery } from "@apollo/client/react";
+import { createFileRoute } from "@tanstack/react-router";
+
+import type { GitBlob } from "@/__generated__/graphql";
+import { FileViewer } from "@/components/code/FileViewer";
+
+const BLOB_QUERY = gql`
+  query CodePageBlob($repo: String, $ref: String!, $path: String!) {
+    repository(ref: $repo) {
+      blob(ref: $ref, path: $path) {
+        path
+        hash
+        text
+        size
+        isBinary
+        isTruncated
+      }
+    }
+  }
+`;
+
+interface BlobQueryData {
+  repository: { blob: GitBlob | null } | null;
+}
+
+export const Route = createFileRoute("/$repo/_code/blob/$ref/$")({
+  component: BlobView,
+});
+
+function BlobView() {
+  const { ref: currentRef, _splat: currentPath = "" } = Route.useParams();
+  const { ref: repoRef } = Route.useRouteContext();
+
+  const { data, loading } = useQuery<BlobQueryData>(BLOB_QUERY, {
+    variables: { repo: repoRef, ref: currentRef, path: currentPath },
+    skip: !currentPath,
+  });
+
+  return <FileViewer blob={data?.repository?.blob ?? null} loading={loading} />;
+}

webui2/src/routes/$repo/_code/commits/$ref.tsx 🔗

@@ -0,0 +1,16 @@
+// Commit history view: /$repo/commits/$ref
+
+import { createFileRoute } from "@tanstack/react-router";
+
+import { CommitList } from "@/components/code/CommitList";
+
+export const Route = createFileRoute("/$repo/_code/commits/$ref")({
+  component: CommitsView,
+});
+
+function CommitsView() {
+  const { ref: currentRef } = Route.useParams();
+  const { ref: repoRef } = Route.useRouteContext();
+
+  return <CommitList repo={repoRef} ref_={currentRef} />;
+}

webui2/src/routes/$repo/_code/tree/$ref/$.tsx 🔗

@@ -0,0 +1,150 @@
+// Tree view: /$repo/tree/$ref/...path
+
+import { gql } from "@apollo/client";
+import { useQuery } from "@apollo/client/react";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+
+import {
+  GitObjectType,
+  type GitTreeEntry,
+  type GitLastCommit,
+  type GitBlob,
+} from "@/__generated__/graphql";
+import { FileTree } from "@/components/code/FileTree";
+import type { TreeEntryWithCommit } from "@/components/code/FileTree";
+import { Markdown } from "@/components/content/Markdown";
+
+const TREE_QUERY = gql`
+  query CodePageTree($repo: String, $ref: String!, $path: String) {
+    repository(ref: $repo) {
+      tree(ref: $ref, path: $path) {
+        name
+        type
+        hash
+      }
+    }
+  }
+`;
+
+const LAST_COMMITS_QUERY = gql`
+  query CodePageLastCommits($repo: String, $ref: String!, $path: String, $names: [String!]!) {
+    repository(ref: $repo) {
+      lastCommits(ref: $ref, path: $path, names: $names) {
+        name
+        commit {
+          hash
+          shortHash
+          message
+          date
+        }
+      }
+    }
+  }
+`;
+
+const BLOB_QUERY = gql`
+  query CodePageReadme($repo: String, $ref: String!, $path: String!) {
+    repository(ref: $repo) {
+      blob(ref: $ref, path: $path) {
+        text
+      }
+    }
+  }
+`;
+
+interface TreeQueryData {
+  repository: { tree: GitTreeEntry[] | null } | null;
+}
+interface LastCommitsQueryData {
+  repository: { lastCommits: GitLastCommit[] | null } | null;
+}
+interface ReadmeQueryData {
+  repository: { blob: GitBlob | null } | null;
+}
+
+export const Route = createFileRoute("/$repo/_code/tree/$ref/$")({
+  component: TreeView,
+});
+
+function TreeView() {
+  const { repo, ref: currentRef, _splat: currentPath = "" } = Route.useParams();
+  const { ref: repoRef } = Route.useRouteContext();
+  const navigate = useNavigate();
+
+  const { data: treeData, loading: treeLoading } = useQuery<TreeQueryData>(TREE_QUERY, {
+    variables: { repo: repoRef, ref: currentRef, path: currentPath || null },
+  });
+  const entries: GitTreeEntry[] = treeData?.repository?.tree ?? [];
+
+  const entryNames = entries.map((e) => e.name);
+  const { data: lastCommitsData } = useQuery<LastCommitsQueryData>(LAST_COMMITS_QUERY, {
+    variables: { repo: repoRef, ref: currentRef, path: currentPath || null, names: entryNames },
+    skip: entryNames.length === 0,
+  });
+  const lastCommitsByName = new Map<string, GitLastCommit>(
+    (lastCommitsData?.repository?.lastCommits ?? []).map((lc) => [lc.name, lc]),
+  );
+  const entriesWithCommits: TreeEntryWithCommit[] = entries.map((e) => ({
+    ...e,
+    lastCommit: lastCommitsByName.get(e.name)?.commit ?? undefined,
+  }));
+
+  const readmeEntry = entries.find(
+    (e) => e.type === GitObjectType.Blob && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name),
+  );
+  const readmePath = readmeEntry
+    ? currentPath
+      ? `${currentPath}/${readmeEntry.name}`
+      : readmeEntry.name
+    : null;
+  const { data: readmeBlobData } = useQuery<ReadmeQueryData>(BLOB_QUERY, {
+    variables: { repo: repoRef, ref: currentRef, path: readmePath },
+    skip: !readmePath,
+  });
+  const readme: string | null = readmeBlobData?.repository?.blob?.text ?? null;
+
+  function handleEntryClick(entry: TreeEntryWithCommit) {
+    const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
+    if (entry.type === GitObjectType.Blob) {
+      void navigate({
+        to: "/$repo/blob/$ref/$",
+        params: { repo, ref: currentRef, _splat: newPath },
+      });
+    } else {
+      void navigate({
+        to: "/$repo/tree/$ref/$",
+        params: { repo, ref: currentRef, _splat: newPath },
+      });
+    }
+  }
+
+  function handleNavigateUp() {
+    const parts = currentPath.split("/").filter(Boolean);
+    parts.pop();
+    void navigate({
+      to: "/$repo/tree/$ref/$",
+      params: { repo, ref: currentRef, _splat: parts.join("/") },
+    });
+  }
+
+  return (
+    <>
+      <FileTree
+        repo={repo}
+        entries={entriesWithCommits}
+        path={currentPath}
+        loading={treeLoading}
+        onNavigate={handleEntryClick}
+        onNavigateUp={handleNavigateUp}
+      />
+      {readme && (
+        <div className="rounded-md border">
+          <div className="text-muted-foreground border-b px-4 py-2 text-xs font-medium">README</div>
+          <div className="px-6 py-4">
+            <Markdown content={readme} />
+          </div>
+        </div>
+      )}
+    </>
+  );
+}

webui2/src/routes/$repo/index.tsx 🔗

@@ -1,40 +1,17 @@
-// Code browser page. Switches between tree view, file viewer, and commit
-// history via ?type= search param. Ref is selected via ?ref=.
+// /$repo index — redirects to the tree view with the default ref.
 
 import { gql } from "@apollo/client";
-import { useQuery, useReadQuery } from "@apollo/client/react";
-import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router";
-import { AlertCircle, GitCommit } from "lucide-react";
-import { useEffect } from "react";
-import * as v from "valibot";
+import { createFileRoute, redirect } from "@tanstack/react-router";
 
-import {
-  GitObjectType,
-  type GitRef,
-  type GitTreeEntry,
-  type GitBlob,
-  type GitLastCommit,
-} from "@/__generated__/graphql";
-import { CodeBreadcrumb } from "@/components/code/CodeBreadcrumb";
-import { CommitList } from "@/components/code/CommitList";
-import { FileTree } from "@/components/code/FileTree";
-import type { TreeEntryWithCommit } from "@/components/code/FileTree";
-import { FileViewer } from "@/components/code/FileViewer";
-import { RefSelector } from "@/components/code/RefSelector";
-import { Markdown } from "@/components/content/Markdown";
-import { ButtonLink } from "@/components/ui/button-link";
-import { Skeleton } from "@/components/ui/skeleton";
+import type { GitRef } from "@/__generated__/graphql";
+import { client } from "@/lib/apollo";
 
 const REFS_QUERY = gql`
-  query CodePageRefs($repo: String) {
+  query RepoDefaultRef($repo: String) {
     repository(ref: $repo) {
-      name
       refs {
         nodes {
-          name
           shortName
-          type
-          hash
           isDefault
         }
       }
@@ -42,265 +19,23 @@ const REFS_QUERY = gql`
   }
 `;
 
-const TREE_QUERY = gql`
-  query CodePageTree($repo: String, $ref: String!, $path: String) {
-    repository(ref: $repo) {
-      tree(ref: $ref, path: $path) {
-        name
-        type
-        hash
-      }
-    }
-  }
-`;
-
-const LAST_COMMITS_QUERY = gql`
-  query CodePageLastCommits($repo: String, $ref: String!, $path: String, $names: [String!]!) {
-    repository(ref: $repo) {
-      lastCommits(ref: $ref, path: $path, names: $names) {
-        name
-        commit {
-          hash
-          shortHash
-          message
-          date
-        }
-      }
-    }
-  }
-`;
-
-const BLOB_QUERY = gql`
-  query CodePageBlob($repo: String, $ref: String!, $path: String!) {
-    repository(ref: $repo) {
-      blob(ref: $ref, path: $path) {
-        path
-        hash
-        text
-        size
-        isBinary
-        isTruncated
-      }
-    }
-  }
-`;
-
-interface RefsQueryData {
-  repository: {
-    name: string;
-    refs: { nodes: GitRef[] } | null;
-  } | null;
-}
-
-interface TreeQueryData {
-  repository: {
-    tree: GitTreeEntry[] | null;
-  } | null;
+interface DefaultRefQueryData {
+  repository: { refs: { nodes: Pick<GitRef, "shortName" | "isDefault">[] } | null } | null;
 }
 
-interface LastCommitsQueryData {
-  repository: {
-    lastCommits: GitLastCommit[] | null;
-  } | null;
-}
-
-interface BlobQueryData {
-  repository: {
-    blob: GitBlob | null;
-  } | null;
-}
-
-const codePageSearchSchema = v.object({
-  ref: v.fallback(v.string(), ""),
-  path: v.fallback(v.string(), ""),
-  type: v.fallback(v.picklist(["tree", "blob", "commits"]), "tree"),
-});
-
-export type CodePageSearch = v.InferOutput<typeof codePageSearchSchema>;
-
-type ViewMode = CodePageSearch["type"];
-
 export const Route = createFileRoute("/$repo/")({
-  component: RouteComponent,
-  pendingComponent: CodePageSkeleton,
-  validateSearch: (search) => v.parse(codePageSearchSchema, search),
-  loader: async ({ context: { preloadQuery, ref } }) => {
-    const refsRef = preloadQuery<RefsQueryData>(REFS_QUERY, {
+  beforeLoad: async ({ context: { ref }, params: { repo } }) => {
+    const { data } = await client.query<DefaultRefQueryData>({
+      query: REFS_QUERY,
       variables: { repo: ref },
     });
-    return { refsRef: await preloadQuery.toPromise(refsRef) };
+    const refs = data?.repository?.refs?.nodes ?? [];
+    const defaultRef = refs.find((r) => r.isDefault) ?? refs[0];
+    const refName = defaultRef?.shortName ?? "master";
+
+    throw redirect({
+      to: "/$repo/tree/$ref/$",
+      params: { repo, ref: refName, _splat: "" },
+    });
   },
 });
-
-function RouteComponent() {
-  const { ref: repoRef } = Route.useRouteContext();
-  const { repo } = Route.useParams();
-  const navigate = useNavigate({ from: "/$repo/" });
-  const { ref: currentRef, path: currentPath, type: viewMode } = useSearch({ from: "/$repo/" });
-
-  const { refsRef } = Route.useLoaderData();
-  const { data: refsData, error: refsError } = useReadQuery(refsRef);
-  const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? [];
-
-  // Set default ref once loaded
-  useEffect(() => {
-    if (refs.length === 0 || currentRef) return;
-    const defaultRef = refs.find((r: GitRef) => r.isDefault) ?? refs[0];
-    if (defaultRef) {
-      void navigate({
-        search: (prev) => ({ ...prev, ref: defaultRef.shortName }),
-        replace: true,
-      });
-    }
-  }, [refs.length]); // eslint-disable-line react-hooks/exhaustive-deps
-
-  const inTreeMode = viewMode === "tree" && !!currentRef;
-  const inBlobMode = viewMode === "blob" && !!currentRef && !!currentPath;
-
-  const { data: treeData, loading: treeLoading } = useQuery<TreeQueryData>(TREE_QUERY, {
-    variables: { repo: repoRef, ref: currentRef, path: currentPath || null },
-    skip: !inTreeMode,
-  });
-  const entries: GitTreeEntry[] = treeData?.repository?.tree ?? [];
-
-  const entryNames = entries.map((e: GitTreeEntry) => e.name);
-  const { data: lastCommitsData } = useQuery<LastCommitsQueryData>(LAST_COMMITS_QUERY, {
-    variables: { repo: repoRef, ref: currentRef, path: currentPath || null, names: entryNames },
-    skip: !inTreeMode || entryNames.length === 0,
-  });
-  const lastCommitsByName = new Map<string, GitLastCommit>(
-    (lastCommitsData?.repository?.lastCommits ?? []).map((lc: GitLastCommit) => [lc.name, lc]),
-  );
-  const entriesWithCommits: TreeEntryWithCommit[] = entries.map((e: GitTreeEntry) => ({
-    ...e,
-    lastCommit: lastCommitsByName.get(e.name)?.commit ?? undefined,
-  }));
-
-  const { data: blobData, loading: blobLoading } = useQuery<BlobQueryData>(BLOB_QUERY, {
-    variables: { repo: repoRef, ref: currentRef, path: currentPath },
-    skip: !inBlobMode,
-  });
-  const blob: GitBlob | null = blobData?.repository?.blob ?? null;
-
-  const readmeEntry = entries.find(
-    (e: GitTreeEntry) =>
-      e.type === GitObjectType.Blob && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name),
-  );
-  const readmePath = readmeEntry
-    ? currentPath
-      ? `${currentPath}/${readmeEntry.name}`
-      : readmeEntry.name
-    : null;
-  const { data: readmeBlobData } = useQuery<BlobQueryData>(BLOB_QUERY, {
-    variables: { repo: repoRef, ref: currentRef, path: readmePath },
-    skip: !inTreeMode || !readmePath,
-  });
-  const readme: string | null = readmeBlobData?.repository?.blob?.text ?? null;
-
-  const repoName = refsData?.repository?.name ?? repoRef ?? "default-repo";
-
-  function navigateTo(path: string, type: ViewMode = "tree") {
-    void navigate({ search: (prev) => ({ ...prev, path, type }) });
-  }
-
-  function handleEntryClick(entry: TreeEntryWithCommit) {
-    const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
-    navigateTo(newPath, entry.type === GitObjectType.Blob ? "blob" : "tree");
-  }
-
-  function handleNavigateUp() {
-    const parts = currentPath.split("/").filter(Boolean);
-    parts.pop();
-    navigateTo(parts.join("/"), "tree");
-  }
-
-  function handleRefSelect(ref: GitRef) {
-    void navigate({ search: { ref: ref.shortName, path: "", type: "tree" } });
-  }
-
-  if (refsError) {
-    return (
-      <div className="flex flex-col items-center gap-3 py-16 text-center">
-        <AlertCircle className="text-muted-foreground size-8" />
-        <p className="text-sm font-medium">Code browser unavailable</p>
-        <p className="text-muted-foreground max-w-sm text-xs">{refsError.message}</p>
-      </div>
-    );
-  }
-
-  return (
-    <div className="space-y-4">
-      <div className="flex flex-wrap items-center justify-between gap-3">
-        <CodeBreadcrumb
-          repoName={repoName}
-          ref={currentRef}
-          path={currentPath}
-          onNavigate={(p) => navigateTo(p, "tree")}
-        />
-        <div className="flex items-center gap-2">
-          <ButtonLink
-            to="/$repo"
-            params={{ repo }}
-            search={{
-              ref: currentRef,
-              path: currentPath,
-              type: viewMode === "commits" ? "tree" : "commits",
-            }}
-            variant={viewMode === "commits" ? "secondary" : "outline"}
-            size="sm"
-          >
-            <GitCommit className="size-3.5" />
-            History
-          </ButtonLink>
-          <RefSelector refs={refs} currentRef={currentRef} onSelect={handleRefSelect} />
-        </div>
-      </div>
-
-      {viewMode === "commits" ? (
-        <CommitList repo={repoRef} ref_={currentRef} path={currentPath || undefined} />
-      ) : viewMode === "tree" || !blob ? (
-        <>
-          <FileTree
-            repo={repo}
-            entries={entriesWithCommits}
-            path={currentPath}
-            loading={treeLoading}
-            onNavigate={handleEntryClick}
-            onNavigateUp={handleNavigateUp}
-          />
-          {readme && (
-            <div className="rounded-md border">
-              <div className="text-muted-foreground border-b px-4 py-2 text-xs font-medium">
-                README
-              </div>
-              <div className="px-6 py-4">
-                <Markdown content={readme} />
-              </div>
-            </div>
-          )}
-        </>
-      ) : (
-        <FileViewer blob={blob} loading={blobLoading} />
-      )}
-    </div>
-  );
-}
-
-function CodePageSkeleton() {
-  return (
-    <div className="space-y-4">
-      <div className="flex items-center justify-between">
-        <Skeleton className="h-5 w-48" />
-        <Skeleton className="h-8 w-28" />
-      </div>
-      <div className="divide-border border-border divide-y rounded-md border">
-        {Array.from({ length: 8 }).map((_, i) => (
-          <div key={i} className="flex items-center gap-3 px-4 py-2">
-            <Skeleton className="size-4 rounded-sm" />
-            <Skeleton className="h-4 w-32" />
-          </div>
-        ))}
-      </div>
-    </div>
-  );
-}

webui2/src/routes/index.tsx 🔗

@@ -35,7 +35,6 @@ function RouteComponent() {
       void navigate({
         to: "/$repo",
         params: { repo: repoSlug(data.repositories.nodes[0]?.name) },
-        search: { ref: "", path: "", type: "tree" as const },
         replace: true,
       });
     }
@@ -61,7 +60,6 @@ function RouteComponent() {
             key={repoSlug(repo.name)}
             to="/$repo"
             params={{ repo: repoSlug(repo.name) }}
-            search={{ ref: "", path: "", type: "tree" as const }}
             className="hover:bg-muted/50 flex items-center gap-3 px-4 py-4 transition-colors"
           >
             <FolderOpen className="text-muted-foreground size-5 shrink-0" />