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

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

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

Visibility: public

[feat\(web\): attach composer state drawers](https://git.cafe/pingdotgg/t3code/pull/7150.md)

Version: 117

Head commit: 0fbf449b86a430d9b4d99a954a807ade37329e08

Base commit: 105cd5e0c57ab8b6df3bd8af5b534a5bc682fdce

Comparison base: 105cd5e0c57ab8b6df3bd8af5b534a5bc682fdce

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

## apps/mobile/src/components/AppSymbol.tsx

Status: modified; +4; −0

```diff
@@ -2,6 +2,7 @@
   IconAdjustmentsHorizontal,
   IconAlertCircle,
   IconAlertTriangle,
+  IconApps,
   IconArchive,
   IconArrowBackUp,
   IconArrowDownCircle,
@@ -13,6 +14,7 @@
   IconArrowsMaximize,
   IconBellRinging,
   IconBolt,
+  IconBox,
   IconCamera,
   IconChartBar,
   IconCheck,
@@ -104,6 +106,7 @@
   checkmark: IconCheck,
   "checkmark.circle": IconCircleCheck,
   clock: IconClock,
+  cube: IconBox,
   "chevron.down": IconChevronDown,
   "chevron.left": IconChevronLeft,
   "chevron.left.forwardslash.chevron.right": IconCode,
@@ -141,6 +144,7 @@
   "sidebar.right": IconLayoutSidebarRight,
   "slider.horizontal.3": IconAdjustmentsHorizontal,
   "square.and.pencil": IconEdit,
+  "square.grid.2x2": IconApps,
   "square.split.2x1": IconLayoutColumns,
   "sun.max": IconSun,
   "stop.fill": IconPlayerStopFilled,
```

## apps/mobile/src/features/threads/ComposerCommandPopover.tsx

Status: modified; +18; −5

```diff
@@ -1,9 +1,13 @@
+import {
+  resolveProviderSkillSourceKind,
-import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger";
+  type ProviderSkillSourceKind,
+} from "@t3tools/client-runtime/providerSkills";
 import type { ServerProviderSkill, ServerProviderSlashCommand } from "@t3tools/contracts";
-import { SymbolView } from "../../components/AppSymbol";
+import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger";
 import { memo } from "react";
 import { Pressable, ScrollView, View, type ViewStyle } from "react-native";
 
+import { SymbolView, type AppSymbolName } from "../../components/AppSymbol";
 import { AppText as Text } from "../../components/AppText";
 import { GlassSurface } from "../../components/GlassSurface";
 import { PierreEntryIcon } from "../../components/PierreEntryIcon";
@@ -61,13 +65,22 @@
   );
 }
 
+const SKILL_SOURCE_SYMBOL_BY_KIND: Record<ProviderSkillSourceKind, AppSymbolName> = {
+  app: "square.grid.2x2",
+  repo: "folder",
+  project: "folder",
+  personal: "person.crop.circle",
+  system: "gearshape",
+  other: "cube",
+};
+
+function itemIcon(item: ComposerCommandItem): AppSymbolName | null {
-function itemIcon(item: ComposerCommandItem) {
   switch (item.type) {
     case "slash-command":
     case "provider-slash-command":
+      return "terminal";
-      return "terminal" as const;
     case "skill":
-      return "cube" as const;
+      return SKILL_SOURCE_SYMBOL_BY_KIND[resolveProviderSkillSourceKind(item.skill)];
     case "path":
       return null;
   }
```

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

Status: modified; +18; −5

```diff
@@ -4183,6 +4183,14 @@
   // partition (same shell, same capability gate, same PR auto-settle input)
   // so the banner and the sidebar row never disagree.
   const activeThreadShell = useThreadShell(isServerThread ? activeThreadRef : null);
+  const activeComposerTasksProgress =
+    activeLatestTurn !== null && !latestTurnSettled
+      ? (activeThreadShell?.planProgress ?? null)
+      : null;
+  const activeComposerTaskSteps =
+    activeComposerTasksProgress && activePlan && activePlan.turnId === activeLatestTurn?.turnId
+      ? activePlan.steps
+      : null;
   const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays);
   const autoSettleOnMerge = useClientSettings((settings) => settings.sidebarAutoSettleOnMerge);
   const activeThreadPr = resolveDisplayedThreadPr({
@@ -4519,13 +4527,13 @@
       ),
       title: working
         ? liveCount > 0
-          ? `${liveCount} ${liveCount === 1 ? "agent" : "agents"} working in the background`
+          ? `${liveCount} ${liveCount === 1 ? "agent" : "agents"} working`
+          : "Background work"
+        : "Monitoring",
-          : "Background work running"
-        : "Monitoring in the background",
       actions: (
         <Button
           size="xs"
-          variant="outline"
+          variant="ghost"
           disabled={isStoppingBackgroundWork}
           onClick={() => void handleStopBackgroundWork()}
         >
@@ -4687,7 +4695,6 @@
     systemComposerBannerItems,
     wokeThreadBannerItem,
   ]);
-
   useEffect(() => {
     setPendingServerThreadEnvMode(null);
     setPendingServerThreadBranch(undefined);
@@ -6282,6 +6289,8 @@
     setDragActive: setIsWorkspaceFileDragActive,
     addFiles: (files) => composerRef.current?.addDroppedFiles(files),
   });
+  const externalComposerDrawerAttached =
+    composerBannerItems.length > 0 || Boolean(threadSyncPhase && !activeEnvironmentUnavailable);
 
   return (
     <div className="relative flex min-h-0 min-w-0 flex-1 overflow-hidden bg-background">
@@ -6485,6 +6494,7 @@
                     <div
                       className={cn(
                         "chat-composer-glass-shell relative mx-auto w-full max-w-3xl",
+                        externalComposerDrawerAttached && "chat-composer-glass-shell-attached",
                         showComposerContextStrip && "chat-composer-glass-shell-with-context",
                       )}
                     >
@@ -6509,6 +6519,7 @@
                             isSendBusy={isSendBusy}
                             sendDisabledReason={threadDetailLoading ? "Messages loading" : null}
                             isPreparingWorktree={isPreparingWorktree}
+                            externalDrawerAttached={externalComposerDrawerAttached}
                             environmentUnavailable={activeEnvironmentUnavailableState}
                             activePendingApproval={activePendingApproval}
                             pendingApprovals={pendingApprovals}
@@ -6521,6 +6532,8 @@
                             respondingRequestIds={respondingRequestIds}
                             showPlanFollowUpPrompt={showPlanFollowUpPrompt}
                             activeProposedPlan={activeProposedPlan}
+                            activeTasksProgress={activeComposerTasksProgress}
+                            activeTaskSteps={activeComposerTaskSteps}
                             runtimeMode={runtimeMode}
                             interactionMode={interactionMode}
                             lockedProvider={lockedProvider}
```

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

Status: modified; +3; −3

```diff
@@ -73,13 +73,13 @@
 import {
   COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME,
   COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
+  COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
   COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,
-  COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME,
   SKILL_CHIP_ICON_SVG,
 } from "./composerInlineChip";
 import { FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip";
 import { ComposerPendingTerminalContextChip } from "./chat/ComposerPendingTerminalContexts";
-import { formatProviderSkillDisplayName } from "~/providerSkillPresentation";
+import { formatProviderSkillDisplayName } from "@t3tools/client-runtime/providerSkills";
 import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
 import { registerComposerInlineTokenPaste } from "./composerInlineTokenPaste";
 
@@ -257,7 +257,7 @@
         className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
         dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }}
       />
+      <span className={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}>{props.skillLabel}</span>
-      <span className={COMPOSER_INLINE_SKILL_CHIP_LABEL_CLASS_NAME}>{props.skillLabel}</span>
     </span>
   );
 
```

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

Status: modified; +615; −469

```diff
@@ -10,6 +10,7 @@
   ScopedThreadRef,
   ServerProvider,
   ThreadId,
+  TurnId,
 } from "@t3tools/contracts";
 import {
   isProviderSendTurnSupportedImageMimeType,
@@ -65,6 +66,12 @@
 } from "../../promptStashStore";
 import { ComposerStashBadge } from "./ComposerStashBadge";
 import { ComposerStashMenu } from "./ComposerStashMenu";
+import {
+  ComposerTasksBadge,
+  ComposerTasksDrawer,
+  type ComposerTaskStep,
+  type ComposerTasksProgress,
+} from "./ComposerTasksBadge";
 import { compressImageForStash, compressImageToByteLimit } from "../../lib/imageCompression";
 import { isCommandPaletteOpen } from "../../commandPaletteBus";
 import { getTerminalFocusOwner } from "../../lib/terminalFocus";
@@ -143,12 +150,27 @@
     }
 
     const updatePosition = () => {
+      const form = anchor.closest<HTMLElement>('[data-chat-composer-form="true"]');
+      const mainSurface = form?.querySelector<HTMLElement>(
-      const rect = anchor.getBoundingClientRect();
+        '[data-chat-composer-main-surface="true"]',
+      );
+      const rect = (mainSurface ?? form ?? anchor).getBoundingClientRect();
+      const rootFontSizePx =
+        Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16;
+      const drawerInsetRem =
+        Number.parseFloat(
+          window.getComputedStyle(form ?? anchor).getPropertyValue("--chat-composer-drawer-inset"),
+        ) || 1.375;
+      const drawerInset = drawerInsetRem * rootFontSizePx;
+      // One extra pixel prevents fractional layout coordinates from exposing
+      // the canvas between the drawer mask and the composer's foreground edge.
+      // Mirrors --chat-composer-attachment-overlap: calc(1rem + 1px).
+      const composerOverlap = rootFontSizePx + 1;
       const next = {
+        bottom: window.innerHeight - rect.top - composerOverlap,
+        left: rect.left + drawerInset,
-        bottom: window.innerHeight - rect.top + 8,
-        left: rect.left,
-        maxHeight: Math.max(96, rect.top - 24),
-        width: rect.width,
+        maxHeight: Math.max(96, rect.top - 24 + composerOverlap),
+        width: Math.max(0, rect.width - drawerInset * 2),
       };
       setPosition((current) =>
         current && composerCommandMenuPositionsEqual(current, next) ? current : next,
@@ -184,6 +206,7 @@
   return createPortal(
     <div
       className="pointer-events-auto fixed z-[70]"
+      data-composer-drawer-layer="true"
       style={{
         bottom: position.bottom,
         left: position.left,
@@ -228,7 +251,7 @@
 import type { PendingUserInputDraftAnswer } from "../../pendingUserInput";
 import type { PendingApproval, PendingUserInput } from "../../session-logic";
 import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow";
-import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";
+import { formatProviderSkillDisplayName } from "@t3tools/client-runtime/providerSkills";
 import { searchProviderSkills } from "../../providerSkillSearch";
 import { useMediaQuery } from "../../hooks/useMediaQuery";
 import type { ReviewCommentContext } from "../../reviewCommentContext";
@@ -261,6 +284,7 @@
 
 const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[];
 const COMPOSER_FLOATING_LAYER_SELECTOR = [
+  '[data-composer-drawer-layer="true"]',
   '[data-slot="popover-popup"]',
   '[data-slot="menu-popup"]',
   '[data-slot="select-popup"]',
@@ -523,6 +547,7 @@
   isSendBusy: boolean;
   sendDisabledReason: string | null;
   isPreparingWorktree: boolean;
+  externalDrawerAttached: boolean;
   environmentUnavailable: {
     readonly label: string;
     readonly connection: EnvironmentConnectionPresentation;
@@ -548,6 +573,8 @@
   // Plan
   showPlanFollowUpPrompt: boolean;
   activeProposedPlan: Thread["proposedPlans"][number] | null;
+  activeTasksProgress: ComposerTasksProgress | null;
+  activeTaskSteps: readonly ComposerTaskStep[] | null;
 
   // Mode
   runtimeMode: RuntimeMode;
@@ -642,6 +669,8 @@
     respondingRequestIds,
     showPlanFollowUpPrompt,
     activeProposedPlan,
+    activeTasksProgress,
+    activeTaskSteps,
     runtimeMode,
     interactionMode,
     lockedProvider,
@@ -960,6 +989,8 @@
   );
   const [composerMenuAnchor, setComposerMenuAnchor] = useState<HTMLDivElement | null>(null);
   const [isStashMenuOpen, setIsStashMenuOpen] = useState(false);
+  const [isTasksDrawerOpen, setIsTasksDrawerOpen] = useState(false);
+  const [dismissedTasksTurnId, setDismissedTasksTurnId] = useState<TurnId | null>(null);
   const [stashPulse, setStashPulse] = useState<{ key: number; active: boolean }>({
     key: 0,
     active: false,
@@ -1146,10 +1177,10 @@
 
   const isComposerApprovalState = activePendingApproval !== null;
   const activePendingUserInput = pendingUserInputs[0] ?? null;
-  const hasComposerHeader =
+  const showComposerTopDrawer =
     isComposerApprovalState ||
     pendingUserInputs.length > 0 ||
-    (showPlanFollowUpPrompt && activeProposedPlan !== null);
+    (!isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan !== null);
   const showCollapsedMobilePromptRow =
     isComposerCollapsedMobile && !isComposerApprovalState && pendingUserInputs.length === 0;
 
@@ -2274,6 +2305,78 @@
   const toggleStashMenu = useCallback(() => {
     setIsStashMenuOpen((open) => !open);
   }, []);
+  const toggleInlineStashMenu = useCallback(() => {
+    if (isComposerCollapsedMobile) {
+      expandMobileComposer();
+      setIsStashMenuOpen(true);
+      return;
+    }
+    toggleStashMenu();
+  }, [expandMobileComposer, isComposerCollapsedMobile, toggleStashMenu]);
+  const toggleTasksDrawer = useCallback(() => {
+    setIsTasksDrawerOpen((open) => !open);
+  }, []);
+  const activeTasksTurnId = activeThread?.latestTurn?.turnId ?? null;
+  const tasksDismissedForActiveTurn =
+    activeTasksTurnId !== null && dismissedTasksTurnId === activeTasksTurnId;
+  const visibleTasksProgress = tasksDismissedForActiveTurn ? null : activeTasksProgress;
+  const visibleTaskSteps = tasksDismissedForActiveTurn ? null : activeTaskSteps;
+  const hasBlockingComposerTopDrawer =
+    activePendingApproval !== null || pendingUserInputs.length > 0;
+  const dismissTasks = useCallback(() => {
+    if (activeTasksTurnId !== null) {
+      setDismissedTasksTurnId(activeTasksTurnId);
+    }
+    setIsTasksDrawerOpen(false);
+  }, [activeTasksTurnId]);
+  const showInlineStashBadge =
+    stashQueue.length > 0 &&
+    !isComposerApprovalState &&
+    (props.externalDrawerAttached ||
+      showComposerTopDrawer ||
+      isTasksDrawerOpen ||
+      isComposerCollapsedMobile);
+  const inlineStashBadge = showInlineStashBadge ? (
+    <ComposerStashBadge
+      count={stashQueue.length}
+      menuOpen={isStashMenuOpen}
+      placement="inline"
+      pulseKey={stashPulse.key}
+      pulsing={stashPulse.active}
+      onToggleMenu={toggleInlineStashMenu}
+    />
+  ) : null;
+  const showInlineTasksBadge =
+    visibleTasksProgress !== null &&
+    visibleTaskSteps !== null &&
+    !isTasksDrawerOpen &&
+    !hasBlockingComposerTopDrawer &&
+    (props.externalDrawerAttached || showComposerTopDrawer || isComposerCollapsedMobile);
+  const inlineTasksBadge = showInlineTasksBadge ? (
+    <ComposerTasksBadge
+      expanded={false}
+      onDismiss={dismissTasks}
+      onToggle={toggleTasksDrawer}
+      placement="inline"
+      progress={visibleTasksProgress}
+      steps={visibleTaskSteps}
+    />
+  ) : null;
+  useEffect(() => {
+    if (visibleTasksProgress === null || visibleTaskSteps === null) {
+      setIsTasksDrawerOpen(false);
+    }
+  }, [visibleTaskSteps, visibleTasksProgress]);
+
+  useEffect(() => {
+    if (hasBlockingComposerTopDrawer) {
+      setIsTasksDrawerOpen(false);
+    }
+  }, [hasBlockingComposerTopDrawer]);
+
+  useEffect(() => {
+    setIsTasksDrawerOpen(false);
+  }, [activeThreadId]);
 
   // Close the stash menu whenever the trigger-driven command menu opens so
   // the two popovers never stack in the same layer, and when the user
@@ -2301,13 +2404,14 @@
       // even when the composer is in a state that can't stash.
       event.preventDefault();
       event.stopPropagation();
+      if (isCommandPaletteOpen()) {
+        return;
+      }
+      if (pendingUserInputs.length > 0 && !isComposerApprovalState) {
+        setIsStashMenuOpen((open) => !open);
-      if (
+        return;
+      }
+      if (isComposerApprovalState || projectSelectionRequired || activePendingProgress !== null) {
-        isCommandPaletteOpen() ||
-        isComposerApprovalState ||
-        pendingUserInputs.length > 0 ||
-        projectSelectionRequired ||
-        activePendingProgress !== null
-      ) {
         return;
       }
       void stashCurrentPrompt();
@@ -2472,7 +2576,7 @@
     },
   });
 
-  const onComposerMentionDragLeaveCapture = (event: React.DragEvent<HTMLDivElement>) => {
+  const onComposerMentionDragLeaveCapture = (event: React.DragEvent<HTMLFormElement>) => {
     if (!dataTransferHasComposerMention(event.dataTransfer.types)) return;
     event.stopPropagation();
     const nextTarget = event.relatedTarget;
@@ -2514,14 +2618,15 @@
         return;
       }
       const composerSurface = composerSurfaceRef.current;
+      const composerForm = composerFormRef.current;
       const activeElement = document.activeElement;
       if (activeElement instanceof Element && isInsideComposerFloatingLayer(activeElement)) {
         return;
       }
       if (
-        composerSurface &&
         activeElement instanceof Node &&
+        ((composerSurface && composerSurface.contains(activeElement)) ||
-        composerSurface.contains(activeElement)
+          (composerForm && composerForm.contains(activeElement)))
       ) {
         return;
       }
@@ -2686,85 +2791,73 @@
     <form
       ref={composerFormRef}
       onSubmit={submitComposer}
+      onFocusCapture={(event) => {
+        const activeElement = event.target;
+        if (
+          isComposerCollapsedMobile &&
+          activeElement instanceof HTMLElement &&
+          activeElement.closest('[data-chat-composer-collapsed-controls="true"]')
+        ) {
+          return;
+        }
+        if (composerBlurFrameRef.current !== null) {
+          window.cancelAnimationFrame(composerBlurFrameRef.current);
+          composerBlurFrameRef.current = null;
+        }
+        setIsComposerFocused(true);
+      }}
+      onBlurCapture={() => {
+        scheduleComposerCollapseCheck();
+      }}
+      onDragEnterCapture={composerMentionDragHandlers.onDragEnter}
+      onDragOverCapture={composerMentionDragHandlers.onDragOver}
+      onDragLeaveCapture={onComposerMentionDragLeaveCapture}
+      onDropCapture={composerMentionDragHandlers.onDrop}
       className="mx-auto w-full min-w-0 max-w-3xl"
       data-chat-composer-form="true"
     >
-      <div
-        className={cn(
-          "group rounded-[22px] p-px transition-colors duration-200",
-          composerProviderState.composerFrameClassName,
-        )}
-        onDragEnterCapture={composerMentionDragHandlers.onDragEnter}
-        onDragOverCapture={composerMentionDragHandlers.onDragOver}
-        onDragLeaveCapture={onComposerMentionDragLeaveCapture}
+      {showComposerTopDrawer && (!isTasksDrawerOpen || hasBlockingComposerTopDrawer) ? (
-        onDropCapture={composerMentionDragHandlers.onDrop}
-      >
         <div
-          ref={composerSurfaceRef}
-          data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"}
-          className={cn(
-            "rounded-[20px] transition-[background-color] duration-200",
-            isDragOverComposer ? "bg-accent/45 ring-1 ring-primary/70" : null,
-            projectSelectionRequired ? "opacity-75" : null,
-            composerProviderState.composerSurfaceClassName,
-          )}
-          onFocusCapture={(event) => {
-            const activeElement = event.target;
-            if (
-              isComposerCollapsedMobile &&
-              activeElement instanceof HTMLElement &&
+          className="chat-composer-top-drawer"
+          data-chat-composer-top-drawer="true"
+          data-variant={activePendingApproval ? "warning" : "info"}
-              activeElement.closest('[data-chat-composer-collapsed-controls="true"]')
-            ) {
-              return;
-            }
-            if (composerBlurFrameRef.current !== null) {
-              window.cancelAnimationFrame(composerBlurFrameRef.current);
-              composerBlurFrameRef.current = null;
-            }
-            setIsComposerFocused(true);
-          }}
-          onBlurCapture={() => {
-            scheduleComposerCollapseCheck();
-          }}
         >
+          {!isComposerCollapsedMobile && activePendingApproval ? (
+            <div className="flex min-w-0 flex-wrap items-center gap-1 px-3 py-1.5 sm:px-4">
+              <ComposerPendingApprovalPanel
+                approval={activePendingApproval}
+                pendingCount={pendingApprovals.length}
+              />
+              <div className="flex min-w-0 flex-wrap items-center gap-0.5">
+                <ComposerPendingApprovalActions
+                  requestId={activePendingApproval.requestId}
+                  isResponding={respondingRequestIds.includes(activePendingApproval.requestId)}
-          {!isComposerCollapsedMobile &&
-            (activePendingApproval ? (
-              <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
-                <ComposerPendingApprovalPanel
-                  approval={activePendingApproval}
-                  pendingCount={pendingApprovals.length}
+                  onRespondToApproval={onRespondToApproval}
                 />
               </div>
-            ) : pendingUserInputs.length > 0 ? (
+            </div>
+          ) : !isComposerCollapsedMobile && pendingUserInputs.length > 0 ? (
+            <ComposerPendingUserInputPanel
+              pendingUserInputs={pendingUserInputs}
+              respondingRequestIds={respondingRequestIds}
+              answers={activePendingDraftAnswers}
+              questionIndex={activePendingQuestionIndex}
+              onToggleOption={onSelectActivePendingUserInputOption}
+              onAdvance={onAdvanceActivePendingUserInput}
+            />
+          ) : !isComposerCollapsedMobile && showPlanFollowUpPrompt && activeProposedPlan ? (
+            <ComposerPlanFollowUpBanner
+              key={activeProposedPlan.id}
+              planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null}
+            />
+          ) : isComposerCollapsedMobile && activePendingApproval ? (
+            <div data-chat-composer-collapsed-controls="true">
-              <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
-                <ComposerPendingUserInputPanel
-                  pendingUserInputs={pendingUserInputs}
-                  respondingRequestIds={respondingRequestIds}
-                  answers={activePendingDraftAnswers}
-                  questionIndex={activePendingQuestionIndex}
-                  onToggleOption={onSelectActivePendingUserInputOption}
-                  onAdvance={onAdvanceActivePendingUserInput}
-                />
-              </div>
-            ) : showPlanFollowUpPrompt && activeProposedPlan ? (
-              <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
-                <ComposerPlanFollowUpBanner
-                  key={activeProposedPlan.id}
-                  planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null}
-                />
-              </div>
-            ) : null)}
-
-          {isComposerCollapsedMobile && activePendingApproval ? (
-            <div
-              className="rounded-t-[19px] border-b border-border/65 bg-muted/20"
-              data-chat-composer-collapsed-controls="true"
-            >
               <ComposerPendingApprovalPanel
                 approval={activePendingApproval}
                 pendingCount={pendingApprovals.length}
+                className="px-3 pt-2 sm:px-4"
               />
-              <div className="flex flex-wrap items-center justify-end gap-2 px-3 pb-3 sm:px-4">
+              <div className="flex flex-wrap items-center justify-end gap-1 px-3 pt-2 pb-3 sm:px-4">
                 <ComposerPendingApprovalActions
                   requestId={activePendingApproval.requestId}
                   isResponding={respondingRequestIds.includes(activePendingApproval.requestId)}
@@ -2773,10 +2866,7 @@
               </div>
             </div>
           ) : isComposerCollapsedMobile && pendingUserInputs.length > 0 ? (
-            <div
-              className="rounded-t-[19px] border-b border-border/65 bg-muted/20"
-              data-chat-composer-collapsed-controls="true"
-            >
+            <div data-chat-composer-collapsed-controls="true">
               <ComposerPendingUserInputPanel
                 pendingUserInputs={pendingUserInputs}
                 respondingRequestIds={respondingRequestIds}
@@ -2806,6 +2896,8 @@
                   >
                     {activePendingProgress?.customAnswer || "Write custom answer"}
                   </button>
+                  {inlineTasksBadge}
+                  {inlineStashBadge}
                   {activePendingProgress?.activeQuestion?.multiSelect ? (
                     <ComposerPrimaryActions
                       compact
@@ -2833,269 +2925,450 @@
               </div>
             </div>
           ) : null}
-
-          {showCollapsedMobilePromptRow ? (
-            <div className="flex items-center justify-between gap-2 px-3 py-2">
-              <button
-                type="button"
-                className={cn(
-                  "min-w-0 flex-1 truncate bg-transparent p-0 text-left text-[14px] focus:outline-none",
-                  (activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
-                    ? "text-foreground"
-                    : "text-placeholder",
-                )}
-                onPointerDown={(event) => event.preventDefault()}
-                onClick={expandMobileComposer}
-                aria-label="Expand composer"
-              >
-                {activePendingProgress
-                  ? activePendingProgress.customAnswer ||
-                    "Type your own answer, or leave this blank to use the selected option"
-                  : prompt.trim() ||
-                    (noProviderAvailable ? "Enable a provider in Settings" : "Ask anything...")}
-              </button>
-              <button
-                type="button"
-                className="flex size-8 shrink-0 items-center justify-center rounded-full bg-message-action text-message-action-foreground hover:bg-message-action-hover disabled:opacity-30"
-                disabled={collapsedComposerPrimaryActionDisabled}
-                aria-label={collapsedComposerPrimaryActionLabel}
-                onPointerDown={(event) => event.preventDefault()}
-                onClick={(event) => {
-                  event.stopPropagation();
-                  submitComposer();
-                }}
-              >
-                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
+        </div>
+      ) : null}
+      {isTasksDrawerOpen &&
+      !hasBlockingComposerTopDrawer &&
+      visibleTasksProgress &&
-                  <path
-                    d="M8 3L8 13M8 3L4 7M8 3L12 7"
-                    stroke="currentColor"
-                    strokeWidth="2"
-                    strokeLinecap="round"
-                    strokeLinejoin="round"
-                  />
-                </svg>
-              </button>
-            </div>
-          ) : null}
-
+      visibleTaskSteps ? (
+        <ComposerTasksDrawer
+          onDismiss={dismissTasks}
+          onCollapse={toggleTasksDrawer}
+          progress={visibleTasksProgress}
+          steps={visibleTaskSteps}
+        />
+      ) : null}
+      <div className="relative">
+        {visibleTasksProgress &&
+        visibleTaskSteps &&
+        !isTasksDrawerOpen &&
+        !props.externalDrawerAttached &&
+        !showComposerTopDrawer &&
+        !isComposerCollapsedMobile ? (
+          <ComposerTasksBadge
+            expanded={false}
+            hasTrailingShoulder={stashQueue.length > 0}
+            onDismiss={dismissTasks}
+            onToggle={toggleTasksDrawer}
+            progress={visibleTasksProgress}
+            steps={visibleTaskSteps}
+          />
+        ) : null}
+        {!props.externalDrawerAttached &&
+        !showComposerTopDrawer &&
+        !isTasksDrawerOpen &&
+        !isComposerCollapsedMobile ? (
+          <ComposerStashBadge
+            count={stashQueue.length}
+            menuOpen={isStashMenuOpen}
+            pulseKey={stashPulse.key}
+            pulsing={stashPulse.active}
+            onToggleMenu={toggleStashMenu}
+          />
+        ) : null}
+        <div
+          data-chat-composer-main-surface="true"
+          className={cn(
+            "group relative z-10 rounded-[22px] p-px transition-colors duration-200",
+            composerProviderState.composerFrameClassName,
+          )}
+        >
           <div
-            ref={setComposerMenuAnchor}
+            ref={composerSurfaceRef}
+            data-chat-composer-surface="true"
+            data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"}
             className={cn(
-              "relative px-3 pb-2 sm:px-4",
-              hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4",
-              isComposerCollapsedMobile && "hidden",
+              "rounded-[20px] transition-[background-color] duration-200",
+              isDragOverComposer ? "bg-accent/45 ring-1 ring-primary/70" : null,
+              projectSelectionRequired ? "opacity-75" : null,
+              composerProviderState.composerSurfaceClassName,
             )}
           >
+            {showCollapsedMobilePromptRow ? (
+              <div className="flex items-center justify-between gap-2 px-3 py-2">
+                <button
+                  type="button"
+                  className={cn(
+                    "min-w-0 flex-1 truncate bg-transparent p-0 text-left text-[14px] focus:outline-none",
+                    (activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
+                      ? "text-foreground"
+                      : "text-placeholder",
+                  )}
+                  onPointerDown={(event) => event.preventDefault()}
+                  onClick={expandMobileComposer}
+                  aria-label="Expand composer"
+                >
+                  {activePendingProgress
+                    ? activePendingProgress.customAnswer ||
+                      "Type your own answer, or leave this blank to use the selected option"
+                    : prompt.trim() ||
+                      (noProviderAvailable ? "Enable a provider in Settings" : "Ask anything...")}
+                </button>
+                {inlineTasksBadge}
+                {inlineStashBadge}
+                <button
+                  type="button"
+                  className="flex size-8 shrink-0 items-center justify-center rounded-full bg-message-action text-message-action-foreground hover:bg-message-action-hover disabled:opacity-30"
+                  disabled={collapsedComposerPrimaryActionDisabled}
+                  aria-label={collapsedComposerPrimaryActionLabel}
+                  onPointerDown={(event) => event.preventDefault()}
+                  onClick={(event) => {
+                    event.stopPropagation();
+                    submitComposer();
+                  }}
+                >
+                  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
+                    <path
+                      d="M8 3L8 13M8 3L4 7M8 3L12 7"
+                      stroke="currentColor"
+                      strokeWidth="2"
+                      strokeLinecap="round"
+                      strokeLinejoin="round"
-            <ComposerStashBadge
-              count={stashQueue.length}
-              pulseKey={stashPulse.key}
-              pulsing={stashPulse.active}
-              menuOpen={isStashMenuOpen}
-              onToggleMenu={toggleStashMenu}
-            />
+                    />
+                  </svg>
+                </button>
+              </div>
+            ) : null}
 
+            <div
-            {isStashMenuOpen && !composerMenuOpen && !isComposerApprovalState && (
-              <ComposerCommandMenuLayer anchor={composerMenuAnchor}>
+              ref={setComposerMenuAnchor}
+              className={cn(
-                <ComposerStashMenu
-                  entries={stashQueue}
-                  onRestore={restoreStashEntry}
-                  onDelete={deleteStashEntry}
-                  onClose={() => setIsStashMenuOpen(false)}
-                />
-              </ComposerCommandMenuLayer>
-            )}
+                "relative px-3 pb-2 sm:px-4",
+                "pt-3.5 sm:pt-4",
+                isComposerApprovalState && "pb-3 sm:pb-4",
+                isComposerCollapsedMobile && "hidden",
+              )}
+            >
+              {isStashMenuOpen && !composerMenuOpen && !isComposerApprovalState && (
+                <ComposerCommandMenuLayer anchor={composerMenuAnchor}>
+                  <ComposerStashMenu
+                    entries={stashQueue}
+                    onRestore={restoreStashEntry}
+                    onDelete={deleteStashEntry}
+                    onClose={() => setIsStashMenuOpen(false)}
+                  />
+                </ComposerCommandMenuLayer>
+              )}
 
-            {composerMenuOpen && !isComposerApprovalState && (
+              {composerMenuOpen && !isComposerApprovalState && (
+                <ComposerCommandMenuLayer anchor={composerMenuAnchor}>
+                  <ComposerCommandMenu
+                    items={composerMenuItems}
+                    resolvedTheme={resolvedTheme}
+                    isLoading={isComposerMenuLoading}
+                    triggerKind={composerTriggerKind}
+                    emptyStateText={composerMenuEmptyState}
+                    activeItemId={activeComposerMenuItem?.id ?? null}
-              <ComposerCommandMenuLayer anchor={composerMenuAnchor}>
-                <ComposerCommandMenu
-                  items={composerMenuItems}
-                  resolvedTheme={resolvedTheme}
+                    onHighlightedItemChange={onComposerMenuItemHighlighted}
+                    onSelect={onSelectComposerItem}
+                  />
+                </ComposerCommandMenuLayer>
-                  isLoading={isComposerMenuLoading}
-                  triggerKind={composerTriggerKind}
-                  groupSlashCommandSections={
-                    composerTrigger?.kind === "slash-command" &&
-                    composerTrigger.query.trim().length === 0
-                  }
-                  emptyStateText={composerMenuEmptyState}
-                  activeItemId={activeComposerMenuItem?.id ?? null}
-                  onHighlightedItemChange={onComposerMenuItemHighlighted}
-                  onSelect={onSelectComposerItem}
-                />
-              </ComposerCommandMenuLayer>
-            )}
-
-            {!isComposerCollapsedMobile &&
-              !isComposerApprovalState &&
-              pendingUserInputs.length === 0 &&
-              composerPreviewAnnotations.length > 0 && (
-                <ComposerPreviewAnnotationCards
-                  annotations={composerPreviewAnnotations}
-                  images={composerImages}
-                  onRemove={(annotationId) =>
-                    removeComposerDraftPreviewAnnotation(composerDraftTarget, annotationId)
-                  }
-                  onExpandImage={(imageId) => {
-                    const preview = buildExpandedImagePreview(composerImages, imageId);
-                    if (preview) onExpandImage(preview);
-                  }}
-                  className="mb-3"
-                />
               )}
 
+              {!isComposerCollapsedMobile &&
+                !isComposerApprovalState &&
+                pendingUserInputs.length === 0 &&
+                composerPreviewAnnotations.length > 0 && (
+                  <ComposerPreviewAnnotationCards
+                    annotations={composerPreviewAnnotations}
+                    images={composerImages}
+                    onRemove={(annotationId) =>
+                      removeComposerDraftPreviewAnnotation(composerDraftTarget, annotationId)
+                    }
+                    onExpandImage={(imageId) => {
+                      const preview = buildExpandedImagePreview(composerImages, imageId);
+                      if (preview) onExpandImage(preview);
-            {!isComposerCollapsedMobile &&
-              !isComposerApprovalState &&
-              pendingUserInputs.length === 0 &&
-              composerReviewComments.length > 0 && (
+                    }}
+                    className="mb-3"
+                  />
+                )}
-                <ComposerPendingReviewComments
-                  comments={composerReviewComments}
-                  onRemove={(commentId) =>
-                    removeComposerDraftReviewComment(composerDraftTarget, commentId)
-                  }
-                  className="mb-3"
-                />
-              )}
 
-            {!isComposerCollapsedMobile &&
-              !isComposerApprovalState &&
-              pendingUserInputs.length === 0 &&
-              composerElementContexts.length > 0 && (
-                <ComposerPendingElementContexts
-                  contexts={composerElementContexts}
-                  onRemove={(contextId) =>
-                    removeComposerDraftElementContext(composerDraftTarget, contextId)
+              {!isComposerCollapsedMobile &&
+                !isComposerApprovalState &&
+                pendingUserInputs.length === 0 &&
+                composerReviewComments.length > 0 && (
+                  <ComposerPendingReviewComments
+                    comments={composerReviewComments}
+                    onRemove={(commentId) =>
+                      removeComposerDraftReviewComment(composerDraftTarget, commentId)
+                    }
+                    className="mb-3"
+                  />
+                )}
+
+              {!isComposerCollapsedMobile &&
+                !isComposerApprovalState &&
+                pendingUserInputs.length === 0 &&
+                composerElementContexts.length > 0 && (
+                  <ComposerPendingElementContexts
+                    contexts={composerElementContexts}
+                    onRemove={(contextId) =>
+                      removeComposerDraftElementContext(composerDraftTarget, contextId)
+                    }
+                    className="mb-3"
+                  />
+                )}
+
+              {!isComposerCollapsedMobile &&
+                !isComposerApprovalState &&
+                pendingUserInputs.length === 0 &&
+                composerImages.some(
+                  (image) =>
+                    !composerPreviewAnnotations.some((annotation) => annotation.id === image.id),
+                ) && (
+                  <div className="mb-3 flex flex-wrap gap-2">
+                    {composerImages
+                      .filter(
+                        (image) =>
+                          !composerPreviewAnnotations.some(
+                            (annotation) => annotation.id === image.id,
+                          ),
+                      )
+                      .map((image) => (
+                        <div
+                          key={image.id}
+                          className="relative h-16 w-16 overflow-hidden rounded-lg border border-border/80 bg-background"
+                        >
+                          {image.previewUrl ? (
+                            <button
+                              type="button"
+                              className="h-full w-full cursor-zoom-in"
+                              aria-label={`Preview ${image.name}`}
+                              onClick={() => {
+                                const preview = buildExpandedImagePreview(composerImages, image.id);
+                                if (!preview) return;
+                                onExpandImage(preview);
+                              }}
+                            >
+                              <img
+                                src={image.previewUrl}
+                                alt={image.name}
+                                className="h-full w-full object-cover"
+                              />
+                            </button>
+                          ) : (
+                            <div className="flex h-full w-full items-center justify-center px-1 text-center text-[10px] text-secondary-label">
+                              {image.name}
+                            </div>
+                          )}
+                          {nonPersistedComposerImageIdSet.has(image.id) && (
+                            <Tooltip>
+                              <TooltipTrigger
+                                render={
+                                  <span
+                                    role="img"
+                                    aria-label="Draft attachment may not persist"
+                                    className="absolute left-1 top-1 inline-flex items-center justify-center rounded bg-background/85 p-0.5 text-amber-600"
+                                  >
+                                    <CircleAlertIcon className="size-3" />
+                                  </span>
+                                }
+                              />
+                              <TooltipPopup
+                                side="top"
+                                className="max-w-64 whitespace-normal leading-tight"
+                              >
+                                Draft attachment could not be saved locally and may be lost on
+                                navigation.
+                              </TooltipPopup>
+                            </Tooltip>
+                          )}
+                          <Button
+                            variant="ghost"
+                            size="icon-xs"
+                            className="absolute right-1 top-1 bg-background/80 hover:bg-background/90"
+                            onClick={() => removeComposerImage(image.id)}
+                            aria-label={`Remove ${image.name}`}
+                          >
+                            <XIcon />
+                          </Button>
+                        </div>
+                      ))}
+                  </div>
+                )}
+
+              <div className="relative">
+                <ComposerPromptEditor
+                  editorRef={composerEditorRef}
+                  value={
+                    isComposerApprovalState
+                      ? ""
+                      : activePendingProgress
+                        ? activePendingProgress.customAnswer
+                        : prompt
+                  }
+                  cursor={composerCursor}
+                  terminalContexts={
+                    !isComposerApprovalState && pendingUserInputs.length === 0
+                      ? composerTerminalContexts
+                      : []
                   }
+                  skills={selectedProviderStatus?.skills ?? []}
+                  {...(showMobilePendingAnswerActions ? { className: "max-sm:pb-11" } : {})}
+                  onRemoveTerminalContext={removeComposerTerminalContextFromDraft}
+                  onChange={onPromptChange}
+                  onCommandKeyDown={onComposerCommandKey}
+                  onPaste={onComposerPaste}
+                  placeholder={
+                    isComposerApprovalState
+                      ? (activePendingApproval?.detail ??
+                        "Resolve this approval request to continue")
+                      : activePendingProgress
+                        ? "Type your own answer, or leave this blank to use the selected option"
+                        : showPlanFollowUpPrompt && activeProposedPlan
+                          ? "Add feedback to refine the plan, or leave this blank to implement it"
+                          : projectSelectionRequired
+                            ? "Choose a project above to start a thread"
+                            : noProviderAvailable
+                              ? "Enable a provider in Settings to send a message"
+                              : phase === "disconnected"
+                                ? DISCONNECTED_COMPOSER_PLACEHOLDER
+                                : "Ask anything, @tag files/folders, $use skills, or / for commands"
+                  }
-                  className="mb-3"
+                  disabled={isConnecting || isComposerApprovalState || projectSelectionRequired}
                 />
+                {showMobilePendingAnswerActions ? (
+                  <div
-              )}
+                    data-chat-composer-mobile-pending-actions="true"
+                    className="absolute bottom-0 right-0 flex items-center justify-end gap-1"
+                  >
+                    {inlineTasksBadge}
+                    {inlineStashBadge}
+                    <ComposerPrimaryActions
+                      compact
+                      pendingAction={pendingPrimaryAction}
+                      isRunning={false}
+                      showPlanFollowUpPrompt={false}
+                      promptHasText={false}
+                      isSendBusy={isSendBusy}
+                      sendDisabledReason={sendDisabledReason}
+                      isConnecting={isConnecting}
+                      isEnvironmentUnavailable={
+                        environmentUnavailable !== null ||
+                        noProviderAvailable ||
+                        projectSelectionRequired
+                      }
+                      isPreparingWorktree={false}
+                      hasSendableContent={false}
+                      preserveComposerFocusOnPointerDown
+                      onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion}
+                      onInterrupt={handleInterruptPrimaryAction}
+                      onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction}
+                    />
+                  </div>
+                ) : null}
+              </div>
+            </div>
 
+            <ComposerPromptLengthValidation
+              message={providerInputSubmissionError ?? composerSubmissionError}
+            />
-            {!isComposerCollapsedMobile &&
-              !isComposerApprovalState &&
-              pendingUserInputs.length === 0 &&
-              composerImages.some(
-                (image) =>
-                  !composerPreviewAnnotations.some((annotation) => annotation.id === image.id),
-              ) && (
-                <div className="mb-3 flex flex-wrap gap-2">
+
+            {/* Bottom toolbar */}
+            {isComposerCollapsedMobile || isComposerApprovalState ? null : (
+              <div
+                data-chat-composer-footer="true"
+                data-chat-composer-footer-compact={isComposerFooterCompact ? "true" : "false"}
+                className={cn(
+                  "flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-visible px-3 pb-3 sm:px-4 sm:pb-4",
+                  pendingUserInputs.length > 0 && "pt-2",
+                  isComposerFooterCompact ? "gap-1.5" : "gap-2 sm:gap-0",
+                  showMobilePendingAnswerActions && "hidden sm:flex",
+                )}
+              >
+                <div className="-m-1 -ms-3.5 flex min-w-0 flex-1 items-center gap-1 overflow-x-auto p-1 ps-3.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
+                  {noProviderAvailable ? (
+                    <Button
+                      type="button"
-                  {composerImages
-                    .filter(
-                      (image) =>
-                        !composerPreviewAnnotations.some(
-                          (annotation) => annotation.id === image.id,
-                        ),
-                    )
-                    .map((image) => (
-                      <div
-                        key={image.id}
-                        className="relative h-16 w-16 overflow-hidden rounded-lg border border-border/80 bg-background"
-                      >
-                        {image.previewUrl ? (
-                          <button
-                            type="button"
-                            className="h-full w-full cursor-zoom-in"
-                            aria-label={`Preview ${image.name}`}
-                            onClick={() => {
-                              const preview = buildExpandedImagePreview(composerImages, image.id);
-                              if (!preview) return;
-                              onExpandImage(preview);
-                            }}
-                          >
-                            <img
-                              src={image.previewUrl}
-                              alt={image.name}
-                              className="h-full w-full object-cover"
-                            />
-                          </button>
-                        ) : (
-                          <div className="flex h-full w-full items-center justify-center px-1 text-center text-[10px] text-secondary-label">
-                            {image.name}
-                          </div>
-                        )}
-                        {nonPersistedComposerImageIdSet.has(image.id) && (
-                          <Tooltip>
-                            <TooltipTrigger
-                              render={
-                                <span
-                                  role="img"
-                                  aria-label="Draft attachment may not persist"
-                                  className="absolute left-1 top-1 inline-flex items-center justify-center rounded bg-background/85 p-0.5 text-amber-600"
-                                >
-                                  <CircleAlertIcon className="size-3" />
-                                </span>
-                              }
-                            />
-                            <TooltipPopup
-                              side="top"
-                              className="max-w-64 whitespace-normal leading-tight"
-                            >
-                              Draft attachment could not be saved locally and may be lost on
-                              navigation.
-                            </TooltipPopup>
-                          </Tooltip>
-                        )}
-                        <Button
-                          variant="ghost"
-                          size="icon-xs"
-                          className="absolute right-1 top-1 bg-background/80 hover:bg-background/90"
-                          onClick={() => removeComposerImage(image.id)}
-                          aria-label={`Remove ${image.name}`}
-                        >
-                          <XIcon />
-                        </Button>
-                      </div>
-                    ))}
+                      size="sm"
+                      variant="ghost"
+                      disabled
+                      data-chat-provider-unavailable="true"
+                      className="shrink-0 gap-2 px-2 text-secondary-label sm:px-3"
+                    >
+                      <CircleAlertIcon className="size-4" />
+                      No provider available
+                    </Button>
+                  ) : (
+                    <ProviderModelPicker
+                      compact={isComposerFooterCompact}
+                      activeInstanceId={selectedInstanceId}
+                      model={selectedModelForPickerWithCustomFallback}
+                      lockedProvider={lockedProvider}
+                      lockedContinuationGroupKey={lockedContinuationGroupKey}
+                      instanceEntries={providerInstanceEntries}
+                      keybindings={keybindings}
+                      modelOptionsByInstance={modelOptionsByInstance}
+                      triggerClassName="-ms-2.5"
+                      terminalOpen={terminalOpen}
+                      open={isComposerModelPickerOpen}
+                      {...(composerProviderState.modelPickerIconClassName
+                        ? {
+                            activeProviderIconClassName:
+                              composerProviderState.modelPickerIconClassName,
+                          }
+                        : {})}
+                      onOpenChange={(open) => {
+                        setIsComposerModelPickerOpen(open);
+                      }}
+                      getModelDisabledReason={getModelDisabledReason}
+                      onInstanceModelChange={onProviderModelSelect}
+                    />
+                  )}
+
+                  {isComposerFooterCompact ? (
+                    <CompactComposerControlsMenu
+                      interactionMode={interactionMode}
+                      runtimeMode={runtimeMode}
+                      showInteractionModeToggle={composerProviderControls.showInteractionModeToggle}
+                      traitsMenuContent={providerTraitsMenuContent}
+                      onToggleInteractionMode={toggleInteractionMode}
+                      onRuntimeModeChange={handleRuntimeModeChange}
+                    />
+                  ) : (
+                    <>
+                      {providerTraitsPicker ? (
+                        <>
+                          <Separator
+                            orientation="vertical"
+                            className="mx-0.5 hidden h-4 sm:block"
+                          />
+                          {providerTraitsPicker}
+                        </>
+                      ) : null}
+                      <ComposerFooterModeControls
+                        showInteractionModeToggle={
+                          composerProviderControls.showInteractionModeToggle
+                        }
+                        interactionMode={interactionMode}
+                        runtimeMode={runtimeMode}
+                        onToggleInteractionMode={toggleInteractionMode}
+                        onRuntimeModeChange={handleRuntimeModeChange}
+                      />
+                    </>
+                  )}
                 </div>
-              )}
 
-            <div className="relative">
-              <ComposerPromptEditor
-                editorRef={composerEditorRef}
-                value={
-                  isComposerApprovalState
-                    ? ""
-                    : activePendingProgress
-                      ? activePendingProgress.customAnswer
-                      : prompt
-                }
-                cursor={composerCursor}
-                terminalContexts={
-                  !isComposerApprovalState && pendingUserInputs.length === 0
-                    ? composerTerminalContexts
-                    : []
-                }
-                skills={selectedProviderStatus?.skills ?? []}
-                {...(showMobilePendingAnswerActions ? { className: "max-sm:pb-11" } : {})}
-                onRemoveTerminalContext={removeComposerTerminalContextFromDraft}
-                onChange={onPromptChange}
-                onCommandKeyDown={onComposerCommandKey}
-                onPaste={onComposerPaste}
-                placeholder={
-                  isComposerApprovalState
-                    ? (activePendingApproval?.detail ?? "Resolve this approval request to continue")
-                    : activePendingProgress
-                      ? "Type your own answer, or leave this blank to use the selected option"
-                      : showPlanFollowUpPrompt && activeProposedPlan
-                        ? "Add feedback to refine the plan, or leave this blank to implement it"
-                        : projectSelectionRequired
-                          ? "Choose a project above to start a thread"
-                          : noProviderAvailable
-                            ? "Enable a provider in Settings to send a message"
-                            : phase === "disconnected"
-                              ? DISCONNECTED_COMPOSER_PLACEHOLDER
+                {/* Right side: send / stop button */}
-                              : "Ask anything, @tag files/folders, $use skills, or / for commands"
-                }
-                disabled={isConnecting || isComposerApprovalState || projectSelectionRequired}
-              />
-              {showMobilePendingAnswerActions ? (
                 <div
+                  data-chat-composer-actions="right"
+                  data-chat-composer-primary-actions-compact={
-                  data-chat-composer-mobile-pending-actions="true"
-                  className="absolute bottom-0 right-0 flex justify-end"
+                    isComposerPrimaryActionsCompact ? "true" : "false"
+                  }
+                  className="flex shrink-0 flex-nowrap items-center justify-end gap-2"
                 >
-                  <ComposerPrimaryActions
-                    compact
+                  {showMobilePendingAnswerActions ? null : inlineTasksBadge}
+                  {showMobilePendingAnswerActions ? null : inlineStashBadge}
+                  <ComposerFooterPrimaryActions
+                    compact={isComposerPrimaryActionsCompact}
+                    activeContextWindow={activeContextWindow}
+                    activeThreadModelDisplayName={activeThreadModelDisplayName}
                     pendingAction={pendingPrimaryAction}
+                    isRunning={phase === "running"}
+                    showPlanFollowUpPrompt={
+                      pendingUserInputs.length === 0 && showPlanFollowUpPrompt
+                    }
-                    isRunning={false}
-                    showPlanFollowUpPrompt={false}
-                    promptHasText={false}
+                    promptHasText={prompt.trim().length > 0}
                     isSendBusy={isSendBusy}
                     sendDisabledReason={sendDisabledReason}
                     isConnecting={isConnecting}
@@ -3104,145 +3377,18 @@
                       noProviderAvailable ||
                       projectSelectionRequired
                     }
+                    isPreparingWorktree={isPreparingWorktree}
+                    hasSendableContent={composerSendState.hasSendableContent}
+                    preserveComposerFocusOnPointerDown={isMobileViewport}
+                    showSendWhileRunning={isMobileViewport}
-                    isPreparingWorktree={false}
-                    hasSendableContent={false}
-                    preserveComposerFocusOnPointerDown
                     onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion}
                     onInterrupt={handleInterruptPrimaryAction}
                     onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction}
                   />
                 </div>
+              </div>
+            )}
-              ) : null}
-            </div>
           </div>
-
-          <ComposerPromptLengthValidation
-            message={providerInputSubmissionError ?? composerSubmissionError}
-          />
-
-          {/* Bottom toolbar */}
-          {isComposerCollapsedMobile ? null : activePendingApproval ? (
-            <div className="flex flex-wrap items-center justify-end gap-2 px-3 pb-3 sm:px-4 sm:pb-4">
-              <ComposerPendingApprovalActions
-                requestId={activePendingApproval.requestId}
-                isResponding={respondingRequestIds.includes(activePendingApproval.requestId)}
-                onRespondToApproval={onRespondToApproval}
-              />
-            </div>
-          ) : (
-            <div
-              data-chat-composer-footer="true"
-              data-chat-composer-footer-compact={isComposerFooterCompact ? "true" : "false"}
-              className={cn(
-                "flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-visible px-3 pb-3 sm:px-4 sm:pb-4",
-                pendingUserInputs.length > 0 && "pt-2",
-                isComposerFooterCompact ? "gap-1.5" : "gap-2 sm:gap-0",
-                showMobilePendingAnswerActions && "hidden sm:flex",
-              )}
-            >
-              <div className="-m-1 -ms-3.5 flex min-w-0 flex-1 items-center gap-1 overflow-x-auto p-1 ps-3.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
-                {noProviderAvailable ? (
-                  <Button
-                    type="button"
-                    size="sm"
-                    variant="ghost"
-                    disabled
-                    data-chat-provider-unavailable="true"
-                    className="shrink-0 gap-2 px-2 text-secondary-label sm:px-3"
-                  >
-                    <CircleAlertIcon className="size-4" />
-                    No provider available
-                  </Button>
-                ) : (
-                  <ProviderModelPicker
-                    compact={isComposerFooterCompact}
-                    activeInstanceId={selectedInstanceId}
-                    model={selectedModelForPickerWithCustomFallback}
-                    lockedProvider={lockedProvider}
-                    lockedContinuationGroupKey={lockedContinuationGroupKey}
-                    instanceEntries={providerInstanceEntries}
-                    keybindings={keybindings}
-                    modelOptionsByInstance={modelOptionsByInstance}
-                    triggerClassName="-ms-2.5"
-                    terminalOpen={terminalOpen}
-                    open={isComposerModelPickerOpen}
-                    {...(composerProviderState.modelPickerIconClassName
-                      ? {
-                          activeProviderIconClassName:
-                            composerProviderState.modelPickerIconClassName,
-                        }
-                      : {})}
-                    onOpenChange={(open) => {
-                      setIsComposerModelPickerOpen(open);
-                    }}
-                    getModelDisabledReason={getModelDisabledReason}
-                    onInstanceModelChange={onProviderModelSelect}
-                  />
-                )}
-
-                {isComposerFooterCompact ? (
-                  <CompactComposerControlsMenu
-                    interactionMode={interactionMode}
-                    runtimeMode={runtimeMode}
-                    showInteractionModeToggle={composerProviderControls.showInteractionModeToggle}
-                    traitsMenuContent={providerTraitsMenuContent}
-                    onToggleInteractionMode={toggleInteractionMode}
-                    onRuntimeModeChange={handleRuntimeModeChange}
-                  />
-                ) : (
-                  <>
-                    {providerTraitsPicker ? (
-                      <>
-                        <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
-                        {providerTraitsPicker}
-                      </>
-                    ) : null}
-                    <ComposerFooterModeControls
-                      showInteractionModeToggle={composerProviderControls.showInteractionModeToggle}
-                      interactionMode={interactionMode}
-                      runtimeMode={runtimeMode}
-                      onToggleInteractionMode={toggleInteractionMode}
-                      onRuntimeModeChange={handleRuntimeModeChange}
-                    />
-                  </>
-                )}
-              </div>
-
-              {/* Right side: send / stop button */}
-              <div
-                data-chat-composer-actions="right"
-                data-chat-composer-primary-actions-compact={
-                  isComposerPrimaryActionsCompact ? "true" : "false"
-                }
-                className="flex shrink-0 flex-nowrap items-center justify-end gap-2"
-              >
-                <ComposerFooterPrimaryActions
-                  compact={isComposerPrimaryActionsCompact}
-                  activeContextWindow={activeContextWindow}
-                  activeThreadModelDisplayName={activeThreadModelDisplayName}
-                  pendingAction={pendingPrimaryAction}
-                  isRunning={phase === "running"}
-                  showPlanFollowUpPrompt={pendingUserInputs.length === 0 && showPlanFollowUpPrompt}
-                  promptHasText={prompt.trim().length > 0}
-                  isSendBusy={isSendBusy}
-                  sendDisabledReason={sendDisabledReason}
-                  isConnecting={isConnecting}
-                  isEnvironmentUnavailable={
-                    environmentUnavailable !== null ||
-                    noProviderAvailable ||
-                    projectSelectionRequired
-                  }
-                  isPreparingWorktree={isPreparingWorktree}
-                  hasSendableContent={composerSendState.hasSendableContent}
-                  preserveComposerFocusOnPointerDown={isMobileViewport}
-                  showSendWhileRunning={isMobileViewport}
-                  onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion}
-                  onInterrupt={handleInterruptPrimaryAction}
-                  onImplementPlanInNewThread={handleImplementPlanInNewThreadPrimaryAction}
-                />
-              </div>
-            </div>
-          )}
         </div>
       </div>
     </form>
```

## apps/web/src/components/chat/ComposerBannerStack.test.tsx

Status: modified; +7; −3

```diff
@@ -36,7 +36,9 @@
     const neutralBehind = renderToStaticMarkup(
       <ComposerBannerStack items={[banner("front", "default"), banner("stacked", "default")]} />,
     );
+    expect(neutralBehind).toContain("chat-composer-banner-stack-cap");
+    expect(neutralBehind).toContain("border-[var(--chat-composer-attached-outline)]");
+    expect(neutralBehind).not.toContain("border-border");
-    expect(neutralBehind).toContain("border-border");
     expect(neutralBehind).not.toContain("border-warning/24");
 
     const warningBehind = renderToStaticMarkup(
@@ -49,12 +51,14 @@
     const markup = renderToStaticMarkup(<ComposerBannerStack items={[banner("front")]} />);
 
     expect(markup).not.toContain("data-composer-banner-stack-expanded-items");
-    expect(markup).toContain("alert-glass");
+    expect(markup).toContain("chat-composer-drawer-surface");
+    expect(markup).toContain("chat-composer-drawer-attached");
+    expect(markup).toContain("text-xs");
+    expect(markup).toContain('data-composer-banner-drawer="true"');
     expect(markup).toContain('data-variant="warning"');
     expect(markup).toContain("transform:none");
     expect(markup).not.toContain("will-change:transform");
   });
-
   it("applies item-specific surface and action layout classes", () => {
     const markup = renderToStaticMarkup(
       <ComposerBannerStack
```

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

Status: modified; +17; −5

```diff
@@ -26,7 +26,7 @@
 // banners are stacked behind it, so its border must match the severity of the
 // first hidden banner — a neutral banner must not masquerade as a warning.
 const stackCapBorderClass: Record<ComposerBannerStackItem["variant"], string> = {
-  default: "border-border",
+  default: "border-[var(--chat-composer-attached-outline)]",
   error: "border-destructive/24",
   info: "border-info/24",
   success: "border-success/24",
@@ -98,7 +98,10 @@
   };
 
   return (
+    <div
+      className={cn("group/banner-stack chat-composer-drawer-slot", className)}
+      data-composer-banner-drawer="true"
+    >
-    <div className={cn("group/banner-stack mx-auto mb-2 max-w-3xl", className)}>
       <div
         className={cn(
           "relative flex flex-col-reverse",
@@ -108,8 +111,8 @@
         {showCollapsedStackCap && firstStackedItem ? (
           <div
             className={cn(
+              "pointer-events-none absolute inset-x-0 -top-3 z-0 mx-auto h-3 rounded-t-2xl",
+              "chat-composer-banner-stack-cap border border-b-0 shadow-[0_6px_18px_rgba(0,0,0,0.06)]",
-              "pointer-events-none absolute inset-x-0 -top-3 z-0 mx-auto h-3 rounded-t-[22px]",
-              "border border-b-0 bg-background/96 shadow-[0_6px_18px_rgba(0,0,0,0.06)]",
               stackCapBorderClass[firstStackedItem.variant],
               "transition-opacity duration-150 ease-out",
               "group-hover/banner-stack:opacity-0 group-focus-within/banner-stack:opacity-0",
@@ -130,6 +133,7 @@
         >
           <ComposerBannerStackAlert
             item={frontItem}
+            attached
             exiting={exitingItemId === frontItem.id}
             onDismissRequest={() => requestDismiss(frontItem)}
           />
@@ -162,6 +166,7 @@
                   >
                     <ComposerBannerStackAlert
                       item={item}
+                      attached={false}
                       exiting={exitingItemId === item.id}
                       onDismissRequest={() => requestDismiss(item)}
                     />
@@ -178,10 +183,12 @@
 
 function ComposerBannerStackAlert({
   item,
+  attached,
   exiting,
   onDismissRequest,
 }: {
   readonly item: ComposerBannerStackItem;
+  readonly attached: boolean;
   readonly exiting: boolean;
   readonly onDismissRequest: () => void;
 }) {
@@ -190,7 +197,12 @@
   return (
     <Alert
       variant={item.variant}
+      className={cn(
+        attached
+          ? "chat-composer-drawer-surface chat-composer-drawer-attached px-3 pt-2 pb-[calc(var(--chat-composer-attachment-overlap)_+_0.375rem)] text-xs sm:px-4"
+          : "alert-glass rounded-[22px]",
+        item.className,
-      className={cn("alert-glass rounded-[22px]", item.className)}
+      )}
       data-variant={item.variant}
     >
       {item.icon}
```

## apps/web/src/components/chat/ComposerCommandMenu.test.tsx

Status: added; +89; −0

```diff
@@ -1,0 +1,89 @@
+import { renderToStaticMarkup } from "react-dom/server";
+import { ProviderDriverKind } from "@t3tools/contracts";
+import { describe, expect, it } from "vite-plus/test";
+
+import { ComposerCommandMenu } from "./ComposerCommandMenu";
+
+describe("ComposerCommandMenu", () => {
+  it("renders slash-command results as an attached composer drawer", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerCommandMenu
+        items={[]}
+        resolvedTheme="dark"
+        isLoading={false}
+        triggerKind="slash-command"
+        activeItemId={null}
+        onHighlightedItemChange={() => {}}
+        onSelect={() => {}}
+      />,
+    );
+
+    expect(markup).toContain('data-composer-command-drawer="true"');
+    expect(markup).toContain("chat-composer-drawer-surface");
+    expect(markup).toContain("chat-composer-drawer-attached");
+    expect(markup).not.toContain("dropdown-glass");
+  });
+
+  it("renders commands without a category heading or invented icons", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerCommandMenu
+        items={[
+          {
+            id: "slash:model",
+            type: "slash-command",
+            command: "model",
+            label: "/model",
+            description: "Switch response model for this thread",
+          },
+        ]}
+        resolvedTheme="dark"
+        isLoading={false}
+        triggerKind="slash-command"
+        activeItemId="slash:model"
+        onHighlightedItemChange={() => {}}
+        onSelect={() => {}}
+      />,
+    );
+
+    expect(markup).toContain("/model");
+    expect(markup).toContain("Switch response model for this thread");
+    expect(markup).not.toContain("Built-in");
+    expect(markup).not.toContain("<svg");
+    expect(markup).toContain("font-sans text-xs font-medium");
+    expect(markup).not.toContain("font-mono");
+    expect(markup).toContain("text-right");
+  });
+
+  it("renders a skill source icon with an accessible source label", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerCommandMenu
+        items={[
+          {
+            id: "skill:codex:browser",
+            type: "skill",
+            provider: ProviderDriverKind.make("codex"),
+            skill: {
+              name: "browser",
+              path: "/Users/maria/.codex/plugins/browser/skills/browser/SKILL.md",
+              scope: "user",
+              enabled: true,
+            },
+            label: "Browser",
+            description: "Open and control the in-app browser",
+          },
+        ]}
+        resolvedTheme="dark"
+        isLoading={false}
+        triggerKind="skill"
+        activeItemId="skill:codex:browser"
+        onHighlightedItemChange={() => {}}
+        onSelect={() => {}}
+      />,
+    );
+
+    expect(markup).toContain("Browser");
+    expect(markup).toContain('<span class="sr-only">App skill</span>');
+    expect(markup).toContain("<svg");
+    expect(markup).toContain("text-icon-muted");
+  });
+});
```

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

Status: modified; +77; −134

```diff
@@ -1,23 +1,27 @@
 import {
+  resolveProviderSkillSourceKind,
+  type ProviderSkillSourceKind,
+} from "@t3tools/client-runtime/providerSkills";
+import {
   type ProjectEntry,
   type ProviderDriverKind,
   type ServerProviderSkill,
   type ServerProviderSlashCommand,
 } from "@t3tools/contracts";
+import {
+  BlocksIcon,
+  FolderGit2Icon,
+  FolderIcon,
+  PackageIcon,
+  SettingsIcon,
+  UserRoundIcon,
+  type LucideIcon,
+} from "lucide-react";
+import { memo, useLayoutEffect, useRef } from "react";
-import { BotIcon } from "lucide-react";
-import { memo, useLayoutEffect, useMemo, useRef } from "react";
 
 import { type ComposerSlashCommand, type ComposerTriggerKind } from "../../composer-logic";
-import { formatProviderSkillInstallSource } from "~/providerSkillPresentation";
 import { cn } from "~/lib/utils";
+import { Command, CommandGroup, CommandItem, CommandList } from "../ui/command";
-import {
-  Command,
-  CommandGroup,
-  CommandGroupLabel,
-  CommandItem,
-  CommandList,
-  CommandSeparator,
-} from "../ui/command";
 import { PierreEntryIcon } from "./PierreEntryIcon";
 
 export type ComposerCommandItem =
@@ -53,73 +57,17 @@
       description: string;
     };
 
-type ComposerCommandGroup = {
-  id: string;
-  label: string | null;
-  items: ComposerCommandItem[];
-};
-
-function SkillGlyph(props: { className?: string }) {
-  return (
-    <svg
-      viewBox="0 0 24 24"
-      fill="none"
-      stroke="currentColor"
-      strokeWidth="1.85"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      className={props.className}
-      aria-hidden="true"
-    >
-      <path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
-      <path d="m3.3 7 8.7 5 8.7-5" />
-      <path d="M12 22V12" />
-    </svg>
-  );
-}
-
-function groupCommandItems(
-  items: ComposerCommandItem[],
-  triggerKind: ComposerTriggerKind | null,
-  groupSlashCommandSections: boolean,
-): ComposerCommandGroup[] {
-  if (triggerKind === "skill") {
-    return items.length > 0 ? [{ id: "skills", label: "Skills", items }] : [];
-  }
-  if (triggerKind !== "slash-command" || !groupSlashCommandSections) {
-    return [{ id: "default", label: null, items }];
-  }
-
-  const builtInItems = items.filter((item) => item.type === "slash-command");
-  const providerItems = items.filter((item) => item.type === "provider-slash-command");
-
-  const groups: ComposerCommandGroup[] = [];
-  if (builtInItems.length > 0) {
-    groups.push({ id: "built-in", label: "Built-in", items: builtInItems });
-  }
-  if (providerItems.length > 0) {
-    groups.push({ id: "provider", label: "Provider", items: providerItems });
-  }
-  return groups;
-}
-
 export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
   items: ComposerCommandItem[];
   resolvedTheme: "light" | "dark";
   isLoading: boolean;
   triggerKind: ComposerTriggerKind | null;
-  groupSlashCommandSections?: boolean;
   emptyStateText?: string;
   activeItemId: string | null;
   onHighlightedItemChange: (itemId: string | null) => void;
   onSelect: (item: ComposerCommandItem) => void;
 }) {
   const listRef = useRef<HTMLDivElement>(null);
-  const groups = useMemo(
-    () =>
-      groupCommandItems(props.items, props.triggerKind, props.groupSlashCommandSections ?? true),
-    [props.groupSlashCommandSections, props.items, props.triggerKind],
-  );
 
   useLayoutEffect(() => {
     if (!props.activeItemId || !listRef.current) return;
@@ -141,57 +89,38 @@
     >
       <div
         ref={listRef}
+        className="chat-composer-drawer-surface chat-composer-drawer-attached relative w-full overflow-hidden **:data-[slot=scroll-area-scrollbar]:data-[orientation=vertical]:my-4"
+        data-composer-command-drawer="true"
-        className="dropdown-glass relative w-full overflow-hidden rounded-[20px] shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] **:data-[slot=scroll-area-scrollbar]:data-[orientation=vertical]:my-4 dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]"
       >
         {props.items.length > 0 ? (
-          <CommandList className="max-h-72 not-empty:py-3">
-            {groups.map((group, groupIndex) => (
-              <div key={group.id}>
-                {groupIndex > 0 ? <CommandSeparator className="my-0.5" /> : null}
-                <CommandGroup>
-                  {group.label ? (
-                    <CommandGroupLabel className="px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-secondary-label">
-                      {group.label}
+          <CommandList className="max-h-72 scroll-pb-6">
+            <CommandGroup>
+              {props.items.map((item) => (
+                <ComposerCommandMenuItem
+                  key={item.id}
+                  item={item}
+                  resolvedTheme={props.resolvedTheme}
+                  isActive={props.activeItemId === item.id}
+                  onHighlight={props.onHighlightedItemChange}
+                  onSelect={props.onSelect}
+                />
+              ))}
+            </CommandGroup>
-                    </CommandGroupLabel>
-                  ) : null}
-                  {group.items.map((item) => (
-                    <ComposerCommandMenuItem
-                      key={item.id}
-                      item={item}
-                      resolvedTheme={props.resolvedTheme}
-                      isActive={props.activeItemId === item.id}
-                      onHighlight={props.onHighlightedItemChange}
-                      onSelect={props.onSelect}
-                    />
-                  ))}
-                </CommandGroup>
-              </div>
-            ))}
           </CommandList>
         ) : (
+          <div className="px-5 pt-3.5 pb-7">
+            <p className="text-secondary-label text-xs">
+              {props.isLoading
+                ? props.triggerKind === "skill"
+                  ? "Searching workspace skills..."
+                  : "Searching workspace files..."
-          <div className="px-5 py-3.5">
+                : (props.emptyStateText ??
-            {props.triggerKind === "skill" ? (
+                  (props.triggerKind === "skill"
+                    ? "No skills found. Try / to browse provider commands."
+                    : props.triggerKind === "path"
-              <CommandGroup>
-                <CommandGroupLabel className="px-0 pt-0 pb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-secondary-label">
-                  Skills
-                </CommandGroupLabel>
-                <p className="text-secondary-label text-xs">
-                  {props.isLoading
-                    ? "Searching workspace skills..."
-                    : (props.emptyStateText ??
-                      "No skills found. Try / to browse provider commands.")}
-                </p>
-              </CommandGroup>
-            ) : (
-              <p className="text-secondary-label text-xs">
-                {props.isLoading
-                  ? "Searching workspace files..."
-                  : (props.emptyStateText ??
-                    (props.triggerKind === "path"
                       ? "No matching files or folders."
                       : "No matching command."))}
-              </p>
+            </p>
-            )}
           </div>
         )}
       </div>
@@ -206,15 +135,15 @@
   onHighlight: (itemId: string | null) => void;
   onSelect: (item: ComposerCommandItem) => void;
 }) {
-  const skillSourceLabel =
+  const skillSourceKind =
-    props.item.type === "skill" ? formatProviderSkillInstallSource(props.item.skill) : null;
+    props.item.type === "skill" ? resolveProviderSkillSourceKind(props.item.skill) : null;
 
   return (
     <CommandItem
       value={props.item.id}
       data-composer-item-id={props.item.id}
       className={cn(
+        "cursor-pointer select-none gap-3 rounded-lg px-3 py-2! hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit",
-        "cursor-pointer select-none gap-2 hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit",
         props.isActive && "bg-accent! text-accent-foreground!",
       )}
       onMouseMove={() => {
@@ -233,29 +162,43 @@
           kind={props.item.pathKind}
           theme={props.resolvedTheme}
         />
-      ) : null}
-      {props.item.type === "slash-command" ? (
-        <BotIcon className="size-4 shrink-0 text-icon-muted" />
-      ) : null}
-      {props.item.type === "provider-slash-command" ? (
-        <span className="inline-flex size-4 shrink-0 items-center justify-center text-icon-muted">
-          <SkillGlyph className="size-3.5" />
-        </span>
-      ) : null}
-      {props.item.type === "skill" ? (
-        <span className="inline-flex size-4 shrink-0 items-center justify-center text-icon-muted">
-          <SkillGlyph className="size-3.5" />
-        </span>
+      ) : skillSourceKind ? (
+        <SkillSourceIcon kind={skillSourceKind} />
       ) : null}
+      <span className="flex min-w-0 flex-1 items-baseline gap-3">
+        <span className="shrink-0 font-sans text-xs font-medium">{props.item.label}</span>
-      <span className="flex min-w-0 flex-1 items-center gap-2">
+        <span className="min-w-0 flex-1 truncate text-right text-secondary-label text-xs">
-        <span className="shrink-0">{props.item.label}</span>
-        <span className="min-w-0 flex-1 truncate text-secondary-label text-xs">
           {props.item.description}
         </span>
       </span>
-      {skillSourceLabel ? (
-        <span className="shrink-0 pl-2 text-secondary-label text-xs">{skillSourceLabel}</span>
-      ) : null}
     </CommandItem>
   );
 });
+
+const SKILL_SOURCE_ICON_BY_KIND: Record<ProviderSkillSourceKind, LucideIcon> = {
+  app: BlocksIcon,
+  repo: FolderGit2Icon,
+  project: FolderIcon,
+  personal: UserRoundIcon,
+  system: SettingsIcon,
+  other: PackageIcon,
+};
+
+const SKILL_SOURCE_LABEL_BY_KIND: Record<ProviderSkillSourceKind, string> = {
+  app: "App",
+  repo: "Repo",
+  project: "Project",
+  personal: "Personal",
+  system: "System",
+  other: "Other",
+};
+
+function SkillSourceIcon(props: { kind: ProviderSkillSourceKind }) {
+  const Icon = SKILL_SOURCE_ICON_BY_KIND[props.kind];
+  return (
+    <>
+      <Icon aria-hidden="true" className="size-4 shrink-0 text-icon-muted" />
+      <span className="sr-only">{SKILL_SOURCE_LABEL_BY_KIND[props.kind]} skill</span>
+    </>
+  );
+}
```

## apps/web/src/components/chat/ComposerPendingApprovalActions.test.tsx

Status: added; +24; −0

```diff
@@ -1,0 +1,24 @@
+import { ApprovalRequestId } from "@t3tools/contracts";
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it } from "vite-plus/test";
+
+import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions";
+
+describe("ComposerPendingApprovalActions", () => {
+  it("states that the persistent approval lasts for this session", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerPendingApprovalActions
+        requestId={ApprovalRequestId.make("approval-1")}
+        isResponding={false}
+        onRespondToApproval={async () => undefined}
+      />,
+    );
+
+    expect(markup).toContain(">Cancel<");
+    expect(markup).toContain("Always allow this session");
+    expect(markup).not.toContain(">Always allow<");
+    expect(markup).toContain("h-5");
+    expect(markup).toContain("sm:text-[11px]");
+    expect(markup).not.toContain("sm:h-6");
+  });
+});
```

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

Status: modified; +16; −10

```diff
@@ -10,6 +10,8 @@
     decision: ProviderApprovalDecision,
   ) => Promise<unknown>;
 }
+
+const APPROVAL_ACTION_CLASS_NAME = "font-normal";
 
 export const ComposerPendingApprovalActions = memo(function ComposerPendingApprovalActions({
   requestId,
@@ -19,36 +21,40 @@
   return (
     <>
       <Button
+        size="micro"
+        variant="ghost-muted"
-        size="sm"
+        className={APPROVAL_ACTION_CLASS_NAME}
-        variant="ghost"
         disabled={isResponding}
         onClick={() => void onRespondToApproval(requestId, "cancel")}
       >
-        Cancel turn
+        Cancel
       </Button>
       <Button
-        size="sm"
+        size="micro"
+        variant="ghost-muted"
-        variant="destructive-outline"
+        className={`${APPROVAL_ACTION_CLASS_NAME} text-destructive-foreground [:hover,[data-pressed]]:text-destructive-foreground`}
         disabled={isResponding}
         onClick={() => void onRespondToApproval(requestId, "decline")}
       >
         Decline
       </Button>
       <Button
+        size="micro"
+        variant="ghost-muted"
+        className={APPROVAL_ACTION_CLASS_NAME}
-        size="sm"
-        variant="outline"
         disabled={isResponding}
         onClick={() => void onRespondToApproval(requestId, "acceptForSession")}
       >
         Always allow this session
       </Button>
       <Button
+        size="micro"
-        size="sm"
+        variant="ghost-muted"
-        variant="default"
+        className={`${APPROVAL_ACTION_CLASS_NAME} text-foreground`}
         disabled={isResponding}
         onClick={() => void onRespondToApproval(requestId, "accept")}
       >
+        Approve
-        Approve once
       </Button>
     </>
   );
```

## apps/web/src/components/chat/ComposerPendingApprovalPanel.test.tsx

Status: modified; +25; −3

```diff
@@ -5,7 +5,7 @@
 import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";
 
 describe("ComposerPendingApprovalPanel", () => {
-  it("renders complete multiline command details without hover or truncation", () => {
+  it("keeps the complete command readable in the compact row", () => {
     const detail = `bun run release -- ${"x".repeat(500)}\nsecond line`;
     const markup = renderToStaticMarkup(
       <ComposerPendingApprovalPanel
@@ -21,11 +21,33 @@
 
     expect(markup).toContain('data-approval-detail="complete"');
     expect(markup).toContain('aria-label="Command"');
+    expect(markup).toContain('role="group"');
+    expect(markup).toContain('tabindex="0"');
     expect(markup).toContain(detail);
+    expect(markup).toContain("max-h-20");
+    expect(markup).toContain("overflow-auto");
+    expect(markup).toContain("whitespace-pre");
+    expect(markup).toContain("[scrollbar-width:thin]");
+    expect(markup).toContain("[&amp;::-webkit-scrollbar]:h-1.5");
     expect(markup).not.toContain("truncate");
     expect(markup).not.toContain("line-clamp");
     expect(markup).toContain("min-w-0");
-    expect(markup).toContain("max-w-full");
+    expect(markup).not.toContain("Command approval requested");
-    expect(markup).toContain("[overflow-wrap:anywhere]");
   });
+
+  it("falls back to the approval kind when the provider sends an empty detail", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerPendingApprovalPanel
+        approval={{
+          requestId: ApprovalRequestId.make("approval-2"),
+          requestKind: "file-read",
+          createdAt: "2026-07-18T00:00:00.000Z",
+          detail: "",
+        }}
+        pendingCount={1}
+      />,
+    );
+
+    expect(markup).toContain("File read approval");
+  });
 });
```

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

Status: modified; +25; −24

```diff
@@ -1,22 +1,25 @@
 import { memo } from "react";
 import { type PendingApproval } from "../../session-logic";
+import { cn } from "~/lib/utils";
 
 interface ComposerPendingApprovalPanelProps {
   approval: PendingApproval;
   pendingCount: number;
+  className?: string;
 }
 
 export const ComposerPendingApprovalPanel = memo(function ComposerPendingApprovalPanel({
   approval,
   pendingCount,
+  className,
 }: ComposerPendingApprovalPanelProps) {
+  const fallbackLabel =
-  const approvalSummary =
     approval.requestKind === "command"
+      ? "Command approval"
-      ? "Command approval requested"
       : approval.requestKind === "file-read"
-        ? "File-read approval requested"
-        : "File-change approval requested";
-  const detailLabel =
+        ? "File read approval"
+        : "File change approval";
+  const detailAriaLabel =
     approval.requestKind === "command"
       ? "Command"
       : approval.requestKind === "file-read"
@@ -24,25 +27,23 @@
         : "File change";
 
   return (
+    <div
+      aria-label={fallbackLabel}
+      className={cn("flex min-w-0 flex-1 items-center gap-2", className)}
+      role="group"
+    >
+      <code
+        aria-label={detailAriaLabel}
+        className="block max-h-20 min-w-0 flex-1 overflow-auto whitespace-pre font-mono text-[11px] text-foreground/85 [scrollbar-width:thin] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70 [&::-webkit-scrollbar]:h-1.5"
+        data-approval-detail="complete"
+        tabIndex={0}
+      >
+        {approval.detail || fallbackLabel}
-    <div className="min-w-0 px-4 py-3.5 sm:px-5 sm:py-4">
-      <div className="flex flex-wrap items-center gap-2">
-        <span className="uppercase text-sm tracking-[0.2em]">PENDING APPROVAL</span>
-        <span className="text-sm font-medium">{approvalSummary}</span>
-        {pendingCount > 1 ? (
-          <span className="text-xs text-muted-foreground">1/{pendingCount}</span>
-        ) : null}
-      </div>
-      {approval.detail ? (
-        <div className="mt-3 min-w-0 max-w-full rounded-lg border border-border/65 bg-background/70 p-3">
-          <p className="text-xs font-medium text-muted-foreground">{detailLabel}</p>
-          <pre
-            aria-label={detailLabel}
-            className="mt-2 min-w-0 max-w-full max-h-40 overflow-auto whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-relaxed text-foreground"
-            data-approval-detail="complete"
-          >
-            {approval.detail}
-          </pre>
+      </code>
+      {pendingCount > 1 ? (
-        </div>
+        <span className="shrink-0 text-[10px] font-medium text-muted-foreground tabular-nums">
+          1/{pendingCount}
+        </span>
       ) : null}
     </div>
   );
```

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

Status: modified; +13; −14

```diff
@@ -167,7 +167,7 @@
 
   return (
     <Collapsible
-      className="py-3"
+      className="py-2"
       open={!isCollapsed}
       onOpenChange={(open) => {
         setCollapsedQuestionId(open ? null : activeQuestion.id);
@@ -179,19 +179,19 @@
           header label and the chevron still line up with the left and right
           edges of the question text below. The negative block margin keeps the
           taller hit area from pushing the panel down. */}
-      <div className="px-1.5 sm:px-2.5">
+      <div className="flex items-center gap-1 px-1 sm:px-2">
         <CollapsibleTrigger
           title={
             isCollapsed ? "Show the question and its options" : "Hide the question and its options"
           }
           data-pending-user-input-toggle={isCollapsed ? "collapsed" : "expanded"}
-          className="group -my-1 flex w-full items-center gap-3 rounded-md px-2.5 py-1.5 text-left outline-none transition-colors duration-150 hover:bg-muted/40 focus-visible:ring-1 focus-visible:ring-primary/25"
+          className="group -my-1 flex min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-left outline-none transition-colors duration-150 hover:bg-muted/35 focus-visible:ring-1 focus-visible:ring-primary/25"
         >
-          <span className="text-secondary-label text-[11px] font-semibold tracking-widest uppercase group-hover:text-foreground">
+          <span className="text-xs font-medium text-muted-foreground group-hover:text-foreground/85">
             {activeQuestion.header}
           </span>
           {prompt.questions.length > 1 ? (
-            <span className="flex h-5 items-center rounded-md bg-muted/60 px-1.5 text-secondary-label text-[10px] font-medium tabular-nums">
+            <span className="text-[10px] font-medium text-muted-foreground tabular-nums">
               {questionIndex + 1}/{prompt.questions.length}
             </span>
           ) : null}
@@ -217,13 +217,13 @@
       {/* The panel carries the horizontal padding itself: it clips its content
           while the height animates, so the option buttons have to sit inside
           that padding or their focus rings get shaved off at the edges. */}
+      <CollapsiblePanel className="px-3 sm:px-4">
-      <CollapsiblePanel className="px-4 sm:px-5">
         <div className="pt-2 pb-0.5">
+          <p className="text-sm text-foreground/85">{activeQuestion.question}</p>
-          <p className="text-sm text-foreground/90">{activeQuestion.question}</p>
           {activeQuestion.multiSelect ? (
             <p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>
           ) : null}
-          <div className="mt-3 space-y-1.5">
+          <div className="mt-2 space-y-0.5">
             {activeQuestion.options.map((option, index) => {
               const isOptimisticallySelected =
                 optimisticSingleSelect?.questionId === activeQuestion.id &&
@@ -233,10 +233,10 @@
                 (!customAnswerActive && progress.selectedOptionLabels.includes(option.label));
               const shortcutKey = index < 9 ? index + 1 : null;
               const className = cn(
-                "group flex w-full items-center gap-3 rounded-lg border px-3 py-2 text-left outline-none transition-all duration-150 focus-visible:border-primary/40 focus-visible:ring-1 focus-visible:ring-primary/25",
+                "group flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left outline-none transition-colors duration-150 focus-visible:ring-1 focus-visible:ring-primary/25",
                 isSelected
+                  ? "bg-muted/55 text-foreground"
+                  : "bg-transparent text-foreground/85 hover:bg-muted/30",
-                  ? "border-primary/30 bg-primary/8 text-foreground"
-                  : "border-transparent bg-muted/22 text-foreground/85 hover:border-border/45 hover:bg-muted/34",
                 isResponding && "opacity-50 cursor-not-allowed",
                 !isResponding && "cursor-pointer",
               );
@@ -245,7 +245,7 @@
                   <div className="min-w-0 flex-1 flex flex-col gap-0.5">
                     <span className="text-sm font-medium">{option.label}</span>
                     {option.description && option.description !== option.label ? (
+                      <span className="text-secondary-label text-[11px]">{option.description}</span>
-                      <span className="text-secondary-label text-xs">{option.description}</span>
                     ) : null}
                   </div>
                   {isSelected ? (
@@ -253,8 +253,7 @@
                   ) : shortcutKey !== null ? (
                     <kbd
                       className={cn(
-                        "flex size-5 shrink-0 items-center justify-center rounded border border-border/50 text-[11px] font-medium tabular-nums transition-colors duration-150",
+                        "flex size-5 shrink-0 items-center justify-center text-[10px] font-medium text-muted-foreground tabular-nums",
-                        "bg-background/35 text-secondary-label group-hover:border-border/70 group-hover:text-foreground",
                       )}
                     >
                       {shortcutKey}
```

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

Status: modified; +3; −10

```diff
@@ -1,5 +1,4 @@
 import { memo } from "react";
-import { Badge } from "../ui/badge";
 
 export const ComposerPlanFollowUpBanner = memo(function ComposerPlanFollowUpBanner({
   planTitle,
@@ -7,17 +6,11 @@
   planTitle: string | null;
 }) {
   return (
-    <div className="px-4 py-3.5 sm:px-5 sm:py-4">
+    <div className="px-3 py-2 sm:px-4">
       <div className="flex flex-wrap items-center gap-2">
+        <span className="text-xs font-medium text-muted-foreground">Plan ready</span>
-        <Badge
-          variant="info"
-          size="sm"
-          className="rounded-md px-1.5 py-0 font-semibold tracking-wide uppercase"
-        >
-          Plan Ready
-        </Badge>
         {planTitle ? (
+          <span className="min-w-0 flex-1 truncate text-xs text-foreground/85">{planTitle}</span>
-          <span className="min-w-0 flex-1 truncate text-sm font-medium">{planTitle}</span>
         ) : null}
       </div>
       {/* <div className="mt-2 text-xs text-muted-foreground">
```

## apps/web/src/components/chat/ComposerStashBadge.test.tsx

Status: added; +68; −0

```diff
@@ -1,0 +1,68 @@
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it } from "vite-plus/test";
+
+import { ComposerStashBadge } from "./ComposerStashBadge";
+
+describe("ComposerStashBadge", () => {
+  it("renders as an attached composer tab instead of a floating pill", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerStashBadge
+        count={3}
+        menuOpen={false}
+        pulseKey={0}
+        pulsing={false}
+        onToggleMenu={() => {}}
+      />,
+    );
+
+    expect(markup).toContain("chat-composer-stash-tab");
+    expect(markup).toContain("rounded-t-xl");
+    expect(markup).toContain("border-b-0");
+    expect(markup).toContain("items-center");
+    expect(markup).not.toContain("items-start");
+    expect(markup).not.toContain("rounded-full");
+    expect(markup).not.toContain("opacity-70");
+    expect(markup).not.toContain("border-border");
+    expect(markup).toContain('aria-expanded="false"');
+  });
+
+  it("reports when the stash drawer is open", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerStashBadge
+        count={3}
+        menuOpen
+        pulseKey={0}
+        pulsing={false}
+        onToggleMenu={() => {}}
+      />,
+    );
+
+    expect(markup).toContain('aria-expanded="true"');
+    expect(markup).toContain("text-foreground");
+    expect(markup).toContain("pointer-events-none");
+    expect(markup).not.toContain("invisible");
+  });
+
+  it("keeps a compact inline entry point when a drawer occupies the tab", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerStashBadge
+        count={3}
+        menuOpen
+        placement="inline"
+        pulseKey={0}
+        pulsing={false}
+        onToggleMenu={() => {}}
+      />,
+    );
+
+    expect(markup).toContain('data-slot="button"');
+    expect(markup).toContain("rounded-sm");
+    expect(markup).toContain("focus-visible:ring-2");
+    expect(markup).toContain("pointer-coarse:after:min-h-11");
+    expect(markup).toContain("Stashed prompts: 3. Open stash.");
+    expect(markup).toContain('aria-expanded="true"');
+    expect(markup).not.toContain("chat-composer-stash-tab");
+    expect(markup).not.toContain("rounded-t-xl");
+    expect(markup).not.toContain(" pointer-events-none ");
+  });
+});
```

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

Status: modified; +49; −19

```diff
@@ -2,10 +2,12 @@
 import { memo } from "react";
 
 import { cn } from "~/lib/utils";
+import { Button } from "../ui/button";
 
 /**
+ * Bookmark control that shows the stash count and opens the stash menu. It
+ * sits behind the composer's shoulder unless another drawer needs that space,
- * Bookmark pill perched on the composer's top-right shoulder. Shows the
+ * then moves into the composer's existing controls.
- * stash count and doubles as the click target for opening the stash menu.
  *
  * On save the badge gives one quiet acknowledgement: it lifts to full
  * opacity and the count ticks over. `pulseKey` changes per stash, remounting
@@ -13,13 +15,50 @@
  */
 export const ComposerStashBadge = memo(function ComposerStashBadge(props: {
   count: number;
+  menuOpen: boolean;
+  placement?: "inline" | "tab";
   pulseKey: number;
   pulsing: boolean;
-  menuOpen: boolean;
   onToggleMenu: () => void;
 }) {
   if (props.count === 0) return null;
+  const inline = props.placement === "inline";
+  const count = (
+    <span
+      key={props.pulseKey}
+      className={cn(
+        "text-[10px] font-medium leading-none tabular-nums",
+        props.pulsing
+          ? "animate-[prompt-stash-count-enter_180ms_ease-out_both] text-primary motion-reduce:animate-none"
+          : "text-muted-foreground",
+      )}
+    >
+      {props.count}
+    </span>
+  );
 
+  if (inline) {
+    return (
+      <Button
+        size="micro"
+        variant="ghost-muted"
+        data-prompt-stash-badge="true"
+        aria-label={`Stashed prompts: ${props.count}. Open stash.`}
+        aria-expanded={props.menuOpen}
+        className={cn(
+          "shrink-0 gap-1 px-1.5",
+          (props.menuOpen || props.pulsing) &&
+            "[--control-icon-color:currentColor] text-foreground",
+        )}
+        onPointerDown={(event) => event.preventDefault()}
+        onClick={props.onToggleMenu}
+      >
+        <BookmarkIcon className="size-3 shrink-0" aria-hidden="true" />
+        {count}
+      </Button>
+    );
+  }
+
   return (
     <button
       type="button"
@@ -27,11 +66,12 @@
       aria-label={`Stashed prompts: ${props.count}. Open stash.`}
       aria-expanded={props.menuOpen}
       className={cn(
-        "absolute -top-3 right-4 z-10 inline-flex cursor-pointer items-center gap-1.5 rounded-full border bg-popover px-2.5 py-0.5 text-xs shadow-sm",
-        "transition-[color,border-color,opacity] duration-200",
+        "chat-composer-shoulder-tab chat-composer-stash-tab absolute -top-7 right-4 z-0 inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-t-xl border border-b-0 px-3 pb-1 text-xs leading-none",
+        "transition-[color,border-color] duration-200",
+        props.menuOpen && "pointer-events-none",
         props.menuOpen || props.pulsing
-          ? "border-border text-foreground opacity-100"
+          ? "text-foreground"
+          : "text-muted-foreground hover:text-foreground",
-          : "border-border/70 text-muted-foreground opacity-70 hover:opacity-100 hover:text-foreground",
       )}
       onPointerDown={(event) => {
         // Keep composer focus so Escape/typing flows stay intact.
@@ -39,19 +79,9 @@
       }}
       onClick={props.onToggleMenu}
     >
-      <BookmarkIcon className="size-3" aria-hidden="true" />
+      <BookmarkIcon className="size-3 shrink-0" aria-hidden="true" />
       Stash
-      <span
-        key={props.pulseKey}
-        className={cn(
+      {count}
-          "rounded-full px-1.5 text-[10px] font-medium tabular-nums",
-          props.pulsing
-            ? "animate-[prompt-stash-count-enter_180ms_ease-out_both] bg-primary text-primary-foreground motion-reduce:animate-none"
-            : "bg-muted text-muted-foreground",
-        )}
-      >
-        {props.count}
-      </span>
     </button>
   );
 });
```

## apps/web/src/components/chat/ComposerStashMenu.test.tsx

Status: added; +74; −0

```diff
@@ -1,0 +1,74 @@
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it } from "vite-plus/test";
+
+import { ComposerStashMenu } from "./ComposerStashMenu";
+
+describe("ComposerStashMenu", () => {
+  it("renders saved prompts as an attached composer drawer", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerStashMenu
+        entries={[]}
+        onRestore={() => {}}
+        onDelete={() => {}}
+        onClose={() => {}}
+      />,
+    );
+
+    expect(markup).toContain('data-composer-stash-drawer="true"');
+    expect(markup).toContain("chat-composer-drawer-surface");
+    expect(markup).toContain("chat-composer-drawer-attached");
+    expect(markup).toContain('aria-label="Close stash"');
+    expect(markup).not.toContain("dropdown-glass");
+    expect(markup).not.toContain("Stashed prompts");
+  });
+
+  it("shows saved image thumbnails and incomplete image states", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerStashMenu
+        entries={[
+          {
+            id: "with-images",
+            createdAt: new Date(0).toISOString(),
+            prompt: "Compare these screenshots",
+            attachments: [
+              {
+                id: "image-one",
+                name: "before.png",
+                mimeType: "image/png",
+                sizeBytes: 128,
+                dataUrl: "data:image/png;base64,AA==",
+              },
+            ],
+            droppedImageNames: ["after.png"],
+            unreadableImageNames: [],
+            pendingImageCount: 0,
+          },
+          {
+            id: "saving-images",
+            createdAt: new Date(0).toISOString(),
+            prompt: "Save this image",
+            attachments: [],
+            droppedImageNames: [],
+            unreadableImageNames: [],
+            pendingImageCount: 1,
+          },
+        ]}
+        onRestore={() => {}}
+        onDelete={() => {}}
+        onClose={() => {}}
+      />,
+    );
+
+    expect(markup).toContain('src="data:image/png;base64,AA=="');
+    expect(markup).toContain("1 image dropped");
+    expect(markup).toContain("saving 1 image");
+    expect(markup).not.toContain("absolute top-1/2 right-2");
+    expect(markup).toContain("pointer-events-none");
+    expect(markup).toContain("pointer-coarse:pointer-events-auto");
+    expect(markup).toContain("pointer-coarse:opacity-100");
+    expect(markup).not.toContain("bg-popover!");
+    expect(markup).toContain("[--control-icon-color:currentColor]");
+    expect(markup).toContain("size-3.5 stroke-2");
+    expect(markup).not.toContain("bg-background/90");
+  });
+});
```

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

Status: modified; +56; −30

```diff
@@ -1,10 +1,10 @@
-import { BookmarkIcon, XIcon } from "lucide-react";
+import { XIcon } from "lucide-react";
-import { memo, useEffect, useState } from "react";
+import { memo, useEffect, useRef, useState } from "react";
 
 import { formatRelativeTimeLabel } from "../../timestampFormat";
 import { cn } from "~/lib/utils";
 import { type PromptStashEntry } from "../../promptStashStore";
+import { Command, CommandGroup, CommandItem, CommandList } from "../ui/command";
-import { Command, CommandGroup, CommandGroupLabel, CommandItem, CommandList } from "../ui/command";
 import { Button } from "../ui/button";
 
 const SNIPPET_MAX_CHARS = 90;
@@ -36,11 +36,28 @@
   onClose: () => void;
 }) {
   const { entries, onRestore, onDelete, onClose } = props;
+  const drawerRef = useRef<HTMLDivElement>(null);
   const [highlightedId, setHighlightedId] = useState<string | null>(entries[0]?.id ?? null);
 
   const highlightedEntry = entries.find((entry) => entry.id === highlightedId) ?? entries[0];
 
   useEffect(() => {
+    const closeOnOutsidePointer = (event: PointerEvent) => {
+      const drawer = drawerRef.current;
+      if (
+        (drawer && event.composedPath().includes(drawer)) ||
+        (event.target instanceof Element &&
+          event.target.closest('[data-prompt-stash-badge="true"]'))
+      ) {
+        return;
+      }
+      onClose();
+    };
+    document.addEventListener("pointerdown", closeOnOutsidePointer, true);
+    return () => document.removeEventListener("pointerdown", closeOnOutsidePointer, true);
+  }, [onClose]);
+
+  useEffect(() => {
     if (entries.length === 0) return;
     if (!entries.some((entry) => entry.id === highlightedId)) {
       setHighlightedId(entries[0]?.id ?? null);
@@ -91,15 +108,26 @@
 
   return (
     <Command autoHighlight={false} mode="none">
+      <div
+        ref={drawerRef}
+        className="chat-composer-drawer-surface chat-composer-drawer-attached relative w-full overflow-hidden"
+        data-composer-stash-drawer="true"
+      >
+        <div className="flex h-7 items-center justify-end px-2 pt-1">
+          <Button
+            variant="ghost-muted"
+            size="icon-micro"
+            aria-label="Close stash"
+            onPointerDown={(event) => event.preventDefault()}
+            onClick={onClose}
+          >
-      <div className="dropdown-glass relative w-full overflow-hidden rounded-[20px] shadow-[0_16px_40px_-18px_rgb(0_0_0/55%)] dark:shadow-[0_18px_44px_-18px_rgb(0_0_0/80%)]">
-        <CommandList className="max-h-72">
+            <XIcon className="size-3" />
+          </Button>
+        </div>
+        <CommandList className="max-h-64 scroll-pb-6">
           <CommandGroup>
-            <CommandGroupLabel className="flex items-center gap-1.5 px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-secondary-label">
-              <BookmarkIcon className="size-3" aria-hidden="true" />
-              Stashed prompts
-            </CommandGroupLabel>
             {entries.length === 0 ? (
-              <p className="px-3 pb-3 pt-1 text-secondary-label text-xs">
+              <p className="px-3 py-1.5 text-secondary-label text-xs">
                 Nothing stashed yet. Press ⌘S with a prompt in the composer to stash it.
               </p>
             ) : (
@@ -108,7 +136,7 @@
                   key={entry.id}
                   value={entry.id}
                   className={cn(
+                    "group/stash relative cursor-pointer select-none gap-3 rounded-lg px-3 py-1.5! hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit",
-                    "group/stash cursor-pointer select-none gap-2 hover:bg-transparent hover:text-inherit data-highlighted:bg-transparent data-highlighted:text-inherit",
                     highlightedId === entry.id && "bg-accent! text-accent-foreground!",
                   )}
                   onMouseMove={() => {
@@ -121,6 +149,20 @@
                     onRestore(entry);
                   }}
                 >
+                  <span className="min-w-0 flex-1 truncate text-xs">
+                    {stashEntrySnippet(entry)}
+                  </span>
+                  {entry.pendingImageCount ? (
+                    <span className="shrink-0 text-[10px] text-secondary-label">
+                      saving {entry.pendingImageCount} image
+                      {entry.pendingImageCount === 1 ? "" : "s"}…
+                    </span>
+                  ) : missingImageCount(entry) > 0 ? (
+                    <span className="shrink-0 text-[10px] text-warning-foreground">
+                      {missingImageCount(entry)} image
+                      {missingImageCount(entry) === 1 ? "" : "s"} dropped
+                    </span>
+                  ) : null}
                   {entry.attachments.length > 0 ? (
                     <span className="flex shrink-0 items-center -space-x-1.5">
                       {entry.attachments.slice(0, 3).map((attachment) => (
@@ -133,37 +175,21 @@
                         />
                       ))}
                     </span>
-                  ) : (
-                    <BookmarkIcon className="size-4 shrink-0 text-icon-muted" />
-                  )}
-                  <span className="min-w-0 flex-1 truncate text-sm">
-                    {stashEntrySnippet(entry)}
-                  </span>
-                  {entry.pendingImageCount ? (
-                    <span className="shrink-0 text-[10px] text-secondary-label">
-                      saving {entry.pendingImageCount} image
-                      {entry.pendingImageCount === 1 ? "" : "s"}…
-                    </span>
-                  ) : missingImageCount(entry) > 0 ? (
-                    <span className="shrink-0 text-[10px] text-warning-foreground">
-                      {missingImageCount(entry)} image
-                      {missingImageCount(entry) === 1 ? "" : "s"} dropped
-                    </span>
                   ) : null}
-                  <span className="shrink-0 text-secondary-label text-xs">
+                  <span className="shrink-0 text-secondary-label text-xs max-sm:hidden">
                     {formatRelativeTimeLabel(entry.createdAt)}
                   </span>
                   <Button
                     variant="ghost"
                     size="icon-xs"
+                    className="pointer-events-none shrink-0 [--control-icon-color:currentColor] opacity-0 shadow-none! transition-opacity pointer-coarse:pointer-events-auto pointer-coarse:opacity-100 group-hover/stash:pointer-events-auto group-hover/stash:opacity-100 group-focus-within/stash:pointer-events-auto group-focus-within/stash:opacity-100 focus-visible:pointer-events-auto focus-visible:opacity-100"
-                    className="shrink-0 opacity-0 transition-opacity group-hover/stash:opacity-100"
                     aria-label="Delete stashed prompt"
                     onClick={(event) => {
                       event.stopPropagation();
                       onDelete(entry);
                     }}
                   >
-                    <XIcon />
+                    <XIcon className="size-3.5 stroke-2" />
                   </Button>
                 </CommandItem>
               ))
```

## apps/web/src/components/chat/ComposerTasksBadge.test.tsx

Status: added; +130; −0

```diff
@@ -1,0 +1,130 @@
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it } from "vite-plus/test";
+
+import { ComposerTasksBadge, ComposerTasksDrawer } from "./ComposerTasksBadge";
+
+const progress = {
+  step: "Attach task progress",
+  completedSteps: 1,
+  totalSteps: 3,
+};
+const steps = [
+  { durationMs: 4_000, step: "Inspect the composer", status: "completed" as const },
+  { step: "Attach task progress", status: "inProgress" as const },
+  { step: "Verify the result", status: "pending" as const },
+];
+
+describe("ComposerTasksBadge", () => {
+  it("renders active progress as an attached composer tab", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerTasksBadge
+        expanded={false}
+        onDismiss={() => undefined}
+        onToggle={() => undefined}
+        progress={progress}
+        steps={steps}
+      />,
+    );
+
+    expect(markup).toContain('data-composer-tasks-badge="true"');
+    expect(markup).toContain('aria-expanded="false"');
+    expect(markup).toContain("chat-composer-shoulder-tab");
+    expect(markup).toContain("chat-composer-tasks-tab");
+    expect(markup).toContain("rounded-t-xl");
+    expect(markup).toContain("border-b-0");
+    expect(markup).toContain("left-4");
+    expect(markup).toContain("right-4");
+    expect(markup).toContain('data-composer-task-current="true"');
+    expect(markup).toContain("min-w-0 flex-1 truncate");
+    expect(markup).toContain("w-20");
+    expect(markup).toContain("Tasks");
+    expect(markup).toContain("Attach task progress");
+    expect(markup).not.toContain("·");
+    expect(markup).toContain("1/3");
+    expect(markup).toContain("Current task: Attach task progress");
+    expect(markup).toContain("lucide-list-todo");
+    expect(markup).toContain('aria-label="Dismiss tasks for this turn"');
+    expect(markup).toContain("lucide-x");
+    expect(markup).not.toContain("lucide-chevron");
+    expect(markup).toContain("bg-success");
+    expect(markup).toContain("bg-primary");
+    expect(markup).toContain("bg-muted-foreground/25");
+  });
+
+  it("leaves room for the stash tab when both shoulders are present", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerTasksBadge
+        expanded={false}
+        hasTrailingShoulder
+        onDismiss={() => undefined}
+        onToggle={() => undefined}
+        progress={progress}
+        steps={steps}
+      />,
+    );
+
+    expect(markup).toContain("right-28");
+    expect(markup).not.toContain("right-4");
+  });
+
+  it("has a compact inline fallback for occupied composer shoulders", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerTasksBadge
+        expanded={false}
+        onDismiss={() => undefined}
+        onToggle={() => undefined}
+        placement="inline"
+        progress={progress}
+        steps={steps}
+      />,
+    );
+
+    expect(markup).toContain("rounded-sm");
+    expect(markup).toContain("1/3");
+    expect(markup).not.toContain("chat-composer-shoulder-tab");
+    expect(markup).not.toContain("rounded-t-xl");
+  });
+
+  it("expands into a read-only attached task list", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerTasksDrawer
+        onCollapse={() => undefined}
+        onDismiss={() => undefined}
+        progress={progress}
+        steps={steps}
+      />,
+    );
+
+    expect(markup).toContain('data-chat-composer-tasks-drawer="true"');
+    expect(markup).not.toContain("data-variant");
+    expect(markup).toContain('aria-expanded="true"');
+    expect(markup).toContain('role="list"');
+    expect(markup).toContain("Inspect the composer");
+    expect(markup).toContain('data-composer-task-duration="true"');
+    expect(markup).toContain("ml-auto w-10");
+    expect(markup).toContain("4.0s");
+    expect(markup).toContain("now");
+    expect(markup).toContain("Attach task progress");
+    expect(markup).toContain("Verify the result");
+    expect(markup).toContain("lucide-list-todo");
+    expect(markup).toContain('aria-label="Dismiss tasks for this turn"');
+    expect(markup).not.toContain("lucide-chevron");
+    expect(markup).not.toContain("bg-success");
+    expect(markup).not.toContain("bg-primary");
+    expect(markup).not.toContain("bg-muted-foreground/25");
+  });
+
+  it("does not render an empty task count", () => {
+    const markup = renderToStaticMarkup(
+      <ComposerTasksBadge
+        expanded={false}
+        onDismiss={() => undefined}
+        onToggle={() => undefined}
+        progress={{ ...progress, totalSteps: 0 }}
+        steps={steps}
+      />,
+    );
+
+    expect(markup).toBe("");
+  });
+});
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/7150/diffs?after=ImFwcHMvd2ViL3NyYy9jb21wb25lbnRzL2NoYXQvQ29tcG9zZXJUYXNrc0JhZGdlLnRlc3QudHN4Ig&expectedVersion=117&format=markdown)
