| author | |
| committer | |
| log | 9842518c9a0af14fb3a3e90993692a6e34032682 |
| tree | a02bf4d11a48cf8b605cd2f60b823a8bd438f875 |
| parent | 30175a8af04c0daa359652b5e8dc8230b40b462a |
| signature | Signed by PGP key B5690EEEBB952194 |
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"; |
| 411 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | 410 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 412 | import { ServerUpdateAction } from "./ServerUpdateAction"; | 411 | import { ServerUpdateAction } from "./ServerUpdateAction"; |
| 413 | import { ComposerServerUpdateStatus } from "./chat/ComposerServerUpdateStatus"; | 412 | import { |
| 413 | ComposerServerUpdateIcon, | ||
| 414 | ComposerServerUpdateStatus, | ||
| 415 | } from "./chat/ComposerServerUpdateStatus"; | ||
| 414 | import { | 416 | import { |
| 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 | <ComposerServerUpdateStatus | 2282 | <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 exposing | 235 | // 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 = activityStackItem | 2900 | 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 | <div | 105 | <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 | <span | 312 | <span |
| 313 | aria-hidden | 313 | 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 @@ |
| 1 | import { useEffect, useId, useRef, useState, type ReactNode } from "react"; | 1 | import { useEffect, useId, useLayoutEffect, useRef, useState, type ReactNode } from "react"; |
| 2 | 2 | ||
| 3 | import { cn } from "~/lib/utils"; | 3 | import { cn } from "~/lib/utils"; |
| 4 | import { ComposerBanner, type ComposerBannerVariant } from "./ComposerBanner"; | 4 | import { 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]); |
| 68 | 70 | ||
| 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 | <div | 146 | <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 | <div | 192 | <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 @@ |
| 1 | import type { ServerUpdateState } from "@t3tools/client-runtime/state/server"; | 1 | import type { ServerUpdateState } from "@t3tools/client-runtime/state/server"; |
| 2 | import { CircleAlertIcon, InfoIcon, LoaderCircleIcon } from "lucide-react"; | ||
| 2 | import { useId, useState } from "react"; | 3 | import { useId, useState } from "react"; |
| 3 | 4 | ||
| 4 | import { serverUpdateStageLabel } from "../ServerUpdateAction"; | 5 | import { serverUpdateStageLabel } from "../ServerUpdateAction"; |
| 5 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 6 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 6 | import { ComposerBanner } from "./ComposerBanner"; | 7 | import { ComposerBanner } from "./ComposerBanner"; |
| 7 | 8 | ||
| 9 | export 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. */ |
| 9 | export function ComposerServerUpdateStatus({ | 24 | export 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 | <div | 84 | <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.Children | 155 | <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 |
| 35 | GitHub Copilot, or OpenCode Zen, beneath its name. Search by that provider name to narrow the list | 35 | GitHub Copilot, or OpenCode Zen, beneath its name. Search by that provider name to narrow the list |
| 36 | when starting a thread or changing an existing thread's model. | 36 | when starting a thread or changing an existing thread's model. |
| 37 | 37 | ||
| 38 | ## Notices above the composer | ||
| 39 | |||
| 40 | On web and desktop, additional notices peek out above the attached banner. Hover over the peek | ||
| 41 | to 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 | ||
| 43 | open the stack. Interacting with the attached banner or composer does not open the stack. | ||
| 44 | |||
| 38 | ## Prompt stash | 45 | ## Prompt stash |
| 39 | 46 | ||
| 40 | Use the default shortcut, `Cmd+S` on macOS or `Ctrl+S` on Windows and Linux, to stash the current | 47 | Use the default shortcut, `Cmd+S` on macOS or `Ctrl+S` on Windows and Linux, to stash the current |