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

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

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

Visibility: public

[refactor\(web\): ChatView's subagent, goal, background-work and branch banners are built in ThreadActivityBanners](https://git.cafe/pingdotgg/t3code/pull/22725.md)

Version: 6

Head commit: 08ca07f2b1229c4de2131e514c73b38577e9f3eb

Base commit: 7de6e9a6fca9d5acba44242a5a102749d4878e08

Comparison base: 7de6e9a6fca9d5acba44242a5a102749d4878e08

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

## apps/web/src/components/ChatView.tsx

Status: modified; +44; −176

```diff
@@ -82,7 +82,6 @@
 import { effectiveSnoozed, threadWokeAt } from "@t3tools/client-runtime/state/thread-settled";
 import { declinePendingApprovals } from "@t3tools/client-runtime/state/thread-requests";
 import { presentThreadShell } from "@t3tools/client-runtime/state/shell";
-import { childThreadInputsTitle } from "@t3tools/client-runtime/state/threads";
 import { useAcknowledgeThreadWoke, useThreadActions } from "../hooks/useThreadActions";
 import {
   deriveProviderSubagentStatus,
@@ -92,8 +91,6 @@
   deriveThreadActivityRun,
   deriveLatestThreadRun,
   deriveThreadRuntime,
-  presentPendingBackgroundWork,
-  presentProviderGoal,
 } from "@t3tools/client-runtime/state/thread-execution";
 import {
   resolveWorkflowOpening,
@@ -137,7 +134,7 @@
 import { resolveThreadReferenceCopyTarget } from "@t3tools/shared/threadReference";
 import { resolveTerminalSessionLabel } from "@t3tools/shared/terminalLabels";
 import { useAtomValue } from "@effect/atom-react";
+import { type SetStateAction, useEffect, useLayoutEffect, useRef, useState } from "react";
-import { Fragment, type SetStateAction, useEffect, useLayoutEffect, useRef, useState } from "react";
 import { flushSync } from "react-dom";
 import { useLocation, useNavigate, useRouter } from "@tanstack/react-router";
 import { assistantCitationFromLocation } from "../lib/assistantCitationNavigation";
@@ -234,13 +231,9 @@
 import {
   AlarmClockIcon,
   CheckCircle2Icon,
-  MessageCircleIcon,
   PaperclipIcon,
   ChevronDownIcon,
   DownloadIcon,
-  GitBranchIcon,
-  ShieldQuestionIcon,
-  TargetIcon,
 } from "lucide-react";
 import { cn } from "~/lib/utils";
 import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
@@ -483,8 +476,7 @@
 import { useAtomCommand } from "../state/use-atom-command";
 import { useOrchestrationCommand } from "../state/use-orchestration-command";
 import { useAtomQueryRunner } from "../state/use-atom-query-runner";
-import { Button, InlineButton } from "./ui/button";
+import { Button } from "./ui/button";
-import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
 import { useAssetUrls } from "../assets/assetUrls";
 import {
   ATTACHMENT_ONLY_BOOTSTRAP_PROMPT,
@@ -494,6 +486,12 @@
 import { ThreadVisitWatermark } from "./chat/ThreadVisitWatermark";
 import { ChatTerminalDrawers } from "./chat/ChatTerminalDrawers";
 import { ChatRightPanel } from "./chat/ChatRightPanel";
+import {
+  backgroundWorkBannerItem,
+  branchMismatchBannerItem,
+  childInputBannerItem,
+  goalBannerItem,
+} from "./chat/ThreadActivityBanners";
 import { useTimelineFollow } from "./chat/useTimelineFollow";
 import {
   type EnvironmentUnavailableState,
@@ -4019,35 +4017,13 @@
   };
   const onOpenRelatedThreadStable = useStableCallback(onOpenRelatedThread);
   const childThreadInputs = useChildThreadInputs(activeThreadRef);
-  const childInputBannerItem = (() => {
-    const first = childThreadInputs[0];
-    const parentAwaitingUser =
+  const childInputBanner = childInputBannerItem({
+    childThreadInputs,
+    parentAwaitingUser: Boolean(
+      activePendingApproval || activePendingUserInput || activeThreadShell?.hasPendingUserInput,
+    ),
+    onOpenThread: onOpenRelatedThreadStable,
+  });
-      activePendingApproval || activePendingUserInput || activeThreadShell?.hasPendingUserInput;
-    if (!first || parentAwaitingUser) return null;
-    return {
-      id: `child-input:${first.id}`,
-      variant: "info",
-      priority: "activity",
-      icon: first.hasPendingApprovals ? <ShieldQuestionIcon /> : <MessageCircleIcon />,
-      title: childThreadInputsTitle(childThreadInputs),
-      description: childThreadInputs.map((child, index) => (
-        <Fragment key={child.id}>
-          {index > 0 ? " " : null}
-          <span className="whitespace-nowrap">
-            <InlineButton tone="muted" onClick={() => onOpenRelatedThreadStable(child.id)}>
-              {child.title}
-            </InlineButton>
-            {index < childThreadInputs.length - 1 ? "," : null}
-          </span>
-        </Fragment>
-      )),
-      actions: (
-        <Button size="xs" variant="ghost" onClick={() => onOpenRelatedThreadStable(first.id)}>
-          {first.hasPendingApprovals ? "Open approval" : "Open question"}
-        </Button>
-      ),
-    };
-  })() as ComposerBannerStackItem | null;
 
   const sendStandaloneCommand = async (text: string, failureMessage: string) => {
     if (!activeThread || !clientSettingsHydrated || sendInFlightRef.current) return;
@@ -4111,104 +4087,26 @@
   };
   const sendStandaloneCommandStable = useStableCallback(sendStandaloneCommand);
   const activeGoal = activeThreadShell?.goal ?? null;
-  const goalBannerItem = (() => {
-    if (!activeThread || activeGoal === null) return null;
-    const presentation = presentProviderGoal(activeGoal, isWorking);
-    return {
-      id: `goal:${activeThread.id}`,
-      variant: "default",
-      priority: "activity",
-      icon: <TargetIcon />,
-      title:
-        presentation.usage === null
-          ? presentation.title
-          : `${presentation.title} · ${presentation.usage}`,
-      description: presentation.objective,
-      actions: isWorking ? undefined : (
-        <>
-          {presentation.canResume ? (
-            <Button
+  const goalBanner =
+    activeThread && activeGoal !== null
-              size="xs"
-              variant="ghost"
-              onClick={() =>
+      ? goalBannerItem({
+          threadId: activeThread.id,
+          goal: activeGoal,
+          isWorking,
+          onSendCommand: sendStandaloneCommandStable,
+        })
+      : null;
-                void sendStandaloneCommandStable("/goal resume", "Failed to resume goal.")
-              }
-            >
-              Resume
-            </Button>
-          ) : null}
-          <Button
-            size="xs"
-            variant="ghost"
-            onClick={() => void sendStandaloneCommandStable("/goal clear", "Failed to clear goal.")}
-          >
-            Clear
-          </Button>
-        </>
-      ),
-    };
-  })() as ComposerBannerStackItem | null;
 
-  const backgroundWorkBannerItem = (() => {
-    const presentation = presentPendingBackgroundWork(activeBackgroundTasks);
-    if (presentation === null || !activeThread) {
-      return null;
-    }
-    return {
-      id: `background-work:${activeThread.id}`,
-      variant: "default",
-      priority: "activity",
-      icon: (
-        <span
-          className={cn(
-            "size-1.5 rounded-full bg-foreground",
-            presentation.waiting && "motion-safe:animate-status-pulse",
-          )}
-          aria-hidden="true"
-        />
-      ),
-      title: presentation.title,
-      description:
-        presentation.items.length === 1 && presentation.items[0]?.childThreadId === undefined
-          ? undefined
-          : presentation.items.map((item, index) => {
-              const childThreadId = item.childThreadId;
-              const separator = index < presentation.items.length - 1 ? "," : null;
-              return (
-                <Fragment key={item.taskId}>
-                  {index > 0 ? " " : null}
-                  {childThreadId === undefined ? (
-                    <>
-                      {item.label}
+  const backgroundWorkBanner = activeThread
+    ? backgroundWorkBannerItem({
+        threadId: activeThread.id,
+        tasks: activeBackgroundTasks,
-                      {separator}
+        stopDisabled: !canOperateThread || isStoppingBackgroundWork,
-                    </>
-                  ) : (
-                    <span className="whitespace-nowrap">
-                      <InlineButton
-                        tone="muted"
-                        aria-label={`${presentation.items.length === 1 ? "Show" : "Open"} subagent ${item.label}`}
-                        onClick={() => onOpenRelatedThreadStable(childThreadId)}
-                      >
-                        {presentation.items.length === 1 ? "Show" : item.label}
-                      </InlineButton>
-                      {separator}
-                    </span>
-                  )}
-                </Fragment>
-              );
-            }),
-      actions: (
-        <Button
-          size="xs"
-          variant="ghost"
-          disabled={!canOperateThread || isStoppingBackgroundWork}
-          onClick={() => void handleStopBackgroundWorkStable()}
-        >
-          {isStoppingBackgroundWork ? "Stopping..." : "Stop"}
-        </Button>
-      ),
-    };
-  })() as ComposerBannerStackItem | null;
+        stopping: isStoppingBackgroundWork,
+        onStop: handleStopBackgroundWorkStable,
+        onOpenThread: onOpenRelatedThreadStable,
+      })
+    : null;
   // The relative times read the clock; nowMinute re-renders them on the minute.
   const snoozedUntil = activeThreadShell?.snoozedUntil ?? null;
   const settledAt = activeThreadShell?.settledAt ?? null;
@@ -4353,11 +4251,9 @@
       : null;
   const composerBannerItems = (() => {
     const limitRecoveryItems = limitRecoveryBanner === null ? [] : [limitRecoveryBanner];
-    const backgroundWorkItems = [
-      childInputBannerItem,
-      goalBannerItem,
+    const backgroundWorkItems = [childInputBanner, goalBanner, backgroundWorkBanner].filter(
+      (item) => item !== null,
+    );
-      backgroundWorkBannerItem,
-    ].filter((item) => item !== null);
     const usageLimitsItems = usageLimitsBanner === null ? [] : [usageLimitsBanner];
     const projectCloneItems = projectCloneBannerItem === null ? [] : [projectCloneBannerItem];
     if (!localCheckoutBranchMismatch || !showBranchMismatchBanner || !activeBranchMismatchKey) {
@@ -4377,45 +4273,17 @@
       ...projectCloneItems,
       ...systemComposerBannerItems,
       ...backgroundWorkItems,
-      {
-        id: `branch-mismatch:${activeBranchMismatchKey}`,
-        variant: "info",
-        compact: true,
-        icon: <GitBranchIcon />,
-        title: (
-          <span className="flex min-w-0 items-baseline gap-1.5">
-            <span className="shrink-0 font-normal text-muted-foreground">Branch changed — was</span>
+      branchMismatchBannerItem({
+        mismatchKey: activeBranchMismatchKey,
+        mismatch: localCheckoutBranchMismatch,
+        restoreDisabled: !canWriteSourceControl || isRestoringThreadBranch,
+        restoring: isRestoringThreadBranch,
+        onRestore: handleRestoreThreadBranchStable,
-            <Tooltip>
-              <TooltipTrigger
-                render={
-                  <code className="min-w-0 truncate font-medium text-foreground">
-                    {localCheckoutBranchMismatch.threadBranch}
-                  </code>
-                }
-              />
-              <TooltipPopup side="top">
-                This thread last ran on {localCheckoutBranchMismatch.threadBranch}. Sending will
-                continue on {localCheckoutBranchMismatch.currentBranch}.
-              </TooltipPopup>
-            </Tooltip>
-          </span>
-        ),
-        actions: (
-          <Button
-            size="xs"
-            variant="ghost"
-            disabled={!canWriteSourceControl || isRestoringThreadBranch}
-            onClick={handleRestoreThreadBranchStable}
-          >
-            {isRestoringThreadBranch ? "Restoring..." : "Restore branch"}
-          </Button>
-        ),
-        dismissLabel: "Dismiss branch change notice",
         onDismiss: () => {
           dismissBranchMismatchForSession(activeBranchMismatchKey);
           setBranchMismatchDismissTick((tick) => tick + 1);
         },
+      }),
-      },
     ];
   })() as ComposerBannerStackItem[];
 
```

## apps/web/src/components/chat/ThreadActivityBanners.tsx

Status: added; +201; −0

```diff
@@ -1,0 +1,201 @@
+import type {
+  OrchestrationV2PendingBackgroundTask,
+  OrchestrationV2ProviderGoal,
+  ThreadId,
+} from "@t3tools/contracts";
+import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
+import {
+  presentPendingBackgroundWork,
+  presentProviderGoal,
+} from "@t3tools/client-runtime/state/thread-execution";
+import { childThreadInputsTitle } from "@t3tools/client-runtime/state/threads";
+import { GitBranchIcon, MessageCircleIcon, ShieldQuestionIcon, TargetIcon } from "lucide-react";
+import { Fragment } from "react";
+import { cn } from "~/lib/utils";
+import { Button, InlineButton } from "../ui/button";
+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
+import type { ComposerBannerStackItem } from "./ComposerBannerStack";
+
+/** Subagents waiting on the user, unless the parent is itself waiting on them. */
+export function childInputBannerItem(input: {
+  readonly childThreadInputs: ReadonlyArray<EnvironmentThreadShell>;
+  readonly parentAwaitingUser: boolean;
+  readonly onOpenThread: (threadId: ThreadId) => void;
+}): ComposerBannerStackItem | null {
+  const { childThreadInputs, onOpenThread } = input;
+  const first = childThreadInputs[0];
+  if (!first || input.parentAwaitingUser) return null;
+  return {
+    id: `child-input:${first.id}`,
+    variant: "info",
+    priority: "activity",
+    icon: first.hasPendingApprovals ? <ShieldQuestionIcon /> : <MessageCircleIcon />,
+    title: childThreadInputsTitle(childThreadInputs),
+    description: childThreadInputs.map((child, index) => (
+      <Fragment key={child.id}>
+        {index > 0 ? " " : null}
+        <span className="whitespace-nowrap">
+          <InlineButton tone="muted" onClick={() => onOpenThread(child.id)}>
+            {child.title}
+          </InlineButton>
+          {index < childThreadInputs.length - 1 ? "," : null}
+        </span>
+      </Fragment>
+    )),
+    actions: (
+      <Button size="xs" variant="ghost" onClick={() => onOpenThread(first.id)}>
+        {first.hasPendingApprovals ? "Open approval" : "Open question"}
+      </Button>
+    ),
+  };
+}
+
+export function goalBannerItem(input: {
+  readonly threadId: ThreadId;
+  readonly goal: OrchestrationV2ProviderGoal;
+  readonly isWorking: boolean;
+  readonly onSendCommand: (text: string, failureMessage: string) => Promise<void>;
+}): ComposerBannerStackItem {
+  const { isWorking, onSendCommand } = input;
+  const presentation = presentProviderGoal(input.goal, isWorking);
+  return {
+    id: `goal:${input.threadId}`,
+    variant: "default",
+    priority: "activity",
+    icon: <TargetIcon />,
+    title:
+      presentation.usage === null
+        ? presentation.title
+        : `${presentation.title} · ${presentation.usage}`,
+    description: presentation.objective,
+    actions: isWorking ? undefined : (
+      <>
+        {presentation.canResume ? (
+          <Button
+            size="xs"
+            variant="ghost"
+            onClick={() => void onSendCommand("/goal resume", "Failed to resume goal.")}
+          >
+            Resume
+          </Button>
+        ) : null}
+        <Button
+          size="xs"
+          variant="ghost"
+          onClick={() => void onSendCommand("/goal clear", "Failed to clear goal.")}
+        >
+          Clear
+        </Button>
+      </>
+    ),
+  };
+}
+
+export function backgroundWorkBannerItem(input: {
+  readonly threadId: ThreadId;
+  readonly tasks: ReadonlyArray<OrchestrationV2PendingBackgroundTask>;
+  readonly stopDisabled: boolean;
+  readonly stopping: boolean;
+  readonly onStop: () => Promise<void>;
+  readonly onOpenThread: (threadId: ThreadId) => void;
+}): ComposerBannerStackItem | null {
+  const { onOpenThread } = input;
+  const presentation = presentPendingBackgroundWork(input.tasks);
+  if (presentation === null) return null;
+  return {
+    id: `background-work:${input.threadId}`,
+    variant: "default",
+    priority: "activity",
+    icon: (
+      <span
+        className={cn(
+          "size-1.5 rounded-full bg-foreground",
+          presentation.waiting && "motion-safe:animate-status-pulse",
+        )}
+        aria-hidden="true"
+      />
+    ),
+    title: presentation.title,
+    description:
+      presentation.items.length === 1 && presentation.items[0]?.childThreadId === undefined
+        ? undefined
+        : presentation.items.map((item, index) => {
+            const childThreadId = item.childThreadId;
+            const separator = index < presentation.items.length - 1 ? "," : null;
+            return (
+              <Fragment key={item.taskId}>
+                {index > 0 ? " " : null}
+                {childThreadId === undefined ? (
+                  <>
+                    {item.label}
+                    {separator}
+                  </>
+                ) : (
+                  <span className="whitespace-nowrap">
+                    <InlineButton
+                      tone="muted"
+                      aria-label={`${presentation.items.length === 1 ? "Show" : "Open"} subagent ${item.label}`}
+                      onClick={() => onOpenThread(childThreadId)}
+                    >
+                      {presentation.items.length === 1 ? "Show" : item.label}
+                    </InlineButton>
+                    {separator}
+                  </span>
+                )}
+              </Fragment>
+            );
+          }),
+    actions: (
+      <Button
+        size="xs"
+        variant="ghost"
+        disabled={input.stopDisabled}
+        onClick={() => void input.onStop()}
+      >
+        {input.stopping ? "Stopping..." : "Stop"}
+      </Button>
+    ),
+  };
+}
+
+/** The local checkout moved off the branch this thread last ran on. */
+export function branchMismatchBannerItem(input: {
+  readonly mismatchKey: string;
+  readonly mismatch: { readonly threadBranch: string; readonly currentBranch: string };
+  readonly restoreDisabled: boolean;
+  readonly restoring: boolean;
+  readonly onRestore: () => void;
+  readonly onDismiss: () => void;
+}): ComposerBannerStackItem {
+  const { mismatch } = input;
+  return {
+    id: `branch-mismatch:${input.mismatchKey}`,
+    variant: "info",
+    icon: <GitBranchIcon />,
+    title: (
+      <span className="flex min-w-0 items-baseline gap-1.5">
+        <span className="shrink-0 font-normal text-muted-foreground">Branch changed — was</span>
+        <Tooltip>
+          <TooltipTrigger
+            render={
+              <code className="min-w-0 truncate font-medium text-foreground">
+                {mismatch.threadBranch}
+              </code>
+            }
+          />
+          <TooltipPopup side="top">
+            This thread last ran on {mismatch.threadBranch}. Sending will continue on{" "}
+            {mismatch.currentBranch}.
+          </TooltipPopup>
+        </Tooltip>
+      </span>
+    ),
+    actions: (
+      <Button size="xs" variant="ghost" disabled={input.restoreDisabled} onClick={input.onRestore}>
+        {input.restoring ? "Restoring..." : "Restore branch"}
+      </Button>
+    ),
+    dismissLabel: "Dismiss branch change notice",
+    onDismiss: input.onDismiss,
+  };
+}
```
