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

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

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

Visibility: public

[refactor\(web\): ChatView's persistent terminal drawers live in their own component](https://git.cafe/pingdotgg/t3code/pull/22681.md)

Version: 9

Head commit: ff41fc5c94b5eecb6d9b6d17dbaebe27b3dc8906

Base commit: f48841f9ef73601c5af03586ca7c334684b92fb5

Comparison base: f48841f9ef73601c5af03586ca7c334684b92fb5

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; +14; −68

```diff
@@ -255,7 +255,6 @@
 import { isEditableFocused } from "../lib/editableFocus";
 import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings";
 import { shortcutLabelForCommand } from "../keybindings";
-import { PersistentThreadTerminalDrawer } from "./PersistentThreadTerminals";
 import {
   AlarmClockIcon,
   CheckCircle2Icon,
@@ -379,7 +378,6 @@
   useThreadHistory,
   useQuietThreadShell,
   useChildThreadInputs,
-  useThreadRefs,
   useThreadVisibleTurnItems,
   useThreadCheckpointSummaries,
   readThreadShell,
@@ -468,7 +466,6 @@
 } from "./chat/draftHeroTransition";
 import type { ComposerDispatchMode } from "@t3tools/client-runtime/state/composer-dispatch";
 import {
-  MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,
   branchMismatchKey,
   buildExpiredTerminalContextToastCopy,
   buildLocalDraftThread,
@@ -490,7 +487,6 @@
   deriveLockedProvider,
   readFileAsDataUrl,
   resolveFileAttachmentUrl,
-  reconcileMountedTerminalThreadIds,
   resolveComposerInteractionMode,
   resolveComposerProviderSelection,
   getAntigravitySendBlockReason,
@@ -547,6 +543,7 @@
 } from "@t3tools/shared/composerContextReferences";
 import { observeResize } from "~/lib/observeResize";
 import { ThreadVisitWatermark } from "./chat/ThreadVisitWatermark";
+import { ChatTerminalDrawers } from "./chat/ChatTerminalDrawers";
 import { useDraftHeroLayoutTransition } from "./chat/useDraftHeroLayoutTransition";
 import { useLocalDispatchState } from "./chat/useLocalDispatchState";
 import {
@@ -1033,31 +1030,12 @@
   const terminalUiState = useTerminalUiStateStore((state) =>
     selectThreadTerminalUiState(state.terminalUiStateByThreadKey, routeThreadRef),
   );
-  const openTerminalThreadKeys = useTerminalUiStateStore(
-    useShallow((state) =>
-      Object.entries(state.terminalUiStateByThreadKey).flatMap(
-        ([nextThreadKey, nextTerminalUiState]) =>
-          nextTerminalUiState.terminalOpen ? [nextThreadKey] : [],
-      ),
-    ),
-  );
   const storeSetTerminalOpen = useTerminalUiStateStore((s) => s.setTerminalOpen);
   const storeEnsureTerminal = useTerminalUiStateStore((state) => state.ensureTerminal);
   const storeSplitTerminal = useTerminalUiStateStore((s) => s.splitTerminal);
   const storeSplitTerminalVertical = useTerminalUiStateStore((s) => s.splitTerminalVertical);
   const storeNewTerminal = useTerminalUiStateStore((s) => s.newTerminal);
   const storeCloseTerminal = useTerminalUiStateStore((s) => s.closeTerminal);
-  const serverThreadRefs = useThreadRefs();
-  const serverThreadKeys = serverThreadRefs.map(scopedThreadKey);
-  const draftThreadsByThreadKey = useComposerDraftStore((store) => store.draftThreadsByThreadKey);
-  const draftThreadKeys = Object.values(draftThreadsByThreadKey).map((draftThread) =>
-    scopedThreadKey(scopeThreadRef(draftThread.environmentId, draftThread.threadId)),
-  );
-  const [mountedTerminalThreadKeys, setMountedTerminalThreadKeys] = useState<string[]>([]);
-  const mountedTerminalThreadRefs = mountedTerminalThreadKeys.flatMap((mountedThreadKey) => {
-    const mountedThreadRef = parseScopedThreadKey(mountedThreadKey);
-    return mountedThreadRef ? [{ key: mountedThreadKey, threadRef: mountedThreadRef }] : [];
-  });
 
   const fallbackDraftProjectRef = draftThread
     ? scopeProjectRef(draftThread.environmentId, draftThread.projectId)
@@ -1310,13 +1288,6 @@
   const rightPanelOpen = rightPanelState.isOpen;
   const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
     usePanelAnimationSettings();
-  const activeTerminalDrawerPresence = usePanelPresence(
-    Boolean(activeThreadKey && terminalUiState.terminalOpen),
-    true,
-    panelAnimationsActive,
-    activeThreadKey,
-    panelAnimationDurationMs,
-  );
   const rightPanelPresenceValue = {
     activeSurface: activeRightPanelSurface,
     surfaces: rightPanelState.surfaces,
@@ -1380,25 +1351,6 @@
     }
   }, [activePreviewMiniPlayer, activePreviewState.sessions, activeThreadRef, previewSessionsReady]);
 
-  const existingOpenTerminalThreadKeys = (() => {
-    const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);
-    return openTerminalThreadKeys.filter((nextThreadKey) => existingThreadKeys.has(nextThreadKey));
-  })();
-  const reconciledMountedTerminalThreadKeys = reconcileMountedTerminalThreadIds({
-    currentThreadIds: mountedTerminalThreadKeys,
-    openThreadIds: existingOpenTerminalThreadKeys,
-    activeThreadId: activeThreadKey,
-    activeThreadTerminalOpen: activeTerminalDrawerPresence.present,
-    maxHiddenThreadCount: MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,
-  });
-  if (
-    reconciledMountedTerminalThreadKeys.length !== mountedTerminalThreadKeys.length ||
-    reconciledMountedTerminalThreadKeys.some(
-      (threadKey, index) => threadKey !== mountedTerminalThreadKeys[index],
-    )
-  ) {
-    setMountedTerminalThreadKeys(reconciledMountedTerminalThreadKeys);
-  }
   const latestRunSettled = isLatestRunSettled(activeLatestRun, activeRuntime);
   const activePlan = deriveActivePlanState(serverProjection, activeActivityRun?.runId);
   const activeActivityRunSettled = isLatestRunSettled(activeActivityRun, activeRuntime);
@@ -8438,25 +8390,19 @@
           </ThreadFindCanvas>
         </div>
 
+        <ChatTerminalDrawers
+          activeThreadKey={activeThreadKey}
+          activeTerminalOpen={Boolean(terminalUiState.terminalOpen)}
+          composerHeight={composerReservedHeight}
+          launchContext={activeTerminalLaunchContext ?? null}
+          focusRequestId={terminalFocusRequestId}
+          splitShortcutLabel={splitTerminalShortcutLabel ?? undefined}
+          splitVerticalShortcutLabel={splitTerminalVerticalShortcutLabel ?? undefined}
+          newShortcutLabel={newTerminalShortcutLabel ?? undefined}
+          closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
+          keybindings={keybindings}
+          onAddTerminalContext={addTerminalContextToDraftStable}
-        {mountedTerminalThreadRefs.map(({ key: mountedThreadKey, threadRef: mountedThreadRef }) => (
-          <PersistentThreadTerminalDrawer
-            key={mountedThreadKey}
-            threadRef={mountedThreadRef}
-            threadId={mountedThreadRef.threadId}
-            active={mountedThreadKey === activeThreadKey}
-            composerHeight={mountedThreadKey === activeThreadKey ? composerReservedHeight : 0}
-            launchContext={
-              mountedThreadKey === activeThreadKey ? (activeTerminalLaunchContext ?? null) : null
-            }
-            focusRequestId={mountedThreadKey === activeThreadKey ? terminalFocusRequestId : 0}
-            splitShortcutLabel={splitTerminalShortcutLabel ?? undefined}
-            splitVerticalShortcutLabel={splitTerminalVerticalShortcutLabel ?? undefined}
-            newShortcutLabel={newTerminalShortcutLabel ?? undefined}
-            closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
-            keybindings={keybindings}
+        />
-            onAddTerminalContext={addTerminalContextToDraft}
-          />
-        ))}
       </div>
 
       {rightPanelPresent && !shouldUsePlanSidebarSheet && activeThreadRef ? (
```

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

Status: added; +109; −0

```diff
@@ -1,0 +1,109 @@
+import type { ResolvedKeybindingsConfig } from "@t3tools/contracts";
+import {
+  parseScopedThreadKey,
+  scopedThreadKey,
+  scopeThreadRef,
+} from "@t3tools/client-runtime/environment";
+import { useState } from "react";
+import { useShallow } from "zustand/react/shallow";
+import { useComposerDraftStore } from "../../composerDraftStore";
+import type { TerminalContextSelection } from "../../lib/terminalContext";
+import { usePanelAnimationSettings, usePanelPresence } from "../../panelAnimations";
+import { useThreadRefs } from "../../state/entities";
+import { useTerminalUiStateStore } from "../../terminalUiStateStore";
+import {
+  MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,
+  reconcileMountedTerminalThreadIds,
+} from "../ChatView.logic";
+import {
+  PersistentThreadTerminalDrawer,
+  type PersistentTerminalLaunchContext,
+} from "../PersistentThreadTerminals";
+
+interface ChatTerminalDrawersProps {
+  readonly activeThreadKey: string | null;
+  readonly activeTerminalOpen: boolean;
+  readonly composerHeight: number;
+  readonly launchContext: PersistentTerminalLaunchContext | null;
+  readonly focusRequestId: number;
+  readonly splitShortcutLabel: string | undefined;
+  readonly splitVerticalShortcutLabel: string | undefined;
+  readonly newShortcutLabel: string | undefined;
+  readonly closeShortcutLabel: string | undefined;
+  readonly keybindings: ResolvedKeybindingsConfig;
+  readonly onAddTerminalContext: (selection: TerminalContextSelection) => void;
+}
+
+/**
+ * Keeps terminal drawers mounted for recently open threads, so switching back
+ * to one finds its sessions as they were. Every thread's terminal and draft
+ * state feeds the reconcile, so it subscribes here instead of in ChatView.
+ */
+export function ChatTerminalDrawers(props: ChatTerminalDrawersProps) {
+  const { activeThreadKey } = props;
+  const openTerminalThreadKeys = useTerminalUiStateStore(
+    useShallow((state) =>
+      Object.entries(state.terminalUiStateByThreadKey).flatMap(
+        ([nextThreadKey, nextTerminalUiState]) =>
+          nextTerminalUiState.terminalOpen ? [nextThreadKey] : [],
+      ),
+    ),
+  );
+  const serverThreadRefs = useThreadRefs();
+  const serverThreadKeys = serverThreadRefs.map(scopedThreadKey);
+  const draftThreadsByThreadKey = useComposerDraftStore((store) => store.draftThreadsByThreadKey);
+  const draftThreadKeys = Object.values(draftThreadsByThreadKey).map((draftThread) =>
+    scopedThreadKey(scopeThreadRef(draftThread.environmentId, draftThread.threadId)),
+  );
+  const { active: panelAnimationsActive, durationMs: panelAnimationDurationMs } =
+    usePanelAnimationSettings();
+  const activeTerminalDrawerPresence = usePanelPresence(
+    Boolean(activeThreadKey && props.activeTerminalOpen),
+    true,
+    panelAnimationsActive,
+    activeThreadKey,
+    panelAnimationDurationMs,
+  );
+  const [mountedTerminalThreadKeys, setMountedTerminalThreadKeys] = useState<string[]>([]);
+  const existingOpenTerminalThreadKeys = (() => {
+    const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);
+    return openTerminalThreadKeys.filter((nextThreadKey) => existingThreadKeys.has(nextThreadKey));
+  })();
+  const reconciledMountedTerminalThreadKeys = reconcileMountedTerminalThreadIds({
+    currentThreadIds: mountedTerminalThreadKeys,
+    openThreadIds: existingOpenTerminalThreadKeys,
+    activeThreadId: activeThreadKey,
+    activeThreadTerminalOpen: activeTerminalDrawerPresence.present,
+    maxHiddenThreadCount: MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,
+  });
+  if (
+    reconciledMountedTerminalThreadKeys.length !== mountedTerminalThreadKeys.length ||
+    reconciledMountedTerminalThreadKeys.some(
+      (threadKey, index) => threadKey !== mountedTerminalThreadKeys[index],
+    )
+  ) {
+    setMountedTerminalThreadKeys(reconciledMountedTerminalThreadKeys);
+  }
+  const mountedTerminalThreadRefs = mountedTerminalThreadKeys.flatMap((mountedThreadKey) => {
+    const mountedThreadRef = parseScopedThreadKey(mountedThreadKey);
+    return mountedThreadRef ? [{ key: mountedThreadKey, threadRef: mountedThreadRef }] : [];
+  });
+
+  return mountedTerminalThreadRefs.map(({ key: mountedThreadKey, threadRef: mountedThreadRef }) => (
+    <PersistentThreadTerminalDrawer
+      key={mountedThreadKey}
+      threadRef={mountedThreadRef}
+      threadId={mountedThreadRef.threadId}
+      active={mountedThreadKey === activeThreadKey}
+      composerHeight={mountedThreadKey === activeThreadKey ? props.composerHeight : 0}
+      launchContext={mountedThreadKey === activeThreadKey ? props.launchContext : null}
+      focusRequestId={mountedThreadKey === activeThreadKey ? props.focusRequestId : 0}
+      splitShortcutLabel={props.splitShortcutLabel}
+      splitVerticalShortcutLabel={props.splitVerticalShortcutLabel}
+      newShortcutLabel={props.newShortcutLabel}
+      closeShortcutLabel={props.closeShortcutLabel}
+      keybindings={props.keybindings}
+      onAddTerminalContext={props.onAddTerminalContext}
+    />
+  ));
+}
```
