# Files changed · PR \#22672 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22672/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[refactor\(web\): the pull request header's crumbs, title editor and base branch live in their own components](https://git.cafe/pingdotgg/t3code/pull/22672.md)

Version: 5

Head commit: deb06508250ef29db3af594ae34852dcd88f9ecc

Base commit: 0f7b1fa34cfd384b42d234f9485a91dbb2d8d188

Comparison base: 0f7b1fa34cfd384b42d234f9485a91dbb2d8d188

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/web/src/components/pullRequest/PullRequestBaseBranch.tsx

Status: added; +137; −0

```diff
@@ -1,0 +1,137 @@
+import type { PullRequestUpdateMethod } from "@t3tools/contracts";
+import { TriangleAlertIcon } from "lucide-react";
+import type { ReactNode } from "react";
+
+import { cn } from "~/lib/utils";
+import { Button } from "../ui/button";
+import { MiddleTruncate } from "../ui/middle-truncate";
+import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
+import { PullRequestGlyph } from "./pullRequestIcons";
+
+interface BaseFreshness {
+  readonly behindBy: number | null;
+  readonly methods: ReadonlyArray<PullRequestUpdateMethod>;
+}
+
+function PullRequestBaseFreshnessWarning({
+  baseBranch,
+  freshness,
+  pending,
+  onUpdate,
+  iconClassName,
+  className,
+  children,
+}: {
+  readonly baseBranch: string;
+  readonly freshness: BaseFreshness;
+  readonly pending: boolean;
+  readonly onUpdate: (method: PullRequestUpdateMethod) => void;
+  readonly iconClassName?: string | undefined;
+  readonly className?: string;
+  readonly children?: ReactNode;
+}) {
+  const behind =
+    freshness.behindBy === null
+      ? ""
+      : ` by ${freshness.behindBy.toLocaleString()} ${
+          freshness.behindBy === 1 ? "commit" : "commits"
+        }`;
+  const summary = `This branch is out-of-date with ${baseBranch}${behind}.`;
+  return (
+    <Popover>
+      <PopoverTrigger
+        openOnHover
+        delay={0}
+        closeDelay={120}
+        render={
+          <button
+            type="button"
+            aria-label={summary}
+            className={cn(
+              "inline-flex min-w-0 shrink-0 cursor-help items-center gap-1 rounded-sm text-warning-foreground outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
+              className,
+            )}
+          />
+        }
+      >
+        {children}
+        <TriangleAlertIcon aria-hidden className={cn("size-3.5 shrink-0", iconClassName)} />
+      </PopoverTrigger>
+      <PopoverPopup align="start" side="bottom" className="max-w-80" padding="compact">
+        <p className="text-xs text-foreground">{summary}</p>
+        <p className="mt-0.5 text-xs text-muted-foreground">Changes can be cleanly merged.</p>
+        {freshness.methods.length > 0 ? (
+          <span className="mt-2 flex flex-wrap items-center gap-1.5">
+            {freshness.methods.map((method) => (
+              <Button
+                key={method}
+                size="xs"
+                variant="outline"
+                disabled={pending}
+                onClick={() => onUpdate(method)}
+              >
+                <PullRequestGlyph.merged aria-hidden className="size-3" />
+                {method === "rebase" ? "Update with rebase" : "Update branch"}
+              </Button>
+            ))}
+          </span>
+        ) : null}
+      </PopoverPopup>
+    </Popover>
+  );
+}
+
+/** The base branch, warning when the pull request has fallen behind it. */
+export function PullRequestBaseBranch({
+  baseBranch,
+  freshness,
+  stacked,
+  pending,
+  onUpdate,
+  iconClassName,
+}: {
+  readonly baseBranch: string;
+  readonly freshness: BaseFreshness | null;
+  readonly stacked: boolean;
+  readonly pending: boolean;
+  readonly onUpdate: (method: PullRequestUpdateMethod) => void;
+  readonly iconClassName?: string;
+}) {
+  const label = (
+    <>
+      {stacked ? (
+        <PullRequestGlyph.stack aria-label="Stacked pull request" className="size-3 shrink-0" />
+      ) : null}
+      <code className="flex min-w-0">
+        <MiddleTruncate value={baseBranch} showTitle={false} />
+      </code>
+    </>
+  );
+  if (freshness) {
+    return (
+      <PullRequestBaseFreshnessWarning
+        baseBranch={baseBranch}
+        freshness={freshness}
+        pending={pending}
+        onUpdate={onUpdate}
+        iconClassName={iconClassName}
+        className="max-w-[40%]"
+      >
+        {label}
+      </PullRequestBaseFreshnessWarning>
+    );
+  }
+  return (
+    <Tooltip>
+      <TooltipTrigger
+        render={
+          <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
+            {label}
+          </span>
+        }
+      />
+      <TooltipPopup side="top">{stacked ? `Stacked on ${baseBranch}` : baseBranch}</TooltipPopup>
+    </Tooltip>
+  );
+}
```

## apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx

Status: added; +193; −0

```diff
@@ -1,0 +1,193 @@
+import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
+import { ArrowLeftIcon, ExternalLinkIcon } from "lucide-react";
+import type { MouseEvent as ReactMouseEvent } from "react";
+
+import { cn } from "~/lib/utils";
+import { readLocalApi } from "~/localApi";
+import { Button } from "../ui/button";
+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
+import { openOnHostLabel, showPullRequestLinkContextMenu } from "./pullRequestLinkContextMenu";
+
+interface CrumbsPullRequest {
+  readonly url: string;
+  readonly provider: string;
+  readonly number: number;
+  readonly repository: string;
+  readonly title: string;
+}
+
+const openNumberContextMenu = (event: ReactMouseEvent, detail: CrumbsPullRequest): void => {
+  event.preventDefault();
+  event.stopPropagation();
+  void showPullRequestLinkContextMenu({
+    url: detail.url,
+    openLabel: openOnHostLabel(sourceControlClients.get(detail.provider)),
+    position: { x: event.clientX, y: event.clientY },
+  });
+};
+
+const ROW_CLASS_NAME =
+  "col-start-1 row-start-1 flex min-w-0 items-center gap-1 text-sm text-muted-foreground transition-[opacity,transform] ease-out motion-reduce:transform-none motion-reduce:transition-none sm:text-xs";
+
+function BackButton({ onBack, focusable }: { onBack: () => void; focusable: boolean }) {
+  return (
+    <Tooltip>
+      <TooltipTrigger
+        render={
+          <Button
+            size="icon-micro"
+            variant="ghost-muted"
+            {...(focusable ? {} : { tabIndex: -1 })}
+            onClick={onBack}
+            className="-ml-1.5"
+            aria-label="Back to this thread's pull requests"
+          >
+            <ArrowLeftIcon aria-hidden className="size-3.5" />
+          </Button>
+        }
+      />
+      <TooltipPopup side="top">Back to pull requests</TooltipPopup>
+    </Tooltip>
+  );
+}
+
+function NumberLink({
+  detail,
+  toneClassName,
+  openOnHostText,
+  focusable,
+}: {
+  detail: CrumbsPullRequest;
+  toneClassName: string;
+  openOnHostText: string;
+  focusable: boolean;
+}) {
+  return (
+    <Tooltip>
+      <TooltipTrigger
+        render={
+          <button
+            type="button"
+            {...(focusable ? {} : { tabIndex: -1 })}
+            onClick={() => void readLocalApi()?.shell.openExternal(detail.url)}
+            onAuxClick={(event) => {
+              if (event.button === 1) void readLocalApi()?.shell.openExternal(detail.url);
+            }}
+            onContextMenu={(event) => openNumberContextMenu(event, detail)}
+            className={cn(
+              "inline-flex shrink-0 cursor-pointer items-center gap-0.5 font-medium underline-offset-2 hover:underline",
+              toneClassName,
+            )}
+            aria-label={`Open pull request #${detail.number} on host`}
+          >
+            #{detail.number}
+            <ExternalLinkIcon aria-hidden className="size-2.5" />
+          </button>
+        }
+      />
+      <TooltipPopup side="top">{openOnHostText}</TooltipPopup>
+    </Tooltip>
+  );
+}
+
+/**
+ * The repository and number above the title, swapped for the number and title once the reader has
+ * scrolled the title away.
+ */
+export function PullRequestDetailCrumbs({
+  detail,
+  toneClassName,
+  condensed,
+  repositoryUrl,
+  openOnHostText,
+  onBack,
+}: {
+  detail: CrumbsPullRequest | null;
+  toneClassName: string | null;
+  condensed: boolean;
+  repositoryUrl: string | null;
+  openOnHostText: string;
+  onBack: (() => void) | undefined;
+}) {
+  const ready = detail !== null && toneClassName !== null;
+  return (
+    <div className="pl-4 grid h-7 min-w-0 items-center overflow-hidden">
+      <div
+        aria-hidden={condensed}
+        inert={condensed}
+        className={cn(
+          ROW_CLASS_NAME,
+          condensed
+            ? "pointer-events-none -translate-y-1 opacity-0 duration-100"
+            : "translate-y-0 opacity-100 delay-50 duration-150",
+        )}
+      >
+        {ready ? (
+          <>
+            {onBack ? <BackButton onBack={onBack} focusable /> : null}
+            <Tooltip>
+              <TooltipTrigger
+                render={
+                  repositoryUrl ? (
+                    <button
+                      type="button"
+                      onClick={() => void readLocalApi()?.shell.openExternal(repositoryUrl)}
+                      className="min-w-0 cursor-pointer truncate text-left font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
+                    >
+                      {detail.repository}
+                    </button>
+                  ) : (
+                    <span className="min-w-0 truncate font-medium text-muted-foreground">
+                      {detail.repository}
+                    </span>
+                  )
+                }
+              />
+              <TooltipPopup side="top">
+                {repositoryUrl ? `Open ${detail.repository} repository` : detail.repository}
+              </TooltipPopup>
+            </Tooltip>
+            <NumberLink
+              detail={detail}
+              toneClassName={toneClassName}
+              openOnHostText={openOnHostText}
+              focusable
+            />
+          </>
+        ) : null}
+      </div>
+      <div
+        aria-hidden={!condensed}
+        inert={!condensed}
+        className={cn(
+          ROW_CLASS_NAME,
+          condensed
+            ? "translate-y-0 opacity-100 delay-50 duration-150"
+            : "pointer-events-none translate-y-1 opacity-0 duration-100",
+        )}
+      >
+        {ready ? (
+          <>
+            {onBack ? <BackButton onBack={onBack} focusable={condensed} /> : null}
+            <NumberLink
+              detail={detail}
+              toneClassName={toneClassName}
+              openOnHostText={openOnHostText}
+              focusable={condensed}
+            />
+            <Tooltip>
+              <TooltipTrigger
+                render={
+                  <span className="min-w-0 truncate font-medium text-foreground">
+                    {detail.title}
+                  </span>
+                }
+              />
+              <TooltipPopup side="top">{detail.title}</TooltipPopup>
+            </Tooltip>
+          </>
+        ) : null}
+      </div>
+    </div>
+  );
+}
```

## apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx

Status: modified; +35; −424

```diff
@@ -35,7 +35,6 @@
   CircleDotIcon,
   CopyIcon,
   ChevronDownIcon,
-  ExternalLinkIcon,
   FileDiffIcon,
   FolderGit2Icon,
   GitBranchIcon,
@@ -50,18 +49,15 @@
   RotateCcwIcon,
   SparklesIcon,
   Trash2Icon,
-  TriangleAlertIcon,
 } from "lucide-react";
 import {
   lazy,
   Suspense,
-  type MouseEvent as ReactMouseEvent,
   useEffect,
   useEffectEvent,
   useLayoutEffect,
   useRef,
   useState,
-  type ReactNode,
 } from "react";
 
 import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
@@ -107,8 +103,6 @@
 import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon";
 import { Badge } from "../ui/badge";
 import { Button } from "../ui/button";
-import { PullRequestEditButton } from "./PullRequestEditButton";
-import { Input } from "../ui/input";
 import {
   Menu,
   MenuItem,
@@ -119,7 +113,6 @@
   MenuShortcut,
   MenuTrigger,
 } from "../ui/menu";
-import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
 import { toastManager } from "../ui/toast";
 import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
 import { MiddleTruncate } from "../ui/middle-truncate";
@@ -127,15 +120,17 @@
   PullRequestChecksStatusLine,
   PullRequestDetailHeaderBody,
   PullRequestDetailTabBar,
-  PullRequestDetailTitleRow,
 } from "./PullRequestDetailLayout";
 import { PullRequestDetailGhost, PullRequestTimelineGhost } from "./PullRequestGhosts";
 import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
+import { PullRequestBaseBranch } from "./PullRequestBaseBranch";
+import { PullRequestDetailCrumbs } from "./PullRequestDetailCrumbs";
+import { PullRequestTitleEditor } from "./PullRequestTitleEditor";
 import { PullRequestActivityUnavailableState } from "./PullRequestActivityUnavailableState";
 import { DiffPanelLoadingState } from "../DiffPanelShell";
 import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState";
 import type { PullRequestAgentSelectionInput } from "./PullRequestCodeTab";
+import { openOnHostLabel } from "./pullRequestLinkContextMenu";
-import { openOnHostLabel, showPullRequestLinkContextMenu } from "./pullRequestLinkContextMenu";
 import { PullRequestMarkdownContext } from "./PullRequestMarkdown";
 import { PullRequestComposer } from "./PullRequestComposer";
 import { PullRequestSummaryTab } from "./PullRequestSummaryTab";
@@ -175,7 +170,6 @@
   stripPullRequestHandoffReferences,
   writePullRequestDetailSnapshot,
 } from "./pullRequestDetail.logic";
-import { canEditPullRequestChangeRequest } from "./pullRequestEditing.logic";
 import {
   resolvePickableEnvironments,
   type PickableEnvironment,
@@ -295,90 +289,6 @@
   );
 }
 
-const openNumberContextMenu = (
-  event: ReactMouseEvent,
-  detail: { readonly url: string; readonly provider: string },
-): void => {
-  event.preventDefault();
-  event.stopPropagation();
-  void showPullRequestLinkContextMenu({
-    url: detail.url,
-    openLabel: openOnHostLabel(sourceControlClients.get(detail.provider)),
-    position: { x: event.clientX, y: event.clientY },
-  });
-};
-
-function PullRequestBaseFreshnessWarning({
-  baseBranch,
-  freshness,
-  pending,
-  onUpdate,
-  iconClassName,
-  className,
-  children,
-}: {
-  readonly baseBranch: string;
-  readonly freshness: {
-    readonly behindBy: number | null;
-    readonly methods: ReadonlyArray<PullRequestUpdateMethod>;
-  };
-  readonly pending: boolean;
-  readonly onUpdate: (method: PullRequestUpdateMethod) => void;
-  readonly iconClassName?: string;
-  readonly className?: string;
-  readonly children?: ReactNode;
-}) {
-  const behind =
-    freshness.behindBy === null
-      ? ""
-      : ` by ${freshness.behindBy.toLocaleString()} ${
-          freshness.behindBy === 1 ? "commit" : "commits"
-        }`;
-  const summary = `This branch is out-of-date with ${baseBranch}${behind}.`;
-  return (
-    <Popover>
-      <PopoverTrigger
-        openOnHover
-        delay={0}
-        closeDelay={120}
-        render={
-          <button
-            type="button"
-            aria-label={summary}
-            className={cn(
-              "inline-flex min-w-0 shrink-0 cursor-help items-center gap-1 rounded-sm text-warning-foreground outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
-              className,
-            )}
-          />
-        }
-      >
-        {children}
-        <TriangleAlertIcon aria-hidden className={cn("size-3.5 shrink-0", iconClassName)} />
-      </PopoverTrigger>
-      <PopoverPopup align="start" side="bottom" className="max-w-80" padding="compact">
-        <p className="text-xs text-foreground">{summary}</p>
-        <p className="mt-0.5 text-xs text-muted-foreground">Changes can be cleanly merged.</p>
-        {freshness.methods.length > 0 ? (
-          <span className="mt-2 flex flex-wrap items-center gap-1.5">
-            {freshness.methods.map((method) => (
-              <Button
-                key={method}
-                size="xs"
-                variant="outline"
-                disabled={pending}
-                onClick={() => onUpdate(method)}
-              >
-                <PullRequestGlyph.merged aria-hidden className="size-3" />
-                {method === "rebase" ? "Update with rebase" : "Update branch"}
-              </Button>
-            ))}
-          </span>
-        ) : null}
-      </PopoverPopup>
-    </Popover>
-  );
-}
-
 function reportCheckoutCommandError(error: Error) {
   toastManager.add({
     type: "error",
@@ -906,13 +816,6 @@
   });
   const [pendingAction, setPendingAction] = useState<PullRequestAction | null>(null);
   const actionPending = pendingAction !== null;
-  const update = useAtomCommand(pullRequestEnvironment.update, { reportFailure: false });
-  const [titleScope, setTitleScope] = useState<{
-    readonly pullRequestKey: string;
-    readonly text: string;
-  } | null>(null);
-  const titleDraft = titleScope?.pullRequestKey === pullRequestKey ? titleScope.text : null;
-  const [titleSaving, setTitleSaving] = useState(false);
   const newThread = useNewThreadHandler();
   const { environments } = useEnvironments();
   const pickableEnvironments =
@@ -1021,31 +924,6 @@
     return { commentPosted: true };
   };
 
-  const saveTitle = async (next: string) => {
-    const title = next.trim();
-    if (!canWriteSourceControl || detail === null || titleSaving) return;
-    if (title.length === 0 || title === detail.title) {
-      setTitleScope(null);
-      return;
-    }
-    setTitleSaving(true);
-    const result = await update({ environmentId, input: { ...reference, title } });
-    setTitleSaving(false);
-    if (result._tag === "Failure") {
-      toastManager.add({
-        type: "error",
-        title: "The title could not be saved",
-        description: readableFailure(
-          squashAtomCommandFailure(result),
-          "The host refused the new title.",
-        ),
-      });
-      return;
-    }
-    setTitleScope(null);
-    refreshDetail();
-  };
-
   type ThreadTask = {
     prompt: string;
     reviewComments?: ReadonlyArray<ReviewCommentContext>;
@@ -1672,156 +1550,14 @@
           !detail && !onClose && "hidden",
         )}
       >
-        <div className="pl-4 grid h-7 min-w-0 items-center overflow-hidden">
-          <div
-            aria-hidden={condensed}
-            inert={condensed}
-            className={cn(
-              "col-start-1 row-start-1 flex min-w-0 items-center gap-1 text-sm text-muted-foreground transition-[opacity,transform] ease-out motion-reduce:transform-none motion-reduce:transition-none sm:text-xs",
-              condensed
-                ? "pointer-events-none -translate-y-1 opacity-0 duration-100"
-                : "translate-y-0 opacity-100 delay-50 duration-150",
-            )}
-          >
-            {detail && statePresentation ? (
-              <>
-                {onBack ? (
+        <PullRequestDetailCrumbs
+          detail={detail}
+          toneClassName={statePresentation?.toneClassName ?? null}
+          condensed={condensed}
+          repositoryUrl={repositoryUrl}
+          openOnHostText={openOnHostText}
+          onBack={onBack}
+        />
-                  <Tooltip>
-                    <TooltipTrigger
-                      render={
-                        <Button
-                          size="icon-micro"
-                          variant="ghost-muted"
-                          onClick={onBack}
-                          className="-ml-1.5"
-                          aria-label="Back to this thread's pull requests"
-                        >
-                          <ArrowLeftIcon aria-hidden className="size-3.5" />
-                        </Button>
-                      }
-                    />
-                    <TooltipPopup side="top">Back to pull requests</TooltipPopup>
-                  </Tooltip>
-                ) : null}
-                <Tooltip>
-                  <TooltipTrigger
-                    render={
-                      repositoryUrl ? (
-                        <button
-                          type="button"
-                          onClick={() => void readLocalApi()?.shell.openExternal(repositoryUrl)}
-                          className="min-w-0 cursor-pointer truncate text-left font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
-                        >
-                          {detail.repository}
-                        </button>
-                      ) : (
-                        <span className="min-w-0 truncate font-medium text-muted-foreground">
-                          {detail.repository}
-                        </span>
-                      )
-                    }
-                  />
-                  <TooltipPopup side="top">
-                    {repositoryUrl ? `Open ${detail.repository} repository` : detail.repository}
-                  </TooltipPopup>
-                </Tooltip>
-                <Tooltip>
-                  <TooltipTrigger
-                    render={
-                      <button
-                        type="button"
-                        onClick={() => void readLocalApi()?.shell.openExternal(detail.url)}
-                        onAuxClick={(event) => {
-                          if (event.button === 1)
-                            void readLocalApi()?.shell.openExternal(detail.url);
-                        }}
-                        onContextMenu={(event) => openNumberContextMenu(event, detail)}
-                        className={cn(
-                          "inline-flex shrink-0 cursor-pointer items-center gap-0.5 font-medium underline-offset-2 hover:underline",
-                          statePresentation.toneClassName,
-                        )}
-                        aria-label={`Open pull request #${detail.number} on host`}
-                      >
-                        #{detail.number}
-                        <ExternalLinkIcon aria-hidden className="size-2.5" />
-                      </button>
-                    }
-                  />
-                  <TooltipPopup side="top">{openOnHostText}</TooltipPopup>
-                </Tooltip>
-              </>
-            ) : null}
-          </div>
-          <div
-            aria-hidden={!condensed}
-            inert={!condensed}
-            className={cn(
-              "col-start-1 row-start-1 flex min-w-0 items-center gap-1 text-sm text-muted-foreground transition-[opacity,transform] ease-out motion-reduce:transform-none motion-reduce:transition-none sm:text-xs",
-              condensed
-                ? "translate-y-0 opacity-100 delay-50 duration-150"
-                : "pointer-events-none translate-y-1 opacity-0 duration-100",
-            )}
-          >
-            {detail && statePresentation ? (
-              <>
-                {onBack ? (
-                  <Tooltip>
-                    <TooltipTrigger
-                      render={
-                        <Button
-                          size="icon-micro"
-                          variant="ghost-muted"
-                          tabIndex={condensed ? 0 : -1}
-                          onClick={onBack}
-                          className="-ml-1.5"
-                          aria-label="Back to this thread's pull requests"
-                        >
-                          <ArrowLeftIcon aria-hidden className="size-3.5" />
-                        </Button>
-                      }
-                    />
-                    <TooltipPopup side="top">Back to pull requests</TooltipPopup>
-                  </Tooltip>
-                ) : null}
-                <Tooltip>
-                  <TooltipTrigger
-                    render={
-                      <button
-                        type="button"
-                        tabIndex={condensed ? 0 : -1}
-                        onClick={() => void readLocalApi()?.shell.openExternal(detail.url)}
-                        onAuxClick={(event) => {
-                          if (event.button === 1)
-                            void readLocalApi()?.shell.openExternal(detail.url);
-                        }}
-                        onContextMenu={(event) => openNumberContextMenu(event, detail)}
-                        className={cn(
-                          "inline-flex shrink-0 cursor-pointer items-center gap-0.5 font-medium underline-offset-2 hover:underline",
-                          statePresentation.toneClassName,
-                        )}
-                        aria-label={`Open pull request #${detail.number} on host`}
-                      >
-                        #{detail.number}
-                        <ExternalLinkIcon aria-hidden className="size-2.5" />
-                      </button>
-                    }
-                  />
-                  <TooltipPopup side="top">{openOnHostText}</TooltipPopup>
-                </Tooltip>
-                <Tooltip>
-                  <TooltipTrigger
-                    render={
-                      <span className="min-w-0 truncate font-medium text-foreground">
-                        {detail.title}
-                      </span>
-                    }
-                  />
-                  <TooltipPopup side="top">{detail.title}</TooltipPopup>
-                </Tooltip>
-              </>
-            ) : null}
-          </div>
-        </div>
         <div className="mr-4 flex h-7 shrink-0 items-center justify-end gap-1">
           {detail ? (
             <TooltipProvider delay={150} closeDelay={150} timeout={400}>
@@ -2362,49 +2098,14 @@
                   </span>
                   <span aria-hidden className="h-3 w-px shrink-0 bg-border/70" />
                   <span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-2xs text-muted-foreground/65">
+                    <PullRequestBaseBranch
+                      baseBranch={detail.baseBranch}
+                      freshness={freshness}
+                      stacked={isStackedPullRequest}
+                      pending={actionPending}
+                      onUpdate={(method) => void perform("update-branch", undefined, method)}
+                      iconClassName="size-3"
+                    />
-                    {freshness ? (
-                      <PullRequestBaseFreshnessWarning
-                        baseBranch={detail.baseBranch}
-                        freshness={freshness}
-                        pending={actionPending}
-                        onUpdate={(method) => void perform("update-branch", undefined, method)}
-                        iconClassName="size-3"
-                        className="max-w-[40%]"
-                      >
-                        {isStackedPullRequest ? (
-                          <PullRequestGlyph.stack
-                            aria-label="Stacked pull request"
-                            className="size-3 shrink-0"
-                          />
-                        ) : null}
-                        <code className="flex min-w-0">
-                          <MiddleTruncate value={detail.baseBranch} showTitle={false} />
-                        </code>
-                      </PullRequestBaseFreshnessWarning>
-                    ) : (
-                      <Tooltip>
-                        <TooltipTrigger
-                          render={
-                            <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
-                              {isStackedPullRequest ? (
-                                <PullRequestGlyph.stack
-                                  aria-label="Stacked pull request"
-                                  className="size-3 shrink-0"
-                                />
-                              ) : null}
-                              <code className="flex min-w-0">
-                                <MiddleTruncate value={detail.baseBranch} showTitle={false} />
-                              </code>
-                            </span>
-                          }
-                        />
-                        <TooltipPopup side="top">
-                          {isStackedPullRequest
-                            ? `Stacked on ${detail.baseBranch}`
-                            : detail.baseBranch}
-                        </TooltipPopup>
-                      </Tooltip>
-                    )}
                     <ArrowLeftIcon
                       aria-label="receives changes from"
                       className="size-3 shrink-0 opacity-60"
@@ -2466,69 +2167,14 @@
             {detail ? (
               <PullRequestDetailHeaderBody
                 title={
+                  <PullRequestTitleEditor
+                    environmentId={environmentId}
+                    reference={reference}
+                    pullRequestKey={pullRequestKey}
+                    detail={detail}
+                    canWriteSourceControl={canWriteSourceControl}
-                  titleDraft === null ? (
-                    <PullRequestDetailTitleRow className="group gap-1">
-                      <Tooltip>
-                        <TooltipTrigger
-                          render={
-                            <h1 className="min-w-0 flex-1 truncate text-base font-semibold leading-snug">
-                              {detail.title}
-                            </h1>
-                          }
-                        />
-                        <TooltipPopup side="top">{detail.title}</TooltipPopup>
-                      </Tooltip>
-                      {canEditPullRequestChangeRequest(detail) ? (
-                        <PullRequestEditButton
-                          aria-label="Edit title"
-                          onClick={() => setTitleScope({ pullRequestKey, text: detail.title })}
-                        />
-                      ) : null}
-                    </PullRequestDetailTitleRow>
-                  ) : (
-                    <div className="space-y-2">
-                      <Input
-                        autoFocus
-                        size="sm"
-                        disabled={titleSaving}
-                        value={titleDraft}
-                        aria-label="Pull request title"
-                        onChange={(event) =>
-                          setTitleScope({ pullRequestKey, text: event.target.value })
-                        }
-                        onKeyDown={(event) => {
-                          if (event.nativeEvent.isComposing || event.keyCode === 229) return;
-                          if (event.key === "Enter") {
-                            event.preventDefault();
-                            void saveTitle(titleDraft);
-                          } else if (event.key === "Escape") {
-                            event.preventDefault();
-                            setTitleScope(null);
-                          }
-                        }}
-                      />
-                      <div className="flex justify-end gap-2">
-                        <Button
-                          size="xs"
-                          variant="ghost"
-                          disabled={titleSaving}
-                          onClick={() => setTitleScope(null)}
-                        >
-                          Cancel
-                        </Button>
-                        <Button
-                          size="xs"
-                          variant="outline"
-                          disabled={
-                            !canWriteSourceControl || titleSaving || titleDraft.trim().length === 0
-                          }
-                          onClick={() => void saveTitle(titleDraft)}
-                        >
-                          {titleSaving ? "Saving..." : "Save"}
-                        </Button>
-                      </div>
-                    </div>
-                  )
+                    onSaved={refreshDetail}
+                  />
                 }
                 author={
                   <PullRequestActorLabel actor={detail.author} profileUrl={authorProfileUrl} />
@@ -2553,48 +2199,13 @@
                   ) : null
                 }
                 base={
+                  <PullRequestBaseBranch
+                    baseBranch={detail.baseBranch}
+                    freshness={freshness}
+                    stacked={isStackedPullRequest}
+                    pending={actionPending}
+                    onUpdate={(method) => void perform("update-branch", undefined, method)}
+                  />
-                  freshness ? (
-                    <PullRequestBaseFreshnessWarning
-                      baseBranch={detail.baseBranch}
-                      freshness={freshness}
-                      pending={actionPending}
-                      onUpdate={(method) => void perform("update-branch", undefined, method)}
-                      className="max-w-[40%]"
-                    >
-                      {isStackedPullRequest ? (
-                        <PullRequestGlyph.stack
-                          aria-label="Stacked pull request"
-                          className="size-3 shrink-0"
-                        />
-                      ) : null}
-                      <code className="flex min-w-0">
-                        <MiddleTruncate value={detail.baseBranch} showTitle={false} />
-                      </code>
-                    </PullRequestBaseFreshnessWarning>
-                  ) : (
-                    <Tooltip>
-                      <TooltipTrigger
-                        render={
-                          <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
-                            {isStackedPullRequest ? (
-                              <PullRequestGlyph.stack
-                                aria-label="Stacked pull request"
-                                className="size-3 shrink-0"
-                              />
-                            ) : null}
-                            <code className="flex min-w-0">
-                              <MiddleTruncate value={detail.baseBranch} showTitle={false} />
-                            </code>
-                          </span>
-                        }
-                      />
-                      <TooltipPopup side="top">
-                        {isStackedPullRequest
-                          ? `Stacked on ${detail.baseBranch}`
-                          : detail.baseBranch}
-                      </TooltipPopup>
-                    </Tooltip>
-                  )
                 }
                 head={
                   <PullRequestCopyableCode
```

## apps/web/src/components/pullRequest/PullRequestTitleEditor.tsx

Status: added; +127; −0

```diff
@@ -1,0 +1,127 @@
+import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
+import type { EnvironmentId, PullRequestDetail, PullRequestRef } from "@t3tools/contracts";
+import { useState } from "react";
+
+import { pullRequestEnvironment } from "~/state/pullRequests";
+import { useAtomCommand } from "~/state/use-atom-command";
+import { Button } from "../ui/button";
+import { Input } from "../ui/input";
+import { toastManager } from "../ui/toast";
+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
+import { PullRequestDetailTitleRow } from "./PullRequestDetailLayout";
+import { PullRequestEditButton } from "./PullRequestEditButton";
+import { readableFailure } from "./pullRequestDetail.logic";
+import { canEditPullRequestChangeRequest } from "./pullRequestEditing.logic";
+
+/** The pull request title, which the reader can rename in place when the host allows it. */
+export function PullRequestTitleEditor({
+  environmentId,
+  reference,
+  pullRequestKey,
+  detail,
+  canWriteSourceControl,
+  onSaved,
+}: {
+  environmentId: EnvironmentId;
+  reference: PullRequestRef;
+  pullRequestKey: string;
+  detail: PullRequestDetail;
+  canWriteSourceControl: boolean;
+  onSaved: () => void;
+}) {
+  const update = useAtomCommand(pullRequestEnvironment.update, { reportFailure: false });
+  const [titleScope, setTitleScope] = useState<{
+    readonly pullRequestKey: string;
+    readonly text: string;
+  } | null>(null);
+  const titleDraft = titleScope?.pullRequestKey === pullRequestKey ? titleScope.text : null;
+  const [titleSaving, setTitleSaving] = useState(false);
+
+  const saveTitle = async (next: string) => {
+    const title = next.trim();
+    if (!canWriteSourceControl || titleSaving) return;
+    if (title.length === 0 || title === detail.title) {
+      setTitleScope(null);
+      return;
+    }
+    setTitleSaving(true);
+    const result = await update({ environmentId, input: { ...reference, title } });
+    setTitleSaving(false);
+    if (result._tag === "Failure") {
+      toastManager.add({
+        type: "error",
+        title: "The title could not be saved",
+        description: readableFailure(
+          squashAtomCommandFailure(result),
+          "The host refused the new title.",
+        ),
+      });
+      return;
+    }
+    setTitleScope(null);
+    onSaved();
+  };
+
+  if (titleDraft === null) {
+    return (
+      <PullRequestDetailTitleRow className="group gap-1">
+        <Tooltip>
+          <TooltipTrigger
+            render={
+              <h1 className="min-w-0 flex-1 truncate text-base font-semibold leading-snug">
+                {detail.title}
+              </h1>
+            }
+          />
+          <TooltipPopup side="top">{detail.title}</TooltipPopup>
+        </Tooltip>
+        {canEditPullRequestChangeRequest(detail) ? (
+          <PullRequestEditButton
+            aria-label="Edit title"
+            onClick={() => setTitleScope({ pullRequestKey, text: detail.title })}
+          />
+        ) : null}
+      </PullRequestDetailTitleRow>
+    );
+  }
+  return (
+    <div className="space-y-2">
+      <Input
+        autoFocus
+        size="sm"
+        disabled={titleSaving}
+        value={titleDraft}
+        aria-label="Pull request title"
+        onChange={(event) => setTitleScope({ pullRequestKey, text: event.target.value })}
+        onKeyDown={(event) => {
+          if (event.nativeEvent.isComposing || event.keyCode === 229) return;
+          if (event.key === "Enter") {
+            event.preventDefault();
+            void saveTitle(titleDraft);
+          } else if (event.key === "Escape") {
+            event.preventDefault();
+            setTitleScope(null);
+          }
+        }}
+      />
+      <div className="flex justify-end gap-2">
+        <Button
+          size="xs"
+          variant="ghost"
+          disabled={titleSaving}
+          onClick={() => setTitleScope(null)}
+        >
+          Cancel
+        </Button>
+        <Button
+          size="xs"
+          variant="outline"
+          disabled={!canWriteSourceControl || titleSaving || titleDraft.trim().length === 0}
+          onClick={() => void saveTitle(titleDraft)}
+        >
+          {titleSaving ? "Saving..." : "Save"}
+        </Button>
+      </div>
+    </div>
+  );
+}
```
