refactor(web): apply oxfmt formatting

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

run oxfmt across the entire codebase to establish consistent formatting
with import sorting and tailwind class sorting

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

Change summary

webui2/README.md                              |   4 
webui2/codegen.ts                             |  26 -
webui2/postcss.config.js                      |   2 
webui2/src/App.tsx                            |  45 +-
webui2/src/components/bugs/BugRow.tsx         |  55 ++-
webui2/src/components/bugs/CommentBox.tsx     |  90 +++---
webui2/src/components/bugs/IssueFilters.tsx   | 200 ++++++++-----
webui2/src/components/bugs/LabelBadge.tsx     |  24 +
webui2/src/components/bugs/LabelEditor.tsx    |  54 ++-
webui2/src/components/bugs/QueryInput.tsx     | 275 ++++++++++---------
webui2/src/components/bugs/StatusBadge.tsx    |  23 
webui2/src/components/bugs/Timeline.tsx       | 161 ++++++-----
webui2/src/components/bugs/TitleEditor.tsx    |  61 ++--
webui2/src/components/code/CodeBreadcrumb.tsx |  22 
webui2/src/components/code/CommitList.tsx     | 113 ++++---
webui2/src/components/code/FileDiffView.tsx   | 135 +++++----
webui2/src/components/code/FileTree.tsx       |  71 ++--
webui2/src/components/code/FileViewer.tsx     |  71 ++--
webui2/src/components/code/RefSelector.tsx    |  61 ++-
webui2/src/components/content/Markdown.tsx    |  48 +-
webui2/src/components/layout/Header.tsx       |  93 +++---
webui2/src/components/layout/Shell.tsx        |   7 
webui2/src/components/ui/avatar.tsx           |  27 +
webui2/src/components/ui/badge.tsx            |  30 +-
webui2/src/components/ui/button.tsx           |  59 ++--
webui2/src/components/ui/input.tsx            |  17 
webui2/src/components/ui/popover.tsx          |  23 
webui2/src/components/ui/separator.tsx        |  19 
webui2/src/components/ui/skeleton.tsx         |  11 
webui2/src/components/ui/textarea.tsx         |  17 
webui2/src/index.css                          |  26 +
webui2/src/lib/apollo.ts                      |  10 
webui2/src/lib/auth.tsx                       | 107 +++----
webui2/src/lib/repo.tsx                       |  12 
webui2/src/lib/theme.tsx                      |  32 +-
webui2/src/lib/utils.ts                       |   6 
webui2/src/main.tsx                           |  22 
webui2/src/pages/BugDetailPage.tsx            |  57 ++--
webui2/src/pages/BugListPage.tsx              | 292 ++++++++++++--------
webui2/src/pages/CodePage.tsx                 | 149 +++++----
webui2/src/pages/CommitPage.tsx               |  57 ++--
webui2/src/pages/ErrorPage.tsx                |  27 -
webui2/src/pages/IdentitySelectPage.tsx       |  78 ++---
webui2/src/pages/NewBugPage.tsx               |  58 ++-
webui2/src/pages/RepoPickerPage.tsx           |  29 +-
webui2/src/pages/UserProfilePage.tsx          | 136 +++++----
webui2/tailwind.config.ts                     |  79 ++--
webui2/tsconfig.app.tsbuildinfo               |   0 
webui2/tsconfig.json                          |   5 
webui2/tsconfig.node.tsbuildinfo              |   2 
webui2/vite.config.ts                         |  29 +-
51 files changed, 1,640 insertions(+), 1,417 deletions(-)

Detailed changes

webui2/README.md πŸ”—

@@ -22,7 +22,7 @@ Node 22 is required. If you use asdf, `.tool-versions` pins the right version au
 ## Routes
 
 | Path                    | Page                                                     |
-|-------------------------|----------------------------------------------------------|
+| ----------------------- | -------------------------------------------------------- |
 | `/`                     | Repo picker β€” auto-redirects when there is only one repo |
 | `/_`                    | Default repo (issues + code browser)                     |
 | `/_/issues`             | Issue list with search and label filtering               |
@@ -62,7 +62,7 @@ pnpm codegen
 **Code browser** uses REST endpoints at `/api/repos/{owner}/{repo}/git/*` implemented in `api/http/git_browse_handler.go`. `_` is used for both owner and repo (local single-user setup). The TypeScript client is `src/lib/gitApi.ts`.
 
 | Endpoint                              | Description                             |
-|---------------------------------------|-----------------------------------------|
+| ------------------------------------- | --------------------------------------- |
 | `GET /git/refs`                       | List branches and tags                  |
 | `GET /git/trees/{ref}?path=`          | Directory listing with last-commit info |
 | `GET /git/blobs/{ref}?path=`          | File content                            |

webui2/codegen.ts πŸ”—

@@ -1,28 +1,24 @@
-import type { CodegenConfig } from '@graphql-codegen/cli'
+import type { CodegenConfig } from "@graphql-codegen/cli";
 
 const config: CodegenConfig = {
-  schema: '../api/graphql/schema/*.graphql',
-  documents: 'src/graphql/**/*.graphql',
+  schema: "../api/graphql/schema/*.graphql",
+  documents: "src/graphql/**/*.graphql",
   generates: {
-    'src/__generated__/graphql.ts': {
-      plugins: [
-        'typescript',
-        'typescript-operations',
-        'typescript-react-apollo',
-      ],
+    "src/__generated__/graphql.ts": {
+      plugins: ["typescript", "typescript-operations", "typescript-react-apollo"],
       config: {
         withHooks: true,
         withComponent: false,
         withHOC: false,
         scalars: {
-          Time: 'string',
-          Hash: 'string',
-          CombinedId: 'string',
-          Color: '{ R: number; G: number; B: number }',
+          Time: "string",
+          Hash: "string",
+          CombinedId: "string",
+          Color: "{ R: number; G: number; B: number }",
         },
       },
     },
   },
-}
+};
 
-export default config
+export default config;

webui2/src/App.tsx πŸ”—

@@ -1,15 +1,16 @@
-import { createBrowserRouter, RouterProvider } from 'react-router-dom'
-import { Shell } from '@/components/layout/Shell'
-import { RepoShell } from '@/lib/repo'
-import { RepoPickerPage } from '@/pages/RepoPickerPage'
-import { BugListPage } from '@/pages/BugListPage'
-import { BugDetailPage } from '@/pages/BugDetailPage'
-import { NewBugPage } from '@/pages/NewBugPage'
-import { CodePage } from '@/pages/CodePage'
-import { UserProfilePage } from '@/pages/UserProfilePage'
-import { CommitPage } from '@/pages/CommitPage'
-import { IdentitySelectPage } from '@/pages/IdentitySelectPage'
-import { ErrorPage } from '@/pages/ErrorPage'
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+
+import { Shell } from "@/components/layout/Shell";
+import { RepoShell } from "@/lib/repo";
+import { BugDetailPage } from "@/pages/BugDetailPage";
+import { BugListPage } from "@/pages/BugListPage";
+import { CodePage } from "@/pages/CodePage";
+import { CommitPage } from "@/pages/CommitPage";
+import { ErrorPage } from "@/pages/ErrorPage";
+import { IdentitySelectPage } from "@/pages/IdentitySelectPage";
+import { NewBugPage } from "@/pages/NewBugPage";
+import { RepoPickerPage } from "@/pages/RepoPickerPage";
+import { UserProfilePage } from "@/pages/UserProfilePage";
 
 // Route structure:
 //   /                          β†’ repo picker (or redirect if single repo)
@@ -18,28 +19,28 @@ import { ErrorPage } from '@/pages/ErrorPage'
 //   /auth/select-identity      β†’ OAuth identity adoption (first-time login)
 const router = createBrowserRouter([
   {
-    path: '/',
+    path: "/",
     element: <Shell />,
     errorElement: <ErrorPage />,
     children: [
       { index: true, element: <RepoPickerPage /> },
-      { path: 'auth/select-identity', element: <IdentitySelectPage /> },
+      { path: "auth/select-identity", element: <IdentitySelectPage /> },
       {
-        path: ':repo',
+        path: ":repo",
         element: <RepoShell />,
         children: [
           { index: true, element: <CodePage /> },
-          { path: 'issues', element: <BugListPage /> },
-          { path: 'issues/new', element: <NewBugPage /> },
-          { path: 'issues/:id', element: <BugDetailPage /> },
-          { path: 'user/:id', element: <UserProfilePage /> },
-          { path: 'commit/:hash', element: <CommitPage /> },
+          { path: "issues", element: <BugListPage /> },
+          { path: "issues/new", element: <NewBugPage /> },
+          { path: "issues/:id", element: <BugDetailPage /> },
+          { path: "user/:id", element: <UserProfilePage /> },
+          { path: "commit/:hash", element: <CommitPage /> },
         ],
       },
     ],
   },
-])
+]);
 
 export function App() {
-  return <RouterProvider router={router} />
+  return <RouterProvider router={router} />;
 }

webui2/src/components/bugs/BugRow.tsx πŸ”—

@@ -1,21 +1,23 @@
-import { Link } from 'react-router-dom'
-import { MessageSquare, CircleDot, CircleCheck } from 'lucide-react'
-import { formatDistanceToNow } from 'date-fns'
-import { LabelBadge } from './LabelBadge'
-import { Status } from '@/__generated__/graphql'
+import { formatDistanceToNow } from "date-fns";
+import { MessageSquare, CircleDot, CircleCheck } from "lucide-react";
+import { Link } from "react-router-dom";
+
+import { Status } from "@/__generated__/graphql";
+
+import { LabelBadge } from "./LabelBadge";
 
 interface BugRowProps {
-  id: string
-  humanId: string
-  status: Status
-  title: string
-  labels: Array<{ name: string; color: { R: number; G: number; B: number } }>
-  author: { humanId: string; displayName: string; avatarUrl?: string | null }
-  createdAt: string
-  commentCount: number
+  id: string;
+  humanId: string;
+  status: Status;
+  title: string;
+  labels: Array<{ name: string; color: { R: number; G: number; B: number } }>;
+  author: { humanId: string; displayName: string; avatarUrl?: string | null };
+  createdAt: string;
+  commentCount: number;
   /** Current repo slug, used to build /:repo/issues/:id and /:repo/user/:id links. */
-  repo: string | null
-  onLabelClick?: (name: string) => void
+  repo: string | null;
+  onLabelClick?: (name: string) => void;
 }
 
 // Single row in the issue list. Shows status icon, title, labels, author and
@@ -31,19 +33,19 @@ export function BugRow({
   repo,
   onLabelClick,
 }: BugRowProps) {
-  const isOpen = status === Status.Open
-  const StatusIcon = isOpen ? CircleDot : CircleCheck
+  const isOpen = status === Status.Open;
+  const StatusIcon = isOpen ? CircleDot : CircleCheck;
 
-  const issueHref = repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}`
-  const authorHref = repo ? `/${repo}/user/${author.humanId}` : `/user/${author.humanId}`
+  const issueHref = repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}`;
+  const authorHref = repo ? `/${repo}/user/${author.humanId}` : `/user/${author.humanId}`;
 
   return (
     <div className="flex items-start gap-3 border-b border-border px-4 py-3 last:border-0 hover:bg-muted/30">
       <StatusIcon
         className={
           isOpen
-            ? 'mt-0.5 size-4 shrink-0 text-green-600 dark:text-green-400'
-            : 'mt-0.5 size-4 shrink-0 text-purple-600 dark:text-purple-400'
+            ? "mt-0.5 size-4 shrink-0 text-green-600 dark:text-green-400"
+            : "mt-0.5 size-4 shrink-0 text-purple-600 dark:text-purple-400"
         }
       />
 
@@ -56,11 +58,16 @@ export function BugRow({
             {title}
           </Link>
           {labels.map((label) => (
-            <LabelBadge key={label.name} name={label.name} color={label.color} onClick={onLabelClick} />
+            <LabelBadge
+              key={label.name}
+              name={label.name}
+              color={label.color}
+              onClick={onLabelClick}
+            />
           ))}
         </div>
         <p className="mt-0.5 text-xs text-muted-foreground">
-          #{humanId} opened {formatDistanceToNow(new Date(createdAt), { addSuffix: true })} by{' '}
+          #{humanId} opened {formatDistanceToNow(new Date(createdAt), { addSuffix: true })} by{" "}
           <Link to={authorHref} className="hover:underline">
             {author.displayName}
           </Link>
@@ -74,5 +81,5 @@ export function BugRow({
         </div>
       )}
     </div>
-  )
+  );
 }

webui2/src/components/bugs/CommentBox.tsx πŸ”—

@@ -1,10 +1,6 @@
-import { useState } from 'react'
-import { Button } from '@/components/ui/button'
-import { Textarea } from '@/components/ui/textarea'
-import { Markdown } from '@/components/content/Markdown'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { useAuth } from '@/lib/auth'
-import { Status } from '@/__generated__/graphql'
+import { useState } from "react";
+
+import { Status } from "@/__generated__/graphql";
 import {
   useBugAddCommentMutation,
   useBugAddCommentAndCloseMutation,
@@ -12,60 +8,68 @@ import {
   useBugStatusCloseMutation,
   useBugStatusOpenMutation,
   BugDetailDocument,
-} from '@/__generated__/graphql'
+} from "@/__generated__/graphql";
+import { Markdown } from "@/components/content/Markdown";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { useAuth } from "@/lib/auth";
 
 interface CommentBoxProps {
-  bugPrefix: string
-  bugStatus: Status
+  bugPrefix: string;
+  bugStatus: Status;
   /** Current repo slug, passed as `ref` in refetch query variables. */
-  ref_?: string | null
+  ref_?: string | null;
 }
 
 // Write/preview comment form at the bottom of BugDetailPage. Also contains the
 // Close / Reopen button. Hidden entirely in read-only mode (no logged-in user).
 export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
-  const { user } = useAuth()
-  const [message, setMessage] = useState('')
-  const [preview, setPreview] = useState(false)
+  const { user } = useAuth();
+  const [message, setMessage] = useState("");
+  const [preview, setPreview] = useState(false);
 
-  const refetchVars = { variables: { ref: ref_, prefix: bugPrefix } }
-  const refetch = { refetchQueries: [{ query: BugDetailDocument, ...refetchVars }] }
+  const refetchVars = { variables: { ref: ref_, prefix: bugPrefix } };
+  const refetch = { refetchQueries: [{ query: BugDetailDocument, ...refetchVars }] };
 
-  const [addComment, { loading: addingComment }] = useBugAddCommentMutation(refetch)
-  const [addAndClose, { loading: addingAndClosing }] = useBugAddCommentAndCloseMutation(refetch)
-  const [addAndReopen, { loading: addingAndReopening }] = useBugAddCommentAndReopenMutation(refetch)
-  const [statusClose, { loading: closing }] = useBugStatusCloseMutation(refetch)
-  const [statusOpen, { loading: reopening }] = useBugStatusOpenMutation(refetch)
+  const [addComment, { loading: addingComment }] = useBugAddCommentMutation(refetch);
+  const [addAndClose, { loading: addingAndClosing }] = useBugAddCommentAndCloseMutation(refetch);
+  const [addAndReopen, { loading: addingAndReopening }] =
+    useBugAddCommentAndReopenMutation(refetch);
+  const [statusClose, { loading: closing }] = useBugStatusCloseMutation(refetch);
+  const [statusOpen, { loading: reopening }] = useBugStatusOpenMutation(refetch);
 
-  const isOpen = bugStatus === Status.Open
-  const busy = addingComment || addingAndClosing || addingAndReopening || closing || reopening
-  const hasMessage = message.trim().length > 0
+  const isOpen = bugStatus === Status.Open;
+  const busy = addingComment || addingAndClosing || addingAndReopening || closing || reopening;
+  const hasMessage = message.trim().length > 0;
 
   async function handleComment() {
-    await addComment({ variables: { input: { prefix: bugPrefix, message: message.trim() } } })
-    setMessage('')
-    setPreview(false)
+    await addComment({ variables: { input: { prefix: bugPrefix, message: message.trim() } } });
+    setMessage("");
+    setPreview(false);
   }
 
   async function handleToggleStatus() {
     if (isOpen) {
       if (hasMessage) {
-        await addAndClose({ variables: { input: { prefix: bugPrefix, message: message.trim() } } })
+        await addAndClose({ variables: { input: { prefix: bugPrefix, message: message.trim() } } });
       } else {
-        await statusClose({ variables: { input: { prefix: bugPrefix } } })
+        await statusClose({ variables: { input: { prefix: bugPrefix } } });
       }
     } else {
       if (hasMessage) {
-        await addAndReopen({ variables: { input: { prefix: bugPrefix, message: message.trim() } } })
+        await addAndReopen({
+          variables: { input: { prefix: bugPrefix, message: message.trim() } },
+        });
       } else {
-        await statusOpen({ variables: { input: { prefix: bugPrefix } } })
+        await statusOpen({ variables: { input: { prefix: bugPrefix } } });
       }
     }
-    setMessage('')
-    setPreview(false)
+    setMessage("");
+    setPreview(false);
   }
 
-  if (!user) return null
+  if (!user) return null;
 
   return (
     <div className="flex gap-3">
@@ -83,8 +87,8 @@ export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
             onClick={() => setPreview(false)}
             className={`px-4 py-2 text-sm font-medium transition-colors ${
               !preview
-                ? 'border-b-2 border-primary text-foreground'
-                : 'text-muted-foreground hover:text-foreground'
+                ? "border-b-2 border-primary text-foreground"
+                : "text-muted-foreground hover:text-foreground"
             }`}
           >
             Write
@@ -94,8 +98,8 @@ export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
             disabled={!hasMessage}
             className={`px-4 py-2 text-sm font-medium transition-colors disabled:opacity-40 ${
               preview
-                ? 'border-b-2 border-primary text-foreground'
-                : 'text-muted-foreground hover:text-foreground'
+                ? "border-b-2 border-primary text-foreground"
+                : "text-muted-foreground hover:text-foreground"
             }`}
           >
             Preview
@@ -124,17 +128,13 @@ export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
             disabled={busy}
             className="min-w-[7.5rem]"
           >
-            {isOpen ? 'Close issue' : 'Reopen issue'}
+            {isOpen ? "Close issue" : "Reopen issue"}
           </Button>
-          <Button
-            size="sm"
-            onClick={handleComment}
-            disabled={!hasMessage || busy}
-          >
+          <Button size="sm" onClick={handleComment} disabled={!hasMessage || busy}>
             Comment
           </Button>
         </div>
       </div>
     </div>
-  )
+  );
 }

webui2/src/components/bugs/IssueFilters.tsx πŸ”—

@@ -1,17 +1,19 @@
-import { useState } from 'react'
-import { ArrowUpDown, ChevronDown, Tag, User, X, Search, Check } from 'lucide-react'
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
-import { LabelBadge } from './LabelBadge'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { useValidLabelsQuery, useAllIdentitiesQuery } from '@/__generated__/graphql'
-import { useAuth } from '@/lib/auth'
-import { cn } from '@/lib/utils'
-import { useRepo } from '@/lib/repo'
+import { ArrowUpDown, ChevronDown, Tag, User, X, Search, Check } from "lucide-react";
+import { useState } from "react";
+
+import { useValidLabelsQuery, useAllIdentitiesQuery } from "@/__generated__/graphql";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { useAuth } from "@/lib/auth";
+import { useRepo } from "@/lib/repo";
+import { cn } from "@/lib/utils";
+
+import { LabelBadge } from "./LabelBadge";
 
 // Max authors shown in the non-searching state. We intentionally cap this to
 // avoid a giant list β€” the current-user + recently-seen pattern covers the
 // common case; typing to search handles the rest.
-const INITIAL_AUTHOR_LIMIT = 8
+const INITIAL_AUTHOR_LIMIT = 8;
 
 // Returns the value passed to author:... in the query string.
 // Preference order: login (never has spaces, safest) > name > humanId.
@@ -22,28 +24,32 @@ const INITIAL_AUTHOR_LIMIT = 8
 // option. git-bug identities can have login="" (empty, not null) when the
 // login field was never set; ?? would return "" and the filter would silently
 // produce author:"" which buildQueryString then drops, making the filter a no-op.
-function authorQueryValue(i: { login?: string | null; name?: string | null; humanId: string }): string {
-  return i.login || i.name || i.humanId
+function authorQueryValue(i: {
+  login?: string | null;
+  name?: string | null;
+  humanId: string;
+}): string {
+  return i.login || i.name || i.humanId;
 }
 
-export type SortValue = 'creation-desc' | 'creation-asc' | 'edit-desc' | 'edit-asc'
+export type SortValue = "creation-desc" | "creation-asc" | "edit-desc" | "edit-asc";
 
 const SORT_OPTIONS: { value: SortValue; label: string }[] = [
-  { value: 'creation-desc', label: 'Newest' },
-  { value: 'creation-asc',  label: 'Oldest' },
-  { value: 'edit-desc',     label: 'Recently updated' },
-  { value: 'edit-asc',      label: 'Least recently updated' },
-]
+  { value: "creation-desc", label: "Newest" },
+  { value: "creation-asc", label: "Oldest" },
+  { value: "edit-desc", label: "Recently updated" },
+  { value: "edit-asc", label: "Least recently updated" },
+];
 
 interface IssueFiltersProps {
-  selectedLabels: string[]
-  onLabelsChange: (labels: string[]) => void
-  selectedAuthorId: string | null
-  onAuthorChange: (humanId: string | null, queryValue: string | null) => void
+  selectedLabels: string[];
+  onLabelsChange: (labels: string[]) => void;
+  selectedAuthorId: string | null;
+  onAuthorChange: (humanId: string | null, queryValue: string | null) => void;
   /** humanIds of authors appearing in the current bug list, used to rank the initial suggestions */
-  recentAuthorIds?: string[]
-  sort: SortValue
-  onSortChange: (sort: SortValue) => void
+  recentAuthorIds?: string[];
+  sort: SortValue;
+  onSortChange: (sort: SortValue) => void;
 }
 
 // Label and author filter dropdowns shown in the issue list header bar.
@@ -66,98 +72,111 @@ export function IssueFilters({
   sort,
   onSortChange,
 }: IssueFiltersProps) {
-  const { user } = useAuth()
-  const repo = useRepo()
-  const { data: labelsData } = useValidLabelsQuery({ variables: { ref: repo } })
-  const { data: authorsData } = useAllIdentitiesQuery({ variables: { ref: repo } })
-  const [labelSearch, setLabelSearch] = useState('')
-  const [authorSearch, setAuthorSearch] = useState('')
+  const { user } = useAuth();
+  const repo = useRepo();
+  const { data: labelsData } = useValidLabelsQuery({ variables: { ref: repo } });
+  const { data: authorsData } = useAllIdentitiesQuery({ variables: { ref: repo } });
+  const [labelSearch, setLabelSearch] = useState("");
+  const [authorSearch, setAuthorSearch] = useState("");
 
   const validLabels = [...(labelsData?.repository?.validLabels.nodes ?? [])].sort((a, b) =>
     a.name.localeCompare(b.name),
-  )
+  );
 
   const allIdentities = [...(authorsData?.repository?.allIdentities.nodes ?? [])].sort((a, b) =>
     a.displayName.localeCompare(b.displayName),
-  )
+  );
 
   const filteredLabels = labelSearch.trim()
     ? validLabels.filter((l) => l.name.toLowerCase().includes(labelSearch.toLowerCase()))
-    : validLabels
+    : validLabels;
 
   // Selected labels float to top, then alphabetical
   const sortedLabels = [
     ...filteredLabels.filter((l) => selectedLabels.includes(l.name)),
     ...filteredLabels.filter((l) => !selectedLabels.includes(l.name)),
-  ]
+  ];
 
   // Build the displayed identity list:
   // - When searching: filter full list reactively as-you-type
   // - When not searching: show current user first, then recently-seen authors,
   //   then others up to INITIAL_AUTHOR_LIMIT
-  const isSearching = authorSearch.trim() !== ''
+  const isSearching = authorSearch.trim() !== "";
 
   const matchesSearch = (i: (typeof allIdentities)[number]) => {
-    const q = authorSearch.toLowerCase()
+    const q = authorSearch.toLowerCase();
     return (
       i.displayName.toLowerCase().includes(q) ||
-      (i.name ?? '').toLowerCase().includes(q) ||
-      (i.login ?? '').toLowerCase().includes(q) ||
-      (i.email ?? '').toLowerCase().includes(q)
-    )
-  }
+      (i.name ?? "").toLowerCase().includes(q) ||
+      (i.login ?? "").toLowerCase().includes(q) ||
+      (i.email ?? "").toLowerCase().includes(q)
+    );
+  };
 
-  let visibleIdentities: typeof allIdentities
+  let visibleIdentities: typeof allIdentities;
   if (isSearching) {
-    visibleIdentities = allIdentities.filter(matchesSearch)
+    visibleIdentities = allIdentities.filter(matchesSearch);
   } else {
-    const pinned = new Set<string>()
-    const result: typeof allIdentities = []
+    const pinned = new Set<string>();
+    const result: typeof allIdentities = [];
 
     // 1. Current user
     if (user) {
-      const me = allIdentities.find((i) => i.id === user.id)
-      if (me) { result.push(me); pinned.add(me.id) }
+      const me = allIdentities.find((i) => i.id === user.id);
+      if (me) {
+        result.push(me);
+        pinned.add(me.id);
+      }
     }
     // 2. Selected author (if not already added)
     if (selectedAuthorId) {
-      const sel = allIdentities.find((i) => i.humanId === selectedAuthorId)
-      if (sel && !pinned.has(sel.id)) { result.push(sel); pinned.add(sel.id) }
+      const sel = allIdentities.find((i) => i.humanId === selectedAuthorId);
+      if (sel && !pinned.has(sel.id)) {
+        result.push(sel);
+        pinned.add(sel.id);
+      }
     }
     // 3. Recently seen authors (recentAuthorIds are humanIds from bug rows)
     for (const humanId of recentAuthorIds) {
-      const match = allIdentities.find((i) => i.humanId === humanId)
-      if (match && !pinned.has(match.id)) { result.push(match); pinned.add(match.id) }
+      const match = allIdentities.find((i) => i.humanId === humanId);
+      if (match && !pinned.has(match.id)) {
+        result.push(match);
+        pinned.add(match.id);
+      }
     }
     // 4. Fill up to limit with remaining alphabetical
     for (const i of allIdentities) {
-      if (result.length >= INITIAL_AUTHOR_LIMIT) break
-      if (!pinned.has(i.id)) result.push(i)
+      if (result.length >= INITIAL_AUTHOR_LIMIT) break;
+      if (!pinned.has(i.id)) result.push(i);
     }
-    visibleIdentities = result
+    visibleIdentities = result;
   }
 
   function toggleLabel(name: string) {
     if (selectedLabels.includes(name)) {
-      onLabelsChange(selectedLabels.filter((l) => l !== name))
+      onLabelsChange(selectedLabels.filter((l) => l !== name));
     } else {
-      onLabelsChange([...selectedLabels, name])
+      onLabelsChange([...selectedLabels, name]);
     }
   }
 
-  const selectedAuthorIdentity = allIdentities.find((i) => i.humanId === selectedAuthorId)
+  const selectedAuthorIdentity = allIdentities.find((i) => i.humanId === selectedAuthorId);
 
   return (
     <div className="flex shrink-0 items-center gap-1">
       {/* Label filter */}
-      <Popover onOpenChange={(open) => { if (!open) setLabelSearch('') }}>
+      <Popover
+        onOpenChange={(open) => {
+          if (!open) setLabelSearch("");
+        }}
+      >
         <PopoverTrigger asChild>
           <button
             className={cn(
-              'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
+              "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
               selectedLabels.length > 0
-                ? 'bg-accent text-accent-foreground'
-                : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
+                ? "bg-accent text-accent-foreground"
+                : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
             )}
           >
             <Tag className="size-3.5" />
@@ -170,7 +189,7 @@ export function IssueFilters({
             <ChevronDown className="size-3" />
           </button>
         </PopoverTrigger>
-        <PopoverContent align="end" className="w-56 p-0 bg-popover shadow-lg">
+        <PopoverContent align="end" className="w-56 bg-popover p-0 shadow-lg">
           {/* Search */}
           <div className="flex items-center gap-2 border-b border-border px-3 py-2">
             <Search className="size-3.5 shrink-0 text-muted-foreground" />
@@ -187,7 +206,7 @@ export function IssueFilters({
               <p className="px-2 py-3 text-center text-xs text-muted-foreground">No labels found</p>
             )}
             {sortedLabels.map((label) => {
-              const active = selectedLabels.includes(label.name)
+              const active = selectedLabels.includes(label.name);
               return (
                 <button
                   key={label.name}
@@ -204,7 +223,7 @@ export function IssueFilters({
                   <LabelBadge name={label.name} color={label.color} />
                   {active && <Check className="ml-auto size-3.5 shrink-0 text-foreground" />}
                 </button>
-              )
+              );
             })}
           </div>
           {selectedLabels.length > 0 && (
@@ -222,12 +241,18 @@ export function IssueFilters({
       </Popover>
 
       {/* Author filter */}
-      <Popover onOpenChange={(open) => { if (!open) setAuthorSearch('') }}>
+      <Popover
+        onOpenChange={(open) => {
+          if (!open) setAuthorSearch("");
+        }}
+      >
         <PopoverTrigger asChild>
           <button
             className={cn(
-              'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
-              selectedAuthorId ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
+              "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+              selectedAuthorId
+                ? "bg-accent text-accent-foreground"
+                : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
             )}
           >
             {selectedAuthorIdentity ? (
@@ -252,7 +277,7 @@ export function IssueFilters({
             <ChevronDown className="size-3" />
           </button>
         </PopoverTrigger>
-        <PopoverContent align="end" className="w-56 p-0 bg-popover shadow-lg">
+        <PopoverContent align="end" className="w-56 bg-popover p-0 shadow-lg">
           {/* Search */}
           <div className="flex items-center gap-2 border-b border-border px-3 py-2">
             <Search className="size-3.5 shrink-0 text-muted-foreground" />
@@ -266,14 +291,21 @@ export function IssueFilters({
           </div>
           <div className="max-h-64 overflow-y-auto p-1">
             {visibleIdentities.length === 0 && (
-              <p className="px-2 py-3 text-center text-xs text-muted-foreground">No authors found</p>
+              <p className="px-2 py-3 text-center text-xs text-muted-foreground">
+                No authors found
+              </p>
             )}
             {visibleIdentities.map((identity) => {
-              const active = selectedAuthorId === identity.humanId
+              const active = selectedAuthorId === identity.humanId;
               return (
                 <button
                   key={identity.id}
-                  onClick={() => onAuthorChange(active ? null : identity.humanId, active ? null : authorQueryValue(identity))}
+                  onClick={() =>
+                    onAuthorChange(
+                      active ? null : identity.humanId,
+                      active ? null : authorQueryValue(identity),
+                    )
+                  }
                   className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted"
                 >
                   <Avatar className="size-5 shrink-0">
@@ -285,12 +317,14 @@ export function IssueFilters({
                   <div className="min-w-0 flex-1 text-left">
                     <div className="truncate">{identity.displayName}</div>
                     {identity.login && identity.login !== identity.displayName && (
-                      <div className="truncate text-xs text-muted-foreground">@{identity.login}</div>
+                      <div className="truncate text-xs text-muted-foreground">
+                        @{identity.login}
+                      </div>
                     )}
                   </div>
                   {active && <Check className="size-3.5 shrink-0 text-foreground" />}
                 </button>
-              )
+              );
             })}
             {!isSearching && allIdentities.length > INITIAL_AUTHOR_LIMIT && (
               <p className="px-2 py-1.5 text-center text-xs text-muted-foreground">
@@ -317,18 +351,18 @@ export function IssueFilters({
         <PopoverTrigger asChild>
           <button
             className={cn(
-              'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors whitespace-nowrap',
-              sort !== 'creation-desc'
-                ? 'bg-accent text-accent-foreground'
-                : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
+              "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors whitespace-nowrap",
+              sort !== "creation-desc"
+                ? "bg-accent text-accent-foreground"
+                : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
             )}
           >
             <ArrowUpDown className="size-3.5" />
-            {SORT_OPTIONS.find((o) => o.value === sort)?.label ?? 'Sort'}
+            {SORT_OPTIONS.find((o) => o.value === sort)?.label ?? "Sort"}
             <ChevronDown className="size-3" />
           </button>
         </PopoverTrigger>
-        <PopoverContent align="end" className="w-56 p-1 bg-popover shadow-lg">
+        <PopoverContent align="end" className="w-56 bg-popover p-1 shadow-lg">
           {SORT_OPTIONS.map((opt) => (
             <button
               key={opt.value}
@@ -336,11 +370,13 @@ export function IssueFilters({
               className="flex w-full items-center gap-2 whitespace-nowrap rounded px-2 py-1.5 text-sm hover:bg-muted"
             >
               {opt.label}
-              {sort === opt.value && <Check className="ml-auto size-3.5 shrink-0 text-foreground" />}
+              {sort === opt.value && (
+                <Check className="ml-auto size-3.5 shrink-0 text-foreground" />
+              )}
             </button>
           ))}
         </PopoverContent>
       </Popover>
     </div>
-  )
+  );
 }

webui2/src/components/bugs/LabelBadge.tsx πŸ”—

@@ -1,20 +1,20 @@
 interface LabelBadgeProps {
-  name: string
-  color: { R: number; G: number; B: number }
-  onClick?: (name: string) => void
+  name: string;
+  color: { R: number; G: number; B: number };
+  onClick?: (name: string) => void;
 }
 
 function contrastColor(r: number, g: number, b: number): string {
   // Perceived luminance β€” pick black or white text for readability
-  const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
-  return luminance > 0.55 ? 'rgba(0,0,0,0.75)' : 'rgba(255,255,255,0.9)'
+  const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+  return luminance > 0.55 ? "rgba(0,0,0,0.75)" : "rgba(255,255,255,0.9)";
 }
 
 // Coloured label pill. Renders as a <button> when onClick is provided,
 // used in BugRow and UserProfilePage to filter issues by label.
 export function LabelBadge({ name, color, onClick }: LabelBadgeProps) {
-  const bg = `rgb(${color.R},${color.G},${color.B})`
-  const text = contrastColor(color.R, color.G, color.B)
+  const bg = `rgb(${color.R},${color.G},${color.B})`;
+  const text = contrastColor(color.R, color.G, color.B);
 
   if (onClick) {
     return (
@@ -22,11 +22,15 @@ export function LabelBadge({ name, color, onClick }: LabelBadgeProps) {
         type="button"
         className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium hover:opacity-80"
         style={{ backgroundColor: bg, color: text }}
-        onClick={(e) => { e.preventDefault(); e.stopPropagation(); onClick(name) }}
+        onClick={(e) => {
+          e.preventDefault();
+          e.stopPropagation();
+          onClick(name);
+        }}
       >
         {name}
       </button>
-    )
+    );
   }
 
   return (
@@ -36,5 +40,5 @@ export function LabelBadge({ name, color, onClick }: LabelBadgeProps) {
     >
       {name}
     </span>
-  )
+  );
 }

webui2/src/components/bugs/LabelEditor.tsx πŸ”—

@@ -1,35 +1,37 @@
-import { Settings2 } from 'lucide-react'
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
-import { LabelBadge } from './LabelBadge'
-import { useAuth } from '@/lib/auth'
+import { Settings2 } from "lucide-react";
+
 import {
   useValidLabelsQuery,
   useBugChangeLabelsMutation,
   BugDetailDocument,
-} from '@/__generated__/graphql'
+} from "@/__generated__/graphql";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { useAuth } from "@/lib/auth";
+
+import { LabelBadge } from "./LabelBadge";
 
 interface LabelEditorProps {
-  bugPrefix: string
-  currentLabels: Array<{ name: string; color: { R: number; G: number; B: number } }>
+  bugPrefix: string;
+  currentLabels: Array<{ name: string; color: { R: number; G: number; B: number } }>;
   /** Current repo slug, passed as `ref` in refetch query variables. */
-  ref_?: string | null
+  ref_?: string | null;
 }
 
 // Gear-icon popover in the BugDetailPage sidebar for adding/removing labels.
 // Loads all valid labels from the repo and toggles them via bugChangeLabels.
 // Hidden in read-only mode.
 export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps) {
-  const { user } = useAuth()
-  const { data } = useValidLabelsQuery({ skip: !user, variables: { ref: ref_ } })
+  const { user } = useAuth();
+  const { data } = useValidLabelsQuery({ skip: !user, variables: { ref: ref_ } });
   const [changeLabels] = useBugChangeLabelsMutation({
     refetchQueries: [{ query: BugDetailDocument, variables: { ref: ref_, prefix: bugPrefix } }],
-  })
+  });
 
-  const validLabels = data?.repository?.validLabels.nodes ?? []
-  const currentNames = new Set(currentLabels.map((l) => l.name))
+  const validLabels = data?.repository?.validLabels.nodes ?? [];
+  const currentNames = new Set(currentLabels.map((l) => l.name));
 
   async function toggleLabel(name: string) {
-    const isSet = currentNames.has(name)
+    const isSet = currentNames.has(name);
     await changeLabels({
       variables: {
         input: {
@@ -38,7 +40,7 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps
           Removed: isSet ? [name] : [],
         },
       },
-    })
+    });
   }
 
   return (
@@ -55,12 +57,10 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps
               </button>
             </PopoverTrigger>
             <PopoverContent align="end" className="w-56 p-2">
-              <p className="mb-2 px-2 text-xs font-medium text-muted-foreground">
-                Apply labels
-              </p>
+              <p className="mb-2 px-2 text-xs font-medium text-muted-foreground">Apply labels</p>
               <div className="space-y-1">
                 {validLabels.map((label) => {
-                  const active = currentNames.has(label.name)
+                  const active = currentNames.has(label.name);
                   return (
                     <button
                       key={label.name}
@@ -69,13 +69,21 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps
                     >
                       <span
                         className={`size-2 rounded-full border-2 transition-colors ${
-                          active ? 'border-transparent' : 'border-muted-foreground/40 bg-transparent'
+                          active
+                            ? "border-transparent"
+                            : "border-muted-foreground/40 bg-transparent"
                         }`}
-                        style={active ? { backgroundColor: `rgb(${label.color.R},${label.color.G},${label.color.B})` } : {}}
+                        style={
+                          active
+                            ? {
+                                backgroundColor: `rgb(${label.color.R},${label.color.G},${label.color.B})`,
+                              }
+                            : {}
+                        }
                       />
                       <LabelBadge name={label.name} color={label.color} />
                     </button>
-                  )
+                  );
                 })}
               </div>
             </PopoverContent>
@@ -93,5 +101,5 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_ }: LabelEditorProps
         </div>
       )}
     </div>
-  )
+  );
 }

webui2/src/components/bugs/QueryInput.tsx πŸ”—

@@ -9,93 +9,101 @@
 // dropdown appears with filtered suggestions fetched from GraphQL. Clicking or
 // keyboard-selecting a suggestion replaces the current token in the input.
 
-import { useState, useRef, useMemo, type ChangeEvent } from 'react'
-import { Search } from 'lucide-react'
-import { cn } from '@/lib/utils'
-import { useValidLabelsQuery, useAllIdentitiesQuery } from '@/__generated__/graphql'
-import { useRepo } from '@/lib/repo'
+import { Search } from "lucide-react";
+import { useState, useRef, useMemo, type ChangeEvent } from "react";
+
+import { useValidLabelsQuery, useAllIdentitiesQuery } from "@/__generated__/graphql";
+import { useRepo } from "@/lib/repo";
+import { cn } from "@/lib/utils";
 
 // ── Segment parsing (for the syntax-highlight backdrop) ───────────────────────
 
-type SegmentType = 'status-open' | 'status-closed' | 'label' | 'author' | 'text' | 'space'
+type SegmentType = "status-open" | "status-closed" | "label" | "author" | "text" | "space";
 
 interface Segment {
-  text: string
-  type: SegmentType
+  text: string;
+  type: SegmentType;
 }
 
 // Parse the query string into typed segments, preserving all whitespace.
 // Walks char-by-char so that quoted values (e.g. label:"my label") are kept as
 // a single token and spaces inside quotes don't split the segment.
 function parseSegments(input: string): Segment[] {
-  const segments: Segment[] = []
-  let i = 0
+  const segments: Segment[] = [];
+  let i = 0;
 
   while (i < input.length) {
     // Whitespace runs β€” preserved as a separate 'space' segment so the backdrop
     // can use whitespace-pre and match the input exactly.
-    if (input[i] === ' ') {
-      let j = i
-      while (j < input.length && input[j] === ' ') j++
-      segments.push({ text: input.slice(i, j), type: 'space' })
-      i = j
-      continue
+    if (input[i] === " ") {
+      let j = i;
+      while (j < input.length && input[j] === " ") j++;
+      segments.push({ text: input.slice(i, j), type: "space" });
+      i = j;
+      continue;
     }
 
     // Token β€” consume until an unquoted space
-    let j = i
-    let inQuote = false
+    let j = i;
+    let inQuote = false;
     while (j < input.length) {
-      if (input[j] === '"') { inQuote = !inQuote; j++; continue }
-      if (!inQuote && input[j] === ' ') break
-      j++
+      if (input[j] === '"') {
+        inQuote = !inQuote;
+        j++;
+        continue;
+      }
+      if (!inQuote && input[j] === " ") break;
+      j++;
     }
 
-    const token = input.slice(i, j)
-    let type: SegmentType = 'text'
-    if (token === 'status:open') type = 'status-open'
-    else if (token === 'status:closed') type = 'status-closed'
-    else if (token.startsWith('label:')) type = 'label'
-    else if (token.startsWith('author:')) type = 'author'
+    const token = input.slice(i, j);
+    let type: SegmentType = "text";
+    if (token === "status:open") type = "status-open";
+    else if (token === "status:closed") type = "status-closed";
+    else if (token.startsWith("label:")) type = "label";
+    else if (token.startsWith("author:")) type = "author";
 
-    segments.push({ text: token, type })
-    i = j
+    segments.push({ text: token, type });
+    i = j;
   }
 
-  return segments
+  return segments;
 }
 
 // Only the key portion (e.g. "label:") is colored; the value stays in foreground.
 function renderSegment(seg: Segment, i: number): React.ReactNode {
-  if (seg.type === 'space' || seg.type === 'text') {
-    return <span key={i}>{seg.text}</span>
+  if (seg.type === "space" || seg.type === "text") {
+    return <span key={i}>{seg.text}</span>;
   }
-  const colon = seg.text.indexOf(':')
-  const key = seg.text.slice(0, colon + 1)
-  const val = seg.text.slice(colon + 1)
+  const colon = seg.text.indexOf(":");
+  const key = seg.text.slice(0, colon + 1);
+  const val = seg.text.slice(colon + 1);
 
   const keyClass =
-    seg.type === 'status-open'   ? 'text-green-600 dark:text-green-400' :
-    seg.type === 'status-closed' ? 'text-purple-600 dark:text-purple-400' :
-    seg.type === 'label'         ? 'text-yellow-600 dark:text-yellow-500' :
-    /* author */                   'text-blue-600 dark:text-blue-400'
+    seg.type === "status-open"
+      ? "text-green-600 dark:text-green-400"
+      : seg.type === "status-closed"
+        ? "text-purple-600 dark:text-purple-400"
+        : seg.type === "label"
+          ? "text-yellow-600 dark:text-yellow-500"
+          : /* author */ "text-blue-600 dark:text-blue-400";
 
   return (
     <span key={i}>
       <span className={keyClass}>{key}</span>
       <span>{val}</span>
     </span>
-  )
+  );
 }
 
 // ── Autocomplete logic ────────────────────────────────────────────────────────
 
 interface CompletionInfo {
-  type: 'label' | 'author'
+  type: "label" | "author";
   /** Text typed after the prefix (e.g. "bu" for "label:bu"). Quotes stripped. */
-  query: string
+  query: string;
   /** Byte position in `value` where the current token starts. */
-  tokenStart: number
+  tokenStart: number;
 }
 
 // Inspects the text to the left of `cursor` to determine if the user is in the
@@ -103,181 +111,187 @@ interface CompletionInfo {
 // Returns null when not in an autocomplete-eligible position.
 function getCompletionInfo(value: string, cursor: number): CompletionInfo | null {
   // Walk backward to find the start of the current token
-  let tokenStart = 0
+  let tokenStart = 0;
   for (let i = cursor - 1; i >= 0; i--) {
-    if (value[i] === ' ') { tokenStart = i + 1; break }
+    if (value[i] === " ") {
+      tokenStart = i + 1;
+      break;
+    }
   }
 
-  const partial = value.slice(tokenStart, cursor)
-  if (partial.startsWith('label:')) {
-    return { type: 'label', query: partial.slice(6), tokenStart }
+  const partial = value.slice(tokenStart, cursor);
+  if (partial.startsWith("label:")) {
+    return { type: "label", query: partial.slice(6), tokenStart };
   }
-  if (partial.startsWith('author:')) {
+  if (partial.startsWith("author:")) {
     // Strip a leading quote that the user may have typed
-    return { type: 'author', query: partial.slice(7).replace(/^"/, ''), tokenStart }
+    return { type: "author", query: partial.slice(7).replace(/^"/, ""), tokenStart };
   }
-  return null
+  return null;
 }
 
 // Find where the current token ends (next unquoted space, or end of string).
 // Used when replacing a token on suggestion selection so we don't leave stale text.
 function getTokenEnd(value: string, tokenStart: number): number {
-  let inQuote = false
+  let inQuote = false;
   for (let i = tokenStart; i < value.length; i++) {
-    if (value[i] === '"') { inQuote = !inQuote; continue }
-    if (!inQuote && value[i] === ' ') return i
+    if (value[i] === '"') {
+      inQuote = !inQuote;
+      continue;
+    }
+    if (!inQuote && value[i] === " ") return i;
   }
-  return value.length
+  return value.length;
 }
 
 // ── Component ─────────────────────────────────────────────────────────────────
 
 interface QueryInputProps {
-  value: string
-  onChange: (value: string) => void
-  onSubmit: () => void
-  placeholder?: string
-  className?: string
+  value: string;
+  onChange: (value: string) => void;
+  onSubmit: () => void;
+  placeholder?: string;
+  className?: string;
 }
 
 export function QueryInput({ value, onChange, onSubmit, placeholder, className }: QueryInputProps) {
-  const inputRef = useRef<HTMLInputElement>(null)
-  const repo = useRepo()
+  const inputRef = useRef<HTMLInputElement>(null);
+  const repo = useRepo();
 
   // Autocomplete state: null when the dropdown is hidden.
-  const [completion, setCompletion] = useState<CompletionInfo | null>(null)
+  const [completion, setCompletion] = useState<CompletionInfo | null>(null);
   // Keyboard-highlighted index within the visible suggestions list.
-  const [acIndex, setAcIndex] = useState(0)
+  const [acIndex, setAcIndex] = useState(0);
 
   // Fetch all labels and identities for autocomplete suggestions.
   // These queries are cheap (cached by Apollo) and already used by IssueFilters,
   // so there is no extra network cost.
-  const { data: labelsData } = useValidLabelsQuery({ variables: { ref: repo } })
-  const { data: authorsData } = useAllIdentitiesQuery({ variables: { ref: repo } })
+  const { data: labelsData } = useValidLabelsQuery({ variables: { ref: repo } });
+  const { data: authorsData } = useAllIdentitiesQuery({ variables: { ref: repo } });
 
-  const allLabels = labelsData?.repository?.validLabels.nodes ?? []
-  const allAuthors = authorsData?.repository?.allIdentities.nodes ?? []
+  const allLabels = labelsData?.repository?.validLabels.nodes ?? [];
+  const allAuthors = authorsData?.repository?.allIdentities.nodes ?? [];
 
   // Compute the filtered suggestion list whenever completion info changes.
   const suggestions = useMemo(() => {
-    if (!completion) return []
+    if (!completion) return [];
 
-    if (completion.type === 'label') {
-      const q = completion.query.toLowerCase()
+    if (completion.type === "label") {
+      const q = completion.query.toLowerCase();
       return allLabels
-        .filter((l) => q === '' || l.name.toLowerCase().includes(q))
+        .filter((l) => q === "" || l.name.toLowerCase().includes(q))
         .slice(0, 8)
         .map((l) => ({
           display: l.name,
           // Quote the token value if the label name contains a space
-          completedToken: `label:${l.name.includes(' ') ? `"${l.name}"` : l.name}`,
+          completedToken: `label:${l.name.includes(" ") ? `"${l.name}"` : l.name}`,
           color: l.color,
-        }))
+        }));
     }
 
     // author suggestions β€” match against displayName, login, and name
-    const q = completion.query.toLowerCase()
+    const q = completion.query.toLowerCase();
     return allAuthors
       .filter(
         (a) =>
-          q === '' ||
+          q === "" ||
           a.displayName.toLowerCase().includes(q) ||
-          (a.login ?? '').toLowerCase().includes(q) ||
-          (a.name ?? '').toLowerCase().includes(q),
+          (a.login ?? "").toLowerCase().includes(q) ||
+          (a.name ?? "").toLowerCase().includes(q),
       )
       .slice(0, 8)
       .map((a) => {
         // Prefer login (no spaces, stable) β†’ name β†’ humanId as the query value.
         // Same preference used by IssueFilters.authorQueryValue.
-        const qv = a.login ?? a.name ?? a.humanId
+        const qv = a.login ?? a.name ?? a.humanId;
         return {
           display: a.displayName,
-          completedToken: `author:${qv.includes(' ') ? `"${qv}"` : qv}`,
+          completedToken: `author:${qv.includes(" ") ? `"${qv}"` : qv}`,
           color: null,
-        }
-      })
-  }, [completion, allLabels, allAuthors])
+        };
+      });
+  }, [completion, allLabels, allAuthors]);
 
   // ── Recompute completion state after every input change or cursor move ──────
 
   function updateCompletion(newValue: string, cursor: number) {
-    const info = getCompletionInfo(newValue, cursor)
-    setCompletion(info)
-    setAcIndex(0)
+    const info = getCompletionInfo(newValue, cursor);
+    setCompletion(info);
+    setAcIndex(0);
   }
 
   function handleChange(e: ChangeEvent<HTMLInputElement>) {
-    const newValue = e.target.value
-    const cursor = e.target.selectionStart ?? newValue.length
-    onChange(newValue)
-    updateCompletion(newValue, cursor)
+    const newValue = e.target.value;
+    const cursor = e.target.selectionStart ?? newValue.length;
+    onChange(newValue);
+    updateCompletion(newValue, cursor);
   }
 
   // onSelect fires on cursor movement (arrow keys, click-to-reposition), which
   // lets us show/hide the dropdown correctly when the cursor moves into or out
   // of an autocomplete-eligible token without changing the text.
   function handleSelect(e: React.SyntheticEvent<HTMLInputElement>) {
-    updateCompletion(value, e.currentTarget.selectionStart ?? value.length)
+    updateCompletion(value, e.currentTarget.selectionStart ?? value.length);
   }
 
   // ── Keyboard navigation ───────────────────────────────────────────────────
 
   function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
-    if (e.key === 'Enter' && !completion) {
-      e.preventDefault()
-      onSubmit()
-      return
+    if (e.key === "Enter" && !completion) {
+      e.preventDefault();
+      onSubmit();
+      return;
     }
 
-    if (!completion || suggestions.length === 0) return
-
-    if (e.key === 'ArrowDown') {
-      e.preventDefault()
-      setAcIndex((i) => (i + 1) % suggestions.length)
-    } else if (e.key === 'ArrowUp') {
-      e.preventDefault()
-      setAcIndex((i) => (i - 1 + suggestions.length) % suggestions.length)
-    } else if (e.key === 'Enter' || e.key === 'Tab') {
-      e.preventDefault()
-      applySuggestion(suggestions[acIndex])
-    } else if (e.key === 'Escape') {
-      setCompletion(null)
+    if (!completion || suggestions.length === 0) return;
+
+    if (e.key === "ArrowDown") {
+      e.preventDefault();
+      setAcIndex((i) => (i + 1) % suggestions.length);
+    } else if (e.key === "ArrowUp") {
+      e.preventDefault();
+      setAcIndex((i) => (i - 1 + suggestions.length) % suggestions.length);
+    } else if (e.key === "Enter" || e.key === "Tab") {
+      e.preventDefault();
+      applySuggestion(suggestions[acIndex]);
+    } else if (e.key === "Escape") {
+      setCompletion(null);
     }
   }
 
   // ── Apply a selected suggestion ──────────────────────────────────────────
 
   function applySuggestion(s: { completedToken: string }) {
-    if (!completion) return
-    const tokenEnd = getTokenEnd(value, completion.tokenStart)
+    if (!completion) return;
+    const tokenEnd = getTokenEnd(value, completion.tokenStart);
     // Replace the current token (from tokenStart to tokenEnd) with the completed
     // token, then add a space so the user can type the next filter immediately.
     const newValue =
       value.slice(0, completion.tokenStart) +
       s.completedToken +
-      ' ' +
-      value.slice(tokenEnd).trimStart()
-    onChange(newValue)
-    setCompletion(null)
+      " " +
+      value.slice(tokenEnd).trimStart();
+    onChange(newValue);
+    setCompletion(null);
 
     // Restore focus and position cursor after the inserted token + space
-    const newCursor = completion.tokenStart + s.completedToken.length + 1
+    const newCursor = completion.tokenStart + s.completedToken.length + 1;
     requestAnimationFrame(() => {
-      inputRef.current?.focus()
-      inputRef.current?.setSelectionRange(newCursor, newCursor)
-    })
+      inputRef.current?.focus();
+      inputRef.current?.setSelectionRange(newCursor, newCursor);
+    });
   }
 
   // ── Render ────────────────────────────────────────────────────────────────
 
-  const segments = parseSegments(value)
-  const showDropdown = completion !== null && suggestions.length > 0
+  const segments = parseSegments(value);
+  const showDropdown = completion !== null && suggestions.length > 0;
 
   return (
     <div
       className={cn(
-        'relative flex-1 flex items-center rounded-md border border-input bg-background',
-        'ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
+        "relative flex-1 flex items-center rounded-md border border-input bg-background",
+        "ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2",
         className,
       )}
       onClick={() => inputRef.current?.focus()}
@@ -288,9 +302,9 @@ export function QueryInput({ value, onChange, onSubmit, placeholder, className }
           screen readers only see the real input, not the duplicate text. */}
       <div
         aria-hidden
-        className="pointer-events-none absolute inset-0 flex items-center overflow-hidden pl-9 pr-3 font-mono text-sm text-foreground whitespace-pre"
+        className="pointer-events-none absolute inset-0 flex items-center overflow-hidden whitespace-pre pl-9 pr-3 font-mono text-sm text-foreground"
       >
-        {value === '' ? null : segments.map((seg, i) => renderSegment(seg, i))}
+        {value === "" ? null : segments.map((seg, i) => renderSegment(seg, i))}
       </div>
 
       {/* Actual input β€” transparent bg and text so the backdrop shows through.
@@ -316,10 +330,13 @@ export function QueryInput({ value, onChange, onSubmit, placeholder, className }
           {suggestions.map((s, i) => (
             <button
               key={s.completedToken}
-              onMouseDown={(e) => { e.preventDefault(); applySuggestion(s) }}
+              onMouseDown={(e) => {
+                e.preventDefault();
+                applySuggestion(s);
+              }}
               className={cn(
-                'flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm',
-                i === acIndex ? 'bg-accent text-accent-foreground' : 'hover:bg-muted',
+                "flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm",
+                i === acIndex ? "bg-accent text-accent-foreground" : "hover:bg-muted",
               )}
             >
               {s.color && (
@@ -329,7 +346,7 @@ export function QueryInput({ value, onChange, onSubmit, placeholder, className }
                 />
               )}
               <span className="font-mono">{s.completedToken}</span>
-              {s.display !== s.completedToken.split(':')[1]?.replace(/"/g, '') && (
+              {s.display !== s.completedToken.split(":")[1]?.replace(/"/g, "") && (
                 <span className="ml-auto text-xs text-muted-foreground">{s.display}</span>
               )}
             </button>
@@ -337,5 +354,5 @@ export function QueryInput({ value, onChange, onSubmit, placeholder, className }
         </div>
       )}
     </div>
-  )
+  );
 }

webui2/src/components/bugs/StatusBadge.tsx πŸ”—

@@ -1,28 +1,29 @@
-import { CircleDot, CircleCheck } from 'lucide-react'
-import { cn } from '@/lib/utils'
-import { Status } from '@/__generated__/graphql'
+import { CircleDot, CircleCheck } from "lucide-react";
+
+import { Status } from "@/__generated__/graphql";
+import { cn } from "@/lib/utils";
 
 interface StatusBadgeProps {
-  status: Status
-  className?: string
+  status: Status;
+  className?: string;
 }
 
 // Open / Closed status badge with icon. Used in BugDetailPage header.
 export function StatusBadge({ status, className }: StatusBadgeProps) {
-  const isOpen = status === Status.Open
+  const isOpen = status === Status.Open;
 
   return (
     <span
       className={cn(
-        'inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium',
+        "inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium",
         isOpen
-          ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
-          : 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400',
+          ? "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
+          : "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400",
         className,
       )}
     >
       {isOpen ? <CircleDot className="size-3" /> : <CircleCheck className="size-3" />}
-      {isOpen ? 'Open' : 'Closed'}
+      {isOpen ? "Open" : "Closed"}
     </span>
-  )
+  );
 }

webui2/src/components/bugs/Timeline.tsx πŸ”—

@@ -1,28 +1,30 @@
-import { useState } from 'react'
-import { formatDistanceToNow } from 'date-fns'
-import { Link } from 'react-router-dom'
-import { Tag, GitPullRequestClosed, Pencil, CircleDot } from 'lucide-react'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { Markdown } from '@/components/content/Markdown'
-import { LabelBadge } from './LabelBadge'
-import { Button } from '@/components/ui/button'
-import { Textarea } from '@/components/ui/textarea'
+import { formatDistanceToNow } from "date-fns";
+import { Tag, GitPullRequestClosed, Pencil, CircleDot } from "lucide-react";
+import { useState } from "react";
+import { Link } from "react-router-dom";
+
 import {
   Status,
   type BugDetailQuery,
   useBugEditCommentMutation,
   BugDetailDocument,
-} from '@/__generated__/graphql'
-import { useAuth } from '@/lib/auth'
-import { useRepo } from '@/lib/repo'
+} from "@/__generated__/graphql";
+import { Markdown } from "@/components/content/Markdown";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { useAuth } from "@/lib/auth";
+import { useRepo } from "@/lib/repo";
+
+import { LabelBadge } from "./LabelBadge";
 
 type TimelineNode = NonNullable<
-  NonNullable<NonNullable<BugDetailQuery['repository']>['bug']>['timeline']['nodes'][number]
->
+  NonNullable<NonNullable<BugDetailQuery["repository"]>["bug"]>["timeline"]["nodes"][number]
+>;
 
 interface TimelineProps {
-  bugPrefix: string
-  items: TimelineNode[]
+  bugPrefix: string;
+  items: TimelineNode[];
 }
 
 // Ordered sequence of events on a bug: comments (create and add-comment) and
@@ -33,56 +35,56 @@ export function Timeline({ bugPrefix, items }: TimelineProps) {
     <div className="space-y-4">
       {items.map((item) => {
         switch (item.__typename) {
-          case 'BugCreateTimelineItem':
-          case 'BugAddCommentTimelineItem':
-            return <CommentItem key={item.id} item={item} bugPrefix={bugPrefix} />
-          case 'BugLabelChangeTimelineItem':
-            return <LabelChangeItem key={item.id} item={item} />
-          case 'BugSetStatusTimelineItem':
-            return <StatusChangeItem key={item.id} item={item} />
-          case 'BugSetTitleTimelineItem':
-            return <TitleChangeItem key={item.id} item={item} />
+          case "BugCreateTimelineItem":
+          case "BugAddCommentTimelineItem":
+            return <CommentItem key={item.id} item={item} bugPrefix={bugPrefix} />;
+          case "BugLabelChangeTimelineItem":
+            return <LabelChangeItem key={item.id} item={item} />;
+          case "BugSetStatusTimelineItem":
+            return <StatusChangeItem key={item.id} item={item} />;
+          case "BugSetTitleTimelineItem":
+            return <TitleChangeItem key={item.id} item={item} />;
           default:
-            return null
+            return null;
         }
       })}
     </div>
-  )
+  );
 }
 
 // ── Comment (create or add-comment) ──────────────────────────────────────────
 
 type CommentItem = Extract<
   TimelineNode,
-  { __typename: 'BugCreateTimelineItem' | 'BugAddCommentTimelineItem' }
->
+  { __typename: "BugCreateTimelineItem" | "BugAddCommentTimelineItem" }
+>;
 
 function CommentItem({ item, bugPrefix }: { item: CommentItem; bugPrefix: string }) {
-  const { user } = useAuth()
-  const repo = useRepo()
-  const [editing, setEditing] = useState(false)
-  const [editValue, setEditValue] = useState(item.message ?? '')
+  const { user } = useAuth();
+  const repo = useRepo();
+  const [editing, setEditing] = useState(false);
+  const [editValue, setEditValue] = useState(item.message ?? "");
 
   const [editComment, { loading }] = useBugEditCommentMutation({
     refetchQueries: [{ query: BugDetailDocument, variables: { prefix: bugPrefix } }],
-  })
+  });
 
   function handleSave() {
-    if (editValue.trim() === (item.message ?? '').trim()) {
-      setEditing(false)
-      return
+    if (editValue.trim() === (item.message ?? "").trim()) {
+      setEditing(false);
+      return;
     }
     editComment({
       variables: { input: { targetPrefix: item.id, message: editValue } },
-    }).then(() => setEditing(false))
+    }).then(() => setEditing(false));
   }
 
   function handleCancel() {
-    setEditValue(item.message ?? '')
-    setEditing(false)
+    setEditValue(item.message ?? "");
+    setEditing(false);
   }
 
-  const canEdit = user !== null && user.id === item.author.id
+  const canEdit = user !== null && user.id === item.author.id;
 
   return (
     <div className="flex gap-3">
@@ -95,15 +97,16 @@ function CommentItem({ item, bugPrefix }: { item: CommentItem; bugPrefix: string
 
       <div className="min-w-0 flex-1 rounded-md border border-border">
         <div className="flex items-center gap-2 border-b border-border bg-muted/40 px-4 py-2 text-sm">
-          <Link to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`} className="font-medium text-foreground hover:underline">
+          <Link
+            to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`}
+            className="font-medium text-foreground hover:underline"
+          >
             {item.author.displayName}
           </Link>
           <span className="text-muted-foreground">
             {formatDistanceToNow(new Date(item.createdAt), { addSuffix: true })}
           </span>
-          {item.edited && !editing && (
-            <span className="text-xs text-muted-foreground">edited</span>
-          )}
+          {item.edited && !editing && <span className="text-xs text-muted-foreground">edited</span>}
           {canEdit && !editing && (
             <button
               onClick={() => setEditing(true)}
@@ -123,13 +126,16 @@ function CommentItem({ item, bugPrefix }: { item: CommentItem; bugPrefix: string
               className="min-h-24 font-mono text-sm"
               autoFocus
               onKeyDown={(e) => {
-                if (e.key === 'Escape') handleCancel()
-                if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); handleSave() }
+                if (e.key === "Escape") handleCancel();
+                if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
+                  e.preventDefault();
+                  handleSave();
+                }
               }}
             />
             <div className="flex gap-2">
               <Button size="sm" onClick={handleSave} disabled={loading}>
-                {loading ? 'Saving…' : 'Save'}
+                {loading ? "Saving…" : "Save"}
               </Button>
               <Button size="sm" variant="ghost" onClick={handleCancel} disabled={loading}>
                 Cancel
@@ -147,14 +153,14 @@ function CommentItem({ item, bugPrefix }: { item: CommentItem; bugPrefix: string
         )}
       </div>
     </div>
-  )
+  );
 }
 
 // ── Inline events ─────────────────────────────────────────────────────────────
 
-type LabelChangeItem = Extract<TimelineNode, { __typename: 'BugLabelChangeTimelineItem' }>
-type StatusChangeItem = Extract<TimelineNode, { __typename: 'BugSetStatusTimelineItem' }>
-type TitleChangeItem = Extract<TimelineNode, { __typename: 'BugSetTitleTimelineItem' }>
+type LabelChangeItem = Extract<TimelineNode, { __typename: "BugLabelChangeTimelineItem" }>;
+type StatusChangeItem = Extract<TimelineNode, { __typename: "BugSetStatusTimelineItem" }>;
+type TitleChangeItem = Extract<TimelineNode, { __typename: "BugSetTitleTimelineItem" }>;
 
 function EventRow({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) {
   return (
@@ -162,40 +168,45 @@ function EventRow({ icon, children }: { icon: React.ReactNode; children: React.R
       <span className="flex size-8 shrink-0 items-center justify-center">{icon}</span>
       {children}
     </div>
-  )
+  );
 }
 
 function LabelChangeItem({ item }: { item: LabelChangeItem }) {
-  const repo = useRepo()
+  const repo = useRepo();
   return (
     <EventRow icon={<Tag className="size-4" />}>
       <span>
-        <Link to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`} className="font-medium text-foreground hover:underline">{item.author.displayName}</Link>{' '}
+        <Link
+          to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`}
+          className="font-medium text-foreground hover:underline"
+        >
+          {item.author.displayName}
+        </Link>{" "}
         {item.added.length > 0 && (
           <>
-            added{' '}
+            added{" "}
             {item.added.map((l) => (
               <LabelBadge key={l.name} name={l.name} color={l.color} />
-            ))}{' '}
+            ))}{" "}
           </>
         )}
         {item.removed.length > 0 && (
           <>
-            removed{' '}
+            removed{" "}
             {item.removed.map((l) => (
               <LabelBadge key={l.name} name={l.name} color={l.color} />
-            ))}{' '}
+            ))}{" "}
           </>
         )}
         {formatDistanceToNow(new Date(item.date), { addSuffix: true })}
       </span>
     </EventRow>
-  )
+  );
 }
 
 function StatusChangeItem({ item }: { item: StatusChangeItem }) {
-  const repo = useRepo()
-  const isOpen = item.status === Status.Open
+  const repo = useRepo();
+  const isOpen = item.status === Status.Open;
   return (
     <EventRow
       icon={
@@ -207,24 +218,34 @@ function StatusChangeItem({ item }: { item: StatusChangeItem }) {
       }
     >
       <span>
-        <Link to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`} className="font-medium text-foreground hover:underline">{item.author.displayName}</Link>{' '}
-        {isOpen ? 'reopened' : 'closed'} this{' '}
+        <Link
+          to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`}
+          className="font-medium text-foreground hover:underline"
+        >
+          {item.author.displayName}
+        </Link>{" "}
+        {isOpen ? "reopened" : "closed"} this{" "}
         {formatDistanceToNow(new Date(item.date), { addSuffix: true })}
       </span>
     </EventRow>
-  )
+  );
 }
 
 function TitleChangeItem({ item }: { item: TitleChangeItem }) {
-  const repo = useRepo()
+  const repo = useRepo();
   return (
     <EventRow icon={<Pencil className="size-4" />}>
       <span>
-        <Link to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`} className="font-medium text-foreground hover:underline">{item.author.displayName}</Link> changed the
-        title from <span className="line-through">{item.was}</span> to{' '}
-        <span className="font-medium text-foreground">{item.title}</span>{' '}
+        <Link
+          to={repo ? `/${repo}/user/${item.author.humanId}` : `/user/${item.author.humanId}`}
+          className="font-medium text-foreground hover:underline"
+        >
+          {item.author.displayName}
+        </Link>{" "}
+        changed the title from <span className="line-through">{item.was}</span> to{" "}
+        <span className="font-medium text-foreground">{item.title}</span>{" "}
         {formatDistanceToNow(new Date(item.date), { addSuffix: true })}
       </span>
     </EventRow>
-  )
+  );
 }

webui2/src/components/bugs/TitleEditor.tsx πŸ”—

@@ -1,52 +1,53 @@
-import { useState, useRef, useEffect } from 'react'
-import { Pencil } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { useAuth } from '@/lib/auth'
-import { useBugSetTitleMutation, BugDetailDocument } from '@/__generated__/graphql'
+import { Pencil } from "lucide-react";
+import { useState, useRef, useEffect } from "react";
+
+import { useBugSetTitleMutation, BugDetailDocument } from "@/__generated__/graphql";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { useAuth } from "@/lib/auth";
 
 interface TitleEditorProps {
-  bugPrefix: string
-  title: string
-  humanId: string
+  bugPrefix: string;
+  title: string;
+  humanId: string;
   /** Current repo slug, passed as `ref` in refetch query variables. */
-  ref_?: string | null
+  ref_?: string | null;
 }
 
 // Inline title editor in BugDetailPage. Shows the title as plain text with a
 // pencil icon on hover (auth-gated). Enter saves, Escape cancels.
 export function TitleEditor({ bugPrefix, title, humanId, ref_ }: TitleEditorProps) {
-  const { user } = useAuth()
-  const [editing, setEditing] = useState(false)
-  const [value, setValue] = useState(title)
-  const inputRef = useRef<HTMLInputElement>(null)
+  const { user } = useAuth();
+  const [editing, setEditing] = useState(false);
+  const [value, setValue] = useState(title);
+  const inputRef = useRef<HTMLInputElement>(null);
 
   const [setTitle, { loading }] = useBugSetTitleMutation({
     refetchQueries: [{ query: BugDetailDocument, variables: { ref: ref_, prefix: bugPrefix } }],
-  })
+  });
 
   useEffect(() => {
-    if (editing) inputRef.current?.focus()
-  }, [editing])
+    if (editing) inputRef.current?.focus();
+  }, [editing]);
 
   // Keep local value in sync if title prop changes (e.g. after refetch)
   useEffect(() => {
-    if (!editing) setValue(title)
-  }, [title, editing])
+    if (!editing) setValue(title);
+  }, [title, editing]);
 
   async function handleSave() {
-    const trimmed = value.trim()
+    const trimmed = value.trim();
     if (trimmed && trimmed !== title) {
-      await setTitle({ variables: { input: { prefix: bugPrefix, title: trimmed } } })
+      await setTitle({ variables: { input: { prefix: bugPrefix, title: trimmed } } });
     }
-    setEditing(false)
+    setEditing(false);
   }
 
   function handleKeyDown(e: React.KeyboardEvent) {
-    if (e.key === 'Enter') handleSave()
-    if (e.key === 'Escape') {
-      setValue(title)
-      setEditing(false)
+    if (e.key === "Enter") handleSave();
+    if (e.key === "Escape") {
+      setValue(title);
+      setEditing(false);
     }
   }
 
@@ -68,14 +69,14 @@ export function TitleEditor({ bugPrefix, title, humanId, ref_ }: TitleEditorProp
           size="sm"
           variant="ghost"
           onClick={() => {
-            setValue(title)
-            setEditing(false)
+            setValue(title);
+            setEditing(false);
           }}
         >
           Cancel
         </Button>
       </div>
-    )
+    );
   }
 
   return (
@@ -94,5 +95,5 @@ export function TitleEditor({ bugPrefix, title, humanId, ref_ }: TitleEditorProp
         </button>
       )}
     </div>
-  )
+  );
 }

webui2/src/components/code/CodeBreadcrumb.tsx πŸ”—

@@ -1,30 +1,30 @@
-import { ChevronRight } from 'lucide-react'
+import { ChevronRight } from "lucide-react";
 
 interface CodeBreadcrumbProps {
-  repoName: string
-  ref: string
-  path: string
+  repoName: string;
+  ref: string;
+  path: string;
   // called when user clicks a breadcrumb segment β€” returns new path
-  onNavigate: (path: string) => void
+  onNavigate: (path: string) => void;
 }
 
 // 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) {
-  const parts = path ? path.split('/').filter(Boolean) : []
+  const parts = path ? path.split("/").filter(Boolean) : [];
 
   return (
     <div className="flex flex-wrap items-center gap-1 font-mono text-sm">
       <button
-        onClick={() => onNavigate('')}
+        onClick={() => onNavigate("")}
         className="font-medium text-foreground hover:underline"
       >
         {repoName}
       </button>
 
       {parts.map((part, i) => {
-        const partPath = parts.slice(0, i + 1).join('/')
-        const isLast = i === parts.length - 1
+        const partPath = parts.slice(0, i + 1).join("/");
+        const isLast = i === parts.length - 1;
         return (
           <span key={partPath} className="flex items-center gap-1">
             <ChevronRight className="size-3.5 text-muted-foreground" />
@@ -39,10 +39,10 @@ export function CodeBreadcrumb({ repoName, ref, path, onNavigate }: CodeBreadcru
               </button>
             )}
           </span>
-        )
+        );
       })}
 
       <span className="ml-2 text-xs text-muted-foreground">@ {ref}</span>
     </div>
-  )
+  );
 }

webui2/src/components/code/CommitList.tsx πŸ”—

@@ -1,14 +1,15 @@
 // Paginated commit history grouped by calendar date. Each row links to the
 // commit detail page. Used in CodePage's "History" view.
 
-import { useState } from 'react'
-import { Link } from 'react-router-dom'
-import { formatDistanceToNow } from 'date-fns'
-import { GitCommit } from 'lucide-react'
-import { gql, useQuery } from '@apollo/client'
-import { Button } from '@/components/ui/button'
-import { Skeleton } from '@/components/ui/skeleton'
-import { useRepo } from '@/lib/repo'
+import { gql, useQuery } from "@apollo/client";
+import { formatDistanceToNow } from "date-fns";
+import { GitCommit } from "lucide-react";
+import { useState } from "react";
+import { Link } from "react-router-dom";
+
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useRepo } from "@/lib/repo";
 
 const COMMITS_QUERY = gql`
   query CommitList($repo: String, $ref: String!, $path: String, $after: String, $first: Int) {
@@ -28,64 +29,66 @@ const COMMITS_QUERY = gql`
       }
     }
   }
-`
+`;
 
-const PAGE_SIZE = 30
+const PAGE_SIZE = 30;
 
 interface CommitListProps {
-  ref_: string
-  path?: string
+  ref_: string;
+  path?: string;
 }
 
 type CommitNode = {
-  hash: string
-  shortHash: string
-  message: string
-  authorName: string
-  date: string
-}
+  hash: string;
+  shortHash: string;
+  message: string;
+  authorName: string;
+  date: string;
+};
 
 export function CommitList({ ref_, path }: CommitListProps) {
-  const repo = useRepo()
-  const [cursor, setCursor] = useState<string | null>(null)
-  const [allCommits, setAllCommits] = useState<CommitNode[]>([])
+  const repo = useRepo();
+  const [cursor, setCursor] = useState<string | null>(null);
+  const [allCommits, setAllCommits] = useState<CommitNode[]>([]);
 
   const { loading, error, fetchMore } = useQuery(COMMITS_QUERY, {
     variables: { repo, ref: ref_, path: path ?? null, after: null, first: PAGE_SIZE },
     skip: !ref_,
     onCompleted(data) {
-      const nodes = data?.repository?.commits?.nodes ?? []
-      setAllCommits(nodes)
-      setCursor(data?.repository?.commits?.pageInfo?.endCursor ?? null)
+      const nodes = data?.repository?.commits?.nodes ?? [];
+      setAllCommits(nodes);
+      setCursor(data?.repository?.commits?.pageInfo?.endCursor ?? null);
     },
-  })
+  });
 
-  const hasMore = !!cursor && allCommits.length > 0 && allCommits.length % PAGE_SIZE === 0
-  const [loadingMore, setLoadingMore] = useState(false)
+  const hasMore = !!cursor && allCommits.length > 0 && allCommits.length % PAGE_SIZE === 0;
+  const [loadingMore, setLoadingMore] = useState(false);
 
   function loadMore() {
-    if (!cursor) return
-    setLoadingMore(true)
+    if (!cursor) return;
+    setLoadingMore(true);
     fetchMore({
       variables: { after: cursor },
-    }).then((result) => {
-      const newNodes = result.data?.repository?.commits?.nodes ?? []
-      setAllCommits((prev) => [...prev, ...newNodes])
-      setCursor(result.data?.repository?.commits?.pageInfo?.endCursor ?? null)
-    }).finally(() => setLoadingMore(false))
+    })
+      .then((result) => {
+        const newNodes = result.data?.repository?.commits?.nodes ?? [];
+        setAllCommits((prev) => [...prev, ...newNodes]);
+        setCursor(result.data?.repository?.commits?.pageInfo?.endCursor ?? null);
+      })
+      .finally(() => setLoadingMore(false));
   }
 
-  if (loading) return <CommitListSkeleton />
+  if (loading) return <CommitListSkeleton />;
 
   if (error) {
     return (
       <div className="rounded-md border border-border px-4 py-8 text-center text-sm text-destructive">
         {error.message}
       </div>
-    )
+    );
   }
 
-  const groups = groupByDate(allCommits)
+  const groups = groupByDate(allCommits);
 
   return (
     <div className="space-y-6">
@@ -94,7 +97,7 @@ export function CommitList({ ref_, path }: CommitListProps) {
           <h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
             Commits on {date}
           </h3>
-          <div className="overflow-hidden rounded-md border border-border divide-y divide-border">
+          <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
             {group.map((commit) => (
               <CommitRow key={commit.hash} commit={commit} repo={repo} />
             ))}
@@ -105,16 +108,16 @@ export function CommitList({ ref_, path }: CommitListProps) {
       {hasMore && (
         <div className="text-center">
           <Button variant="outline" size="sm" onClick={loadMore} disabled={loadingMore}>
-            {loadingMore ? 'Loading…' : 'Load more commits'}
+            {loadingMore ? "Loading…" : "Load more commits"}
           </Button>
         </div>
       )}
     </div>
-  )
+  );
 }
 
 function CommitRow({ commit, repo }: { commit: CommitNode; repo: string | null }) {
-  const commitPath = repo ? `/${repo}/commit/${commit.hash}` : `/commit/${commit.hash}`
+  const commitPath = repo ? `/${repo}/commit/${commit.hash}` : `/commit/${commit.hash}`;
   return (
     <div className="flex items-center gap-3 bg-background px-4 py-3 hover:bg-muted/30">
       <GitCommit className="size-4 shrink-0 text-muted-foreground" />
@@ -126,7 +129,7 @@ function CommitRow({ commit, repo }: { commit: CommitNode; repo: string | null }
           {commit.message}
         </Link>
         <p className="mt-0.5 text-xs text-muted-foreground">
-          {commit.authorName} &middot;{' '}
+          {commit.authorName} &middot;{" "}
           {formatDistanceToNow(new Date(commit.date), { addSuffix: true })}
         </p>
       </div>
@@ -138,22 +141,22 @@ function CommitRow({ commit, repo }: { commit: CommitNode; repo: string | null }
         {commit.shortHash}
       </Link>
     </div>
-  )
+  );
 }
 
 function groupByDate(commits: CommitNode[]): [string, CommitNode[]][] {
-  const map = new Map<string, CommitNode[]>()
+  const map = new Map<string, CommitNode[]>();
   for (const c of commits) {
-    const date = new Date(c.date).toLocaleDateString('en-US', {
-      year: 'numeric',
-      month: 'long',
-      day: 'numeric',
-    })
-    const group = map.get(date) ?? []
-    group.push(c)
-    map.set(date, group)
+    const date = new Date(c.date).toLocaleDateString("en-US", {
+      year: "numeric",
+      month: "long",
+      day: "numeric",
+    });
+    const group = map.get(date) ?? [];
+    group.push(c);
+    map.set(date, group);
   }
-  return Array.from(map.entries())
+  return Array.from(map.entries());
 }
 
 function CommitListSkeleton() {
@@ -162,7 +165,7 @@ function CommitListSkeleton() {
       {Array.from({ length: 2 }).map((_, g) => (
         <div key={g}>
           <Skeleton className="mb-2 h-3 w-32" />
-          <div className="overflow-hidden rounded-md border border-border divide-y divide-border">
+          <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
             {Array.from({ length: 4 }).map((_, i) => (
               <div key={i} className="flex items-center gap-3 px-4 py-3">
                 <Skeleton className="size-4 rounded" />
@@ -177,5 +180,5 @@ function CommitListSkeleton() {
         </div>
       ))}
     </div>
-  )
+  );
 }

webui2/src/components/code/FileDiffView.tsx πŸ”—

@@ -1,11 +1,12 @@
 // Collapsible diff view for a single file in a commit.
 // Diff is fetched lazily on first expand via GraphQL.
 
-import { useState } from 'react'
-import { ChevronRight, FilePlus, FileMinus, FileEdit } from 'lucide-react'
-import { gql, useLazyQuery } from '@apollo/client'
-import { cn } from '@/lib/utils'
-import { useRepo } from '@/lib/repo'
+import { gql, useLazyQuery } from "@apollo/client";
+import { ChevronRight, FilePlus, FileMinus, FileEdit } from "lucide-react";
+import { useState } from "react";
+
+import { useRepo } from "@/lib/repo";
+import { cn } from "@/lib/utils";
 
 const DIFF_QUERY = gql`
   query FileDiff($repo: String, $hash: String!, $path: String!) {
@@ -33,128 +34,144 @@ const DIFF_QUERY = gql`
       }
     }
   }
-`
+`;
 
 interface FileDiffViewProps {
-  hash: string
-  path: string
-  oldPath?: string
-  status: string
+  hash: string;
+  path: string;
+  oldPath?: string;
+  status: string;
 }
 
 const statusIcon: Record<string, React.ReactNode> = {
-  ADDED:    <FilePlus  className="size-3.5 text-green-600 dark:text-green-400" />,
-  DELETED:  <FileMinus className="size-3.5 text-red-500  dark:text-red-400" />,
-  MODIFIED: <FileEdit  className="size-3.5 text-yellow-500 dark:text-yellow-400" />,
-  RENAMED:  <FileEdit  className="size-3.5 text-blue-500  dark:text-blue-400" />,
-}
-const statusBadge: Record<string, string> = { ADDED: 'A', DELETED: 'D', MODIFIED: 'M', RENAMED: 'R' }
+  ADDED: <FilePlus className="size-3.5 text-green-600 dark:text-green-400" />,
+  DELETED: <FileMinus className="size-3.5 text-red-500 dark:text-red-400" />,
+  MODIFIED: <FileEdit className="size-3.5 text-yellow-500 dark:text-yellow-400" />,
+  RENAMED: <FileEdit className="size-3.5 text-blue-500 dark:text-blue-400" />,
+};
+const statusBadge: Record<string, string> = {
+  ADDED: "A",
+  DELETED: "D",
+  MODIFIED: "M",
+  RENAMED: "R",
+};
 
 export function FileDiffView({ hash, path, oldPath, status }: FileDiffViewProps) {
-  const repo = useRepo()
-  const [open, setOpen] = useState(false)
-  const [fetchDiff, { data, loading, error }] = useLazyQuery(DIFF_QUERY)
+  const repo = useRepo();
+  const [open, setOpen] = useState(false);
+  const [fetchDiff, { data, loading, error }] = useLazyQuery(DIFF_QUERY);
 
   function toggle() {
     if (!open && !data && !loading) {
-      fetchDiff({ variables: { repo, hash, path } })
+      fetchDiff({ variables: { repo, hash, path } });
     }
-    setOpen((v) => !v)
+    setOpen((v) => !v);
   }
 
-  const diff = data?.repository?.commit?.diff
+  const diff = data?.repository?.commit?.diff;
 
   return (
     <div className="divide-y divide-border">
       <button
         onClick={toggle}
-        className="flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-muted/50 transition-colors"
+        className="flex w-full items-center gap-3 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
       >
         <ChevronRight
           className={cn(
-            'size-3.5 shrink-0 text-muted-foreground transition-transform duration-150',
-            open && 'rotate-90',
+            "size-3.5 shrink-0 text-muted-foreground transition-transform duration-150",
+            open && "rotate-90",
           )}
         />
         {statusIcon[status] ?? <FileEdit className="size-3.5 text-muted-foreground" />}
         <span className="min-w-0 flex-1 font-mono text-sm">
-          {status === 'RENAMED' ? (
+          {status === "RENAMED" ? (
             <>
               <span className="text-muted-foreground line-through">{oldPath}</span>
-              {' β†’ '}
+              {" β†’ "}
               <span>{path}</span>
             </>
-          ) : path}
+          ) : (
+            path
+          )}
         </span>
         <span className="shrink-0 rounded border border-border px-1.5 py-0.5 font-mono text-xs text-muted-foreground">
-          {statusBadge[status] ?? '?'}
+          {statusBadge[status] ?? "?"}
         </span>
       </button>
 
       {open && (
         <div className="overflow-x-auto">
-          {loading && (
-            <div className="px-4 py-3 text-xs text-muted-foreground">Loading diff…</div>
-          )}
+          {loading && <div className="px-4 py-3 text-xs text-muted-foreground">Loading diff…</div>}
           {error && (
-            <div className="px-4 py-3 text-xs text-destructive">Failed to load diff: {error.message}</div>
+            <div className="px-4 py-3 text-xs text-destructive">
+              Failed to load diff: {error.message}
+            </div>
           )}
-          {diff && (
-            diff.isBinary ? (
+          {diff &&
+            (diff.isBinary ? (
               <div className="px-4 py-3 text-xs text-muted-foreground">Binary file</div>
             ) : diff.hunks.length === 0 ? (
               <div className="px-4 py-3 text-xs text-muted-foreground">No changes</div>
             ) : (
               diff.hunks.map((hunk: HunkType, i: number) => <Hunk key={i} hunk={hunk} />)
-            )
-          )}
+            ))}
         </div>
       )}
     </div>
-  )
+  );
 }
 
-type LineType = { type: string; content: string; oldLine: number; newLine: number }
-type HunkType = { oldStart: number; oldLines: number; newStart: number; newLines: number; lines: LineType[] }
+type LineType = { type: string; content: string; oldLine: number; newLine: number };
+type HunkType = {
+  oldStart: number;
+  oldLines: number;
+  newStart: number;
+  newLines: number;
+  lines: LineType[];
+};
 
 function Hunk({ hunk }: { hunk: HunkType }) {
   return (
     <div className="font-mono text-xs leading-5">
-      <div className="bg-blue-50 px-4 py-0.5 text-blue-600 dark:bg-blue-950/40 dark:text-blue-400 select-none">
+      <div className="select-none bg-blue-50 px-4 py-0.5 text-blue-600 dark:bg-blue-950/40 dark:text-blue-400">
         @@ -{hunk.oldStart},{hunk.oldLines} +{hunk.newStart},{hunk.newLines} @@
       </div>
       {hunk.lines.map((line, i) => (
         <div
           key={i}
           className={cn(
-            'flex',
-            line.type === 'ADDED'   && 'bg-green-50  dark:bg-green-950/30',
-            line.type === 'DELETED' && 'bg-red-50    dark:bg-red-950/30',
+            "flex",
+            line.type === "ADDED" && "bg-green-50  dark:bg-green-950/30",
+            line.type === "DELETED" && "bg-red-50    dark:bg-red-950/30",
           )}
         >
           <span className="w-10 shrink-0 select-none border-r border-border/50 px-2 text-right text-muted-foreground/50">
-            {line.oldLine || ''}
+            {line.oldLine || ""}
           </span>
           <span className="w-10 shrink-0 select-none border-r border-border/50 px-2 text-right text-muted-foreground/50">
-            {line.newLine || ''}
+            {line.newLine || ""}
           </span>
-          <span className={cn(
-            'w-5 shrink-0 select-none text-center',
-            line.type === 'ADDED'   && 'text-green-600 dark:text-green-400',
-            line.type === 'DELETED' && 'text-red-500   dark:text-red-400',
-            line.type === 'CONTEXT' && 'text-muted-foreground/40',
-          )}>
-            {line.type === 'ADDED' ? '+' : line.type === 'DELETED' ? '-' : ' '}
+          <span
+            className={cn(
+              "w-5 shrink-0 select-none text-center",
+              line.type === "ADDED" && "text-green-600 dark:text-green-400",
+              line.type === "DELETED" && "text-red-500   dark:text-red-400",
+              line.type === "CONTEXT" && "text-muted-foreground/40",
+            )}
+          >
+            {line.type === "ADDED" ? "+" : line.type === "DELETED" ? "-" : " "}
           </span>
-          <pre className={cn(
-            'flex-1 overflow-visible whitespace-pre px-2',
-            line.type === 'ADDED'   && 'text-green-900 dark:text-green-200',
-            line.type === 'DELETED' && 'text-red-900   dark:text-red-200',
-          )}>
+          <pre
+            className={cn(
+              "flex-1 overflow-visible whitespace-pre px-2",
+              line.type === "ADDED" && "text-green-900 dark:text-green-200",
+              line.type === "DELETED" && "text-red-900   dark:text-red-200",
+            )}
+          >
             {line.content}
           </pre>
         </div>
       ))}
     </div>
-  )
+  );
 }

webui2/src/components/code/FileTree.tsx πŸ”—

@@ -1,25 +1,26 @@
-import { Folder, File } from 'lucide-react'
-import { Link } from 'react-router-dom'
-import { formatDistanceToNow } from 'date-fns'
-import { Skeleton } from '@/components/ui/skeleton'
-import { useRepo } from '@/lib/repo'
-import type { GitTreeEntry } from '@/__generated__/graphql'
+import { formatDistanceToNow } from "date-fns";
+import { Folder, File } from "lucide-react";
+import { Link } from "react-router-dom";
+
+import type { GitTreeEntry } from "@/__generated__/graphql";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useRepo } from "@/lib/repo";
 
 export interface TreeEntryWithCommit extends GitTreeEntry {
   lastCommit?: {
-    hash: string
-    shortHash: string
-    message: string
-    date: string
-  }
+    hash: string;
+    shortHash: string;
+    message: string;
+    date: string;
+  };
 }
 
 interface FileTreeProps {
-  entries: TreeEntryWithCommit[]
-  path: string
-  loading?: boolean
-  onNavigate: (entry: TreeEntryWithCommit) => void
-  onNavigateUp: () => void
+  entries: TreeEntryWithCommit[];
+  path: string;
+  loading?: boolean;
+  onNavigate: (entry: TreeEntryWithCommit) => void;
+  onNavigateUp: () => void;
 }
 
 // Directory listing table for the code browser. Shows each entry's icon,
@@ -27,21 +28,18 @@ interface FileTreeProps {
 export function FileTree({ entries, path, loading, onNavigate, onNavigateUp }: FileTreeProps) {
   // Directories first, then files β€” each group alphabetical
   const sorted = [...entries].sort((a, b) => {
-    if (a.type !== b.type) return a.type === 'TREE' ? -1 : 1
-    return a.name.localeCompare(b.name)
-  })
+    if (a.type !== b.type) return a.type === "TREE" ? -1 : 1;
+    return a.name.localeCompare(b.name);
+  });
 
-  if (loading) return <FileTreeSkeleton />
+  if (loading) return <FileTreeSkeleton />;
 
   return (
     <div className="overflow-hidden rounded-md border border-border">
       <table className="w-full text-sm">
         <tbody className="divide-y divide-border">
           {path && (
-            <tr
-              className="cursor-pointer hover:bg-muted/40"
-              onClick={onNavigateUp}
-            >
+            <tr className="cursor-pointer hover:bg-muted/40" onClick={onNavigateUp}>
               <td className="w-6 py-2 pl-4">
                 <Folder className="size-4 text-blue-500 dark:text-blue-400" />
               </td>
@@ -56,24 +54,21 @@ export function FileTree({ entries, path, loading, onNavigate, onNavigateUp }: F
         </tbody>
       </table>
     </div>
-  )
+  );
 }
 
 function FileTreeRow({
   entry,
   onNavigate,
 }: {
-  entry: TreeEntryWithCommit
-  onNavigate: (entry: TreeEntryWithCommit) => void
+  entry: TreeEntryWithCommit;
+  onNavigate: (entry: TreeEntryWithCommit) => void;
 }) {
-  const isDir = entry.type === 'TREE'
-  const repo = useRepo()
+  const isDir = entry.type === "TREE";
+  const repo = useRepo();
 
   return (
-    <tr
-      className="cursor-pointer hover:bg-muted/40"
-      onClick={() => onNavigate(entry)}
-    >
+    <tr className="cursor-pointer hover:bg-muted/40" onClick={() => onNavigate(entry)}>
       <td className="w-6 py-2 pl-4">
         {isDir ? (
           <Folder className="size-4 text-blue-500 dark:text-blue-400" />
@@ -82,14 +77,16 @@ function FileTreeRow({
         )}
       </td>
       <td className="px-3 py-2">
-        <span className={`font-mono ${isDir ? 'font-medium text-foreground' : 'text-foreground'}`}>
+        <span className={`font-mono ${isDir ? "font-medium text-foreground" : "text-foreground"}`}>
           {entry.name}
         </span>
       </td>
       <td className="hidden max-w-xs truncate px-3 py-2 text-muted-foreground md:table-cell">
         {entry.lastCommit && (
           <Link
-            to={repo ? `/${repo}/commit/${entry.lastCommit.hash}` : `/commit/${entry.lastCommit.hash}`}
+            to={
+              repo ? `/${repo}/commit/${entry.lastCommit.hash}` : `/commit/${entry.lastCommit.hash}`
+            }
             className="hover:text-foreground hover:underline"
             onClick={(e) => e.stopPropagation()}
           >
@@ -102,7 +99,7 @@ function FileTreeRow({
           formatDistanceToNow(new Date(entry.lastCommit.date), { addSuffix: true })}
       </td>
     </tr>
-  )
+  );
 }
 
 function FileTreeSkeleton() {
@@ -119,5 +116,5 @@ function FileTreeSkeleton() {
         ))}
       </div>
     </div>
-  )
+  );
 }

webui2/src/components/code/FileViewer.tsx πŸ”—

@@ -1,46 +1,49 @@
 // Syntax-highlighted file viewer with line numbers and copy button.
 // highlight.js is loaded lazily so it doesn't bloat the initial bundle.
 
-import { useState, useEffect } from 'react'
-import { Copy } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { Skeleton } from '@/components/ui/skeleton'
-import type { GitBlob } from '@/__generated__/graphql'
+import { Copy } from "lucide-react";
+import { useState, useEffect } from "react";
+
+import type { GitBlob } from "@/__generated__/graphql";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
 
 interface FileViewerProps {
-  blob: GitBlob
-  loading?: boolean
+  blob: GitBlob;
+  loading?: boolean;
 }
 
 export function FileViewer({ blob, loading }: FileViewerProps) {
-  const [highlighted, setHighlighted] = useState<{ html: string; lineCount: number } | null>(null)
+  const [highlighted, setHighlighted] = useState<{ html: string; lineCount: number } | null>(null);
 
   useEffect(() => {
     if (blob.isBinary || !blob.text) {
-      setHighlighted({ html: '', lineCount: 0 })
-      return
+      setHighlighted({ html: "", lineCount: 0 });
+      return;
     }
-    setHighlighted(null)
-    let cancelled = false
-    import('highlight.js').then(({ default: hljs }) => {
-      if (cancelled) return
-      const ext = blob.path.split('.').pop() ?? ''
+    setHighlighted(null);
+    let cancelled = false;
+    import("highlight.js").then(({ default: hljs }) => {
+      if (cancelled) return;
+      const ext = blob.path.split(".").pop() ?? "";
       const result = hljs.getLanguage(ext)
         ? hljs.highlight(blob.text!, { language: ext })
-        : hljs.highlightAuto(blob.text!)
+        : hljs.highlightAuto(blob.text!);
       setHighlighted({
         html: result.value,
-        lineCount: blob.text!.split('\n').length,
-      })
-    })
-    return () => { cancelled = true }
-  }, [blob])
+        lineCount: blob.text!.split("\n").length,
+      });
+    });
+    return () => {
+      cancelled = true;
+    };
+  }, [blob]);
 
-  if (loading || highlighted === null) return <FileViewerSkeleton />
-  const { html, lineCount } = highlighted
+  if (loading || highlighted === null) return <FileViewerSkeleton />;
+  const { html, lineCount } = highlighted;
 
   function copyToClipboard() {
-    if (blob.text) navigator.clipboard.writeText(blob.text)
+    if (blob.text) navigator.clipboard.writeText(blob.text);
   }
 
   return (
@@ -48,9 +51,15 @@ export function FileViewer({ blob, loading }: FileViewerProps) {
       <div className="flex items-center justify-between border-b border-border bg-muted/40 px-4 py-2 text-xs text-muted-foreground">
         <span>
           {lineCount.toLocaleString()} lines Β· {formatBytes(blob.size)}
-          {blob.isTruncated && ' Β· truncated'}
+          {blob.isTruncated && " Β· truncated"}
         </span>
-        <Button variant="ghost" size="icon" className="size-7" onClick={copyToClipboard} title="Copy">
+        <Button
+          variant="ghost"
+          size="icon"
+          className="size-7"
+          onClick={copyToClipboard}
+          title="Copy"
+        >
           <Copy className="size-3.5" />
         </Button>
       </div>
@@ -75,13 +84,13 @@ export function FileViewer({ blob, loading }: FileViewerProps) {
         </div>
       )}
     </div>
-  )
+  );
 }
 
 function formatBytes(bytes: number): string {
-  if (bytes < 1024) return `${bytes} B`
-  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
-  return `${(bytes / 1024 / 1024).toFixed(1)} MB`
+  if (bytes < 1024) return `${bytes} B`;
+  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
 }
 
 function FileViewerSkeleton() {
@@ -103,5 +112,5 @@ function FileViewerSkeleton() {
         </div>
       </div>
     </div>
-  )
+  );
 }

webui2/src/components/code/RefSelector.tsx πŸ”—

@@ -1,28 +1,27 @@
-import { useState } from 'react'
-import { GitBranch, Tag, Check, ChevronsUpDown } from 'lucide-react'
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import type { GitRef } from '@/__generated__/graphql'
-import { cn } from '@/lib/utils'
+import { GitBranch, Tag, Check, ChevronsUpDown } from "lucide-react";
+import { useState } from "react";
+
+import type { GitRef } from "@/__generated__/graphql";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
 
 interface RefSelectorProps {
-  refs: GitRef[]
-  currentRef: string
-  onSelect: (ref: GitRef) => void
+  refs: GitRef[];
+  currentRef: string;
+  onSelect: (ref: GitRef) => void;
 }
 
 // Branch / tag selector dropdown for the code browser. Shown in two groups
 // (branches, tags) with an inline search filter.
 export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
-  const [open, setOpen] = useState(false)
-  const [filter, setFilter] = useState('')
+  const [open, setOpen] = useState(false);
+  const [filter, setFilter] = useState("");
 
-  const filtered = refs.filter((r) =>
-    r.shortName.toLowerCase().includes(filter.toLowerCase()),
-  )
-  const branches = filtered.filter((r) => r.type === 'BRANCH')
-  const tags = filtered.filter((r) => r.type === 'TAG')
+  const filtered = refs.filter((r) => r.shortName.toLowerCase().includes(filter.toLowerCase()));
+  const branches = filtered.filter((r) => r.type === "BRANCH");
+  const tags = filtered.filter((r) => r.type === "TAG");
 
   return (
     <Popover open={open} onOpenChange={setOpen}>
@@ -51,7 +50,11 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
                   key={ref.name}
                   ref_={ref}
                   active={ref.shortName === currentRef}
-                  onSelect={() => { onSelect(ref); setOpen(false); setFilter('') }}
+                  onSelect={() => {
+                    onSelect(ref);
+                    setOpen(false);
+                    setFilter("");
+                  }}
                 />
               ))}
             </div>
@@ -64,7 +67,11 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
                   key={ref.name}
                   ref_={ref}
                   active={ref.shortName === currentRef}
-                  onSelect={() => { onSelect(ref); setOpen(false); setFilter('') }}
+                  onSelect={() => {
+                    onSelect(ref);
+                    setOpen(false);
+                    setFilter("");
+                  }}
                 />
               ))}
             </div>
@@ -75,7 +82,7 @@ export function RefSelector({ refs, currentRef, onSelect }: RefSelectorProps) {
         </div>
       </PopoverContent>
     </Popover>
-  )
+  );
 }
 
 function RefItem({
@@ -83,19 +90,19 @@ function RefItem({
   active,
   onSelect,
 }: {
-  ref_: GitRef
-  active: boolean
-  onSelect: () => void
+  ref_: GitRef;
+  active: boolean;
+  onSelect: () => void;
 }) {
   return (
     <button
       onClick={onSelect}
       className={cn(
-        'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-muted',
-        active && 'font-medium',
+        "flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-muted",
+        active && "font-medium",
       )}
     >
-      {ref_.type === 'BRANCH' ? (
+      {ref_.type === "BRANCH" ? (
         <GitBranch className="size-3 shrink-0 text-muted-foreground" />
       ) : (
         <Tag className="size-3 shrink-0 text-muted-foreground" />
@@ -103,5 +110,5 @@ function RefItem({
       <span className="flex-1 truncate font-mono">{ref_.shortName}</span>
       {active && <Check className="size-3 text-muted-foreground" />}
     </button>
-  )
+  );
 }

webui2/src/components/content/Markdown.tsx πŸ”—

@@ -1,12 +1,13 @@
-import ReactMarkdown from 'react-markdown'
-import remarkGfm from 'remark-gfm'
-import remarkEmoji from 'remark-emoji'
-import rehypeRaw from 'rehype-raw'
-import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
-import rehypeSlug from 'rehype-slug'
-import rehypeAutolinkHeadings from 'rehype-autolink-headings'
-import rehypeExternalLinks from 'rehype-external-links'
-import { cn } from '@/lib/utils'
+import ReactMarkdown from "react-markdown";
+import rehypeAutolinkHeadings from "rehype-autolink-headings";
+import rehypeExternalLinks from "rehype-external-links";
+import rehypeRaw from "rehype-raw";
+import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
+import rehypeSlug from "rehype-slug";
+import remarkEmoji from "remark-emoji";
+import remarkGfm from "remark-gfm";
+
+import { cn } from "@/lib/utils";
 
 // Sanitization schema: start from the safe default and allow a small set of
 // presentational/structural HTML tags commonly found in READMEs.
@@ -16,20 +17,17 @@ import { cn } from '@/lib/utils'
 // allow those attributes on anchors.
 const sanitizeSchema = {
   ...defaultSchema,
-  tagNames: [
-    ...(defaultSchema.tagNames ?? []),
-    'details', 'summary', 'picture', 'source',
-  ],
+  tagNames: [...(defaultSchema.tagNames ?? []), "details", "summary", "picture", "source"],
   attributes: {
     ...defaultSchema.attributes,
-    a: [...(defaultSchema.attributes?.a ?? []), 'aria-hidden', 'class'],
-    '*': [...(defaultSchema.attributes?.['*'] ?? []), 'id'],
+    a: [...(defaultSchema.attributes?.a ?? []), "aria-hidden", "class"],
+    "*": [...(defaultSchema.attributes?.["*"] ?? []), "id"],
   },
-}
+};
 
 interface MarkdownProps {
-  content: string
-  className?: string
+  content: string;
+  className?: string;
 }
 
 // Renders a Markdown string with GitHub-flavoured extensions (tables, task
@@ -42,18 +40,18 @@ export function Markdown({ content, className }: MarkdownProps) {
         rehypeRaw,
         [rehypeSanitize, sanitizeSchema],
         rehypeSlug,
-        [rehypeAutolinkHeadings, { behavior: 'append' }],
-        [rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }],
+        [rehypeAutolinkHeadings, { behavior: "append" }],
+        [rehypeExternalLinks, { target: "_blank", rel: ["noopener", "noreferrer"] }],
       ]}
       className={cn(
-        'prose prose-sm dark:prose-invert max-w-none',
-        'prose-pre:bg-muted prose-pre:text-foreground',
-        'prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:before:content-none prose-code:after:content-none',
-        'prose-img:inline prose-img:my-0',
+        "prose prose-sm dark:prose-invert max-w-none",
+        "prose-pre:bg-muted prose-pre:text-foreground",
+        "prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:before:content-none prose-code:after:content-none",
+        "prose-img:inline prose-img:my-0",
         className,
       )}
     >
       {content}
     </ReactMarkdown>
-  )
+  );
 }

webui2/src/components/layout/Header.tsx πŸ”—

@@ -6,41 +6,42 @@
 // In external mode, shows a "Sign in" button when logged out and a sign-out
 // action when logged in.
 
-import { Link, useMatch, NavLink } from 'react-router-dom'
-import { Bug, Plus, Sun, Moon, LogIn, LogOut } from 'lucide-react'
-import { cn } from '@/lib/utils'
-import { useAuth } from '@/lib/auth'
-import { useTheme } from '@/lib/theme'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { Button } from '@/components/ui/button'
+import { Bug, Plus, Sun, Moon, LogIn, LogOut } from "lucide-react";
+import { Link, useMatch, NavLink } from "react-router-dom";
+
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { useAuth } from "@/lib/auth";
+import { useTheme } from "@/lib/theme";
+import { cn } from "@/lib/utils";
 
 // SignOutButton sends a POST to /auth/logout and reloads the page.
 // A full reload is the simplest way to reset all Apollo cache + React state.
 function SignOutButton() {
   function handleSignOut() {
-    fetch('/auth/logout', { method: 'POST', credentials: 'include' }).finally(
-      () => window.location.assign('/'),
-    )
+    fetch("/auth/logout", { method: "POST", credentials: "include" }).finally(() =>
+      window.location.assign("/"),
+    );
   }
   return (
     <Button variant="ghost" size="sm" onClick={handleSignOut} title="Sign out">
       <LogOut className="size-4" />
     </Button>
-  )
+  );
 }
 
 export function Header() {
-  const { user, mode, loginProviders } = useAuth()
-  const { theme, toggle } = useTheme()
+  const { user, mode, loginProviders } = useAuth();
+  const { theme, toggle } = useTheme();
 
   // Detect if we're inside a /:repo route and grab the slug.
   // useMatch works from any component in the tree, unlike useParams which is
   // scoped to the nearest Route element.
-  const repoMatch = useMatch({ path: '/:repo/*', end: false })
-  const repo = repoMatch?.params.repo ?? null
+  const repoMatch = useMatch({ path: "/:repo/*", end: false });
+  const repo = repoMatch?.params.repo ?? null;
 
   // Don't show repo nav on the /auth/* pages.
-  const effectiveRepo = repo === 'auth' ? null : repo
+  const effectiveRepo = repo === "auth" ? null : repo;
 
   return (
     <header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur">
@@ -57,23 +58,27 @@ export function Header() {
             <NavLink
               to={`/${effectiveRepo}`}
               end
-              className={({ isActive }) => cn(
-                'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
-                isActive
-                  ? 'bg-accent text-accent-foreground'
-                  : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
-              )}
+              className={({ isActive }) =>
+                cn(
+                  "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+                  isActive
+                    ? "bg-accent text-accent-foreground"
+                    : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
+                )
+              }
             >
               Code
             </NavLink>
             <NavLink
               to={`/${effectiveRepo}/issues`}
-              className={({ isActive }) => cn(
-                'rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
-                isActive
-                  ? 'bg-accent text-accent-foreground'
-                  : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
-              )}
+              className={({ isActive }) =>
+                cn(
+                  "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+                  isActive
+                    ? "bg-accent text-accent-foreground"
+                    : "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
+                )
+              }
             >
               Issues
             </NavLink>
@@ -81,23 +86,23 @@ export function Header() {
         )}
 
         <div className="ml-auto flex items-center gap-2">
-          {mode === 'readonly' && (
-            <span className="text-xs text-muted-foreground">Read only</span>
-          )}
+          {mode === "readonly" && <span className="text-xs text-muted-foreground">Read only</span>}
 
           <Button variant="ghost" size="icon" onClick={toggle} title="Toggle theme">
-            {theme === 'light' ? <Moon className="size-4" /> : <Sun className="size-4" />}
+            {theme === "light" ? <Moon className="size-4" /> : <Sun className="size-4" />}
           </Button>
 
           {/* External mode: show sign-in buttons when logged out */}
-          {mode === 'external' && !user && loginProviders.map((p) => (
-            <Button key={p} asChild size="sm">
-              <a href={`/auth/login?provider=${p}`}>
-                <LogIn className="size-4" />
-                Sign in with {providerLabel(p)}
-              </a>
-            </Button>
-          ))}
+          {mode === "external" &&
+            !user &&
+            loginProviders.map((p) => (
+              <Button key={p} asChild size="sm">
+                <a href={`/auth/login?provider=${p}`}>
+                  <LogIn className="size-4" />
+                  Sign in with {providerLabel(p)}
+                </a>
+              </Button>
+            ))}
 
           {user && effectiveRepo && (
             <>
@@ -119,14 +124,14 @@ export function Header() {
           )}
 
           {/* Sign out only shown in external mode when logged in */}
-          {mode === 'external' && user && <SignOutButton />}
+          {mode === "external" && user && <SignOutButton />}
         </div>
       </div>
     </header>
-  )
+  );
 }
 
 function providerLabel(name: string): string {
-  const labels: Record<string, string> = { github: 'GitHub', gitlab: 'GitLab', gitea: 'Gitea' }
-  return labels[name] ?? name
+  const labels: Record<string, string> = { github: "GitHub", gitlab: "GitLab", gitea: "Gitea" };
+  return labels[name] ?? name;
 }

webui2/src/components/layout/Shell.tsx πŸ”—

@@ -1,5 +1,6 @@
-import { Outlet } from 'react-router-dom'
-import { Header } from './Header'
+import { Outlet } from "react-router-dom";
+
+import { Header } from "./Header";
 
 // Top-level page wrapper used as the root layout in App.tsx. Renders the
 // Header above the current route's page component via <Outlet>.
@@ -11,5 +12,5 @@ export function Shell() {
         <Outlet />
       </main>
     </div>
-  )
+  );
 }

webui2/src/components/ui/avatar.tsx πŸ”—

@@ -1,6 +1,7 @@
-import * as React from 'react'
-import * as AvatarPrimitive from '@radix-ui/react-avatar'
-import { cn } from '@/lib/utils'
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
 
 const Avatar = React.forwardRef<
   React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -8,11 +9,11 @@ const Avatar = React.forwardRef<
 >(({ className, ...props }, ref) => (
   <AvatarPrimitive.Root
     ref={ref}
-    className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
+    className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
     {...props}
   />
-))
-Avatar.displayName = AvatarPrimitive.Root.displayName
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
 
 const AvatarImage = React.forwardRef<
   React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -20,11 +21,11 @@ const AvatarImage = React.forwardRef<
 >(({ className, ...props }, ref) => (
   <AvatarPrimitive.Image
     ref={ref}
-    className={cn('aspect-square h-full w-full', className)}
+    className={cn("aspect-square h-full w-full", className)}
     {...props}
   />
-))
-AvatarImage.displayName = AvatarPrimitive.Image.displayName
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
 
 const AvatarFallback = React.forwardRef<
   React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -33,12 +34,12 @@ const AvatarFallback = React.forwardRef<
   <AvatarPrimitive.Fallback
     ref={ref}
     className={cn(
-      'flex h-full w-full items-center justify-center rounded-full bg-muted text-xs font-medium',
+      "flex h-full w-full items-center justify-center rounded-full bg-muted text-xs font-medium",
       className,
     )}
     {...props}
   />
-))
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
 
-export { Avatar, AvatarImage, AvatarFallback }
+export { Avatar, AvatarImage, AvatarFallback };

webui2/src/components/ui/badge.tsx πŸ”—

@@ -1,30 +1,32 @@
-import * as React from 'react'
-import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '@/lib/utils'
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
 
 const badgeVariants = cva(
-  'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+  "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
   {
     variants: {
       variant: {
-        default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
-        secondary: 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
-        destructive: 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
-        outline: 'text-foreground',
+        default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+        secondary:
+          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+        destructive:
+          "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+        outline: "text-foreground",
       },
     },
     defaultVariants: {
-      variant: 'default',
+      variant: "default",
     },
   },
-)
+);
 
 export interface BadgeProps
-  extends React.HTMLAttributes<HTMLDivElement>,
-    VariantProps<typeof badgeVariants> {}
+  extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
 
 function Badge({ className, variant, ...props }: BadgeProps) {
-  return <div className={cn(badgeVariants({ variant }), className)} {...props} />
+  return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
 }
 
-export { Badge, badgeVariants }
+export { Badge, badgeVariants };

webui2/src/components/ui/button.tsx πŸ”—

@@ -1,52 +1,49 @@
-import * as React from 'react'
-import { Slot } from '@radix-ui/react-slot'
-import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '@/lib/utils'
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
 
 const buttonVariants = cva(
-  'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
+  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
   {
     variants: {
       variant: {
-        default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
-        destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
-        outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
-        secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
-        ghost: 'hover:bg-accent hover:text-accent-foreground',
-        link: 'text-primary underline-offset-4 hover:underline',
+        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+        destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+        outline:
+          "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+        secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+        ghost: "hover:bg-accent hover:text-accent-foreground",
+        link: "text-primary underline-offset-4 hover:underline",
       },
       size: {
-        default: 'h-9 px-4 py-2',
-        sm: 'h-8 rounded-md px-3 text-xs',
-        lg: 'h-10 rounded-md px-8',
-        icon: 'h-9 w-9',
+        default: "h-9 px-4 py-2",
+        sm: "h-8 rounded-md px-3 text-xs",
+        lg: "h-10 rounded-md px-8",
+        icon: "h-9 w-9",
       },
     },
     defaultVariants: {
-      variant: 'default',
-      size: 'default',
+      variant: "default",
+      size: "default",
     },
   },
-)
+);
 
 export interface ButtonProps
-  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
-    VariantProps<typeof buttonVariants> {
-  asChild?: boolean
+  extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
+  asChild?: boolean;
 }
 
 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
   ({ className, variant, size, asChild = false, ...props }, ref) => {
-    const Comp = asChild ? Slot : 'button'
+    const Comp = asChild ? Slot : "button";
     return (
-      <Comp
-        className={cn(buttonVariants({ variant, size, className }))}
-        ref={ref}
-        {...props}
-      />
-    )
+      <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
+    );
   },
-)
-Button.displayName = 'Button'
+);
+Button.displayName = "Button";
 
-export { Button, buttonVariants }
+export { Button, buttonVariants };

webui2/src/components/ui/input.tsx πŸ”—

@@ -1,21 +1,22 @@
-import * as React from 'react'
-import { cn } from '@/lib/utils'
+import * as React from "react";
 
-const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
+import { cn } from "@/lib/utils";
+
+const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
   ({ className, type, ...props }, ref) => {
     return (
       <input
         type={type}
         className={cn(
-          'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
+          "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
           className,
         )}
         ref={ref}
         {...props}
       />
-    )
+    );
   },
-)
-Input.displayName = 'Input'
+);
+Input.displayName = "Input";
 
-export { Input }
+export { Input };

webui2/src/components/ui/popover.tsx πŸ”—

@@ -1,28 +1,29 @@
-import * as React from 'react'
-import * as PopoverPrimitive from '@radix-ui/react-popover'
-import { cn } from '@/lib/utils'
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+import * as React from "react";
 
-const Popover = PopoverPrimitive.Root
-const PopoverTrigger = PopoverPrimitive.Trigger
-const PopoverAnchor = PopoverPrimitive.Anchor
+import { cn } from "@/lib/utils";
+
+const Popover = PopoverPrimitive.Root;
+const PopoverTrigger = PopoverPrimitive.Trigger;
+const PopoverAnchor = PopoverPrimitive.Anchor;
 
 const PopoverContent = React.forwardRef<
   React.ElementRef<typeof PopoverPrimitive.Content>,
   React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
->(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
   <PopoverPrimitive.Portal>
     <PopoverPrimitive.Content
       ref={ref}
       align={align}
       sideOffset={sideOffset}
       className={cn(
-        'z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
+        "z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
         className,
       )}
       {...props}
     />
   </PopoverPrimitive.Portal>
-))
-PopoverContent.displayName = PopoverPrimitive.Content.displayName
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
 
-export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent }
+export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };

webui2/src/components/ui/separator.tsx πŸ”—

@@ -1,23 +1,24 @@
-import * as React from 'react'
-import * as SeparatorPrimitive from '@radix-ui/react-separator'
-import { cn } from '@/lib/utils'
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
 
 const Separator = React.forwardRef<
   React.ElementRef<typeof SeparatorPrimitive.Root>,
   React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
->(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
+>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
   <SeparatorPrimitive.Root
     ref={ref}
     decorative={decorative}
     orientation={orientation}
     className={cn(
-      'shrink-0 bg-border',
-      orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
+      "shrink-0 bg-border",
+      orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
       className,
     )}
     {...props}
   />
-))
-Separator.displayName = SeparatorPrimitive.Root.displayName
+));
+Separator.displayName = SeparatorPrimitive.Root.displayName;
 
-export { Separator }
+export { Separator };

webui2/src/components/ui/skeleton.tsx πŸ”—

@@ -1,12 +1,7 @@
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils";
 
 function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
-  return (
-    <div
-      className={cn('animate-pulse rounded-md bg-primary/10', className)}
-      {...props}
-    />
-  )
+  return <div className={cn("animate-pulse rounded-md bg-primary/10", className)} {...props} />;
 }
 
-export { Skeleton }
+export { Skeleton };

webui2/src/components/ui/textarea.tsx πŸ”—

@@ -1,20 +1,21 @@
-import * as React from 'react'
-import { cn } from '@/lib/utils'
+import * as React from "react";
 
-const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<'textarea'>>(
+import { cn } from "@/lib/utils";
+
+const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
   ({ className, ...props }, ref) => {
     return (
       <textarea
         className={cn(
-          'flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
+          "flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
           className,
         )}
         ref={ref}
         {...props}
       />
-    )
+    );
   },
-)
-Textarea.displayName = 'Textarea'
+);
+Textarea.displayName = "Textarea";
 
-export { Textarea }
+export { Textarea };

webui2/src/index.css πŸ”—

@@ -1,5 +1,5 @@
 /* highlight.js theme must be imported before any @layer rules (PostCSS requirement) */
-@import 'highlight.js/styles/github.css';
+@import "highlight.js/styles/github.css";
 
 @tailwind base;
 @tailwind components;
@@ -71,12 +71,24 @@
 }
 .dark .hljs-keyword,
 .dark .hljs-selector-tag,
-.dark .hljs-built_in { color: #ff7b72; }
+.dark .hljs-built_in {
+  color: #ff7b72;
+}
 .dark .hljs-string,
-.dark .hljs-attr { color: #a5d6ff; }
-.dark .hljs-comment { color: hsl(220, 8%, 50%); }
+.dark .hljs-attr {
+  color: #a5d6ff;
+}
+.dark .hljs-comment {
+  color: hsl(220, 8%, 50%);
+}
 .dark .hljs-number,
-.dark .hljs-literal { color: #79c0ff; }
+.dark .hljs-literal {
+  color: #79c0ff;
+}
 .dark .hljs-title,
-.dark .hljs-name { color: #d2a8ff; }
-.dark .hljs-type { color: #ffa657; }
+.dark .hljs-name {
+  color: #d2a8ff;
+}
+.dark .hljs-type {
+  color: #ffa657;
+}

webui2/src/lib/apollo.ts πŸ”—

@@ -1,10 +1,10 @@
-import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'
+import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
 
 const httpLink = createHttpLink({
-  uri: '/graphql',
+  uri: "/graphql",
   // include credentials so future httpOnly auth cookies are sent automatically
-  credentials: 'include',
-})
+  credentials: "include",
+});
 
 export const client = new ApolloClient({
   link: httpLink,
@@ -16,4 +16,4 @@ export const client = new ApolloClient({
       },
     },
   }),
-})
+});

webui2/src/lib/auth.tsx πŸ”—

@@ -14,40 +14,41 @@
 // All three modes expose the same AuthContextValue shape, so the rest of the
 // component tree doesn't need to know which mode is active.
 
-import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
-import { gql, useQuery } from '@apollo/client'
-import { useServerConfigQuery } from '@/__generated__/graphql'
+import { gql, useQuery } from "@apollo/client";
+import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
+
+import { useServerConfigQuery } from "@/__generated__/graphql";
 
 // AuthUser matches the Identity type fields we care about for auth purposes.
 export interface AuthUser {
-  id: string
-  humanId: string
-  name: string | null
-  displayName: string
-  avatarUrl: string | null
-  email: string | null
-  login: string | null
+  id: string;
+  humanId: string;
+  name: string | null;
+  displayName: string;
+  avatarUrl: string | null;
+  email: string | null;
+  login: string | null;
 }
 
 // 'local'    β€” single-user mode, identity from git config
 // 'external' β€” multi-user mode, identity from OAuth/OIDC session
 // 'readonly' β€” no identity, write operations disabled
-export type AuthMode = 'local' | 'external' | 'readonly'
+export type AuthMode = "local" | "external" | "readonly";
 
 export interface AuthContextValue {
-  user: AuthUser | null
-  mode: AuthMode
+  user: AuthUser | null;
+  mode: AuthMode;
   // List of enabled login provider names, e.g. ['github']. Only set in external mode.
-  loginProviders: string[]
-  loading: boolean
+  loginProviders: string[];
+  loading: boolean;
 }
 
 const AuthContext = createContext<AuthContextValue>({
   user: null,
-  mode: 'readonly',
+  mode: "readonly",
   loginProviders: [],
   loading: true,
-})
+});
 
 // ── Local mode ────────────────────────────────────────────────────────────────
 
@@ -65,23 +66,23 @@ const USER_IDENTITY_QUERY = gql`
       }
     }
   }
-`
+`;
 
 function LocalAuthProvider({
   children,
   loginProviders,
 }: {
-  children: ReactNode
-  loginProviders: string[]
+  children: ReactNode;
+  loginProviders: string[];
 }) {
-  const { data, loading } = useQuery(USER_IDENTITY_QUERY)
-  const user: AuthUser | null = data?.repository?.userIdentity ?? null
-  const mode: AuthMode = loading ? 'local' : user ? 'local' : 'readonly'
+  const { data, loading } = useQuery(USER_IDENTITY_QUERY);
+  const user: AuthUser | null = data?.repository?.userIdentity ?? null;
+  const mode: AuthMode = loading ? "local" : user ? "local" : "readonly";
   return (
     <AuthContext.Provider value={{ user, mode, loginProviders, loading }}>
       {children}
     </AuthContext.Provider>
-  )
+  );
 }
 
 // ── External (OAuth / OIDC) mode ──────────────────────────────────────────────
@@ -93,31 +94,29 @@ function ExternalAuthProvider({
   children,
   loginProviders,
 }: {
-  children: ReactNode
-  loginProviders: string[]
+  children: ReactNode;
+  loginProviders: string[];
 }) {
-  const [user, setUser] = useState<AuthUser | null>(null)
-  const [loading, setLoading] = useState(true)
+  const [user, setUser] = useState<AuthUser | null>(null);
+  const [loading, setLoading] = useState(true);
 
   useEffect(() => {
-    fetch('/auth/user', { credentials: 'include' })
+    fetch("/auth/user", { credentials: "include" })
       .then((res) => {
-        if (res.status === 401) return null
-        if (!res.ok) throw new Error(`/auth/user returned ${res.status}`)
-        return res.json() as Promise<AuthUser>
+        if (res.status === 401) return null;
+        if (!res.ok) throw new Error(`/auth/user returned ${res.status}`);
+        return res.json() as Promise<AuthUser>;
       })
       .then((u) => setUser(u))
       .catch(() => setUser(null))
-      .finally(() => setLoading(false))
-  }, [])
+      .finally(() => setLoading(false));
+  }, []);
 
   return (
-    <AuthContext.Provider
-      value={{ user, mode: 'external', loginProviders, loading }}
-    >
+    <AuthContext.Provider value={{ user, mode: "external", loginProviders, loading }}>
       {children}
     </AuthContext.Provider>
-  )
+  );
 }
 
 // ── Read-only mode ────────────────────────────────────────────────────────────
@@ -125,11 +124,11 @@ function ExternalAuthProvider({
 function ReadonlyAuthProvider({ children }: { children: ReactNode }) {
   return (
     <AuthContext.Provider
-      value={{ user: null, mode: 'readonly', loginProviders: [], loading: false }}
+      value={{ user: null, mode: "readonly", loginProviders: [], loading: false }}
     >
       {children}
     </AuthContext.Provider>
-  )
+  );
 }
 
 // ── Root provider ─────────────────────────────────────────────────────────────
@@ -137,41 +136,33 @@ function ReadonlyAuthProvider({ children }: { children: ReactNode }) {
 // AuthProvider first fetches serverConfig to learn the auth mode, then renders
 // the appropriate sub-provider. The split avoids conditional hook calls.
 export function AuthProvider({ children }: { children: ReactNode }) {
-  const { data, loading } = useServerConfigQuery()
+  const { data, loading } = useServerConfigQuery();
 
   if (loading || !data) {
     // Keep the default context (readonly + loading:true) while the config loads.
     return (
       <AuthContext.Provider
-        value={{ user: null, mode: 'readonly', loginProviders: [], loading: true }}
+        value={{ user: null, mode: "readonly", loginProviders: [], loading: true }}
       >
         {children}
       </AuthContext.Provider>
-    )
+    );
   }
 
-  const { authMode, loginProviders } = data.serverConfig
+  const { authMode, loginProviders } = data.serverConfig;
 
-  if (authMode === 'readonly') {
-    return <ReadonlyAuthProvider>{children}</ReadonlyAuthProvider>
+  if (authMode === "readonly") {
+    return <ReadonlyAuthProvider>{children}</ReadonlyAuthProvider>;
   }
 
-  if (authMode === 'external') {
-    return (
-      <ExternalAuthProvider loginProviders={loginProviders}>
-        {children}
-      </ExternalAuthProvider>
-    )
+  if (authMode === "external") {
+    return <ExternalAuthProvider loginProviders={loginProviders}>{children}</ExternalAuthProvider>;
   }
 
   // Default: 'local'
-  return (
-    <LocalAuthProvider loginProviders={loginProviders}>
-      {children}
-    </LocalAuthProvider>
-  )
+  return <LocalAuthProvider loginProviders={loginProviders}>{children}</LocalAuthProvider>;
 }
 
 export function useAuth(): AuthContextValue {
-  return useContext(AuthContext)
+  return useContext(AuthContext);
 }

webui2/src/lib/repo.tsx πŸ”—

@@ -6,24 +6,24 @@
 //   - Read the current slug in any child component with useRepo().
 //   - Pass the slug as `ref` to all GraphQL repository queries.
 
-import { createContext, useContext } from 'react'
-import { useParams, Outlet } from 'react-router-dom'
+import { createContext, useContext } from "react";
+import { useParams, Outlet } from "react-router-dom";
 
-const RepoContext = createContext<string | null>(null)
+const RepoContext = createContext<string | null>(null);
 
 // Route element for /:repo routes. Reads the :repo param and provides it
 // via context so any descendant can call useRepo() without prop drilling.
 export function RepoShell() {
-  const { repo } = useParams<{ repo: string }>()
+  const { repo } = useParams<{ repo: string }>();
   return (
     <RepoContext.Provider value={repo ?? null}>
       <Outlet />
     </RepoContext.Provider>
-  )
+  );
 }
 
 // Returns the current repo slug from the nearest RepoShell ancestor.
 // Returns null when rendered outside of a /:repo route (e.g. the picker page).
 export function useRepo(): string | null {
-  return useContext(RepoContext)
+  return useContext(RepoContext);
 }

webui2/src/lib/theme.tsx πŸ”—

@@ -1,38 +1,38 @@
-import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
+import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
 
-type Theme = 'light' | 'dark'
+type Theme = "light" | "dark";
 
 interface ThemeContextValue {
-  theme: Theme
-  toggle: () => void
+  theme: Theme;
+  toggle: () => void;
 }
 
 const ThemeContext = createContext<ThemeContextValue>({
-  theme: 'light',
+  theme: "light",
   toggle: () => {},
-})
+});
 
 function getInitialTheme(): Theme {
-  const stored = localStorage.getItem('theme')
-  if (stored === 'light' || stored === 'dark') return stored
-  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+  const stored = localStorage.getItem("theme");
+  if (stored === "light" || stored === "dark") return stored;
+  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
 }
 
 export function ThemeProvider({ children }: { children: ReactNode }) {
-  const [theme, setTheme] = useState<Theme>(getInitialTheme)
+  const [theme, setTheme] = useState<Theme>(getInitialTheme);
 
   useEffect(() => {
-    document.documentElement.classList.toggle('dark', theme === 'dark')
-    localStorage.setItem('theme', theme)
-  }, [theme])
+    document.documentElement.classList.toggle("dark", theme === "dark");
+    localStorage.setItem("theme", theme);
+  }, [theme]);
 
   function toggle() {
-    setTheme((t) => (t === 'light' ? 'dark' : 'light'))
+    setTheme((t) => (t === "light" ? "dark" : "light"));
   }
 
-  return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>
+  return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>;
 }
 
 export function useTheme() {
-  return useContext(ThemeContext)
+  return useContext(ThemeContext);
 }

webui2/src/lib/utils.ts πŸ”—

@@ -1,6 +1,6 @@
-import { type ClassValue, clsx } from 'clsx'
-import { twMerge } from 'tailwind-merge'
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
 
 export function cn(...inputs: ClassValue[]) {
-  return twMerge(clsx(inputs))
+  return twMerge(clsx(inputs));
 }

webui2/src/main.tsx πŸ”—

@@ -1,13 +1,15 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import { ApolloProvider } from '@apollo/client'
-import './index.css'
-import { client } from '@/lib/apollo'
-import { AuthProvider } from '@/lib/auth'
-import { ThemeProvider } from '@/lib/theme'
-import { App } from './App'
+import { ApolloProvider } from "@apollo/client";
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
 
-createRoot(document.getElementById('root')!).render(
+import "./index.css";
+import { client } from "@/lib/apollo";
+import { AuthProvider } from "@/lib/auth";
+import { ThemeProvider } from "@/lib/theme";
+
+import { App } from "./App";
+
+createRoot(document.getElementById("root")!).render(
   <StrictMode>
     <ThemeProvider>
       <ApolloProvider client={client}>
@@ -17,4 +19,4 @@ createRoot(document.getElementById('root')!).render(
       </ApolloProvider>
     </ThemeProvider>
   </StrictMode>,
-)
+);

webui2/src/pages/BugDetailPage.tsx πŸ”—

@@ -1,47 +1,46 @@
-import { useParams, Link } from 'react-router-dom'
-import { ArrowLeft } from 'lucide-react'
-import { formatDistanceToNow } from 'date-fns'
-import { Skeleton } from '@/components/ui/skeleton'
-import { Separator } from '@/components/ui/separator'
-import { StatusBadge } from '@/components/bugs/StatusBadge'
-import { LabelEditor } from '@/components/bugs/LabelEditor'
-import { TitleEditor } from '@/components/bugs/TitleEditor'
-import { Timeline } from '@/components/bugs/Timeline'
-import { CommentBox } from '@/components/bugs/CommentBox'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { useBugDetailQuery } from '@/__generated__/graphql'
-import { useRepo } from '@/lib/repo'
+import { formatDistanceToNow } from "date-fns";
+import { ArrowLeft } from "lucide-react";
+import { useParams, Link } from "react-router-dom";
+
+import { useBugDetailQuery } from "@/__generated__/graphql";
+import { CommentBox } from "@/components/bugs/CommentBox";
+import { LabelEditor } from "@/components/bugs/LabelEditor";
+import { StatusBadge } from "@/components/bugs/StatusBadge";
+import { Timeline } from "@/components/bugs/Timeline";
+import { TitleEditor } from "@/components/bugs/TitleEditor";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useRepo } from "@/lib/repo";
 
 // Issue detail page (/:repo/issues/:id). Shows title, status, timeline of
 // comments and events, and a sidebar with labels and participants.
 export function BugDetailPage() {
-  const { id } = useParams<{ id: string }>()
-  const repo = useRepo()
+  const { id } = useParams<{ id: string }>();
+  const repo = useRepo();
   const { data, loading, error } = useBugDetailQuery({
     variables: { ref: repo, prefix: id! },
-  })
+  });
 
   if (error) {
     return (
       <div className="py-16 text-center text-sm text-destructive">
         Failed to load issue: {error.message}
       </div>
-    )
+    );
   }
 
   if (loading && !data) {
-    return <BugDetailSkeleton />
+    return <BugDetailSkeleton />;
   }
 
-  const bug = data?.repository?.bug
+  const bug = data?.repository?.bug;
   if (!bug) {
-    return (
-      <div className="py-16 text-center text-sm text-muted-foreground">Issue not found.</div>
-    )
+    return <div className="py-16 text-center text-sm text-muted-foreground">Issue not found.</div>;
   }
 
-  const issuesHref = repo ? `/${repo}/issues` : '/issues'
-  const authorHref = repo ? `/${repo}/user/${bug.author.humanId}` : `/user/${bug.author.humanId}`
+  const issuesHref = repo ? `/${repo}/issues` : "/issues";
+  const authorHref = repo ? `/${repo}/user/${bug.author.humanId}` : `/user/${bug.author.humanId}`;
 
   return (
     <div>
@@ -63,7 +62,7 @@ export function BugDetailPage() {
         <span>
           <Link to={authorHref} className="font-medium text-foreground hover:underline">
             {bug.author.displayName}
-          </Link>{' '}
+          </Link>{" "}
           opened this issue {formatDistanceToNow(new Date(bug.createdAt), { addSuffix: true })}
         </span>
       </div>
@@ -89,7 +88,7 @@ export function BugDetailPage() {
             </h3>
             <div className="flex flex-wrap gap-1.5">
               {bug.participants.nodes.map((p) => {
-                const participantHref = repo ? `/${repo}/user/${p.humanId}` : `/user/${p.humanId}`
+                const participantHref = repo ? `/${repo}/user/${p.humanId}` : `/user/${p.humanId}`;
                 return (
                   <Link key={p.id} to={participantHref} title={p.displayName}>
                     <Avatar className="size-6">
@@ -99,14 +98,14 @@ export function BugDetailPage() {
                       </AvatarFallback>
                     </Avatar>
                   </Link>
-                )
+                );
               })}
             </div>
           </div>
         </aside>
       </div>
     </div>
-  )
+  );
 }
 
 function BugDetailSkeleton() {
@@ -130,5 +129,5 @@ function BugDetailSkeleton() {
         </div>
       </div>
     </div>
-  )
+  );
 }

webui2/src/pages/BugListPage.tsx πŸ”—

@@ -1,59 +1,75 @@
-import { useState, useEffect } from 'react'
-import { CircleDot, CircleCheck, ChevronLeft, ChevronRight } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { Skeleton } from '@/components/ui/skeleton'
-import { BugRow } from '@/components/bugs/BugRow'
-import { IssueFilters } from '@/components/bugs/IssueFilters'
-import type { SortValue } from '@/components/bugs/IssueFilters'
-import { QueryInput } from '@/components/bugs/QueryInput'
-import { useBugListQuery } from '@/__generated__/graphql'
-import { cn } from '@/lib/utils'
-import { useRepo } from '@/lib/repo'
+import { CircleDot, CircleCheck, ChevronLeft, ChevronRight } from "lucide-react";
+import { useState, useEffect } from "react";
 
-const PAGE_SIZE = 25
+import { useBugListQuery } from "@/__generated__/graphql";
+import { BugRow } from "@/components/bugs/BugRow";
+import { IssueFilters } from "@/components/bugs/IssueFilters";
+import type { SortValue } from "@/components/bugs/IssueFilters";
+import { QueryInput } from "@/components/bugs/QueryInput";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useRepo } from "@/lib/repo";
+import { cn } from "@/lib/utils";
 
-type StatusFilter = 'open' | 'closed'
+const PAGE_SIZE = 25;
+
+type StatusFilter = "open" | "closed";
 
 // Issue list page (/:repo/issues). Search bar with structured query, open/closed toggle,
 // label+author filter dropdowns, and paginated bug rows.
 export function BugListPage() {
-  const repo = useRepo()
-  const [statusFilter, setStatusFilter] = useState<StatusFilter>('open')
-  const [selectedLabels, setSelectedLabels] = useState<string[]>([])
+  const repo = useRepo();
+  const [statusFilter, setStatusFilter] = useState<StatusFilter>("open");
+  const [selectedLabels, setSelectedLabels] = useState<string[]>([]);
   // humanId β€” uniquely identifies the selection for the dropdown UI
-  const [selectedAuthorId, setSelectedAuthorId] = useState<string | null>(null)
+  const [selectedAuthorId, setSelectedAuthorId] = useState<string | null>(null);
   // query value (login/name) β€” what goes into author:... in the query string
-  const [selectedAuthorQuery, setSelectedAuthorQuery] = useState<string | null>(null)
-  const [freeText, setFreeText] = useState('')
-  const [sort, setSort] = useState<SortValue>('creation-desc')
-  const [draft, setDraft] = useState(() => buildQueryString('open', [], null, '', 'creation-desc'))
+  const [selectedAuthorQuery, setSelectedAuthorQuery] = useState<string | null>(null);
+  const [freeText, setFreeText] = useState("");
+  const [sort, setSort] = useState<SortValue>("creation-desc");
+  const [draft, setDraft] = useState(() => buildQueryString("open", [], null, "", "creation-desc"));
 
   // Cursor-stack pagination: cursors[i] is the `after` value to fetch page i.
   // cursors[0] is always undefined (first page needs no cursor).
-  const [cursors, setCursors] = useState<(string | undefined)[]>([undefined])
-  const page = cursors.length - 1  // 0-indexed current page
+  const [cursors, setCursors] = useState<(string | undefined)[]>([undefined]);
+  const page = cursors.length - 1; // 0-indexed current page
 
   // Build separate query strings: two for the always-visible counts (open/closed),
   // one for the paginated list. The count queries share all filters except status.
-  const baseQuery = buildBaseQuery(selectedLabels, selectedAuthorQuery, freeText)
-  const openQuery = `status:open ${baseQuery}`.trim()
-  const closedQuery = `status:closed ${baseQuery}`.trim()
-  const listQuery = buildQueryString(statusFilter, selectedLabels, selectedAuthorQuery, freeText, sort)
+  const baseQuery = buildBaseQuery(selectedLabels, selectedAuthorQuery, freeText);
+  const openQuery = `status:open ${baseQuery}`.trim();
+  const closedQuery = `status:closed ${baseQuery}`.trim();
+  const listQuery = buildQueryString(
+    statusFilter,
+    selectedLabels,
+    selectedAuthorQuery,
+    freeText,
+    sort,
+  );
 
   const { data, loading, error } = useBugListQuery({
-    variables: { ref: repo, openQuery, closedQuery, listQuery, first: PAGE_SIZE, after: cursors[page] },
-  })
+    variables: {
+      ref: repo,
+      openQuery,
+      closedQuery,
+      listQuery,
+      first: PAGE_SIZE,
+      after: cursors[page],
+    },
+  });
 
-  const openCount = data?.repository?.openCount.totalCount ?? 0
-  const closedCount = data?.repository?.closedCount.totalCount ?? 0
-  const bugs = data?.repository?.bugs
-  const totalCount = bugs?.totalCount ?? 0
-  const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE))
-  const hasNext = bugs?.pageInfo.hasNextPage ?? false
-  const hasPrev = page > 0
+  const openCount = data?.repository?.openCount.totalCount ?? 0;
+  const closedCount = data?.repository?.closedCount.totalCount ?? 0;
+  const bugs = data?.repository?.bugs;
+  const totalCount = bugs?.totalCount ?? 0;
+  const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
+  const hasNext = bugs?.pageInfo.hasNextPage ?? false;
+  const hasPrev = page > 0;
 
   // Reset to page 1 whenever the list query changes.
-  useEffect(() => { setCursors([undefined]) }, [listQuery])
+  useEffect(() => {
+    setCursors([undefined]);
+  }, [listQuery]);
 
   // Apply all filters at once, keeping draft in sync with the structured state.
   function applyFilters(
@@ -64,13 +80,13 @@ export function BugListPage() {
     text: string,
     sortVal: SortValue = sort,
   ) {
-    setStatusFilter(status)
-    setSelectedLabels(labels)
-    setSelectedAuthorId(authorId)
-    setSelectedAuthorQuery(authorQuery)
-    setFreeText(text)
-    setSort(sortVal)
-    setDraft(buildQueryString(status, labels, authorQuery, text, sortVal))
+    setStatusFilter(status);
+    setSelectedLabels(labels);
+    setSelectedAuthorId(authorId);
+    setSelectedAuthorQuery(authorQuery);
+    setFreeText(text);
+    setSort(sortVal);
+    setDraft(buildQueryString(status, labels, authorQuery, text, sortVal));
   }
 
   // Parse the draft text box on submit so manual edits update the dropdowns too.
@@ -78,19 +94,19 @@ export function BugListPage() {
   // Called both from the <form> onSubmit (with event) and from QueryInput's
   // Enter-key handler (without event), so e is optional.
   function handleSearch(e?: React.FormEvent) {
-    e?.preventDefault()
-    const p = parseQueryString(draft)
-    applyFilters(p.status, p.labels, null, p.author, p.freeText, p.sort)
+    e?.preventDefault();
+    const p = parseQueryString(draft);
+    applyFilters(p.status, p.labels, null, p.author, p.freeText, p.sort);
   }
 
   function goNext() {
-    const endCursor = bugs?.pageInfo.endCursor
-    if (!endCursor) return
-    setCursors((prev) => [...prev, endCursor])
+    const endCursor = bugs?.pageInfo.endCursor;
+    if (!endCursor) return;
+    setCursors((prev) => [...prev, endCursor]);
   }
 
   function goPrev() {
-    setCursors((prev) => prev.slice(0, -1))
+    setCursors((prev) => prev.slice(0, -1));
   }
 
   return (
@@ -103,9 +119,7 @@ export function BugListPage() {
           onSubmit={handleSearch}
           placeholder="status:open author:… label:…"
         />
-        <Button type="submit">
-          Search
-        </Button>
+        <Button type="submit">Search</Button>
       </form>
 
       {/* List container */}
@@ -114,33 +128,59 @@ export function BugListPage() {
         <div className="flex items-center gap-2 overflow-x-auto border-b border-border px-4 py-2">
           <div className="flex shrink-0 items-center gap-1">
             <button
-              onClick={() => applyFilters('open', selectedLabels, selectedAuthorId, selectedAuthorQuery, freeText)}
+              onClick={() =>
+                applyFilters(
+                  "open",
+                  selectedLabels,
+                  selectedAuthorId,
+                  selectedAuthorQuery,
+                  freeText,
+                )
+              }
               className={cn(
-                'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
-                statusFilter === 'open'
-                  ? 'bg-accent text-accent-foreground'
-                  : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
+                "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+                statusFilter === "open"
+                  ? "bg-accent text-accent-foreground"
+                  : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
               )}
             >
-              <CircleDot className={cn('size-4', statusFilter === 'open' && 'text-green-600 dark:text-green-400')} />
+              <CircleDot
+                className={cn(
+                  "size-4",
+                  statusFilter === "open" && "text-green-600 dark:text-green-400",
+                )}
+              />
               Open
-              <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs leading-none tabular-nums">
+              <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs tabular-nums leading-none">
                 {openCount}
               </span>
             </button>
 
             <button
-              onClick={() => applyFilters('closed', selectedLabels, selectedAuthorId, selectedAuthorQuery, freeText)}
+              onClick={() =>
+                applyFilters(
+                  "closed",
+                  selectedLabels,
+                  selectedAuthorId,
+                  selectedAuthorQuery,
+                  freeText,
+                )
+              }
               className={cn(
-                'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
-                statusFilter === 'closed'
-                  ? 'bg-accent text-accent-foreground'
-                  : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
+                "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+                statusFilter === "closed"
+                  ? "bg-accent text-accent-foreground"
+                  : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
               )}
             >
-              <CircleCheck className={cn('size-4', statusFilter === 'closed' && 'text-purple-600 dark:text-purple-400')} />
+              <CircleCheck
+                className={cn(
+                  "size-4",
+                  statusFilter === "closed" && "text-purple-600 dark:text-purple-400",
+                )}
+              />
               Closed
-              <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs leading-none tabular-nums">
+              <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs tabular-nums leading-none">
                 {closedCount}
               </span>
             </button>
@@ -149,12 +189,25 @@ export function BugListPage() {
           <div className="ml-auto">
             <IssueFilters
               selectedLabels={selectedLabels}
-              onLabelsChange={(labels) => applyFilters(statusFilter, labels, selectedAuthorId, selectedAuthorQuery, freeText)}
+              onLabelsChange={(labels) =>
+                applyFilters(statusFilter, labels, selectedAuthorId, selectedAuthorQuery, freeText)
+              }
               selectedAuthorId={selectedAuthorId}
-              onAuthorChange={(id, qv) => applyFilters(statusFilter, selectedLabels, id, qv, freeText)}
+              onAuthorChange={(id, qv) =>
+                applyFilters(statusFilter, selectedLabels, id, qv, freeText)
+              }
               recentAuthorIds={bugs?.nodes?.map((b) => b.author.humanId) ?? []}
               sort={sort}
-              onSortChange={(s) => applyFilters(statusFilter, selectedLabels, selectedAuthorId, selectedAuthorQuery, freeText, s)}
+              onSortChange={(s) =>
+                applyFilters(
+                  statusFilter,
+                  selectedLabels,
+                  selectedAuthorId,
+                  selectedAuthorQuery,
+                  freeText,
+                  s,
+                )
+              }
             />
           </div>
         </div>
@@ -188,7 +241,13 @@ export function BugListPage() {
             repo={repo}
             onLabelClick={(name) => {
               if (!selectedLabels.includes(name)) {
-                applyFilters(statusFilter, [...selectedLabels, name], selectedAuthorId, selectedAuthorQuery, freeText)
+                applyFilters(
+                  statusFilter,
+                  [...selectedLabels, name],
+                  selectedAuthorId,
+                  selectedAuthorQuery,
+                  freeText,
+                );
               }
             }}
           />
@@ -223,21 +282,21 @@ export function BugListPage() {
         )}
       </div>
     </div>
-  )
+  );
 }
 
 // buildBaseQuery returns the filter parts (labels, author, freeText) without
 // the status prefix, so it can be combined with "status:open" / "status:closed".
 function buildBaseQuery(labels: string[], author: string | null, freeText: string): string {
-  const parts: string[] = []
+  const parts: string[] = [];
   for (const label of labels) {
-    parts.push(label.includes(' ') ? `label:"${label}"` : `label:${label}`)
+    parts.push(label.includes(" ") ? `label:"${label}"` : `label:${label}`);
   }
   if (author) {
-    parts.push(author.includes(' ') ? `author:"${author}"` : `author:${author}`)
+    parts.push(author.includes(" ") ? `author:"${author}"` : `author:${author}`);
   }
-  if (freeText.trim()) parts.push(freeText.trim())
-  return parts.join(' ')
+  if (freeText.trim()) parts.push(freeText.trim());
+  return parts.join(" ");
 }
 
 // Build the structured query string sent to the GraphQL allBugs(query:) argument.
@@ -248,63 +307,68 @@ function buildQueryString(
   labels: string[],
   author: string | null,
   freeText: string,
-  sort: SortValue = 'creation-desc',
+  sort: SortValue = "creation-desc",
 ): string {
-  const parts = [`status:${status}`]
-  const base = buildBaseQuery(labels, author, freeText)
-  if (base) parts.push(base)
-  if (sort !== 'creation-desc') parts.push(`sort:${sort}`)
-  return parts.join(' ')
+  const parts = [`status:${status}`];
+  const base = buildBaseQuery(labels, author, freeText);
+  if (base) parts.push(base);
+  if (sort !== "creation-desc") parts.push(`sort:${sort}`);
+  return parts.join(" ");
 }
 
 // Tokenize a query string, keeping quoted spans (e.g. author:"RenΓ© Descartes")
 // as single tokens. Quotes are preserved in the output so callers can strip them
 // when extracting values.
 function tokenizeQuery(input: string): string[] {
-  const tokens: string[] = []
-  let current = ''
-  let inQuote = false
+  const tokens: string[] = [];
+  let current = "";
+  let inQuote = false;
   for (const ch of input.trim()) {
-    if (ch === '"') { inQuote = !inQuote; current += ch }
-    else if (ch === ' ' && !inQuote) { if (current) { tokens.push(current); current = '' } }
-    else current += ch
+    if (ch === '"') {
+      inQuote = !inQuote;
+      current += ch;
+    } else if (ch === " " && !inQuote) {
+      if (current) {
+        tokens.push(current);
+        current = "";
+      }
+    } else current += ch;
   }
-  if (current) tokens.push(current)
-  return tokens
+  if (current) tokens.push(current);
+  return tokens;
 }
 
 // Parse a free-text query string back into structured filter state so that
 // manual edits to the search box are reflected in the dropdown UI on submit.
 // Strips surrounding quotes from values (they're an encoding detail, not part
 // of the value itself). Unknown tokens fall through to freeText.
-const VALID_SORTS = new Set<SortValue>(['creation-desc', 'creation-asc', 'edit-desc', 'edit-asc'])
+const VALID_SORTS = new Set<SortValue>(["creation-desc", "creation-asc", "edit-desc", "edit-asc"]);
 
 function parseQueryString(input: string): {
-  status: StatusFilter
-  labels: string[]
-  author: string | null
-  freeText: string
-  sort: SortValue
+  status: StatusFilter;
+  labels: string[];
+  author: string | null;
+  freeText: string;
+  sort: SortValue;
 } {
-  let status: StatusFilter = 'open'
-  const labels: string[] = []
-  let author: string | null = null
-  let sort: SortValue = 'creation-desc'
-  const free: string[] = []
+  let status: StatusFilter = "open";
+  const labels: string[] = [];
+  let author: string | null = null;
+  let sort: SortValue = "creation-desc";
+  const free: string[] = [];
 
   for (const token of tokenizeQuery(input)) {
-    if (token === 'status:open') status = 'open'
-    else if (token === 'status:closed') status = 'closed'
-    else if (token.startsWith('label:')) labels.push(token.slice(6))
-    else if (token.startsWith('author:')) author = token.slice(7).replace(/^"|"$/g, '')
-    else if (token.startsWith('sort:')) {
-      const v = token.slice(5) as SortValue
-      if (VALID_SORTS.has(v)) sort = v
-    }
-    else free.push(token)
+    if (token === "status:open") status = "open";
+    else if (token === "status:closed") status = "closed";
+    else if (token.startsWith("label:")) labels.push(token.slice(6));
+    else if (token.startsWith("author:")) author = token.slice(7).replace(/^"|"$/g, "");
+    else if (token.startsWith("sort:")) {
+      const v = token.slice(5) as SortValue;
+      if (VALID_SORTS.has(v)) sort = v;
+    } else free.push(token);
   }
 
-  return { status, labels, author, freeText: free.join(' '), sort }
+  return { status, labels, author, freeText: free.join(" "), sort };
 }
 
 function BugListSkeleton() {
@@ -320,5 +384,5 @@ function BugListSkeleton() {
         </div>
       ))}
     </div>
-  )
+  );
 }

webui2/src/pages/CodePage.tsx πŸ”—

@@ -1,21 +1,22 @@
 // Code browser page. Switches between tree view, file viewer, and commit
 // history via ?type= search param. Ref is selected via ?ref=.
 
-import { useEffect } from 'react'
-import { useSearchParams } from 'react-router-dom'
-import { gql, useQuery } from '@apollo/client'
-import { AlertCircle, GitCommit } from 'lucide-react'
-import { CodeBreadcrumb } from '@/components/code/CodeBreadcrumb'
-import { RefSelector } from '@/components/code/RefSelector'
-import { FileTree } from '@/components/code/FileTree'
-import { FileViewer } from '@/components/code/FileViewer'
-import { CommitList } from '@/components/code/CommitList'
-import { Skeleton } from '@/components/ui/skeleton'
-import { Button } from '@/components/ui/button'
-import { useRepo } from '@/lib/repo'
-import { Markdown } from '@/components/content/Markdown'
-import type { GitRef, GitTreeEntry, GitBlob, GitLastCommit } from '@/__generated__/graphql'
-import type { TreeEntryWithCommit } from '@/components/code/FileTree'
+import { gql, useQuery } from "@apollo/client";
+import { AlertCircle, GitCommit } from "lucide-react";
+import { useEffect } from "react";
+import { useSearchParams } from "react-router-dom";
+
+import type { GitRef, GitTreeEntry, GitBlob, 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 { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useRepo } from "@/lib/repo";
 
 const REFS_QUERY = gql`
   query CodePageRefs($repo: String) {
@@ -32,7 +33,7 @@ const REFS_QUERY = gql`
       }
     }
   }
-`
+`;
 
 const TREE_QUERY = gql`
   query CodePageTree($repo: String, $ref: String!, $path: String) {
@@ -44,7 +45,7 @@ const TREE_QUERY = gql`
       }
     }
   }
-`
+`;
 
 const LAST_COMMITS_QUERY = gql`
   query CodePageLastCommits($repo: String, $ref: String!, $path: String, $names: [String!]!) {
@@ -60,7 +61,7 @@ const LAST_COMMITS_QUERY = gql`
       }
     }
   }
-`
+`;
 
 const BLOB_QUERY = gql`
   query CodePageBlob($repo: String, $ref: String!, $path: String!) {
@@ -75,96 +76,112 @@ const BLOB_QUERY = gql`
       }
     }
   }
-`
+`;
 
-type ViewMode = 'tree' | 'blob' | 'commits'
+type ViewMode = "tree" | "blob" | "commits";
 
 export function CodePage() {
-  const repo = useRepo()
-  const [searchParams, setSearchParams] = useSearchParams()
+  const repo = useRepo();
+  const [searchParams, setSearchParams] = useSearchParams();
 
-  const currentRef = searchParams.get('ref') ?? ''
-  const currentPath = searchParams.get('path') ?? ''
-  const viewMode: ViewMode = (searchParams.get('type') as ViewMode) ?? 'tree'
+  const currentRef = searchParams.get("ref") ?? "";
+  const currentPath = searchParams.get("path") ?? "";
+  const viewMode: ViewMode = (searchParams.get("type") as ViewMode) ?? "tree";
 
-  const { data: refsData, loading: refsLoading, error: refsError } = useQuery(REFS_QUERY, {
+  const {
+    data: refsData,
+    loading: refsLoading,
+    error: refsError,
+  } = useQuery(REFS_QUERY, {
     variables: { repo },
-  })
-  const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? []
+  });
+  const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? [];
 
   // Set default ref from query result once loaded
   useEffect(() => {
-    if (refsLoading || refs.length === 0 || searchParams.get('ref')) return
-    const defaultRef = refs.find((r: GitRef) => r.isDefault) ?? refs[0]
+    if (refsLoading || refs.length === 0 || searchParams.get("ref")) return;
+    const defaultRef = refs.find((r: GitRef) => r.isDefault) ?? refs[0];
     if (defaultRef) {
       setSearchParams(
-        (prev) => { prev.set('ref', defaultRef.shortName); return prev },
+        (prev) => {
+          prev.set("ref", defaultRef.shortName);
+          return prev;
+        },
         { replace: true },
-      )
+      );
     }
-  }, [refsLoading, refs.length]) // eslint-disable-line react-hooks/exhaustive-deps
+  }, [refsLoading, refs.length]); // eslint-disable-line react-hooks/exhaustive-deps
 
-  const inTreeMode = viewMode === 'tree' && !!currentRef
-  const inBlobMode = viewMode === 'blob' && !!currentRef && !!currentPath
+  const inTreeMode = viewMode === "tree" && !!currentRef;
+  const inBlobMode = viewMode === "blob" && !!currentRef && !!currentPath;
 
   const { data: treeData, loading: treeLoading } = useQuery(TREE_QUERY, {
     variables: { repo, ref: currentRef, path: currentPath || null },
     skip: !inTreeMode,
-  })
-  const entries: GitTreeEntry[] = treeData?.repository?.tree ?? []
+  });
+  const entries: GitTreeEntry[] = treeData?.repository?.tree ?? [];
 
-  const entryNames = entries.map((e: GitTreeEntry) => e.name)
+  const entryNames = entries.map((e: GitTreeEntry) => e.name);
   const { data: lastCommitsData } = useQuery(LAST_COMMITS_QUERY, {
     variables: { repo, 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(BLOB_QUERY, {
     variables: { repo, ref: currentRef, path: currentPath },
     skip: !inBlobMode,
-  })
-  const blob: GitBlob | null = blobData?.repository?.blob ?? null
+  });
+  const blob: GitBlob | null = blobData?.repository?.blob ?? null;
 
   const readmeEntry = entries.find(
-    (e: GitTreeEntry) => e.type === 'BLOB' && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name),
-  )
+    (e: GitTreeEntry) => e.type === "BLOB" && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name),
+  );
   const readmePath = readmeEntry
-    ? (currentPath ? `${currentPath}/${readmeEntry.name}` : readmeEntry.name)
-    : null
+    ? currentPath
+      ? `${currentPath}/${readmeEntry.name}`
+      : readmeEntry.name
+    : null;
   const { data: readmeBlobData } = useQuery(BLOB_QUERY, {
     variables: { repo, ref: currentRef, path: readmePath },
     skip: !inTreeMode || !readmePath,
-  })
-  const readme: string | null = readmeBlobData?.repository?.blob?.text ?? null
+  });
+  const readme: string | null = readmeBlobData?.repository?.blob?.text ?? null;
 
-  const repoName = refsData?.repository?.name ?? repo ?? 'default-repo'
+  const repoName = refsData?.repository?.name ?? repo ?? "default-repo";
 
-  function navigate(path: string, type: ViewMode = 'tree') {
-    setSearchParams((prev) => { prev.set('path', path); prev.set('type', type); return prev })
+  function navigate(path: string, type: ViewMode = "tree") {
+    setSearchParams((prev) => {
+      prev.set("path", path);
+      prev.set("type", type);
+      return prev;
+    });
   }
 
   function handleEntryClick(entry: TreeEntryWithCommit) {
-    const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name
-    navigate(newPath, entry.type === 'BLOB' ? 'blob' : 'tree')
+    const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
+    navigate(newPath, entry.type === "BLOB" ? "blob" : "tree");
   }
 
   function handleNavigateUp() {
-    const parts = currentPath.split('/').filter(Boolean)
-    parts.pop()
-    navigate(parts.join('/'), 'tree')
+    const parts = currentPath.split("/").filter(Boolean);
+    parts.pop();
+    navigate(parts.join("/"), "tree");
   }
 
   function handleRefSelect(ref: GitRef) {
     setSearchParams((prev) => {
-      prev.set('ref', ref.shortName); prev.set('path', ''); prev.set('type', 'tree'); return prev
-    })
+      prev.set("ref", ref.shortName);
+      prev.set("path", "");
+      prev.set("type", "tree");
+      return prev;
+    });
   }
 
   if (refsError) {
@@ -174,7 +191,7 @@ export function CodePage() {
         <p className="text-sm font-medium">Code browser unavailable</p>
         <p className="max-w-sm text-xs text-muted-foreground">{refsError.message}</p>
       </div>
-    )
+    );
   }
 
   return (
@@ -187,15 +204,15 @@ export function CodePage() {
             repoName={repoName}
             ref={currentRef}
             path={currentPath}
-            onNavigate={(p) => navigate(p, 'tree')}
+            onNavigate={(p) => navigate(p, "tree")}
           />
         )}
         <div className="flex items-center gap-2">
           {!refsLoading && (
             <Button
-              variant={viewMode === 'commits' ? 'secondary' : 'outline'}
+              variant={viewMode === "commits" ? "secondary" : "outline"}
               size="sm"
-              onClick={() => navigate(currentPath, viewMode === 'commits' ? 'tree' : 'commits')}
+              onClick={() => navigate(currentPath, viewMode === "commits" ? "tree" : "commits")}
             >
               <GitCommit className="size-3.5" />
               History
@@ -209,9 +226,9 @@ export function CodePage() {
         </div>
       </div>
 
-      {viewMode === 'commits' ? (
+      {viewMode === "commits" ? (
         <CommitList ref_={currentRef} path={currentPath || undefined} />
-      ) : viewMode === 'tree' || !blob ? (
+      ) : viewMode === "tree" || !blob ? (
         <>
           <FileTree
             entries={entriesWithCommits}
@@ -235,5 +252,5 @@ export function CodePage() {
         <FileViewer blob={blob} loading={blobLoading} />
       )}
     </div>
-  )
+  );
 }

webui2/src/pages/CommitPage.tsx πŸ”—

@@ -1,13 +1,14 @@
 // Commit detail page (/:repo/commit/:hash). Shows commit metadata, full
 // message, parent links, and changed files with lazy diffs.
 
-import { Link, useParams, useNavigate } from 'react-router-dom'
-import { format } from 'date-fns'
-import { ArrowLeft, GitCommit } from 'lucide-react'
-import { gql, useQuery } from '@apollo/client'
-import { Skeleton } from '@/components/ui/skeleton'
-import { useRepo } from '@/lib/repo'
-import { FileDiffView } from '@/components/code/FileDiffView'
+import { gql, useQuery } from "@apollo/client";
+import { format } from "date-fns";
+import { ArrowLeft, GitCommit } from "lucide-react";
+import { Link, useParams, useNavigate } from "react-router-dom";
+
+import { FileDiffView } from "@/components/code/FileDiffView";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useRepo } from "@/lib/repo";
 
 const COMMIT_QUERY = gql`
   query CommitPageDetail($repo: String, $hash: String!) {
@@ -31,33 +32,33 @@ const COMMIT_QUERY = gql`
       }
     }
   }
-`
+`;
 
 export function CommitPage() {
-  const { hash } = useParams<{ hash: string }>()
-  const navigate = useNavigate()
-  const repo = useRepo()
+  const { hash } = useParams<{ hash: string }>();
+  const navigate = useNavigate();
+  const repo = useRepo();
 
   const { data, loading, error } = useQuery(COMMIT_QUERY, {
     variables: { repo, hash },
     skip: !hash,
-  })
+  });
 
-  if (loading) return <CommitPageSkeleton />
+  if (loading) return <CommitPageSkeleton />;
 
   if (error) {
     return (
       <div className="py-16 text-center text-sm text-destructive">
         Failed to load commit: {error.message}
       </div>
-    )
+    );
   }
 
-  const commit = data?.repository?.commit
-  if (!commit) return null
+  const commit = data?.repository?.commit;
+  if (!commit) return null;
 
-  const date = new Date(commit.date)
-  const files = commit.files?.nodes ?? []
+  const date = new Date(commit.date);
+  const files = commit.files?.nodes ?? [];
 
   return (
     <div>
@@ -75,9 +76,9 @@ export function CommitPage() {
           <h1 className="text-lg font-semibold leading-snug">{commit.message}</h1>
         </div>
 
-        {commit.fullMessage.includes('\n') && (
+        {commit.fullMessage.includes("\n") && (
           <pre className="mb-4 ml-8 mt-3 whitespace-pre-wrap font-sans text-sm text-muted-foreground">
-            {commit.fullMessage.split('\n').slice(1).join('\n').trim()}
+            {commit.fullMessage.split("\n").slice(1).join("\n").trim()}
           </pre>
         )}
 
@@ -86,7 +87,7 @@ export function CommitPage() {
             <span className="font-medium text-foreground">{commit.authorName}</span>
             {commit.authorEmail && <span> &lt;{commit.authorEmail}&gt;</span>}
           </span>
-          <span title={date.toISOString()}>{format(date, 'PPP')}</span>
+          <span title={date.toISOString()}>{format(date, "PPP")}</span>
         </div>
 
         <div className="ml-8 mt-3 flex flex-wrap gap-3 text-xs">
@@ -95,7 +96,7 @@ export function CommitPage() {
           </span>
           {commit.parents.map((p: string) => (
             <span key={p} className="text-muted-foreground">
-              parent{' '}
+              parent{" "}
               <Link
                 to={repo ? `/${repo}/commit/${p}` : `/commit/${p}`}
                 className="font-mono text-foreground hover:underline"
@@ -109,9 +110,9 @@ export function CommitPage() {
 
       <div>
         <h2 className="mb-3 text-sm font-semibold text-muted-foreground">
-          {files.length} file{files.length !== 1 ? 's' : ''} changed
+          {files.length} file{files.length !== 1 ? "s" : ""} changed
         </h2>
-        <div className="overflow-hidden rounded-md border border-border divide-y divide-border">
+        <div className="divide-y divide-border overflow-hidden rounded-md border border-border">
           {files.length === 0 && (
             <p className="px-4 py-4 text-sm text-muted-foreground">No file changes.</p>
           )}
@@ -127,19 +128,19 @@ export function CommitPage() {
         </div>
       </div>
     </div>
-  )
+  );
 }
 
 function CommitPageSkeleton() {
   return (
     <div className="space-y-6">
       <Skeleton className="h-4 w-24" />
-      <div className="rounded-md border border-border p-5 space-y-3">
+      <div className="space-y-3 rounded-md border border-border p-5">
         <Skeleton className="h-6 w-3/4" />
         <Skeleton className="h-4 w-1/3" />
         <Skeleton className="h-3 w-1/2" />
       </div>
-      <div className="rounded-md border border-border divide-y divide-border">
+      <div className="divide-y divide-border rounded-md border border-border">
         {Array.from({ length: 5 }).map((_, i) => (
           <div key={i} className="flex items-center gap-3 px-4 py-2.5">
             <Skeleton className="size-4" />
@@ -148,5 +149,5 @@ function CommitPageSkeleton() {
         ))}
       </div>
     </div>
-  )
+  );
 }

webui2/src/pages/ErrorPage.tsx πŸ”—

@@ -2,35 +2,34 @@
 // or when navigation results in a 404. Replaces the default "Unexpected
 // Application Error!" screen.
 
-import { useRouteError, isRouteErrorResponse, Link } from 'react-router-dom'
-import { AlertTriangle } from 'lucide-react'
-import { Button } from '@/components/ui/button'
+import { AlertTriangle } from "lucide-react";
+import { useRouteError, isRouteErrorResponse, Link } from "react-router-dom";
+
+import { Button } from "@/components/ui/button";
 
 export function ErrorPage() {
-  const error = useRouteError()
+  const error = useRouteError();
 
-  let status: number | undefined
-  let message: string
+  let status: number | undefined;
+  let message: string;
 
   if (isRouteErrorResponse(error)) {
-    status = error.status
-    message = error.statusText || error.data
+    status = error.status;
+    message = error.statusText || error.data;
   } else if (error instanceof Error) {
-    message = error.message
+    message = error.message;
   } else {
-    message = 'An unexpected error occurred.'
+    message = "An unexpected error occurred.";
   }
 
   return (
     <div className="flex min-h-screen flex-col items-center justify-center gap-4 text-center">
       <AlertTriangle className="size-10 text-muted-foreground" />
-      {status && (
-        <p className="text-5xl font-bold tracking-tight">{status}</p>
-      )}
+      {status && <p className="text-5xl font-bold tracking-tight">{status}</p>}
       <p className="text-sm text-muted-foreground">{message}</p>
       <Button variant="outline" size="sm" asChild>
         <Link to="/">Go home</Link>
       </Button>
     </div>
-  )
+  );
 }

webui2/src/pages/IdentitySelectPage.tsx πŸ”—

@@ -6,50 +6,51 @@
 // OAuth account for future logins β€” or create a fresh one from their OAuth
 // profile.
 
-import { useEffect, useState } from 'react'
-import { UserCircle, Plus, AlertCircle } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { Skeleton } from '@/components/ui/skeleton'
+import { UserCircle, Plus, AlertCircle } from "lucide-react";
+import { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
 
 interface IdentityItem {
-  repoSlug: string
-  id: string
-  humanId: string
-  displayName: string
-  login?: string
-  avatarUrl?: string
+  repoSlug: string;
+  id: string;
+  humanId: string;
+  displayName: string;
+  login?: string;
+  avatarUrl?: string;
 }
 
 export function IdentitySelectPage() {
-  const [identities, setIdentities] = useState<IdentityItem[] | null>(null)
-  const [error, setError] = useState<string | null>(null)
-  const [working, setWorking] = useState(false)
+  const [identities, setIdentities] = useState<IdentityItem[] | null>(null);
+  const [error, setError] = useState<string | null>(null);
+  const [working, setWorking] = useState(false);
 
   useEffect(() => {
-    fetch('/auth/identities', { credentials: 'include' })
+    fetch("/auth/identities", { credentials: "include" })
       .then((res) => {
-        if (!res.ok) throw new Error(`unexpected status ${res.status}`)
-        return res.json() as Promise<IdentityItem[]>
+        if (!res.ok) throw new Error(`unexpected status ${res.status}`);
+        return res.json() as Promise<IdentityItem[]>;
       })
       .then(setIdentities)
-      .catch((e) => setError(String(e)))
-  }, [])
+      .catch((e) => setError(String(e)));
+  }, []);
 
   async function adopt(identityId: string | null) {
-    setWorking(true)
+    setWorking(true);
     try {
-      const res = await fetch('/auth/adopt', {
-        method: 'POST',
-        credentials: 'include',
-        headers: { 'Content-Type': 'application/json' },
+      const res = await fetch("/auth/adopt", {
+        method: "POST",
+        credentials: "include",
+        headers: { "Content-Type": "application/json" },
         body: JSON.stringify(identityId ? { identityId } : {}),
-      })
-      if (!res.ok) throw new Error(`adopt failed: ${res.status}`)
+      });
+      if (!res.ok) throw new Error(`adopt failed: ${res.status}`);
       // Full page reload to reset Apollo cache and auth state cleanly.
-      window.location.assign('/')
+      window.location.assign("/");
     } catch (e) {
-      setError(String(e))
-      setWorking(false)
+      setError(String(e));
+      setWorking(false);
     }
   }
 
@@ -60,8 +61,8 @@ export function IdentitySelectPage() {
         <h1 className="text-xl font-semibold">Choose your identity</h1>
       </div>
       <p className="mb-8 text-sm text-muted-foreground">
-        No git-bug identity was found linked to your account. Select an
-        existing identity to link it, or create a new one from your profile.
+        No git-bug identity was found linked to your account. Select an existing identity to link
+        it, or create a new one from your profile.
       </p>
 
       {error && (
@@ -85,14 +86,11 @@ export function IdentitySelectPage() {
             <div className="min-w-0 flex-1">
               <p className="font-medium">{id.displayName}</p>
               <p className="text-xs text-muted-foreground">
-                {id.login ? `@${id.login} Β· ` : ''}{id.repoSlug} Β· {id.humanId}
+                {id.login ? `@${id.login} Β· ` : ""}
+                {id.repoSlug} Β· {id.humanId}
               </p>
             </div>
-            <Button
-              size="sm"
-              disabled={working}
-              onClick={() => adopt(id.id)}
-            >
+            <Button size="sm" disabled={working} onClick={() => adopt(id.id)}>
               Adopt
             </Button>
           </div>
@@ -106,16 +104,12 @@ export function IdentitySelectPage() {
               A fresh git-bug identity will be created from your OAuth profile.
             </p>
           </div>
-          <Button
-            size="sm"
-            disabled={working}
-            onClick={() => adopt(null)}
-          >
+          <Button size="sm" disabled={working} onClick={() => adopt(null)}>
             <Plus className="size-4" />
             Create
           </Button>
         </div>
       </div>
     </div>
-  )
+  );
 }

webui2/src/pages/NewBugPage.tsx πŸ”—

@@ -1,36 +1,37 @@
-import { useState } from 'react'
-import { useNavigate, Link } from 'react-router-dom'
-import { ArrowLeft } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { Textarea } from '@/components/ui/textarea'
-import { Markdown } from '@/components/content/Markdown'
-import { useBugCreateMutation } from '@/__generated__/graphql'
-import { useRepo } from '@/lib/repo'
+import { ArrowLeft } from "lucide-react";
+import { useState } from "react";
+import { useNavigate, Link } from "react-router-dom";
+
+import { useBugCreateMutation } from "@/__generated__/graphql";
+import { Markdown } from "@/components/content/Markdown";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { useRepo } from "@/lib/repo";
 
 // New issue form (/:repo/issues/new). Title + body with write/preview tabs.
 export function NewBugPage() {
-  const navigate = useNavigate()
-  const repo = useRepo()
-  const [title, setTitle] = useState('')
-  const [message, setMessage] = useState('')
-  const [preview, setPreview] = useState(false)
+  const navigate = useNavigate();
+  const repo = useRepo();
+  const [title, setTitle] = useState("");
+  const [message, setMessage] = useState("");
+  const [preview, setPreview] = useState(false);
 
-  const [createBug, { loading, error }] = useBugCreateMutation()
+  const [createBug, { loading, error }] = useBugCreateMutation();
 
   async function handleSubmit(e: React.FormEvent) {
-    e.preventDefault()
-    if (!title.trim()) return
+    e.preventDefault();
+    if (!title.trim()) return;
     const result = await createBug({
       variables: { input: { title: title.trim(), message: message.trim() } },
-    })
-    const humanId = result.data?.bugCreate.bug.humanId
+    });
+    const humanId = result.data?.bugCreate.bug.humanId;
     if (humanId) {
-      navigate(repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}`)
+      navigate(repo ? `/${repo}/issues/${humanId}` : `/issues/${humanId}`);
     }
   }
 
-  const issuesHref = repo ? `/${repo}/issues` : '/issues'
+  const issuesHref = repo ? `/${repo}/issues` : "/issues";
 
   return (
     <div className="mx-auto max-w-3xl">
@@ -67,7 +68,7 @@ export function NewBugPage() {
                 type="button"
                 onClick={() => setPreview(false)}
                 className={`rounded px-2 py-0.5 transition-colors ${
-                  !preview ? 'bg-muted font-medium' : 'text-muted-foreground hover:text-foreground'
+                  !preview ? "bg-muted font-medium" : "text-muted-foreground hover:text-foreground"
                 }`}
               >
                 Write
@@ -77,7 +78,7 @@ export function NewBugPage() {
                 onClick={() => setPreview(true)}
                 disabled={!message.trim()}
                 className={`rounded px-2 py-0.5 transition-colors disabled:opacity-40 ${
-                  preview ? 'bg-muted font-medium' : 'text-muted-foreground hover:text-foreground'
+                  preview ? "bg-muted font-medium" : "text-muted-foreground hover:text-foreground"
                 }`}
               >
                 Preview
@@ -105,14 +106,19 @@ export function NewBugPage() {
         )}
 
         <div className="flex justify-end gap-2">
-          <Button type="button" variant="ghost" onClick={() => navigate(issuesHref)} disabled={loading}>
+          <Button
+            type="button"
+            variant="ghost"
+            onClick={() => navigate(issuesHref)}
+            disabled={loading}
+          >
             Cancel
           </Button>
           <Button type="submit" disabled={!title.trim() || loading}>
-            {loading ? 'Creating…' : 'Submit new issue'}
+            {loading ? "Creating…" : "Submit new issue"}
           </Button>
         </div>
       </form>
     </div>
-  )
+  );
 }

webui2/src/pages/RepoPickerPage.tsx πŸ”—

@@ -1,30 +1,31 @@
 // Repository picker page (/). Auto-redirects when there is exactly one repo.
 // Shows a list when multiple repos are registered.
 
-import { useEffect } from 'react'
-import { Link, useNavigate } from 'react-router-dom'
-import { GitFork, FolderOpen, AlertCircle } from 'lucide-react'
-import { Skeleton } from '@/components/ui/skeleton'
-import { useRepositoriesQuery } from '@/__generated__/graphql'
+import { GitFork, FolderOpen, AlertCircle } from "lucide-react";
+import { useEffect } from "react";
+import { Link, useNavigate } from "react-router-dom";
+
+import { useRepositoriesQuery } from "@/__generated__/graphql";
+import { Skeleton } from "@/components/ui/skeleton";
 
 function repoSlug(name: string | null | undefined): string {
-  return name ?? '_'
+  return name ?? "_";
 }
 
 function repoLabel(name: string | null | undefined): string {
-  return name ?? 'default'
+  return name ?? "default";
 }
 
 export function RepoPickerPage() {
-  const { data, loading, error } = useRepositoriesQuery()
-  const navigate = useNavigate()
+  const { data, loading, error } = useRepositoriesQuery();
+  const navigate = useNavigate();
 
   // Auto-redirect when there is exactly one repo β€” no need to pick.
   useEffect(() => {
     if (data?.repositories.nodes.length === 1) {
-      navigate('/' + repoSlug(data.repositories.nodes[0].name), { replace: true })
+      navigate("/" + repoSlug(data.repositories.nodes[0].name), { replace: true });
     }
-  }, [data, navigate])
+  }, [data, navigate]);
 
   return (
     <div className="mx-auto max-w-lg py-12">
@@ -40,7 +41,7 @@ export function RepoPickerPage() {
         </div>
       )}
 
-      {(loading && !data) && (
+      {loading && !data && (
         <div className="space-y-2">
           {Array.from({ length: 3 }).map((_, i) => (
             <Skeleton key={i} className="h-16 w-full rounded-md" />
@@ -53,7 +54,7 @@ export function RepoPickerPage() {
           <Link
             key={repoSlug(repo.name)}
             to={`/${repoSlug(repo.name)}`}
-            className="flex items-center gap-3 px-4 py-4 hover:bg-muted/50 transition-colors"
+            className="flex items-center gap-3 px-4 py-4 transition-colors hover:bg-muted/50"
           >
             <FolderOpen className="size-5 shrink-0 text-muted-foreground" />
             <p className="font-medium text-foreground">{repoLabel(repo.name)}</p>
@@ -67,5 +68,5 @@ export function RepoPickerPage() {
         )}
       </div>
     </div>
-  )
+  );
 }

webui2/src/pages/UserProfilePage.tsx πŸ”—

@@ -6,32 +6,38 @@
 // The :id param is treated as a humanId prefix and passed directly to the
 // identity(prefix) and allBugs(query:"author:...") GraphQL arguments.
 
-import { useState } from 'react'
-import { useParams, Link } from 'react-router-dom'
-import { formatDistanceToNow } from 'date-fns'
+import { formatDistanceToNow } from "date-fns";
 import {
-  ArrowLeft, MessageSquare, CircleDot, CircleCheck, ShieldCheck,
-  ChevronLeft, ChevronRight,
-} from 'lucide-react'
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { Button } from '@/components/ui/button'
-import { Skeleton } from '@/components/ui/skeleton'
-import { LabelBadge } from '@/components/bugs/LabelBadge'
-import { cn } from '@/lib/utils'
-import { Status, useUserProfileQuery } from '@/__generated__/graphql'
-import { useRepo } from '@/lib/repo'
+  ArrowLeft,
+  MessageSquare,
+  CircleDot,
+  CircleCheck,
+  ShieldCheck,
+  ChevronLeft,
+  ChevronRight,
+} from "lucide-react";
+import { useState } from "react";
+import { useParams, Link } from "react-router-dom";
 
-const PAGE_SIZE = 25
+import { Status, useUserProfileQuery } from "@/__generated__/graphql";
+import { LabelBadge } from "@/components/bugs/LabelBadge";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useRepo } from "@/lib/repo";
+import { cn } from "@/lib/utils";
+
+const PAGE_SIZE = 25;
 
 export function UserProfilePage() {
-  const { id } = useParams<{ id: string }>()
-  const repo = useRepo()
-  const [statusFilter, setStatusFilter] = useState<'open' | 'closed'>('open')
+  const { id } = useParams<{ id: string }>();
+  const repo = useRepo();
+  const [statusFilter, setStatusFilter] = useState<"open" | "closed">("open");
 
   // Cursor-stack pagination: cursors[i] is the `after` value to fetch page i.
   // Resetting to [undefined] returns to page 1. Shared pattern with BugListPage.
-  const [cursors, setCursors] = useState<(string | undefined)[]>([undefined])
-  const page = cursors.length - 1
+  const [cursors, setCursors] = useState<(string | undefined)[]>([undefined]);
+  const page = cursors.length - 1;
 
   // Three allBugs aliases in one round-trip:
   //   openCount / closedCount β€” always fetched so both badge numbers are visible
@@ -45,12 +51,12 @@ export function UserProfilePage() {
       listQuery: `author:${id} status:${statusFilter}`,
       after: cursors[page],
     },
-  })
+  });
 
-  function switchStatus(next: 'open' | 'closed') {
-    if (next === statusFilter) return
-    setStatusFilter(next)
-    setCursors([undefined]) // reset to page 1 on tab change
+  function switchStatus(next: "open" | "closed") {
+    if (next === statusFilter) return;
+    setStatusFilter(next);
+    setCursors([undefined]); // reset to page 1 on tab change
   }
 
   if (error) {
@@ -58,36 +64,34 @@ export function UserProfilePage() {
       <div className="py-16 text-center text-sm text-destructive">
         Failed to load profile: {error.message}
       </div>
-    )
+    );
   }
 
-  if (loading && !data) return <ProfileSkeleton />
+  if (loading && !data) return <ProfileSkeleton />;
 
-  const identity = data?.repository?.identity
+  const identity = data?.repository?.identity;
   if (!identity) {
-    return (
-      <div className="py-16 text-center text-sm text-muted-foreground">User not found.</div>
-    )
+    return <div className="py-16 text-center text-sm text-muted-foreground">User not found.</div>;
   }
 
-  const openCount = data?.repository?.openCount.totalCount ?? 0
-  const closedCount = data?.repository?.closedCount.totalCount ?? 0
+  const openCount = data?.repository?.openCount.totalCount ?? 0;
+  const closedCount = data?.repository?.closedCount.totalCount ?? 0;
 
-  const bugs = data?.repository?.bugs
-  const totalPages = Math.max(1, Math.ceil((bugs?.totalCount ?? 0) / PAGE_SIZE))
-  const hasNext = bugs?.pageInfo.hasNextPage ?? false
-  const hasPrev = page > 0
+  const bugs = data?.repository?.bugs;
+  const totalPages = Math.max(1, Math.ceil((bugs?.totalCount ?? 0) / PAGE_SIZE));
+  const hasNext = bugs?.pageInfo.hasNextPage ?? false;
+  const hasPrev = page > 0;
 
   function goNext() {
-    const cursor = bugs?.pageInfo.endCursor
-    if (cursor) setCursors((prev) => [...prev, cursor])
+    const cursor = bugs?.pageInfo.endCursor;
+    if (cursor) setCursors((prev) => [...prev, cursor]);
   }
 
   function goPrev() {
-    setCursors((prev) => prev.slice(0, -1))
+    setCursors((prev) => prev.slice(0, -1));
   }
 
-  const issuesHref = repo ? `/${repo}/issues` : '/issues'
+  const issuesHref = repo ? `/${repo}/issues` : "/issues";
 
   return (
     <div>
@@ -143,15 +147,20 @@ export function UserProfilePage() {
         {/* Open / Closed toggle β€” mirrors BugListPage style */}
         <div className="flex items-center gap-1 border-b border-border px-4 py-2">
           <button
-            onClick={() => switchStatus('open')}
+            onClick={() => switchStatus("open")}
             className={cn(
-              'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
-              statusFilter === 'open'
-                ? 'bg-accent text-accent-foreground'
-                : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
+              "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+              statusFilter === "open"
+                ? "bg-accent text-accent-foreground"
+                : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
             )}
           >
-            <CircleDot className={cn('size-4', statusFilter === 'open' && 'text-green-600 dark:text-green-400')} />
+            <CircleDot
+              className={cn(
+                "size-4",
+                statusFilter === "open" && "text-green-600 dark:text-green-400",
+              )}
+            />
             Open
             <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs leading-none">
               {openCount}
@@ -159,15 +168,20 @@ export function UserProfilePage() {
           </button>
 
           <button
-            onClick={() => switchStatus('closed')}
+            onClick={() => switchStatus("closed")}
             className={cn(
-              'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
-              statusFilter === 'closed'
-                ? 'bg-accent text-accent-foreground'
-                : 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
+              "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
+              statusFilter === "closed"
+                ? "bg-accent text-accent-foreground"
+                : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
             )}
           >
-            <CircleCheck className={cn('size-4', statusFilter === 'closed' && 'text-purple-600 dark:text-purple-400')} />
+            <CircleCheck
+              className={cn(
+                "size-4",
+                statusFilter === "closed" && "text-purple-600 dark:text-purple-400",
+              )}
+            />
             Closed
             <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-xs leading-none">
               {closedCount}
@@ -182,8 +196,8 @@ export function UserProfilePage() {
         )}
 
         {bugs?.nodes.map((bug) => {
-          const isOpen = bug.status === Status.Open
-          const StatusIcon = isOpen ? CircleDot : CircleCheck
+          const isOpen = bug.status === Status.Open;
+          const StatusIcon = isOpen ? CircleDot : CircleCheck;
           return (
             <div
               key={bug.id}
@@ -191,10 +205,10 @@ export function UserProfilePage() {
             >
               <StatusIcon
                 className={cn(
-                  'mt-0.5 size-4 shrink-0',
+                  "mt-0.5 size-4 shrink-0",
                   isOpen
-                    ? 'text-green-600 dark:text-green-400'
-                    : 'text-purple-600 dark:text-purple-400',
+                    ? "text-green-600 dark:text-green-400"
+                    : "text-purple-600 dark:text-purple-400",
                 )}
               />
               <div className="min-w-0 flex-1">
@@ -210,7 +224,7 @@ export function UserProfilePage() {
                   ))}
                 </div>
                 <p className="mt-0.5 text-xs text-muted-foreground">
-                  #{bug.humanId} opened{' '}
+                  #{bug.humanId} opened{" "}
                   {formatDistanceToNow(new Date(bug.createdAt), { addSuffix: true })}
                 </p>
               </div>
@@ -221,7 +235,7 @@ export function UserProfilePage() {
                 </div>
               )}
             </div>
-          )
+          );
         })}
 
         {/* Pagination footer β€” only shown when there is more than one page */}
@@ -254,7 +268,7 @@ export function UserProfilePage() {
         )}
       </div>
     </div>
-  )
+  );
 }
 
 function ProfileSkeleton() {
@@ -274,5 +288,5 @@ function ProfileSkeleton() {
         ))}
       </div>
     </div>
-  )
+  );
 }

webui2/tailwind.config.ts πŸ”—

@@ -1,74 +1,71 @@
-import type { Config } from 'tailwindcss'
+import type { Config } from "tailwindcss";
 
 const config: Config = {
-  darkMode: ['class'],
-  content: ['./index.html', './src/**/*.{ts,tsx}'],
+  darkMode: ["class"],
+  content: ["./index.html", "./src/**/*.{ts,tsx}"],
   theme: {
     extend: {
       fontFamily: {
-        sans: ['ui-sans-serif', 'system-ui', 'sans-serif'],
-        mono: ['ui-monospace', 'SFMono-Regular', 'SF Mono', 'Menlo', 'monospace'],
+        sans: ["ui-sans-serif", "system-ui", "sans-serif"],
+        mono: ["ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "monospace"],
       },
       colors: {
-        border: 'hsl(var(--border))',
-        input: 'hsl(var(--input))',
-        ring: 'hsl(var(--ring))',
-        background: 'hsl(var(--background))',
-        foreground: 'hsl(var(--foreground))',
+        border: "hsl(var(--border))",
+        input: "hsl(var(--input))",
+        ring: "hsl(var(--ring))",
+        background: "hsl(var(--background))",
+        foreground: "hsl(var(--foreground))",
         primary: {
-          DEFAULT: 'hsl(var(--primary))',
-          foreground: 'hsl(var(--primary-foreground))',
+          DEFAULT: "hsl(var(--primary))",
+          foreground: "hsl(var(--primary-foreground))",
         },
         secondary: {
-          DEFAULT: 'hsl(var(--secondary))',
-          foreground: 'hsl(var(--secondary-foreground))',
+          DEFAULT: "hsl(var(--secondary))",
+          foreground: "hsl(var(--secondary-foreground))",
         },
         destructive: {
-          DEFAULT: 'hsl(var(--destructive))',
-          foreground: 'hsl(var(--destructive-foreground))',
+          DEFAULT: "hsl(var(--destructive))",
+          foreground: "hsl(var(--destructive-foreground))",
         },
         muted: {
-          DEFAULT: 'hsl(var(--muted))',
-          foreground: 'hsl(var(--muted-foreground))',
+          DEFAULT: "hsl(var(--muted))",
+          foreground: "hsl(var(--muted-foreground))",
         },
         accent: {
-          DEFAULT: 'hsl(var(--accent))',
-          foreground: 'hsl(var(--accent-foreground))',
+          DEFAULT: "hsl(var(--accent))",
+          foreground: "hsl(var(--accent-foreground))",
         },
         popover: {
-          DEFAULT: 'hsl(var(--popover))',
-          foreground: 'hsl(var(--popover-foreground))',
+          DEFAULT: "hsl(var(--popover))",
+          foreground: "hsl(var(--popover-foreground))",
         },
         card: {
-          DEFAULT: 'hsl(var(--card))',
-          foreground: 'hsl(var(--card-foreground))',
+          DEFAULT: "hsl(var(--card))",
+          foreground: "hsl(var(--card-foreground))",
         },
       },
       borderRadius: {
-        lg: 'var(--radius)',
-        md: 'calc(var(--radius) - 2px)',
-        sm: 'calc(var(--radius) - 4px)',
+        lg: "var(--radius)",
+        md: "calc(var(--radius) - 2px)",
+        sm: "calc(var(--radius) - 4px)",
       },
       keyframes: {
-        'accordion-down': {
-          from: { height: '0' },
-          to: { height: 'var(--radix-accordion-content-height)' },
+        "accordion-down": {
+          from: { height: "0" },
+          to: { height: "var(--radix-accordion-content-height)" },
         },
-        'accordion-up': {
-          from: { height: 'var(--radix-accordion-content-height)' },
-          to: { height: '0' },
+        "accordion-up": {
+          from: { height: "var(--radix-accordion-content-height)" },
+          to: { height: "0" },
         },
       },
       animation: {
-        'accordion-down': 'accordion-down 0.2s ease-out',
-        'accordion-up': 'accordion-up 0.2s ease-out',
+        "accordion-down": "accordion-down 0.2s ease-out",
+        "accordion-up": "accordion-up 0.2s ease-out",
       },
     },
   },
-  plugins: [
-    require('tailwindcss-animate'),
-    require('@tailwindcss/typography'),
-  ],
-}
+  plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
+};
 
-export default config
+export default config;

webui2/tsconfig.json πŸ”—

@@ -1,7 +1,4 @@
 {
   "files": [],
-  "references": [
-    { "path": "./tsconfig.app.json" },
-    { "path": "./tsconfig.node.json" }
-  ]
+  "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
 }

webui2/tsconfig.node.tsbuildinfo πŸ”—

@@ -1 +1 @@
-{"root":["./vite.config.ts","./tailwind.config.ts","./codegen.ts"],"version":"5.9.3"}
+{"root":["./vite.config.ts","./tailwind.config.ts","./codegen.ts"],"version":"6.0.2"}

webui2/vite.config.ts πŸ”—

@@ -1,15 +1,16 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
-import path from 'path'
+import path from "path";
+
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vite";
 
 // The Go backend URL. Run: git-bug webui --port 3000
-const API_URL = process.env.VITE_API_URL || 'http://localhost:3000'
+const API_URL = process.env.VITE_API_URL || "http://localhost:3000";
 
 export default defineConfig({
   plugins: [react()],
   resolve: {
     alias: {
-      '@': path.resolve(__dirname, './src'),
+      "@": path.resolve(__dirname, "./src"),
     },
   },
   build: {
@@ -18,20 +19,20 @@ export default defineConfig({
     rollupOptions: {
       output: {
         manualChunks: {
-          'vendor-react': ['react', 'react-dom', 'react-router-dom'],
-          'vendor-apollo': ['@apollo/client', 'graphql'],
-          'vendor-markdown': ['react-markdown', 'remark-gfm'],
-          'vendor-highlight': ['highlight.js'],
+          "vendor-react": ["react", "react-dom", "react-router-dom"],
+          "vendor-apollo": ["@apollo/client", "graphql"],
+          "vendor-markdown": ["react-markdown", "remark-gfm"],
+          "vendor-highlight": ["highlight.js"],
         },
       },
     },
   },
   server: {
     proxy: {
-      '/graphql': { target: API_URL, changeOrigin: true },
-      '/gitfile': { target: API_URL, changeOrigin: true },
-      '/upload': { target: API_URL, changeOrigin: true },
-'/auth': { target: API_URL, changeOrigin: true },
+      "/graphql": { target: API_URL, changeOrigin: true },
+      "/gitfile": { target: API_URL, changeOrigin: true },
+      "/upload": { target: API_URL, changeOrigin: true },
+      "/auth": { target: API_URL, changeOrigin: true },
     },
   },
-})
+});