Merge remote-tracking branch 'origin/main' into HEAD

# Conflicts:
#	webui/src/components/settings/SettingsView.tsx
This commit is contained in:
chengyongru
2026-06-15 18:11:22 +08:00
45 changed files with 1010 additions and 203 deletions
+1 -1
View File
@@ -80,7 +80,7 @@ export function DeleteConfirm({
</div>
) : null}
</AlertDialogHeader>
<AlertDialogFooter className="mt-7 grid grid-cols-2 gap-3 space-x-0">
<AlertDialogFooter className="mt-7 grid grid-cols-1 gap-3 space-x-0 sm:grid-cols-2">
<AlertDialogCancel
onClick={onCancel}
className="mt-0 h-11 rounded-full border-0 bg-muted/70 px-5 text-[15px] font-semibold text-foreground shadow-none hover:bg-muted"
+2 -2
View File
@@ -107,7 +107,7 @@ export function FilePreviewPanel({
"--file-preview-slot-width": !entered || isClosing ? "0px" : `${desktopWidth}px`,
} as CSSProperties}
className={cn(
"absolute inset-y-0 right-0 z-30 w-[min(92vw,var(--file-preview-slot-width))] overflow-hidden",
"absolute inset-y-0 right-0 z-30 w-[min(100vw,var(--file-preview-slot-width))] overflow-hidden",
"transition-[width] duration-300 ease-out will-change-[width]",
"md:relative md:z-auto md:w-[var(--file-preview-slot-width)] md:min-w-0 md:shrink-0",
isClosing && "pointer-events-none",
@@ -117,7 +117,7 @@ export function FilePreviewPanel({
>
<div
className={cn(
"absolute inset-y-0 right-0 flex w-[min(92vw,var(--file-preview-width))] flex-col overflow-hidden md:w-[var(--file-preview-width)]",
"absolute inset-y-0 right-0 flex w-[min(100vw,var(--file-preview-width))] flex-col overflow-hidden pb-[env(safe-area-inset-bottom)] md:w-[var(--file-preview-width)] md:pb-0",
"border-l border-border/70 bg-background shadow-2xl md:shadow-none",
"transition-[opacity,transform] duration-300 ease-out will-change-transform",
!entered || isClosing ? "translate-x-full opacity-0" : "translate-x-0 opacity-100",
@@ -1715,7 +1715,7 @@ export function SettingsView({
<main className="min-w-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]">
<div
className={cn(
"mx-auto w-full px-5 py-8 sm:px-8 lg:py-12",
"mx-auto w-full px-4 py-6 sm:px-8 sm:py-8 lg:py-12",
activeSection === "automations"
? "max-w-[1220px] 2xl:max-w-[1320px]"
: "max-w-[920px]",
@@ -1806,7 +1806,7 @@ function SettingsSidebar({
return (
<aside
className={cn(
"flex w-full shrink-0 flex-col border-b border-border/55 bg-card/62 px-4 pb-3 shadow-[inset_0_-1px_0_rgba(255,255,255,0.55)] backdrop-blur-xl dark:bg-card/45 dark:shadow-none md:w-[17rem] md:border-b-0 md:border-r md:px-3 md:pb-4 md:shadow-[inset_-1px_0_0_rgba(255,255,255,0.55)]",
"flex w-full shrink-0 flex-col border-b border-border/55 bg-card/62 px-3 pb-2 shadow-[inset_0_-1px_0_rgba(255,255,255,0.55)] backdrop-blur-xl dark:bg-card/45 dark:shadow-none md:w-[17rem] md:border-b-0 md:border-r md:px-3 md:pb-4 md:shadow-[inset_-1px_0_0_rgba(255,255,255,0.55)]",
hostChromeInset ? "pt-[4.25rem] md:pt-[4.25rem]" : "pt-4 md:pt-4",
)}
>
@@ -2096,7 +2096,10 @@ function VersionCheckRow({ currentVersion }: { currentVersion?: string }) {
{result?.type === "update" ? (
<span className="inline-flex items-center gap-1.5 text-[12px] text-blue-600 dark:text-blue-300">
<ArrowUpCircle className="h-3 w-3" aria-hidden />
{tx("settings.about.updateAvailable", "Update available")}{result.latestVersion && ` v${result.latestVersion}`}
{t("settings.about.updateAvailable", {
defaultValue: "Update available v{{version}}",
version: result.latestVersion,
})}
{result.pypiUrl ? (
<a
href={result.pypiUrl}
@@ -7064,7 +7067,7 @@ function SettingsRow({
</div>
) : null}
</div>
{children ? <div className="shrink-0 sm:ml-6">{children}</div> : null}
{children ? <div className="min-w-0 sm:ml-6 sm:shrink-0">{children}</div> : null}
</div>
);
}
@@ -7080,7 +7083,7 @@ function ReadOnlyRow({
}) {
return (
<SettingsRow title={title} description={description}>
<span className="block max-w-[320px] truncate text-right text-[13px] text-muted-foreground">
<span className="block max-w-full truncate text-left text-[13px] text-muted-foreground sm:max-w-[320px] sm:text-right">
{value}
</span>
</SettingsRow>
@@ -7526,7 +7529,7 @@ function NumberInput({
const parsed = Number(event.target.value);
if (Number.isFinite(parsed)) onChange(parsed);
}}
className="h-8 w-24 rounded-full text-[13px]"
className="h-8 w-24 max-w-full rounded-full text-[13px]"
/>
{suffix ? <span className="text-[12px] text-muted-foreground">{suffix}</span> : null}
</div>
@@ -177,7 +177,7 @@ export function TokenUsageHeatmap({
return (
<div className="overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<div className="mx-auto w-full min-w-[760px] max-w-[1054px] px-0.5">
<div className="mx-auto w-full min-w-0 max-w-[1054px] px-0.5 sm:min-w-[760px]">
<div className="mb-2 flex justify-end">
<span className="text-[11px] font-normal leading-none text-muted-foreground/64">
{tx("settings.usage.shortTitle", "Token Usage")}
@@ -195,7 +195,7 @@ export function TokenUsageHeatmap({
))}
</div>
<div
className="grid grid-flow-col grid-rows-7 gap-1.5"
className="grid grid-flow-col grid-rows-7 gap-[3px] sm:gap-1.5"
style={{ gridTemplateColumns: `repeat(${TOKEN_HEATMAP_COLUMNS}, minmax(0, 1fr))` }}
aria-label={tx("settings.usage.title", "Token activity")}
>
@@ -224,7 +224,7 @@ export function TokenUsageHeatmap({
<span
aria-label={ariaLabel}
className={cn(
"aspect-square w-full rounded-[4px] transition-transform hover:scale-110",
"aspect-square w-full rounded-[2px] transition-transform hover:scale-110 sm:rounded-[4px]",
tokenUsageCellClass(level, cell.future),
)}
/>
+43 -16
View File
@@ -419,9 +419,20 @@ function suppressNativeDragPreview(dataTransfer: DataTransfer): void {
window.setTimeout(() => ghost.remove(), 0);
}
function visualViewportBounds(): { top: number; bottom: number; height: number } {
const viewport = window.visualViewport;
if (!viewport) {
return { top: 0, bottom: window.innerHeight, height: window.innerHeight };
}
const top = Math.max(0, viewport.offsetTop);
const height = Math.max(0, viewport.height);
return { top, bottom: top + height, height };
}
function getVisibleBounds(el: HTMLElement): { top: number; bottom: number } {
let top = 0;
let bottom = window.innerHeight;
const viewport = visualViewportBounds();
let top = viewport.top;
let bottom = viewport.bottom;
let parent = el.parentElement;
while (parent) {
@@ -455,11 +466,12 @@ const GOAL_PANEL_MIN_HEIGHT_PX = 112;
const GOAL_PANEL_MAX_VIEWPORT_RATIO = 0.62;
function measureGoalPanelMaxCssHeight(stripTopY: number): number {
const viewport = visualViewportBounds();
const spaceAboveStrip =
stripTopY - GOAL_PANEL_VIEWPORT_TOP_PAD - GOAL_PANEL_GAP_ABOVE_STRIP_PX;
stripTopY - viewport.top - GOAL_PANEL_VIEWPORT_TOP_PAD - GOAL_PANEL_GAP_ABOVE_STRIP_PX;
return Math.min(
Math.max(spaceAboveStrip, GOAL_PANEL_MIN_HEIGHT_PX),
Math.floor(window.innerHeight * GOAL_PANEL_MAX_VIEWPORT_RATIO),
Math.floor(viewport.height * GOAL_PANEL_MAX_VIEWPORT_RATIO),
);
}
@@ -593,10 +605,15 @@ function RunElapsedStrip({
if (stripWrapperRef.current && ro) {
ro.observe(stripWrapperRef.current);
}
const viewport = window.visualViewport;
viewport?.addEventListener("resize", relayout);
viewport?.addEventListener("scroll", relayout);
window.addEventListener("resize", relayout);
window.addEventListener("scroll", relayout, true);
return () => {
ro?.disconnect();
viewport?.removeEventListener("resize", relayout);
viewport?.removeEventListener("scroll", relayout);
window.removeEventListener("resize", relayout);
window.removeEventListener("scroll", relayout, true);
};
@@ -1111,9 +1128,14 @@ export function ThreadComposer({
};
updateLayout();
const viewport = window.visualViewport;
viewport?.addEventListener("resize", updateLayout);
viewport?.addEventListener("scroll", updateLayout);
window.addEventListener("resize", updateLayout);
document.addEventListener("scroll", updateLayout, true);
return () => {
viewport?.removeEventListener("resize", updateLayout);
viewport?.removeEventListener("scroll", updateLayout);
window.removeEventListener("resize", updateLayout);
document.removeEventListener("scroll", updateLayout, true);
};
@@ -1544,10 +1566,10 @@ export function ThreadComposer({
"w-full resize-none bg-transparent",
isHero
? cn(
"min-h-[78px] px-5 text-[15px] leading-6",
"min-h-[78px] px-4 text-[15px] leading-6 sm:px-5",
relaxedHeroInput ? "pb-2 pt-[27px]" : "pb-1.5 pt-4",
)
: "min-h-[50px] px-4 pb-1.5 pt-3 text-[13.5px] leading-5",
: "min-h-[50px] px-3.5 pb-1.5 pt-3 text-[13.5px] leading-5 sm:px-4",
);
return (
@@ -1699,11 +1721,13 @@ export function ThreadComposer({
) : null}
<div
className={cn(
"flex items-center justify-between",
isHero ? cn("gap-1.5 px-4", showProjectPicker ? "pb-1.5" : "pb-3.5") : "gap-2 px-3 pb-2",
"flex flex-wrap items-center justify-between gap-y-2",
isHero
? cn("gap-x-1.5 px-3 sm:px-4", showProjectPicker ? "pb-1.5" : "pb-3.5")
: "gap-x-2 px-2.5 pb-2 sm:px-3",
)}
>
<div className={cn("flex min-w-0 flex-1 items-center", isHero ? "gap-1.5" : "gap-2")}>
<div className={cn("flex min-w-0 flex-1 basis-[8rem] items-center", isHero ? "gap-1.5" : "gap-2")}>
<input
ref={fileInputRef}
type="file"
@@ -1746,7 +1770,7 @@ export function ThreadComposer({
/>
) : null}
</div>
<div className={cn("flex shrink-0 items-center", isHero ? "gap-1.5" : "gap-2")}>
<div className={cn("ml-auto flex min-w-0 shrink-0 items-center", isHero ? "gap-1.5" : "gap-2")}>
{modelLabel && !voiceRecorder.isRecording ? (
<ComposerModelBadge
label={modelLabel}
@@ -1768,6 +1792,7 @@ export function ThreadComposer({
disabled={voiceRecorder.buttonDisabled}
aria-label={voiceButtonLabel}
aria-keyshortcuts={VOICE_SHORTCUT_ARIA}
title={voiceButtonTooltip}
onPointerDown={voiceRecorder.beginPress}
onPointerUp={voiceRecorder.endPress}
onPointerCancel={voiceRecorder.endPress}
@@ -2071,7 +2096,9 @@ function ComposerModelBadge({
"shadow-[0_2px_8px_rgba(15,23,42,0.045)]",
interactive && "cursor-pointer hover:bg-accent/55 hover:text-foreground",
needsSetup && "border-amber-500/35 bg-amber-50/70 text-amber-900 dark:bg-amber-500/10 dark:text-amber-200",
isHero ? "h-8 max-w-[12.5rem] gap-1.5 px-2 text-[11.5px]" : "h-9 max-w-[12rem] gap-2 px-2.5 text-[12px]",
isHero
? "h-8 max-w-[min(12.5rem,44vw)] gap-1.5 px-2 text-[11.5px]"
: "h-9 max-w-[min(12rem,44vw)] gap-2 px-2.5 text-[12px]",
)}
>
<span
@@ -2252,7 +2279,7 @@ function CliAppMentionPalette({
onChoose(candidate);
}}
className={cn(
"flex h-10 w-full items-center gap-2.5 rounded-[13px] px-2.5 text-left transition-colors",
"flex min-h-10 w-full items-center gap-2.5 rounded-[13px] px-2.5 py-1.5 text-left transition-colors",
selected
? "bg-foreground/[0.055] text-foreground"
: "text-foreground/90 hover:bg-foreground/[0.04]",
@@ -2260,7 +2287,7 @@ function CliAppMentionPalette({
>
<MentionCandidateLogo candidate={candidate} selected={selected} />
<span className="flex min-w-0 flex-1 items-baseline gap-2">
<span className="shrink-0 text-[15px] font-medium tracking-normal text-foreground">
<span className="min-w-0 truncate text-[15px] font-medium tracking-normal text-foreground">
{displayName}
</span>
<span className="truncate text-[15px] font-normal tracking-normal text-muted-foreground/72">
@@ -2396,7 +2423,7 @@ function SlashCommandPalette({
>
<Icon className="h-4 w-4" />
</span>
<span className="flex min-w-0 flex-1 items-baseline gap-2">
<span className="flex min-w-0 flex-1 flex-col gap-0.5 sm:flex-row sm:items-baseline sm:gap-2">
<span className="min-w-0 truncate text-[13.5px] font-semibold tracking-normal text-foreground">
{title}
</span>
@@ -2404,7 +2431,7 @@ function SlashCommandPalette({
{command.detail || description}
</span>
</span>
<span className="ml-2 flex shrink-0 items-center gap-1.5">
<span className="ml-2 flex max-w-[42%] shrink-0 items-center gap-1.5 sm:max-w-none">
{command.badge || command.recent ? (
<span className="hidden rounded-full bg-foreground/[0.055] px-2 py-1 text-[11px] font-medium text-muted-foreground sm:inline-flex">
{command.badge ?? t("thread.composer.slash.badges.recent")}
@@ -2486,7 +2513,7 @@ function AttachmentChip({
) : null}
</div>
<div className="flex min-w-0 flex-col text-[11.5px] leading-4">
<span className="truncate max-w-[14rem] font-medium" title={image.file.name}>
<span className="max-w-[min(14rem,calc(100vw-8rem))] truncate font-medium" title={image.file.name}>
{image.file.name}
</span>
<span className="truncate text-muted-foreground">
+1 -1
View File
@@ -725,7 +725,7 @@ export function ThreadShell({
</div>
) : (
<div className="flex w-full flex-col items-center text-center animate-in fade-in-0 slide-in-from-bottom-2 duration-500">
<h1 className="text-balance text-[40px] font-normal leading-tight tracking-[-0.045em] text-foreground sm:text-[48px]">
<h1 className="max-w-[30rem] text-balance text-[34px] font-normal leading-[1.08] tracking-normal text-foreground sm:text-[48px] sm:leading-tight">
{t(heroGreetingKey)}
</h1>
</div>
+125 -15
View File
@@ -50,6 +50,8 @@ const NEAR_BOTTOM_PX = 48;
const NEAR_TOP_PX = 96;
const DEFAULT_SCROLL_BUTTON_BOTTOM_PX = 192;
const SCROLL_BUTTON_COMPOSER_GAP_PX = 16;
const SOFT_KEYBOARD_MIN_INSET_PX = 80;
const KEYBOARD_SCROLL_FRAMES = 18;
export const INITIAL_HISTORY_WINDOW = 160;
export const HISTORY_WINDOW_INCREMENT = 120;
@@ -66,6 +68,35 @@ export function windowMessages(messages: UIMessage[], visibleCount: number): UIM
return messages.slice(start);
}
function isKeyboardEditableElement(element: Element | null): element is HTMLElement {
if (!(element instanceof HTMLElement)) return false;
if (element.isContentEditable) return true;
if (element instanceof HTMLTextAreaElement) return true;
if (!(element instanceof HTMLInputElement)) return false;
return ![
"button",
"checkbox",
"color",
"file",
"hidden",
"image",
"radio",
"range",
"reset",
"submit",
].includes(element.type);
}
function readSoftKeyboardInsetBottom(container: HTMLElement | null): number {
const viewport = window.visualViewport;
if (!viewport) return 0;
const active = document.activeElement;
if (!isKeyboardEditableElement(active) || !container?.contains(active)) return 0;
const layoutHeight = window.innerHeight || document.documentElement.clientHeight;
const inset = layoutHeight - viewport.height - viewport.offsetTop;
return inset >= SOFT_KEYBOARD_MIN_INSET_PX ? Math.ceil(inset) : 0;
}
export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportProps>(function ThreadViewport({
messages,
isStreaming,
@@ -99,6 +130,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
const userReadingHistoryRef = useRef(false);
const [atBottom, setAtBottom] = useState(true);
const [composerDockHeight, setComposerDockHeight] = useState(0);
const [keyboardInsetBottom, setKeyboardInsetBottom] = useState(0);
const [visibleMessageCount, setVisibleMessageCount] =
useState(INITIAL_HISTORY_WINDOW);
const hasMessages = messages.length > 0;
@@ -116,9 +148,13 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
forkBoundaryMessageCount !== null && forkBoundaryMessageCount > hiddenMessageCount
? forkBoundaryMessageCount - hiddenMessageCount
: null;
const scrollButtonBottom = composerDockHeight > 0
? composerDockHeight + SCROLL_BUTTON_COMPOSER_GAP_PX
: DEFAULT_SCROLL_BUTTON_BOTTOM_PX;
const scrollButtonBottom =
keyboardInsetBottom
+ (composerDockHeight > 0
? composerDockHeight + SCROLL_BUTTON_COMPOSER_GAP_PX
: DEFAULT_SCROLL_BUTTON_BOTTOM_PX);
const scrollViewportStyle =
keyboardInsetBottom > 0 ? { bottom: keyboardInsetBottom } : undefined;
const cancelScheduledBottomScroll = useCallback(() => {
for (const id of scrollFrameIdsRef.current) {
@@ -131,10 +167,20 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
const el = scrollRef.current;
const marker = bottomRef.current;
const behavior: ScrollBehavior = smooth ? "smooth" : "auto";
if (marker) {
if (el) {
const top = Math.max(0, el.scrollHeight - el.clientHeight);
try {
el.scrollTo?.({ top, behavior });
if (!smooth) el.scrollTop = top;
} catch {
try {
el.scrollTop = top;
} catch {
// Test DOMs can expose read-only scrollTop; browsers keep this writable.
}
}
} else if (marker) {
marker.scrollIntoView({ block: "end", behavior });
} else if (el) {
el.scrollTo({ top: el.scrollHeight, behavior });
}
userReadingHistoryRef.current = false;
setAtBottom(true);
@@ -148,14 +194,18 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
if (!force && userReadingHistoryRef.current) return;
scrollToBottomNow(smooth);
};
run();
for (let i = 1; i < frames; i += 1) {
const scheduleNext = (remainingFrames: number) => {
if (remainingFrames <= 0) return;
const id = window.requestAnimationFrame(() => {
scrollFrameIdsRef.current = scrollFrameIdsRef.current.filter((frameId) => frameId !== id);
if (!force && userReadingHistoryRef.current) return;
scrollToBottomNow(smooth);
scheduleNext(remainingFrames - 1);
});
scrollFrameIdsRef.current.push(id);
}
};
run();
scheduleNext(frames - 1);
},
[cancelScheduledBottomScroll, scrollToBottomNow],
);
@@ -216,6 +266,37 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
);
}, []);
useLayoutEffect(() => {
const updateKeyboardInset = () => {
const scrollEl = scrollRef.current;
const next = readSoftKeyboardInsetBottom(scrollEl);
const active = document.activeElement;
const composerFocused =
hasMessages && isKeyboardEditableElement(active) && Boolean(scrollEl?.contains(active));
setKeyboardInsetBottom((current) =>
Math.abs(current - next) < 1 ? current : next,
);
if (composerFocused) {
userReadingHistoryRef.current = false;
scrollToBottom(false, KEYBOARD_SCROLL_FRAMES, { force: true });
}
};
updateKeyboardInset();
const viewport = window.visualViewport;
viewport?.addEventListener("resize", updateKeyboardInset);
viewport?.addEventListener("scroll", updateKeyboardInset);
window.addEventListener("resize", updateKeyboardInset);
document.addEventListener("focusin", updateKeyboardInset);
document.addEventListener("focusout", updateKeyboardInset);
return () => {
viewport?.removeEventListener("resize", updateKeyboardInset);
viewport?.removeEventListener("scroll", updateKeyboardInset);
window.removeEventListener("resize", updateKeyboardInset);
document.removeEventListener("focusin", updateKeyboardInset);
document.removeEventListener("focusout", updateKeyboardInset);
};
}, [hasMessages, scrollToBottom]);
useEffect(() => {
if (!atBottom) return;
// Instant jump: CSS scroll-smooth + behavior "auto" still animates in some
@@ -223,6 +304,31 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
scrollToBottom(false);
}, [messages, atBottom, scrollToBottom]);
useLayoutEffect(() => {
if (keyboardInsetBottom > 0) {
userReadingHistoryRef.current = false;
scrollToBottom(false, KEYBOARD_SCROLL_FRAMES, { force: true });
return;
}
if (userReadingHistoryRef.current) return;
scrollToBottom(false, 4);
}, [keyboardInsetBottom, scrollToBottom]);
useEffect(() => {
const scrollEl = scrollRef.current;
if (!scrollEl) return;
const onComposerFocus = () => {
const active = document.activeElement;
if (!hasMessages || !isKeyboardEditableElement(active) || !scrollEl.contains(active)) return;
userReadingHistoryRef.current = false;
scrollToBottom(false, KEYBOARD_SCROLL_FRAMES, { force: true });
};
document.addEventListener("focusin", onComposerFocus);
return () => document.removeEventListener("focusin", onComposerFocus);
}, [hasMessages, scrollToBottom]);
useEffect(() => {
if (scrollToBottomSignal <= 0) return;
userReadingHistoryRef.current = false;
@@ -332,10 +438,14 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
"[&::-webkit-scrollbar-thumb]:bg-muted-foreground/30",
"[&::-webkit-scrollbar-track]:bg-transparent",
)}
style={scrollViewportStyle}
>
{hasMessages ? (
<div ref={contentRef} className="mx-auto flex min-h-full w-full max-w-[64rem] flex-col">
<div className="flex-1 px-4 pb-20 pt-4">
<div
data-testid="thread-message-region"
className="flex min-h-0 flex-1 flex-col justify-end px-3 pb-4 pt-4 sm:px-4"
>
<div className="mx-auto w-full max-w-[49.5rem]">
<ThreadMessages
messages={visibleMessages}
@@ -355,16 +465,16 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
data-testid="thread-composer-dock"
className="sticky bottom-0 z-10 mt-auto bg-background"
>
<div className="px-4 pb-3">
<div className="px-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] sm:px-4">
{composer}
</div>
</div>
</div>
) : (
<div ref={contentRef} className="mx-auto flex min-h-full w-full max-w-[72rem] flex-col px-4">
<div className="flex w-full flex-1 items-center justify-center py-10 sm:py-12">
<div className="relative w-full max-w-[58rem]">
<div className="absolute inset-x-0 bottom-[calc(100%+1.5rem)] flex justify-center">
<div ref={contentRef} className="mx-auto flex min-h-full w-full max-w-[72rem] flex-col px-3 sm:px-4">
<div className="flex w-full flex-1 items-center justify-center py-6 sm:py-12">
<div className="relative flex w-full max-w-[58rem] flex-col items-center gap-5 sm:block">
<div className="flex justify-center sm:absolute sm:inset-x-0 sm:bottom-[calc(100%+1.5rem)]">
{emptyState}
</div>
<div className="w-full">{composer}</div>
@@ -106,7 +106,7 @@ export function WorkspaceProjectPicker({
if (nativeProjectPicker) {
return (
<div className="flex items-center rounded-b-[28px] border-t border-border/25 bg-muted/60 px-4 py-1.5 dark:bg-white/[0.055]">
<div className="flex min-w-0 items-center rounded-b-[28px] border-t border-border/25 bg-muted/60 px-3 py-1.5 dark:bg-white/[0.055] sm:px-4">
<button
type="button"
disabled={disabled || pickingFolder}
@@ -114,7 +114,7 @@ export function WorkspaceProjectPicker({
title={currentProjectScope?.project_path}
onClick={() => void pickNativeFolder()}
className={cn(
"inline-flex h-7 max-w-[18rem] items-center gap-2 rounded-full px-2.5",
"inline-flex h-7 max-w-full items-center gap-2 rounded-full px-2.5 sm:max-w-[18rem]",
"text-[12px] font-medium text-muted-foreground/90 transition-colors",
"hover:bg-background/70 hover:text-foreground disabled:pointer-events-none disabled:opacity-55",
currentProjectScope && "text-foreground/82",
@@ -124,7 +124,7 @@ export function WorkspaceProjectPicker({
<span className="truncate">{projectLabel}</span>
</button>
{pathError || error ? (
<span role="alert" className="ml-2 truncate text-[11.5px] font-medium text-destructive">
<span role="alert" className="ml-2 min-w-0 truncate text-[11.5px] font-medium text-destructive">
{pathError ?? error}
</span>
) : null}
@@ -133,7 +133,7 @@ export function WorkspaceProjectPicker({
}
return (
<div className="flex items-center rounded-b-[28px] border-t border-border/25 bg-muted/60 px-4 py-1.5 dark:bg-white/[0.055]">
<div className="flex min-w-0 items-center rounded-b-[28px] border-t border-border/25 bg-muted/60 px-3 py-1.5 dark:bg-white/[0.055] sm:px-4">
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<button
@@ -141,7 +141,7 @@ export function WorkspaceProjectPicker({
disabled={disabled}
aria-label={t("thread.composer.workspace.projectAria")}
className={cn(
"inline-flex h-7 max-w-[18rem] items-center gap-2 rounded-full px-2.5",
"inline-flex h-7 max-w-full items-center gap-2 rounded-full px-2.5 sm:max-w-[18rem]",
"text-[12px] font-medium text-muted-foreground/90 transition-colors",
"hover:bg-background/70 hover:text-foreground disabled:pointer-events-none disabled:opacity-55",
currentProjectScope && "text-foreground/82",
@@ -254,7 +254,7 @@ export function WorkspaceAccessMenu({
variant="ghost"
aria-label={t("thread.composer.workspace.accessAria")}
className={cn(
"max-w-[12.5rem] rounded-[10px] border border-transparent font-semibold shadow-none",
"max-w-[min(12.5rem,42vw)] rounded-[10px] border border-transparent font-semibold shadow-none",
isHero ? "h-8 px-2.5 text-[12px]" : "h-9 px-3 text-[12.5px]",
isFull
? "bg-transparent text-orange-600 hover:bg-orange-500/8 dark:text-orange-300 dark:hover:bg-orange-400/10"