authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-09-17 21:42:22+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 15:42:22-03:00
log4749035bda13b4b6260499caedbc0d69a2f60e6f
tree2cd697819f084feb7bb33c886721500879cdf6fc
parent03950089ffa5cecf0ce731227f58551f6437495f
signature Signed by PGP key B5690EEEBB952194

fix(web): keep composer banners compact and readable (#12166)


5 files changed, 87 insertions(+), 76 deletions(-)

apps/web/src/components/ChatView.tsx+2
...@@ -2201,6 +2201,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -2201,6 +2201,7 @@ export default function ChatView(props: ChatViewProps) {
2201 return {2201 return {
2202 id: `project-clone:${projectId}`,2202 id: `project-clone:${projectId}`,
2203 variant: "info",2203 variant: "info",
2204 compact: true,
2204 priority: "activity",2205 priority: "activity",
2205 icon: <DownloadIcon />,2206 icon: <DownloadIcon />,
2206 title: `Cloning ${name}`,2207 title: `Cloning ${name}`,
...@@ -2224,6 +2225,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -2224,6 +2225,7 @@ export default function ChatView(props: ChatViewProps) {
2224 return {2225 return {
2225 id: `project-clone:${projectId}`,2226 id: `project-clone:${projectId}`,
2226 variant: cancelled ? "warning" : "error",2227 variant: cancelled ? "warning" : "error",
2228 compact: true,
2227 icon: <DownloadIcon />,2229 icon: <DownloadIcon />,
2228 title: cancelled ? `Cancelled cloning ${name}` : `Failed to clone ${name}`,2230 title: cancelled ? `Cancelled cloning ${name}` : `Failed to clone ${name}`,
2229 description: cancelled ? "Retry to bring in the repository." : activeProjectClone.error,2231 description: cancelled ? "Retry to bring in the repository." : activeProjectClone.error,
apps/web/src/components/chat/ComposerBanner.tsx+1-1
...@@ -257,7 +257,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) {...@@ -257,7 +257,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) {
257 className={cn(257 className={cn(
258 "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1",258 "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1",
259 "group-data-[composer-banner-layout=approval]/banner-row:self-center group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:row-start-2",259 "group-data-[composer-banner-layout=approval]/banner-row:self-center group-data-[composer-banner-layout=approval]/banner-row:gap-1.5 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-start-2 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:col-end-4 @max-[560px]:group-data-[composer-banner-layout=approval]/banner-row:row-start-2",
260 "@max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-start",260 "@max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[400px]:group-data-[composer-banner-layout=wrap-actions]/banner-row:has-[>:nth-child(2)]:justify-end",
261 "@max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:justify-start",261 "@max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:col-end-4 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:row-start-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:-ms-2 @max-[320px]:group-data-[composer-banner-layout=wrap-actions-narrow]/banner-row:has-[>:nth-child(2)]:justify-start",
262 className,262 className,
263 )}263 )}
apps/web/src/components/chat/ComposerBannerStack.tsx+20-15
...@@ -13,6 +13,7 @@ export interface ComposerBannerStackItem {...@@ -13,6 +13,7 @@ export interface ComposerBannerStackItem {
13 readonly id: string;13 readonly id: string;
14 readonly variant: ComposerBannerVariant;14 readonly variant: ComposerBannerVariant;
15 readonly priority?: "urgent" | "activity" | "notice";15 readonly priority?: "urgent" | "activity" | "notice";
16 readonly compact?: boolean;
16 readonly icon: ReactNode;17 readonly icon: ReactNode;
17 readonly title: ReactNode;18 readonly title: ReactNode;
18 readonly description?: ReactNode;19 readonly description?: ReactNode;
...@@ -270,22 +271,20 @@ function ComposerBannerStackAlert({...@@ -270,22 +271,20 @@ function ComposerBannerStackAlert({
270 variant={item.variant}271 variant={item.variant}
271 density="comfortable"272 density="comfortable"
272 >273 >
273 <ComposerBanner.Row layout="wrap-actions-narrow">274 <ComposerBanner.Row layout={item.compact ? "wrap-actions-narrow" : "wrap-actions"}>
274 <ComposerBanner.Icon className="h-(--composer-banner-icon-column) self-start">275 <ComposerBanner.Icon className="h-(--composer-banner-icon-column) self-start">
275 {item.icon}276 {item.icon}
276 </ComposerBanner.Icon>277 </ComposerBanner.Icon>
277 <ComposerBanner.Content className="whitespace-nowrap">278 <ComposerBanner.Content className="whitespace-nowrap">
278 <span279 <span className="min-w-0 truncate font-medium leading-7 sm:leading-6">{item.title}</span>
279 className={cn(
280 "min-w-0 font-medium leading-7 sm:leading-6",
281 typeof item.title === "string" && "truncate",
282 )}
283 >
284 {item.title}
285 </span>
286 {item.description ? (280 {item.description ? (
287 <>281 <span className={item.compact ? "contents" : "flex min-w-8 flex-1 items-center gap-1"}>
288 <span className="min-w-0 shrink-[9999] truncate text-muted-foreground @max-[400px]:sr-only">282 <span
283 className={cn(
284 "min-w-0 truncate text-muted-foreground",
285 item.compact && "shrink-[9999] @max-[400px]:sr-only",
286 )}
287 >
289 {item.description}288 {item.description}
290 </span>289 </span>
291 <Popover>290 <Popover>
...@@ -296,21 +295,27 @@ function ComposerBannerStackAlert({...@@ -296,21 +295,27 @@ function ComposerBannerStackAlert({
296 size="icon-xs"295 size="icon-xs"
297 variant="ghost"296 variant="ghost"
298 aria-label="Show notice details"297 aria-label="Show notice details"
299 className="hidden flex-none text-muted-foreground hover:text-foreground @max-[400px]:inline-flex"298 className={cn(
299 "flex-none text-muted-foreground hover:text-foreground",
300 item.compact && "hidden @max-[400px]:inline-flex",
301 )}
300 />302 />
301 }303 }
302 >304 >
303 <InfoIcon className="size-3.5" />305 <InfoIcon className="size-3.5" />
304 </PopoverTrigger>306 </PopoverTrigger>
305 <PopoverPopup307 <PopoverPopup
308 aria-label="Notice details"
306 tooltipStyle309 tooltipStyle
307 side="top"310 side="top"
308 className="max-w-72 whitespace-normal text-pretty"311 className="max-w-80 whitespace-normal text-pretty wrap-anywhere"
309 >312 >
310 {item.description}313 <ComposerBanner.Scroll className="max-h-[min(var(--available-height),24rem,40dvh)]">
314 {item.description}
315 </ComposerBanner.Scroll>
311 </PopoverPopup>316 </PopoverPopup>
312 </Popover>317 </Popover>
313 </>318 </span>
314 ) : null}319 ) : null}
315 </ComposerBanner.Content>320 </ComposerBanner.Content>
316 {item.actions || item.onDismiss ? (321 {item.actions || item.onDismiss ? (
apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx+63-59
...@@ -228,66 +228,70 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(...@@ -228,66 +228,70 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard(
228 </ComposerBanner.Actions>228 </ComposerBanner.Actions>
229 </CollapsibleTrigger>229 </CollapsibleTrigger>
230 <CollapsiblePanel>230 <CollapsiblePanel>
231 <ComposerBanner.Body className="pe-1 pb-1">231 <ComposerBanner.Scroll>
232 <p className="text-sm text-foreground/85">{activeQuestion.question}</p>232 <ComposerBanner.Body className="pe-1 pb-1 wrap-anywhere">
233 {activeQuestion.multiSelect ? (233 <p className="text-sm text-foreground/85">{activeQuestion.question}</p>
234 <p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>234 {activeQuestion.multiSelect ? (
235 ) : null}235 <p className="mt-1 text-secondary-label text-xs">Select one or more options.</p>
236 <div className="mt-2 space-y-0.5">236 ) : null}
237 {activeQuestion.options.map((option, index) => {237 <div className="mt-2 space-y-0.5">
238 const optionValue = option.value ?? option.label;238 {activeQuestion.options.map((option, index) => {
239 const isOptimisticallySelected =239 const optionValue = option.value ?? option.label;
240 optimisticSingleSelect?.questionId === activeQuestion.id &&240 const isOptimisticallySelected =
241 optimisticSingleSelect.optionValue === optionValue;241 optimisticSingleSelect?.questionId === activeQuestion.id &&
242 const isSelected =242 optimisticSingleSelect.optionValue === optionValue;
243 isOptimisticallySelected ||243 const isSelected =
244 (!customAnswerActive && progress.selectedOptionValues.includes(optionValue));244 isOptimisticallySelected ||
245 const shortcutKey = index < 9 ? index + 1 : null;245 (!customAnswerActive && progress.selectedOptionValues.includes(optionValue));
246 const className = cn(246 const shortcutKey = index < 9 ? index + 1 : null;
247 "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",247 const className = cn(
248 isSelected248 "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",
249 ? "bg-muted/55 text-foreground"249 isSelected
250 : "bg-transparent text-foreground/85 hover:bg-muted/30",250 ? "bg-muted/55 text-foreground"
251 isResponding && "opacity-50 cursor-not-allowed",251 : "bg-transparent text-foreground/85 hover:bg-muted/30",
252 !isResponding && "cursor-pointer",252 isResponding && "opacity-50 cursor-not-allowed",
253 );253 !isResponding && "cursor-pointer",
254 const content = (254 );
255 <>255 const content = (
256 <div className="min-w-0 flex-1 flex flex-col gap-0.5">256 <>
257 <span className="text-sm font-medium">{option.label}</span>257 <div className="min-w-0 flex-1 flex flex-col gap-0.5">
258 {option.description && option.description !== option.label ? (258 <span className="text-sm font-medium">{option.label}</span>
259 <span className="text-secondary-label text-[11px]">{option.description}</span>259 {option.description && option.description !== option.label ? (
260 <span className="text-secondary-label text-[11px]">
261 {option.description}
262 </span>
263 ) : null}
264 </div>
265 {isSelected ? (
266 <CheckIcon className="size-3.5 shrink-0 text-primary" />
267 ) : shortcutKey !== null ? (
268 <kbd
269 className={cn(
270 "flex size-5 shrink-0 items-center justify-center text-[10px] font-medium text-muted-foreground tabular-nums",
271 )}
272 >
273 {shortcutKey}
274 </kbd>
260 ) : null}275 ) : null}
261 </div>276 </>
262 {isSelected ? (277 );
263 <CheckIcon className="size-3.5 shrink-0 text-primary" />278 return (
264 ) : shortcutKey !== null ? (279 <button
265 <kbd280 key={`${activeQuestion.id}:${optionValue}`}
266 className={cn(281 type="button"
267 "flex size-5 shrink-0 items-center justify-center text-[10px] font-medium text-muted-foreground tabular-nums",282 disabled={isResponding}
268 )}283 onClick={() => {
269 >284 handleOptionSelection(activeQuestion.id, optionValue);
270 {shortcutKey}285 }}
271 </kbd>286 className={className}
272 ) : null}287 >
273 </>288 {content}
274 );289 </button>
275 return (290 );
276 <button291 })}
277 key={`${activeQuestion.id}:${optionValue}`}292 </div>
278 type="button"293 </ComposerBanner.Body>
279 disabled={isResponding}294 </ComposerBanner.Scroll>
280 onClick={() => {
281 handleOptionSelection(activeQuestion.id, optionValue);
282 }}
283 className={className}
284 >
285 {content}
286 </button>
287 );
288 })}
289 </div>
290 </ComposerBanner.Body>
291 </CollapsiblePanel>295 </CollapsiblePanel>
292 </Collapsible>296 </Collapsible>
293 );297 );
apps/web/src/components/chat/ComposerTasksBadge.tsx+1-1
...@@ -92,7 +92,7 @@ function TaskSummary({...@@ -92,7 +92,7 @@ function TaskSummary({
92 >92 >
93 {progress.completedSteps}/{progress.totalSteps}93 {progress.completedSteps}/{progress.totalSteps}
94 </ComposerBanner.Count>94 </ComposerBanner.Count>
95 <TaskSegments className="hidden w-20 sm:flex" steps={steps} />95 <TaskSegments className="hidden w-20 @min-[560px]:flex" steps={steps} />
96 <ComposerBanner.ToggleIcon expanded={expanded} />96 <ComposerBanner.ToggleIcon expanded={expanded} />
97 </ComposerBanner.Actions>97 </ComposerBanner.Actions>
98 </>98 </>