refactor(web): add GitRefFields fragment for ref-selector

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

Define a GitRefFields fragment in ref-selector.tsx and use it in the
CodePageRefs query. The RefSelector takes unmasked fragment data
directly (no useSuspenseFragment needed — it's a self-contained UI
component) and its onSelect callback returns the shortName string.

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

Change summary

webui2/src/__generated__/gql.ts             |   6 
webui2/src/__generated__/graphql.ts         |   4 
webui2/src/components/code/ref-selector.tsx | 119 ++++++++++------------
webui2/src/routes/$repo.tsx                 |  12 --
webui2/src/routes/$repo/_code.tsx           |   8 
5 files changed, 63 insertions(+), 86 deletions(-)

Detailed changes

webui2/src/__generated__/gql.ts 🔗

@@ -29,11 +29,12 @@ type Documents = {
     "\n  mutation BugSetTitle($input: BugSetTitleInput!) {\n    bugSetTitle(input: $input) {\n      bug {\n        id\n        title\n      }\n    }\n  }\n": typeof types.BugSetTitleDocument,
     "\n  query CommitList($repo: String, $ref: String!, $path: String, $after: String, $first: Int) {\n    repository(ref: $repo) {\n      commits(ref: $ref, path: $path, after: $after, first: $first) {\n        nodes {\n          hash\n          shortHash\n          message\n          authorName\n          date\n        }\n        pageInfo {\n          hasNextPage\n          endCursor\n        }\n      }\n    }\n  }\n": typeof types.CommitListDocument,
     "\n  query FileDiff($repo: String, $hash: String!, $path: String!) {\n    repository(ref: $repo) {\n      commit(hash: $hash) {\n        diff(path: $path) {\n          path\n          oldPath\n          isBinary\n          isNew\n          isDelete\n          hunks {\n            oldStart\n            oldLines\n            newStart\n            newLines\n            lines {\n              type\n              content\n              oldLine\n              newLine\n            }\n          }\n        }\n      }\n    }\n  }\n": typeof types.FileDiffDocument,
+    "\n  fragment RefSelectorRefs on GitRefConnection {\n    nodes {\n      name\n      shortName\n      type\n    }\n  }\n": typeof types.RefSelectorRefsFragmentDoc,
     "\n  fragment IdentitySummary on Identity {\n    id\n    humanId\n    displayName\n    avatarUrl\n  }\n": typeof types.IdentitySummaryFragmentDoc,
     "\n  fragment BugSummary on Bug {\n    id\n    humanId\n    status\n    title\n    labels {\n      name\n      ...LabelFields\n    }\n    author {\n      ...IdentitySummary\n    }\n    createdAt\n    comments {\n      totalCount\n    }\n  }\n": typeof types.BugSummaryFragmentDoc,
     "\n  fragment LabelFields on Label {\n    name\n    color {\n      R\n      G\n      B\n    }\n  }\n": typeof types.LabelFieldsFragmentDoc,
     "\n  query UserIdentity {\n    repository {\n      userIdentity {\n        id\n        humanId\n        name\n        displayName\n        avatarUrl\n        email\n        login\n      }\n    }\n  }\n": typeof types.UserIdentityDocument,
-    "\n  query CodePageRefs($repo: String) {\n    repository(ref: $repo) {\n      name\n      head {\n        shortName\n      }\n      refs {\n        nodes {\n          name\n          shortName\n          type\n          hash\n        }\n      }\n    }\n  }\n": typeof types.CodePageRefsDocument,
+    "\n  query CodePageRefs($repo: String) {\n    repository(ref: $repo) {\n      name\n      head {\n        shortName\n      }\n      refs {\n        ...RefSelectorRefs\n      }\n    }\n  }\n": typeof types.CodePageRefsDocument,
     "\n  query CodePageBlob($repo: String, $ref: String!, $path: String!) {\n    repository(ref: $repo) {\n      blob(ref: $ref, path: $path) {\n        path\n        hash\n        text\n        size\n        isBinary\n        isTruncated\n      }\n    }\n  }\n": typeof types.CodePageBlobDocument,
     "\n  query CodePageTree($repo: String, $ref: String!, $path: String) {\n    repository(ref: $repo) {\n      tree(ref: $ref, path: $path) {\n        name\n        type\n        hash\n      }\n    }\n  }\n": typeof types.CodePageTreeDocument,
     "\n  query CodePageLastCommits(\n    $repo: String\n    $ref: String!\n    $path: String\n    $names: [String!]!\n  ) {\n    repository(ref: $repo) {\n      lastCommits(ref: $ref, path: $path, names: $names) {\n        name\n        commit {\n          hash\n          shortHash\n          message\n          date\n        }\n      }\n    }\n  }\n": typeof types.CodePageLastCommitsDocument,
@@ -63,11 +64,12 @@ const documents: Documents = {
     "\n  mutation BugSetTitle($input: BugSetTitleInput!) {\n    bugSetTitle(input: $input) {\n      bug {\n        id\n        title\n      }\n    }\n  }\n": types.BugSetTitleDocument,
     "\n  query CommitList($repo: String, $ref: String!, $path: String, $after: String, $first: Int) {\n    repository(ref: $repo) {\n      commits(ref: $ref, path: $path, after: $after, first: $first) {\n        nodes {\n          hash\n          shortHash\n          message\n          authorName\n          date\n        }\n        pageInfo {\n          hasNextPage\n          endCursor\n        }\n      }\n    }\n  }\n": types.CommitListDocument,
     "\n  query FileDiff($repo: String, $hash: String!, $path: String!) {\n    repository(ref: $repo) {\n      commit(hash: $hash) {\n        diff(path: $path) {\n          path\n          oldPath\n          isBinary\n          isNew\n          isDelete\n          hunks {\n            oldStart\n            oldLines\n            newStart\n            newLines\n            lines {\n              type\n              content\n              oldLine\n              newLine\n            }\n          }\n        }\n      }\n    }\n  }\n": types.FileDiffDocument,
+    "\n  fragment RefSelectorRefs on GitRefConnection {\n    nodes {\n      name\n      shortName\n      type\n    }\n  }\n": types.RefSelectorRefsFragmentDoc,
     "\n  fragment IdentitySummary on Identity {\n    id\n    humanId\n    displayName\n    avatarUrl\n  }\n": types.IdentitySummaryFragmentDoc,
     "\n  fragment BugSummary on Bug {\n    id\n    humanId\n    status\n    title\n    labels {\n      name\n      ...LabelFields\n    }\n    author {\n      ...IdentitySummary\n    }\n    createdAt\n    comments {\n      totalCount\n    }\n  }\n": types.BugSummaryFragmentDoc,
     "\n  fragment LabelFields on Label {\n    name\n    color {\n      R\n      G\n      B\n    }\n  }\n": types.LabelFieldsFragmentDoc,
     "\n  query UserIdentity {\n    repository {\n      userIdentity {\n        id\n        humanId\n        name\n        displayName\n        avatarUrl\n        email\n        login\n      }\n    }\n  }\n": types.UserIdentityDocument,
-    "\n  query CodePageRefs($repo: String) {\n    repository(ref: $repo) {\n      name\n      head {\n        shortName\n      }\n      refs {\n        nodes {\n          name\n          shortName\n          type\n          hash\n        }\n      }\n    }\n  }\n": types.CodePageRefsDocument,
+    "\n  query CodePageRefs($repo: String) {\n    repository(ref: $repo) {\n      name\n      head {\n        shortName\n      }\n      refs {\n        ...RefSelectorRefs\n      }\n    }\n  }\n": types.CodePageRefsDocument,
     "\n  query CodePageBlob($repo: String, $ref: String!, $path: String!) {\n    repository(ref: $repo) {\n      blob(ref: $ref, path: $path) {\n        path\n        hash\n        text\n        size\n        isBinary\n        isTruncated\n      }\n    }\n  }\n": types.CodePageBlobDocument,
     "\n  query CodePageTree($repo: String, $ref: String!, $path: String) {\n    repository(ref: $repo) {\n      tree(ref: $ref, path: $path) {\n        name\n        type\n        hash\n      }\n    }\n  }\n": types.CodePageTreeDocument,
     "\n  query CodePageLastCommits(\n    $repo: String\n    $ref: String!\n    $path: String\n    $names: [String!]!\n  ) {\n    repository(ref: $repo) {\n      lastCommits(ref: $ref, path: $path, names: $names) {\n        name\n        commit {\n          hash\n          shortHash\n          message\n          date\n        }\n      }\n    }\n  }\n": types.CodePageLastCommitsDocument,
@@ -156,6 +158,10 @@ export function graphql(source: "\n  query CommitList($repo: String, $ref: Strin
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */

webui2/src/__generated__/graphql.ts 🔗

@@ -1245,6 +1245,8 @@ export type FileDiffQueryVariables = Exact<{
 
 export type FileDiffQuery = { repository: { __typename: 'Repository', commit: { __typename: 'GitCommit', diff: { __typename: 'GitFileDiff', path: string, oldPath: string | null, isBinary: boolean, isNew: boolean, isDelete: boolean, hunks: Array<{ __typename: 'GitDiffHunk', oldStart: number, oldLines: number, newStart: number, newLines: number, lines: Array<{ __typename: 'GitDiffLine', type: GitDiffLineType, content: string, oldLine: number, newLine: number }> }> } | null } | null } | null };
 
+export type RefSelectorRefsFragment = { __typename: 'GitRefConnection', nodes: Array<{ __typename: 'GitRef', name: string, shortName: string, type: GitRefType }> };
+
 export type IdentitySummaryFragment = { __typename: 'Identity', id: string, humanId: string, displayName: string, avatarUrl: string | null };
 
 export type BugSummaryFragment = { __typename: 'Bug', id: string, humanId: string, status: Status, title: string, createdAt: string, labels: Array<{ __typename: 'Label', name: string, color: { __typename: 'Color', R: number, G: number, B: number } }>, author: { __typename: 'Identity', id: string, humanId: string, displayName: string, avatarUrl: string | null }, comments: { __typename: 'BugCommentConnection', totalCount: number } };
@@ -1261,7 +1263,7 @@ export type CodePageRefsQueryVariables = Exact<{
 }>;
 
 
-export type CodePageRefsQuery = { repository: { __typename: 'Repository', name: string | null, head: { __typename: 'GitRef', shortName: string } | null, refs: { __typename: 'GitRefConnection', nodes: Array<{ __typename: 'GitRef', name: string, shortName: string, type: GitRefType, hash: string }> } } | null };
+export type CodePageRefsQuery = { repository: { __typename: 'Repository', name: string | null, head: { __typename: 'GitRef', shortName: string } | null, refs: { __typename: 'GitRefConnection', nodes: Array<{ __typename: 'GitRef', name: string, shortName: string, type: GitRefType }> } } | null };
 
 export type CodePageBlobQueryVariables = Exact<{
   repo?: InputMaybe<Scalars['String']['input']>;
@@ -1379,6 +1381,7 @@ export const LabelFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":

webui2/src/components/code/ref-selector.tsx 🔗

@@ -1,3 +1,5 @@
+import { useSuspenseFragment } from "@apollo/client/react";
+import type { FragmentType } from "@apollo/client/masking";
 import {
   useFloating,
   useClick,
@@ -14,20 +16,33 @@ import { GitBranch, Tag } from "lucide-react";
 import { useEffect, useRef, useState } from "react";
 
 import { GitRefType } from "@/__generated__/graphql";
-import type { RefsQueryRef } from "@/routes/$repo";
+import { graphql } from "@/__generated__/gql";
 import { Button } from "@/components/ui/button";
 import * as Listbox from "@/components/ui/listbox";
 import { cn } from "@/lib/utils";
 
+export const REF_SELECTOR_REFS_FRAGMENT = graphql(`
+  fragment RefSelectorRefs on GitRefConnection {
+    nodes {
+      name
+      shortName
+      type
+    }
+  }
+`);
+
 interface RefSelectorProps {
-  gitRefs: RefsQueryRef[];
+  refs: FragmentType<typeof REF_SELECTOR_REFS_FRAGMENT>;
   currentRef: string;
-  onSelect: (ref: RefsQueryRef) => void;
+  onSelect: (shortName: string) => void;
 }
 
 // Branch / tag selector dropdown for the code browser. Shown in two groups
 // (branches, tags) with an inline search filter.
-export function RefSelector({ gitRefs, currentRef, onSelect }: RefSelectorProps) {
+export function RefSelector({ refs: refsProp, currentRef, onSelect }: RefSelectorProps) {
+  const { data } = useSuspenseFragment({ fragment: REF_SELECTOR_REFS_FRAGMENT, from: refsProp });
+  const gitRefs = data.nodes;
+
   const [open, setOpen] = useState(false);
   const [filter, setFilter] = useState("");
   const [activeIndex, setActiveIndex] = useState<number | null>(null);
@@ -84,7 +99,7 @@ export function RefSelector({ gitRefs, currentRef, onSelect }: RefSelectorProps)
       e.preventDefault();
       const ref = flatItems[activeIndex];
       if (ref) {
-        onSelect(ref);
+        onSelect(ref.shortName);
         setOpen(false);
         setFilter("");
       }
@@ -136,21 +151,26 @@ export function RefSelector({ gitRefs, currentRef, onSelect }: RefSelectorProps)
                     {branches.map((ref) => {
                       const i = itemIndex++;
                       return (
-                        <RefItem
+                        <Listbox.Item
                           key={ref.name}
                           id={`ref-option-${i}`}
-                          ref_={ref}
-                          index={i}
+                          ref={(el) => {
+                            elementsRef.current[i] = el;
+                          }}
                           active={activeIndex === i}
                           selected={ref.shortName === currentRef}
-                          elementsRef={elementsRef}
-                          getItemProps={getItemProps}
-                          onSelect={() => {
-                            onSelect(ref);
-                            setOpen(false);
-                            setFilter("");
-                          }}
-                        />
+                          className={cn("text-xs", ref.shortName === currentRef && "font-medium")}
+                          {...getItemProps({
+                            onClick: () => {
+                              onSelect(ref.shortName);
+                              setOpen(false);
+                              setFilter("");
+                            },
+                          })}
+                        >
+                          <GitBranch className="text-muted-foreground size-3 shrink-0" />
+                          <span className="flex-1 truncate font-mono">{ref.shortName}</span>
+                        </Listbox.Item>
                       );
                     })}
                   </Listbox.Group>
@@ -161,21 +181,26 @@ export function RefSelector({ gitRefs, currentRef, onSelect }: RefSelectorProps)
                     {tags.map((ref) => {
                       const i = itemIndex++;
                       return (
-                        <RefItem
+                        <Listbox.Item
                           key={ref.name}
                           id={`ref-option-${i}`}
-                          ref_={ref}
-                          index={i}
+                          ref={(el) => {
+                            elementsRef.current[i] = el;
+                          }}
                           active={activeIndex === i}
                           selected={ref.shortName === currentRef}
-                          elementsRef={elementsRef}
-                          getItemProps={getItemProps}
-                          onSelect={() => {
-                            onSelect(ref);
-                            setOpen(false);
-                            setFilter("");
-                          }}
-                        />
+                          className={cn("text-xs", ref.shortName === currentRef && "font-medium")}
+                          {...getItemProps({
+                            onClick: () => {
+                              onSelect(ref.shortName);
+                              setOpen(false);
+                              setFilter("");
+                            },
+                          })}
+                        >
+                          <Tag className="text-muted-foreground size-3 shrink-0" />
+                          <span className="flex-1 truncate font-mono">{ref.shortName}</span>
+                        </Listbox.Item>
                       );
                     })}
                   </Listbox.Group>
@@ -189,43 +214,3 @@ export function RefSelector({ gitRefs, currentRef, onSelect }: RefSelectorProps)
     </>
   );
 }
-
-function RefItem({
-  id,
-  ref_,
-  index,
-  active,
-  selected,
-  elementsRef,
-  getItemProps,
-  onSelect,
-}: {
-  id: string;
-  ref_: RefsQueryRef;
-  index: number;
-  active: boolean;
-  selected: boolean;
-  elementsRef: React.MutableRefObject<(HTMLElement | null)[]>;
-  getItemProps: (props?: Record<string, unknown>) => Record<string, unknown>;
-  onSelect: () => void;
-}) {
-  return (
-    <Listbox.Item
-      id={id}
-      ref={(el) => {
-        elementsRef.current[index] = el;
-      }}
-      active={active}
-      selected={selected}
-      className={cn("text-xs", selected && "font-medium")}
-      {...getItemProps({ onClick: onSelect })}
-    >
-      {ref_.type === GitRefType.Branch ? (
-        <GitBranch className="text-muted-foreground size-3 shrink-0" />
-      ) : (
-        <Tag className="text-muted-foreground size-3 shrink-0" />
-      )}
-      <span className="flex-1 truncate font-mono">{ref_.shortName}</span>
-    </Listbox.Item>
-  );
-}

webui2/src/routes/$repo.tsx 🔗

@@ -11,12 +11,7 @@ export const REFS_QUERY = graphql(`
         shortName
       }
       refs {
-        nodes {
-          name
-          shortName
-          type
-          hash
-        }
+        ...RefSelectorRefs
       }
     }
   }
@@ -24,11 +19,6 @@ export const REFS_QUERY = graphql(`
 
 export type RefsQueryData = ResultOf<typeof REFS_QUERY>;
 
-/** A single git ref as returned by the REFS_QUERY. */
-export type RefsQueryRef = NonNullable<
-  NonNullable<RefsQueryData["repository"]>["refs"]
->["nodes"][number];
-
 export const Route = createFileRoute("/$repo")({
   beforeLoad: ({ params: { repo }, context: { preloadQuery } }) => {
     // Normalize the repo slug: "_" means the default (null) repo

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

@@ -12,7 +12,6 @@ import {
 } from "@tanstack/react-router";
 import { GitCommit } from "lucide-react";
 
-import type { RefsQueryRef } from "@/routes/$repo";
 import { CodeBreadcrumb } from "@/components/code/code-breadcrumb";
 import { RefSelector } from "@/components/code/ref-selector";
 import { ButtonLink } from "@/components/ui/button-link";
@@ -29,7 +28,7 @@ function CodeLayout() {
   const { repo } = Route.useParams();
   const { ref: repoRef, refsRef } = Route.useRouteContext();
   const { data: refsData } = useReadQuery(refsRef);
-  const refs: RefsQueryRef[] = refsData?.repository?.refs?.nodes ?? [];
+  const refs = refsData?.repository?.refs;
   const repoName = refsData?.repository?.name ?? repoRef ?? "default-repo";
 
   // Read child route params (ref and splat path)
@@ -51,8 +50,7 @@ function CodeLayout() {
 
   const navigate = useNavigate();
 
-  function handleRefSelect(newRef: RefsQueryRef) {
-    const refName = newRef.shortName;
+  function handleRefSelect(refName: string) {
     if (viewMode === "commits") {
       void navigate({ to: "/$repo/commits/$ref", params: { repo, ref: refName }, search: { path: currentPath || undefined } });
     } else if (viewMode === "blob") {
@@ -100,7 +98,7 @@ function CodeLayout() {
               History
             </ButtonLink>
           )}
-          <RefSelector gitRefs={refs} currentRef={currentRef} onSelect={handleRefSelect} />
+          {refs && <RefSelector refs={refs} currentRef={currentRef} onSelect={handleRefSelect} />}
         </div>
       </div>