refactor(web): apply fragment colocation to timeline and code browser

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

- Timeline: define TimelineItems fragment on BugTimelineItemConnection,
  receive masked data via `timeline` prop, unmask with useSuspenseFragment
- FileViewer: define FileViewerBlob fragment on GitBlob, receive masked
  data via `blob` prop
- FileTree: replace wide GitTreeEntry import with local interface (data
  comes from two merged queries, fragment doesn't fit)
- BugDetail query now spreads ...TimelineItems instead of inlining
- Blob query now spreads ...FileViewerBlob instead of inlining

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

Change summary

webui2/src/__generated__/gql.ts                | 15 ++++-
webui2/src/__generated__/graphql.ts            | 10 ++++
webui2/src/components/bugs/timeline.tsx        | 47 ++++++++++++++++---
webui2/src/components/code/file-tree.tsx       | 12 ++--
webui2/src/components/code/file-viewer.tsx     | 36 ++++++++------
webui2/src/routes/$repo/_code/blob/$ref/$.tsx  | 11 +--
webui2/src/routes/$repo/_issues/issues/$id.tsx | 22 --------
7 files changed, 94 insertions(+), 59 deletions(-)

Detailed changes

webui2/src/__generated__/gql.ts 🔗

@@ -25,23 +25,25 @@ type Documents = {
     "\n  fragment LabelChangeFields on BugLabelChangeTimelineItem {\n    author {\n      humanId\n      displayName\n    }\n    date\n    added {\n      ...LabelFields\n    }\n    removed {\n      ...LabelFields\n    }\n  }\n": typeof types.LabelChangeFieldsFragmentDoc,
     "\n  fragment StatusChangeFields on BugSetStatusTimelineItem {\n    author {\n      humanId\n      displayName\n    }\n    date\n    status\n  }\n": typeof types.StatusChangeFieldsFragmentDoc,
     "\n  fragment TitleChangeFields on BugSetTitleTimelineItem {\n    author {\n      humanId\n      displayName\n    }\n    date\n    title\n    was\n  }\n": typeof types.TitleChangeFieldsFragmentDoc,
+    "\n  fragment TimelineItems on BugTimelineItemConnection {\n    nodes {\n      __typename\n      id\n      ... on BugCreateTimelineItem {\n        ...BugCreateCommentFields\n      }\n      ... on BugAddCommentTimelineItem {\n        ...BugAddCommentFields\n      }\n      ... on BugLabelChangeTimelineItem {\n        ...LabelChangeFields\n      }\n      ... on BugSetStatusTimelineItem {\n        ...StatusChangeFields\n      }\n      ... on BugSetTitleTimelineItem {\n        ...TitleChangeFields\n      }\n    }\n  }\n": typeof types.TimelineItemsFragmentDoc,
     "\n  mutation BugEditComment($input: BugEditCommentInput!) {\n    bugEditComment(input: $input) {\n      bug {\n        id\n      }\n    }\n  }\n": typeof types.BugEditCommentDocument,
     "\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 FileViewerBlob on GitBlob {\n    path\n    hash\n    text\n    size\n    isBinary\n    isTruncated\n  }\n": typeof types.FileViewerBlobFragmentDoc,
     "\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        ...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 CodePageBlob($repo: String, $ref: String!, $path: String!) {\n    repository(ref: $repo) {\n      blob(ref: $ref, path: $path) {\n        ...FileViewerBlob\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,
     "\n  query CodePageReadme($repo: String, $ref: String!, $path: String!) {\n    repository(ref: $repo) {\n      blob(ref: $ref, path: $path) {\n        text\n      }\n    }\n  }\n": typeof types.CodePageReadmeDocument,
     "\n  query AllIdentities($ref: String) {\n    repository(ref: $ref) {\n      allIdentities(first: 1000) {\n        nodes {\n          id\n          humanId\n          name\n          email\n          login\n          displayName\n          avatarUrl\n        }\n      }\n    }\n  }\n": typeof types.AllIdentitiesDocument,
     "\n  query ValidLabels($ref: String) {\n    repository(ref: $ref) {\n      validLabels {\n        nodes {\n          name\n          color {\n            R\n            G\n            B\n          }\n          ...LabelFields\n        }\n      }\n    }\n  }\n": typeof types.ValidLabelsDocument,
-    "\n  query BugDetail($ref: String, $prefix: String!) {\n    repository(ref: $ref) {\n      bug(prefix: $prefix) {\n        ...BugSummary\n        lastEdit\n        participants(first: 20) {\n          nodes {\n            ...IdentitySummary\n          }\n        }\n        timeline(first: 250) {\n          nodes {\n            __typename\n            id\n            ... on BugCreateTimelineItem {\n              ...BugCreateCommentFields\n            }\n            ... on BugAddCommentTimelineItem {\n              ...BugAddCommentFields\n            }\n            ... on BugLabelChangeTimelineItem {\n              ...LabelChangeFields\n            }\n            ... on BugSetStatusTimelineItem {\n              ...StatusChangeFields\n            }\n            ... on BugSetTitleTimelineItem {\n              ...TitleChangeFields\n            }\n          }\n        }\n      }\n    }\n  }\n": typeof types.BugDetailDocument,
+    "\n  query BugDetail($ref: String, $prefix: String!) {\n    repository(ref: $ref) {\n      bug(prefix: $prefix) {\n        ...BugSummary\n        lastEdit\n        participants(first: 20) {\n          nodes {\n            ...IdentitySummary\n          }\n        }\n        timeline(first: 250) {\n          ...TimelineItems\n        }\n      }\n    }\n  }\n": typeof types.BugDetailDocument,
     "\n  query BugList(\n    $ref: String\n    $openQuery: String!\n    $closedQuery: String!\n    $listQuery: String!\n    $first: Int\n    $after: String\n  ) {\n    repository(ref: $ref) {\n      openCount: allBugs(query: $openQuery, first: 1) {\n        totalCount\n      }\n      closedCount: allBugs(query: $closedQuery, first: 1) {\n        totalCount\n      }\n      bugs: allBugs(query: $listQuery, first: $first, after: $after) {\n        totalCount\n        pageInfo {\n          hasNextPage\n          endCursor\n        }\n        nodes {\n          ...BugSummary\n        }\n      }\n    }\n  }\n": typeof types.BugListDocument,
     "\n  mutation BugCreate($input: BugCreateInput!) {\n    bugCreate(input: $input) {\n      bug {\n        id\n        humanId\n      }\n    }\n  }\n": typeof types.BugCreateDocument,
     "\n  query UserProfile(\n    $ref: String\n    $prefix: String!\n    $openQuery: String!\n    $closedQuery: String!\n    $listQuery: String!\n    $after: String\n  ) {\n    repository(ref: $ref) {\n      identity(prefix: $prefix) {\n        id\n        humanId\n        name\n        email\n        login\n        displayName\n        avatarUrl\n        isProtected\n      }\n      openCount: allBugs(query: $openQuery, first: 1) {\n        totalCount\n      }\n      closedCount: allBugs(query: $closedQuery, first: 1) {\n        totalCount\n      }\n      bugs: allBugs(query: $listQuery, first: 25, after: $after) {\n        totalCount\n        pageInfo {\n          hasNextPage\n          endCursor\n        }\n        nodes {\n          ...BugSummary\n        }\n      }\n    }\n  }\n": typeof types.UserProfileDocument,
@@ -60,23 +62,25 @@ const documents: Documents = {
     "\n  fragment LabelChangeFields on BugLabelChangeTimelineItem {\n    author {\n      humanId\n      displayName\n    }\n    date\n    added {\n      ...LabelFields\n    }\n    removed {\n      ...LabelFields\n    }\n  }\n": types.LabelChangeFieldsFragmentDoc,
     "\n  fragment StatusChangeFields on BugSetStatusTimelineItem {\n    author {\n      humanId\n      displayName\n    }\n    date\n    status\n  }\n": types.StatusChangeFieldsFragmentDoc,
     "\n  fragment TitleChangeFields on BugSetTitleTimelineItem {\n    author {\n      humanId\n      displayName\n    }\n    date\n    title\n    was\n  }\n": types.TitleChangeFieldsFragmentDoc,
+    "\n  fragment TimelineItems on BugTimelineItemConnection {\n    nodes {\n      __typename\n      id\n      ... on BugCreateTimelineItem {\n        ...BugCreateCommentFields\n      }\n      ... on BugAddCommentTimelineItem {\n        ...BugAddCommentFields\n      }\n      ... on BugLabelChangeTimelineItem {\n        ...LabelChangeFields\n      }\n      ... on BugSetStatusTimelineItem {\n        ...StatusChangeFields\n      }\n      ... on BugSetTitleTimelineItem {\n        ...TitleChangeFields\n      }\n    }\n  }\n": types.TimelineItemsFragmentDoc,
     "\n  mutation BugEditComment($input: BugEditCommentInput!) {\n    bugEditComment(input: $input) {\n      bug {\n        id\n      }\n    }\n  }\n": types.BugEditCommentDocument,
     "\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 FileViewerBlob on GitBlob {\n    path\n    hash\n    text\n    size\n    isBinary\n    isTruncated\n  }\n": types.FileViewerBlobFragmentDoc,
     "\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        ...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 CodePageBlob($repo: String, $ref: String!, $path: String!) {\n    repository(ref: $repo) {\n      blob(ref: $ref, path: $path) {\n        ...FileViewerBlob\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,
     "\n  query CodePageReadme($repo: String, $ref: String!, $path: String!) {\n    repository(ref: $repo) {\n      blob(ref: $ref, path: $path) {\n        text\n      }\n    }\n  }\n": types.CodePageReadmeDocument,
     "\n  query AllIdentities($ref: String) {\n    repository(ref: $ref) {\n      allIdentities(first: 1000) {\n        nodes {\n          id\n          humanId\n          name\n          email\n          login\n          displayName\n          avatarUrl\n        }\n      }\n    }\n  }\n": types.AllIdentitiesDocument,
     "\n  query ValidLabels($ref: String) {\n    repository(ref: $ref) {\n      validLabels {\n        nodes {\n          name\n          color {\n            R\n            G\n            B\n          }\n          ...LabelFields\n        }\n      }\n    }\n  }\n": types.ValidLabelsDocument,
-    "\n  query BugDetail($ref: String, $prefix: String!) {\n    repository(ref: $ref) {\n      bug(prefix: $prefix) {\n        ...BugSummary\n        lastEdit\n        participants(first: 20) {\n          nodes {\n            ...IdentitySummary\n          }\n        }\n        timeline(first: 250) {\n          nodes {\n            __typename\n            id\n            ... on BugCreateTimelineItem {\n              ...BugCreateCommentFields\n            }\n            ... on BugAddCommentTimelineItem {\n              ...BugAddCommentFields\n            }\n            ... on BugLabelChangeTimelineItem {\n              ...LabelChangeFields\n            }\n            ... on BugSetStatusTimelineItem {\n              ...StatusChangeFields\n            }\n            ... on BugSetTitleTimelineItem {\n              ...TitleChangeFields\n            }\n          }\n        }\n      }\n    }\n  }\n": types.BugDetailDocument,
+    "\n  query BugDetail($ref: String, $prefix: String!) {\n    repository(ref: $ref) {\n      bug(prefix: $prefix) {\n        ...BugSummary\n        lastEdit\n        participants(first: 20) {\n          nodes {\n            ...IdentitySummary\n          }\n        }\n        timeline(first: 250) {\n          ...TimelineItems\n        }\n      }\n    }\n  }\n": types.BugDetailDocument,
     "\n  query BugList(\n    $ref: String\n    $openQuery: String!\n    $closedQuery: String!\n    $listQuery: String!\n    $first: Int\n    $after: String\n  ) {\n    repository(ref: $ref) {\n      openCount: allBugs(query: $openQuery, first: 1) {\n        totalCount\n      }\n      closedCount: allBugs(query: $closedQuery, first: 1) {\n        totalCount\n      }\n      bugs: allBugs(query: $listQuery, first: $first, after: $after) {\n        totalCount\n        pageInfo {\n          hasNextPage\n          endCursor\n        }\n        nodes {\n          ...BugSummary\n        }\n      }\n    }\n  }\n": types.BugListDocument,
     "\n  mutation BugCreate($input: BugCreateInput!) {\n    bugCreate(input: $input) {\n      bug {\n        id\n        humanId\n      }\n    }\n  }\n": types.BugCreateDocument,
     "\n  query UserProfile(\n    $ref: String\n    $prefix: String!\n    $openQuery: String!\n    $closedQuery: String!\n    $listQuery: String!\n    $after: String\n  ) {\n    repository(ref: $ref) {\n      identity(prefix: $prefix) {\n        id\n        humanId\n        name\n        email\n        login\n        displayName\n        avatarUrl\n        isProtected\n      }\n      openCount: allBugs(query: $openQuery, first: 1) {\n        totalCount\n      }\n      closedCount: allBugs(query: $closedQuery, first: 1) {\n        totalCount\n      }\n      bugs: allBugs(query: $listQuery, first: 25, after: $after) {\n        totalCount\n        pageInfo {\n          hasNextPage\n          endCursor\n        }\n        nodes {\n          ...BugSummary\n        }\n      }\n    }\n  }\n": types.UserProfileDocument,
@@ -142,6 +146,10 @@ export function graphql(source: "\n  fragment StatusChangeFields on BugSetStatus
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
 export function graphql(source: "\n  fragment TitleChangeFields on BugSetTitleTimelineItem {\n    author {\n      humanId\n      displayName\n    }\n    date\n    title\n    was\n  }\n"): (typeof documents)["\n  fragment TitleChangeFields on BugSetTitleTimelineItem {\n    author {\n      humanId\n      displayName\n    }\n    date\n    title\n    was\n  }\n"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */

webui2/src/__generated__/graphql.ts 🔗

@@ -1211,6 +1211,14 @@ export type StatusChangeFieldsFragment = { __typename: 'BugSetStatusTimelineItem
 
 export type TitleChangeFieldsFragment = { __typename: 'BugSetTitleTimelineItem', date: string, title: string, was: string, author: { __typename: 'Identity', humanId: string, displayName: string } };
 
+export type TimelineItemsFragment = { __typename: 'BugTimelineItemConnection', nodes: Array<
+    | { __typename: 'BugAddCommentTimelineItem', id: string, message: string, createdAt: string, lastEdit: string, edited: boolean, author: { __typename: 'Identity', id: string, humanId: string, displayName: string, avatarUrl: string | null } }
+    | { __typename: 'BugCreateTimelineItem', id: string, message: string, createdAt: string, lastEdit: string, edited: boolean, author: { __typename: 'Identity', id: string, humanId: string, displayName: string, avatarUrl: string | null } }
+    | { __typename: 'BugLabelChangeTimelineItem', id: string, date: string, author: { __typename: 'Identity', humanId: string, displayName: string }, added: Array<{ __typename: 'Label', name: string, color: { __typename: 'Color', R: number, G: number, B: number } }>, removed: Array<{ __typename: 'Label', name: string, color: { __typename: 'Color', R: number, G: number, B: number } }> }
+    | { __typename: 'BugSetStatusTimelineItem', id: string, date: string, status: Status, author: { __typename: 'Identity', humanId: string, displayName: string } }
+    | { __typename: 'BugSetTitleTimelineItem', id: string, date: string, title: string, was: string, author: { __typename: 'Identity', humanId: string, displayName: string } }
+  > };
+
 export type BugEditCommentMutationVariables = Exact<{
   input: BugEditCommentInput;
 }>;
@@ -1245,6 +1253,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 FileViewerBlobFragment = { __typename: 'GitBlob', path: string, hash: string, text: string | null, size: number, isBinary: boolean, isTruncated: boolean };
+
 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 };
@@ -1381,6 +1391,8 @@ export const LabelFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":

webui2/src/components/bugs/timeline.tsx 🔗

@@ -1,10 +1,11 @@
-import { useMutation } from "@apollo/client/react";
+import { useMutation, useSuspenseFragment } from "@apollo/client/react";
+import type { FragmentType } from "@apollo/client/masking";
 import { Link } from "@tanstack/react-router";
 import { formatDistanceToNow } from "date-fns";
 import { Tag, GitPullRequestClosed, Pencil, CircleDot } from "lucide-react";
 import { useState } from "react";
 
-import { Status, type BugDetailQuery, BugDetailDocument } from "@/__generated__/graphql";
+import { Status, BugDetailDocument } from "@/__generated__/graphql";
 import { graphql } from "@/__generated__/gql";
 import { Markdown } from "@/components/content/markdown";
 import { Button } from "@/components/ui/button";
@@ -76,6 +77,30 @@ graphql(`
   }
 `);
 
+export const TIMELINE_ITEMS_FRAGMENT = graphql(`
+  fragment TimelineItems on BugTimelineItemConnection {
+    nodes {
+      __typename
+      id
+      ... on BugCreateTimelineItem {
+        ...BugCreateCommentFields
+      }
+      ... on BugAddCommentTimelineItem {
+        ...BugAddCommentFields
+      }
+      ... on BugLabelChangeTimelineItem {
+        ...LabelChangeFields
+      }
+      ... on BugSetStatusTimelineItem {
+        ...StatusChangeFields
+      }
+      ... on BugSetTitleTimelineItem {
+        ...TitleChangeFields
+      }
+    }
+  }
+`);
+
 const BUG_EDIT_COMMENT_MUTATION = graphql(`
   mutation BugEditComment($input: BugEditCommentInput!) {
     bugEditComment(input: $input) {
@@ -86,23 +111,29 @@ const BUG_EDIT_COMMENT_MUTATION = graphql(`
   }
 `);
 
-type TimelineNode = NonNullable<
-  NonNullable<NonNullable<BugDetailQuery["repository"]>["bug"]>["timeline"]["nodes"][number]
->;
+type TimelineData = ReturnType<
+  typeof useSuspenseFragment<typeof TIMELINE_ITEMS_FRAGMENT>
+>["data"];
+type TimelineNode = TimelineData["nodes"][number];
 
 interface TimelineProps {
   repo: string | null;
   bugPrefix: string;
-  items: TimelineNode[];
+  timeline: FragmentType<typeof TIMELINE_ITEMS_FRAGMENT>;
 }
 
 // Ordered sequence of events on a bug: comments (create and add-comment) and
 // inline events (label changes, status changes, title edits). Comment items
 // support inline editing for the logged-in user.
-export function Timeline({ repo, bugPrefix, items }: TimelineProps) {
+export function Timeline({ repo, bugPrefix, timeline }: TimelineProps) {
+  const { data } = useSuspenseFragment({
+    fragment: TIMELINE_ITEMS_FRAGMENT,
+    from: timeline,
+  });
+
   return (
     <div className="space-y-4">
-      {items.map((item) => {
+      {data.nodes.map((item) => {
         switch (item.__typename) {
           case "BugCreateTimelineItem":
           case "BugAddCommentTimelineItem":

webui2/src/components/code/file-tree.tsx 🔗

@@ -2,9 +2,11 @@ import { Link } from "@tanstack/react-router";
 import { formatDistanceToNow } from "date-fns";
 import { Folder, File } from "lucide-react";
 
-import { GitObjectType, type GitTreeEntry } from "@/__generated__/graphql";
-
-export interface TreeEntryWithCommit extends GitTreeEntry {
+/** A single tree entry with optional last-commit metadata, merged from two queries. */
+export interface TreeEntryWithCommit {
+  name: string;
+  type: "BLOB" | "TREE" | "SYMLINK" | "SUBMODULE";
+  hash: string;
   lastCommit?: {
     hash: string;
     shortHash: string;
@@ -25,7 +27,7 @@ interface FileTreeProps {
 export function FileTree({ repo, currentRef, currentPath, entries }: FileTreeProps) {
   // Directories first, then files — each group alphabetical
   const sorted = entries.toSorted((a, b) => {
-    if (a.type !== b.type) return a.type === GitObjectType.Tree ? -1 : 1;
+    if (a.type !== b.type) return a.type === "TREE" ? -1 : 1;
     return a.name.localeCompare(b.name);
   });
 
@@ -77,7 +79,7 @@ function FileTreeRow({
   currentRef: string;
   currentPath: string;
 }) {
-  const isDir = entry.type === GitObjectType.Tree;
+  const isDir = entry.type === "TREE";
   const entryPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
 
   const entryLink = isDir

webui2/src/components/code/file-viewer.tsx 🔗

@@ -8,13 +8,27 @@ import { useState, useEffect, useCallback, Fragment, type ReactNode } from "reac
 import { jsx, jsxs } from "react/jsx-runtime";
 import type { ShikiTransformer } from "shiki/core";
 
-import type { GitBlob } from "@/__generated__/graphql";
+import { useSuspenseFragment } from "@apollo/client/react";
+import type { FragmentType } from "@apollo/client/masking";
+
+import { graphql } from "@/__generated__/gql";
 import { Button } from "@/components/ui/button";
 import { Skeleton } from "@/components/ui/skeleton";
 import { getHighlighter, SHIKI_THEMES } from "@/lib/shiki";
 
 import styles from "./file-viewer.module.css";
 
+export const FILE_VIEWER_BLOB_FRAGMENT = graphql(`
+  fragment FileViewerBlob on GitBlob {
+    path
+    hash
+    text
+    size
+    isBinary
+    isTruncated
+  }
+`);
+
 interface LangEntry {
   id: string;
   load: () => Promise<unknown>;
@@ -124,22 +138,14 @@ function buildHash(range: LineRange): string {
 // ── Component ─────────────────────────────────────────────────────────────────
 
 interface FileViewerProps {
-  blob: GitBlob | null;
+  blob: FragmentType<typeof FILE_VIEWER_BLOB_FRAGMENT>;
 }
 
-export function FileViewer({ blob }: FileViewerProps) {
-  if (!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>
-    );
-  }
+export function FileViewer({ blob: blobProp }: FileViewerProps) {
+  const { data: blob } = useSuspenseFragment({
+    fragment: FILE_VIEWER_BLOB_FRAGMENT,
+    from: blobProp,
+  });
 
   const [highlighted, setHighlighted] = useState<{ node: ReactNode; lineCount: number } | null>(
     null,

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

@@ -11,12 +11,7 @@ const BLOB_QUERY = graphql(`
   query CodePageBlob($repo: String, $ref: String!, $path: String!) {
     repository(ref: $repo) {
       blob(ref: $ref, path: $path) {
-        path
-        hash
-        text
-        size
-        isBinary
-        isTruncated
+        ...FileViewerBlob
       }
     }
   }
@@ -51,5 +46,7 @@ function BlobView() {
   const { blobRef } = Route.useLoaderData();
   const { data } = useReadQuery(blobRef);
 
-  return <FileViewer blob={data?.repository?.blob ?? null} />;
+  const blob = data?.repository?.blob;
+  if (!blob) return <BlobSkeleton />;
+  return <FileViewer blob={blob} />;
 }

webui2/src/routes/$repo/_issues/issues/$id.tsx 🔗

@@ -17,25 +17,7 @@ const BUG_DETAIL_QUERY = graphql(`
           }
         }
         timeline(first: 250) {
-          nodes {
-            __typename
-            id
-            ... on BugCreateTimelineItem {
-              ...BugCreateCommentFields
-            }
-            ... on BugAddCommentTimelineItem {
-              ...BugAddCommentFields
-            }
-            ... on BugLabelChangeTimelineItem {
-              ...LabelChangeFields
-            }
-            ... on BugSetStatusTimelineItem {
-              ...StatusChangeFields
-            }
-            ... on BugSetTitleTimelineItem {
-              ...TitleChangeFields
-            }
-          }
+          ...TimelineItems
         }
       }
     }
@@ -110,7 +92,7 @@ function RouteComponent() {
       <div className="flex gap-8">
         {/* Timeline + comment box */}
         <div className="min-w-0 flex-1 space-y-4">
-          <Timeline repo={repo} bugPrefix={bug.humanId} items={bug.timeline.nodes} />
+          <Timeline repo={repo} bugPrefix={bug.humanId} timeline={bug.timeline} />
           <CommentBox bugPrefix={bug.humanId} bugStatus={bug.status} ref_={ref} />
         </div>