authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-08-30 21:37:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-30 21:37:12-07:00
log9842518c9a0af14fb3a3e90993692a6e34032682
treea02bf4d11a48cf8b605cd2f60b823a8bd438f875
parent30175a8af04c0daa359652b5e8dc8230b40b462a
signature Signed by PGP key B5690EEEBB952194

fix(web): address composer banner review follow-ups (#8850)


10 files changed, 77 insertions(+), 24 deletions(-)

apps/web/src/components/ChatView.tsx+5-3
...@@ -188,7 +188,6 @@ import {...@@ -188,7 +188,6 @@ import {
188 CheckCircle2Icon,188 CheckCircle2Icon,
189 ChevronDownIcon,189 ChevronDownIcon,
190 GitBranchIcon,190 GitBranchIcon,
191 InfoIcon,
192 Minimize2Icon,191 Minimize2Icon,
193 PaperclipIcon,192 PaperclipIcon,
194 WifiOffIcon,193 WifiOffIcon,
...@@ -410,7 +409,10 @@ import {...@@ -410,7 +409,10 @@ import {
410} from "./ui/alert-dialog";409} from "./ui/alert-dialog";
411import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";410import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
412import { ServerUpdateAction } from "./ServerUpdateAction";411import { ServerUpdateAction } from "./ServerUpdateAction";
413import { ComposerServerUpdateStatus } from "./chat/ComposerServerUpdateStatus";412import {
413 ComposerServerUpdateIcon,
414 ComposerServerUpdateStatus,
415} from "./chat/ComposerServerUpdateStatus";
414import {416import {
415 buildVersionMismatchDismissalKey,417 buildVersionMismatchDismissalKey,
416 dismissServerUpdateFailure,418 dismissServerUpdateFailure,
...@@ -2274,7 +2276,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -2274,7 +2276,7 @@ function ChatViewContent(props: ChatViewProps) {
2274 variant: updateFailed ? "error" : "default",2276 variant: updateFailed ? "error" : "default",
2275 // Prioritize update progress over passive notices, but keep activity attached.2277 // Prioritize update progress over passive notices, but keep activity attached.
2276 priority: updateInProgress ? "urgent" : "notice",2278 priority: updateInProgress ? "urgent" : "notice",
2277 icon: <InfoIcon aria-hidden />,2279 icon: <ComposerServerUpdateIcon status={serverUpdateState.status} />,
2278 title:2280 title:
2279 updateInProgress || updateFailed ? (2281 updateInProgress || updateFailed ? (
2280 <ComposerServerUpdateStatus2282 <ComposerServerUpdateStatus
apps/web/src/components/chat/ChatComposer.tsx+4-5
...@@ -228,10 +228,9 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children:...@@ -228,10 +228,9 @@ function ComposerCommandMenuLayer(props: { anchor: HTMLElement | null; children:
228 const rect = (mainSurface ?? form ?? anchor).getBoundingClientRect();228 const rect = (mainSurface ?? form ?? anchor).getBoundingClientRect();
229 const rootFontSizePx =229 const rootFontSizePx =
230 Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16;230 Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16;
231 const drawerInsetRem =231 const drawerInsetRem = Number.parseFloat(
232 Number.parseFloat(232 window.getComputedStyle(form ?? anchor).getPropertyValue("--chat-composer-drawer-inset"),
233 window.getComputedStyle(form ?? anchor).getPropertyValue("--chat-composer-drawer-inset"),233 );
234 ) || 1.375;
235 const drawerInset = drawerInsetRem * rootFontSizePx;234 const drawerInset = drawerInsetRem * rootFontSizePx;
236 // One extra pixel prevents fractional layout coordinates from exposing235 // One extra pixel prevents fractional layout coordinates from exposing
237 // the canvas between the drawer mask and the composer's foreground edge.236 // the canvas between the drawer mask and the composer's foreground edge.
...@@ -2899,7 +2898,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2899,7 +2898,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2899 }2898 }
2900 : null;2899 : null;
2901 const bannerStackItems = activityStackItem2900 const bannerStackItems = activityStackItem
2902 ? [...props.bannerItems, activityStackItem]2901 ? [activityStackItem, ...props.bannerItems]
2903 : props.bannerItems;2902 : props.bannerItems;
2904 useEffect(() => {2903 useEffect(() => {
2905 if (activeTasksProgress === null || activeTaskSteps === null) {2904 if (activeTasksProgress === null || activeTaskSteps === null) {
apps/web/src/components/chat/ComposerBanner.tsx+6-2
...@@ -105,7 +105,7 @@ function Attachment({ className, ...props }: ComponentProps<"div">) {...@@ -105,7 +105,7 @@ function Attachment({ className, ...props }: ComponentProps<"div">) {
105 <div105 <div
106 data-slot="composer-banner-attachment"106 data-slot="composer-banner-attachment"
107 className={cn(107 className={cn(
108 "mx-auto -mb-[calc(1rem+1px)] w-[calc(100%-2.75rem)] max-w-[45.25rem]",108 "mx-auto -mb-[calc(1rem+1px)] w-[calc(100%-2*var(--chat-composer-drawer-inset))]",
109 // Adjacent attachments share their outline, including notices outside the form.109 // Adjacent attachments share their outline, including notices outside the form.
110 "[&+[data-slot=composer-banner-attachment]_[data-composer-banner-surface=attached]]:before:rounded-none [&+[data-slot=composer-banner-attachment]_[data-composer-banner-surface=attached]]:before:border-t-0",110 "[&+[data-slot=composer-banner-attachment]_[data-composer-banner-surface=attached]]:before:rounded-none [&+[data-slot=composer-banner-attachment]_[data-composer-banner-surface=attached]]:before:border-t-0",
111 "[&+:has([data-chat-composer-form])_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:rounded-none [&+:has([data-chat-composer-form])_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:border-t-0",111 "[&+:has([data-chat-composer-form])_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:rounded-none [&+:has([data-chat-composer-form])_[data-chat-composer-form]>[data-slot=composer-banner-attachment]:first-child_[data-composer-banner-surface=attached]]:before:border-t-0",
...@@ -311,7 +311,11 @@ function ToggleIcon({ expanded, className }: { expanded: boolean; className?: st...@@ -311,7 +311,11 @@ function ToggleIcon({ expanded, className }: { expanded: boolean; className?: st
311 return (311 return (
312 <span312 <span
313 aria-hidden313 aria-hidden
314 className={cn(buttonVariants({ size: "icon-xs", variant: "ghost" }), className)}314 className={cn(
315 buttonVariants({ size: "icon-xs", variant: "ghost" }),
316 "pointer-events-none",
317 className,
318 )}
315 >319 >
316 <ChevronDownIcon className={cn("size-3.5", !expanded && "rotate-180")} />320 <ChevronDownIcon className={cn("size-3.5", !expanded && "rotate-180")} />
317 </span>321 </span>
apps/web/src/components/chat/ComposerBannerStack.tsx+34-8
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { useEffect, useId, useRef, useState, type ReactNode } from "react";1import { useEffect, useId, useLayoutEffect, useRef, useState, type ReactNode } from "react";
22
3import { cn } from "~/lib/utils";3import { cn } from "~/lib/utils";
4import { ComposerBanner, type ComposerBannerVariant } from "./ComposerBanner";4import { ComposerBanner, type ComposerBannerVariant } from "./ComposerBanner";
...@@ -46,6 +46,8 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro...@@ -46,6 +46,8 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
46 const [stackExpanded, setStackExpanded] = useState(false);46 const [stackExpanded, setStackExpanded] = useState(false);
47 const noticesRef = useRef<HTMLDivElement>(null);47 const noticesRef = useRef<HTMLDivElement>(null);
48 const peekRef = useRef<HTMLButtonElement>(null);48 const peekRef = useRef<HTMLButtonElement>(null);
49 const expandedItemsRef = useRef<HTMLDivElement>(null);
50 const pendingFocusRef = useRef<"peek" | "notice" | null>(null);
49 const expandedItemsId = useId();51 const expandedItemsId = useId();
50 const [requestedExitingItemId, setExitingItemId] = useState<string | null>(null);52 const [requestedExitingItemId, setExitingItemId] = useState<string | null>(null);
51 const dismissTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);53 const dismissTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
...@@ -66,6 +68,19 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro...@@ -66,6 +68,19 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
66 if (items.length < 2) setStackExpanded(false);68 if (items.length < 2) setStackExpanded(false);
67 }, [items.length]);69 }, [items.length]);
6870
71 useLayoutEffect(() => {
72 if (stackExpanded && pendingFocusRef.current === "notice") {
73 pendingFocusRef.current = null;
74 const firstControl = expandedItemsRef.current?.querySelector<HTMLElement>(
75 'button:not(:disabled), a[href], input:not(:disabled), [tabindex="0"]',
76 );
77 (firstControl ?? expandedItemsRef.current)?.focus({ preventScroll: true });
78 } else if (!stackExpanded && pendingFocusRef.current === "peek") {
79 pendingFocusRef.current = null;
80 peekRef.current?.focus({ preventScroll: true });
81 }
82 }, [stackExpanded]);
83
69 if (items.length === 0) {84 if (items.length === 0) {
70 return null;85 return null;
71 }86 }
...@@ -130,14 +145,17 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro...@@ -130,14 +145,17 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
130 {hasStack ? (145 {hasStack ? (
131 <div146 <div
132 ref={noticesRef}147 ref={noticesRef}
133 className="relative z-20"148 className={cn("relative z-20", stackExpanded && "min-h-3")}
134 onPointerEnter={(event) => {149 onPointerEnter={(event) => {
135 if (event.pointerType !== "touch") setStackExpanded(true);150 if (event.pointerType === "touch") return;
151 if (document.activeElement === peekRef.current) {
152 pendingFocusRef.current = "notice";
153 }
154 setStackExpanded(true);
136 }}155 }}
137 onPointerLeave={(event) => {156 onPointerLeave={(event) => {
138 if (!event.currentTarget.contains(document.activeElement)) setStackExpanded(false);157 if (!event.currentTarget.contains(document.activeElement)) setStackExpanded(false);
139 }}158 }}
140 onFocusCapture={() => setStackExpanded(true)}
141 onBlurCapture={(event) => {159 onBlurCapture={(event) => {
142 if (160 if (
143 !event.currentTarget.contains(event.relatedTarget) &&161 !event.currentTarget.contains(event.relatedTarget) &&
...@@ -147,9 +165,10 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro...@@ -147,9 +165,10 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
147 }165 }
148 }}166 }}
149 onKeyDown={(event) => {167 onKeyDown={(event) => {
150 if (event.key !== "Escape") return;168 if (event.key !== "Escape" || !stackExpanded) return;
169 event.preventDefault();
151 event.stopPropagation();170 event.stopPropagation();
152 peekRef.current?.focus({ preventScroll: true });171 pendingFocusRef.current = "peek";
153 setStackExpanded(false);172 setStackExpanded(false);
154 }}173 }}
155 >174 >
...@@ -160,18 +179,25 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro...@@ -160,18 +179,25 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro
160 aria-label="Show other notices"179 aria-label="Show other notices"
161 aria-expanded={stackExpanded}180 aria-expanded={stackExpanded}
162 aria-controls={expandedItemsId}181 aria-controls={expandedItemsId}
182 aria-hidden={stackExpanded || undefined}
183 tabIndex={stackExpanded ? -1 : 0}
163 onClick={(event) => {184 onClick={(event) => {
164 event.currentTarget.focus({ preventScroll: true });185 event.currentTarget.focus({ preventScroll: true });
186 pendingFocusRef.current = "notice";
165 setStackExpanded(true);187 setStackExpanded(true);
166 }}188 }}
167 className={cn(stackExpanded && "opacity-0")}189 className={cn(stackExpanded && "pointer-events-none invisible opacity-0")}
168 />190 />
169 ) : null}191 ) : null}
170 <div192 <div
171 id={expandedItemsId}193 id={expandedItemsId}
194 ref={expandedItemsRef}
195 role="group"
196 aria-label="Other notices"
197 tabIndex={-1}
172 data-composer-banner-stack-expanded-items="true"198 data-composer-banner-stack-expanded-items="true"
173 className={cn(199 className={cn(
174 "grid transition-[grid-template-rows] duration-150 ease-out",200 "grid transition-[grid-template-rows] duration-150 ease-out focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring",
175 stackExpanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]",201 stackExpanded ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
176 )}202 )}
177 >203 >
apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx+1-1
...@@ -201,7 +201,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(...@@ -201,7 +201,7 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
201 </ComposerBanner.Actions>201 </ComposerBanner.Actions>
202 </CollapsibleTrigger>202 </CollapsibleTrigger>
203 <CollapsiblePanel>203 <CollapsiblePanel>
204 <ComposerBanner.Body>204 <ComposerBanner.Body className="pe-1 pb-1">
205 <p className="text-sm text-foreground/85">{activeQuestion.question}</p>205 <p className="text-sm text-foreground/85">{activeQuestion.question}</p>
206 {activeQuestion.multiSelect ? (206 {activeQuestion.multiSelect ? (
207 <p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>207 <p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>
apps/web/src/components/chat/ComposerServerUpdateStatus.tsx+15
...@@ -1,10 +1,25 @@...@@ -1,10 +1,25 @@
1import type { ServerUpdateState } from "@t3tools/client-runtime/state/server";1import type { ServerUpdateState } from "@t3tools/client-runtime/state/server";
2import { CircleAlertIcon, InfoIcon, LoaderCircleIcon } from "lucide-react";
2import { useId, useState } from "react";3import { useId, useState } from "react";
34
4import { serverUpdateStageLabel } from "../ServerUpdateAction";5import { serverUpdateStageLabel } from "../ServerUpdateAction";
5import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";6import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
6import { ComposerBanner } from "./ComposerBanner";7import { ComposerBanner } from "./ComposerBanner";
78
9export function ComposerServerUpdateIcon({
10 status,
11}: {
12 readonly status: ServerUpdateState["status"];
13}) {
14 if (status === "running") {
15 return <LoaderCircleIcon aria-hidden className="motion-safe:animate-spin" />;
16 }
17 if (status === "failed") {
18 return <CircleAlertIcon aria-hidden className="text-error" />;
19 }
20 return <InfoIcon aria-hidden />;
21}
22
8/** One text line, clipped at the end so the error detail never squeezes its title. */23/** One text line, clipped at the end so the error detail never squeezes its title. */
9export function ComposerServerUpdateStatus({24export function ComposerServerUpdateStatus({
10 state,25 state,
apps/web/src/components/chat/ComposerStashMenu.tsx+1-1
...@@ -130,7 +130,7 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: {...@@ -130,7 +130,7 @@ export const ComposerStashMenu = memo(function ComposerStashMenu(props: {
130 </ComposerBanner.Actions>130 </ComposerBanner.Actions>
131 </ComposerBanner.Row>131 </ComposerBanner.Row>
132 <ComposerBanner.Scroll>132 <ComposerBanner.Scroll>
133 <ComposerBanner.Children render={<ul />} aria-label="Stashed prompts">133 <ComposerBanner.Children render={<ul role="list" />} aria-label="Stashed prompts">
134 {entries.length === 0 ? (134 {entries.length === 0 ? (
135 <ComposerBanner.Row render={<li />}>135 <ComposerBanner.Row render={<li />}>
136 <ComposerBanner.Icon />136 <ComposerBanner.Icon />
apps/web/src/components/chat/ComposerSurface.tsx+3-3
...@@ -14,7 +14,7 @@ function Shell({...@@ -14,7 +14,7 @@ function Shell({
14 data-with-context={contextStrip || undefined}14 data-with-context={contextStrip || undefined}
15 className={cn(15 className={cn(
16 "group/composer-surface relative isolate mx-auto w-full max-w-3xl",16 "group/composer-surface relative isolate mx-auto w-full max-w-3xl",
17 "[--chat-composer-glass-surface:var(--card)] [--chat-composer-outline:rgb(0_0_0/8%)]",17 "[--chat-composer-drawer-inset:1.375rem] [--chat-composer-glass-surface:var(--card)] [--chat-composer-outline:rgb(0_0_0/8%)]",
18 "dark:[--chat-composer-glass-surface:color-mix(in_srgb,var(--background)_96%,var(--color-white))] dark:[--chat-composer-highlight:rgb(255_255_255/3%)] dark:[--chat-composer-outline:color-mix(in_srgb,var(--color-white)_5%,transparent)]",18 "dark:[--chat-composer-glass-surface:color-mix(in_srgb,var(--background)_96%,var(--color-white))] dark:[--chat-composer-highlight:rgb(255_255_255/3%)] dark:[--chat-composer-outline:color-mix(in_srgb,var(--color-white)_5%,transparent)]",
19 "[html[data-theme-id]_&]:[--chat-composer-glass-surface:var(--app-theme-surface-raised)] [html[data-theme-id]_&]:[--chat-composer-outline:var(--app-theme-toolbar-border)]",19 "[html[data-theme-id]_&]:[--chat-composer-glass-surface:var(--app-theme-surface-raised)] [html[data-theme-id]_&]:[--chat-composer-outline:var(--app-theme-toolbar-border)]",
20 "dark:[html[data-theme-id]:not([data-theme-id=t3-chat])_&]:[--chat-composer-highlight:color-mix(in_srgb,var(--app-theme-input)_12%,transparent)] dark:[html[data-theme-id]:not([data-theme-id=t3-chat])_&]:[--chat-composer-outline:color-mix(in_srgb,var(--app-theme-input)_30%,var(--background))]",20 "dark:[html[data-theme-id]:not([data-theme-id=t3-chat])_&]:[--chat-composer-highlight:color-mix(in_srgb,var(--app-theme-input)_12%,transparent)] dark:[html[data-theme-id]:not([data-theme-id=t3-chat])_&]:[--chat-composer-outline:color-mix(in_srgb,var(--app-theme-input)_30%,var(--background))]",
...@@ -26,7 +26,7 @@ function Shell({...@@ -26,7 +26,7 @@ function Shell({
26 "[--chat-composer-context-extension:2.25rem] sm:[--chat-composer-context-extension:2rem]",26 "[--chat-composer-context-extension:2.25rem] sm:[--chat-composer-context-extension:2rem]",
27 // Keep one continuous backdrop around the fixed-pixel corners and rem-sized strip inset.27 // Keep one continuous backdrop around the fixed-pixel corners and rem-sized strip inset.
28 "supports-[clip-path:shape(from_0_0,line_to_1px_1px)]:before:rounded-none",28 "supports-[clip-path:shape(from_0_0,line_to_1px_1px)]:before:rounded-none",
29 "before:[clip-path:shape(from_0_22px,curve_to_22px_0_with_0_9.85px/9.85px_0,line_to_calc(100%-22px)_0,curve_to_100%_22px_with_calc(100%-9.85px)_0/100%_9.85px,line_to_100%_calc(100%-var(--chat-composer-context-extension)-1.375rem),curve_to_calc(100%-1.375rem)_calc(100%-var(--chat-composer-context-extension))_with_100%_calc(100%-var(--chat-composer-context-extension)-0.6156rem)/calc(100%-0.6156rem)_calc(100%-var(--chat-composer-context-extension)),line_to_calc(100%-1.375rem)_calc(100%-16px),curve_to_calc(100%-1.375rem-16px)_100%_with_calc(100%-1.375rem)_calc(100%-7.16px)/calc(100%-1.375rem-7.16px)_100%,line_to_calc(1.375rem+16px)_100%,curve_to_1.375rem_calc(100%-16px)_with_calc(1.375rem+7.16px)_100%/1.375rem_calc(100%-7.16px),line_to_1.375rem_calc(100%-var(--chat-composer-context-extension)),curve_to_0_calc(100%-var(--chat-composer-context-extension)-1.375rem)_with_0.6156rem_calc(100%-var(--chat-composer-context-extension))/0_calc(100%-var(--chat-composer-context-extension)-0.6156rem),line_to_0_22px,close)]",29 "before:[clip-path:shape(from_0_22px,curve_to_22px_0_with_0_9.85px/9.85px_0,line_to_calc(100%-22px)_0,curve_to_100%_22px_with_calc(100%-9.85px)_0/100%_9.85px,line_to_100%_calc(100%-var(--chat-composer-context-extension)-var(--chat-composer-drawer-inset)),curve_to_calc(100%-var(--chat-composer-drawer-inset))_calc(100%-var(--chat-composer-context-extension))_with_100%_calc(100%-var(--chat-composer-context-extension)-var(--chat-composer-drawer-inset)*0.4477)/calc(100%-var(--chat-composer-drawer-inset)*0.4477)_calc(100%-var(--chat-composer-context-extension)),line_to_calc(100%-var(--chat-composer-drawer-inset))_calc(100%-16px),curve_to_calc(100%-var(--chat-composer-drawer-inset)-16px)_100%_with_calc(100%-var(--chat-composer-drawer-inset))_calc(100%-7.16px)/calc(100%-var(--chat-composer-drawer-inset)-7.16px)_100%,line_to_calc(var(--chat-composer-drawer-inset)+16px)_100%,curve_to_var(--chat-composer-drawer-inset)_calc(100%-16px)_with_calc(var(--chat-composer-drawer-inset)+7.16px)_100%/var(--chat-composer-drawer-inset)_calc(100%-7.16px),line_to_var(--chat-composer-drawer-inset)_calc(100%-var(--chat-composer-context-extension)),curve_to_0_calc(100%-var(--chat-composer-context-extension)-var(--chat-composer-drawer-inset))_with_calc(var(--chat-composer-drawer-inset)*0.4477)_calc(100%-var(--chat-composer-context-extension))/0_calc(100%-var(--chat-composer-context-extension)-var(--chat-composer-drawer-inset)*0.4477),line_to_0_22px,close)]",
30 "not-supports-[clip-path:shape(from_0_0,line_to_1px_1px)]:before:bottom-(--chat-composer-context-extension)",30 "not-supports-[clip-path:shape(from_0_0,line_to_1px_1px)]:before:bottom-(--chat-composer-context-extension)",
31 ],31 ],
32 className,32 className,
...@@ -84,7 +84,7 @@ function ContextStrip({ className, ...props }: ComponentProps<"div">) {...@@ -84,7 +84,7 @@ function ContextStrip({ className, ...props }: ComponentProps<"div">) {
84 <div84 <div
85 data-slot="composer-context-strip"85 data-slot="composer-context-strip"
86 className={cn(86 className={cn(
87 "group/composer-context relative isolate mx-auto -mt-4 flex w-[calc(100%-2.75rem)] max-w-181 items-center gap-2 overflow-x-clip overflow-y-visible ps-1 pe-2 pt-5 pb-1",87 "group/composer-context relative isolate mx-auto -mt-4 flex w-[calc(100%-2*var(--chat-composer-drawer-inset))] items-center gap-2 overflow-x-clip overflow-y-visible ps-1 pe-2 pt-5 pb-1",
88 "before:absolute before:inset-0 before:-z-1 before:rounded-b-[16px] before:border before:border-(--chat-composer-outline) before:mask-[linear-gradient(to_bottom,transparent_0_1rem,black_1rem)] before:shadow-[0_12px_28px_-18px_rgb(0_0_0/40%)]",88 "before:absolute before:inset-0 before:-z-1 before:rounded-b-[16px] before:border before:border-(--chat-composer-outline) before:mask-[linear-gradient(to_bottom,transparent_0_1rem,black_1rem)] before:shadow-[0_12px_28px_-18px_rgb(0_0_0/40%)]",
89 "dark:before:border-white/7 dark:before:bg-[linear-gradient(to_bottom,transparent_0_1rem,rgb(0_0_0/18%)_1rem,transparent_calc(1rem+10px)),linear-gradient(rgb(255_255_255/2%),rgb(255_255_255/2%))] dark:before:shadow-[0_14px_32px_-18px_rgb(0_0_0/75%)]",89 "dark:before:border-white/7 dark:before:bg-[linear-gradient(to_bottom,transparent_0_1rem,rgb(0_0_0/18%)_1rem,transparent_calc(1rem+10px)),linear-gradient(rgb(255_255_255/2%),rgb(255_255_255/2%))] dark:before:shadow-[0_14px_32px_-18px_rgb(0_0_0/75%)]",
90 "group-has-data-[composer-banner-surface=attached]/composer-surface:before:bg-[color-mix(in_srgb,var(--chat-composer-glass-surface)_var(--glass-opacity),transparent)] group-has-data-[composer-banner-surface=attached]/composer-surface:before:backdrop-blur-(--glass-blur) group-has-data-[composer-banner-surface=attached]/composer-surface:before:backdrop-saturate-(--glass-saturation)",90 "group-has-data-[composer-banner-surface=attached]/composer-surface:before:bg-[color-mix(in_srgb,var(--chat-composer-glass-surface)_var(--glass-opacity),transparent)] group-has-data-[composer-banner-surface=attached]/composer-surface:before:backdrop-blur-(--glass-blur) group-has-data-[composer-banner-surface=attached]/composer-surface:before:backdrop-saturate-(--glass-saturation)",
apps/web/src/components/chat/ComposerTasksBadge.tsx+1-1
...@@ -153,7 +153,7 @@ export const ComposerTasksContent = memo(function ComposerTasksContent({...@@ -153,7 +153,7 @@ export const ComposerTasksContent = memo(function ComposerTasksContent({
153 {expanded ? (153 {expanded ? (
154 <ComposerBanner.Scroll data-composer-tasks-scroll="true">154 <ComposerBanner.Scroll data-composer-tasks-scroll="true">
155 <ComposerBanner.Children155 <ComposerBanner.Children
156 render={<ul />}156 render={<ul role="list" />}
157 aria-label={`Task list. ${progress.completedSteps} of ${progress.totalSteps} complete.`}157 aria-label={`Task list. ${progress.completedSteps} of ${progress.totalSteps} complete.`}
158 data-composer-tasks-list="true"158 data-composer-tasks-list="true"
159 >159 >
docs/user/composer.md+7
...@@ -35,6 +35,13 @@ On mobile, the model picker shows each OpenCode model's upstream provider, such...@@ -35,6 +35,13 @@ On mobile, the model picker shows each OpenCode model's upstream provider, such
35GitHub Copilot, or OpenCode Zen, beneath its name. Search by that provider name to narrow the list35GitHub Copilot, or OpenCode Zen, beneath its name. Search by that provider name to narrow the list
36when starting a thread or changing an existing thread's model.36when starting a thread or changing an existing thread's model.
3737
38## Notices above the composer
39
40On web and desktop, additional notices peek out above the attached banner. Hover over the peek
41to reveal them, or focus **Show other notices** with `Tab` and press `Enter` or `Space`. Press
42`Escape` to close the stack and return focus to that control. On a touchscreen, tap the peek to
43open the stack. Interacting with the attached banner or composer does not open the stack.
44
38## Prompt stash45## Prompt stash
3946
40Use the default shortcut, `Cmd+S` on macOS or `Ctrl+S` on Windows and Linux, to stash the current47Use the default shortcut, `Cmd+S` on macOS or `Ctrl+S` on Windows and Linux, to stash the current