authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-23 03:39:36+02:00
committergravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-07-23 01:13:29-04:00
log14b6bfdf157c0ce020a5a55e1c8d3d68552931ef
treea37f0cfb3bdb47dda4e317809e62f8cbffd408f5
parentb6a2563db7c0cd1b2df56540852d48c3c96aeaac

fix(web): unify and tune glass surfaces

Co-authored-by: codex <codex@users.noreply.github.com>

15 files changed, 263 insertions(+), 189 deletions(-)

apps/web/src/components/BranchToolbar.tsx+1-1
...@@ -248,7 +248,7 @@ export const BranchToolbar = memo(function BranchToolbar({...@@ -248,7 +248,7 @@ export const BranchToolbar = memo(function BranchToolbar({
248 if (!hasActiveThread || !activeProject) return null;248 if (!hasActiveThread || !activeProject) return null;
249249
250 return (250 return (
251 <div className="chat-composer-context-strip -mt-2 mx-auto flex w-[calc(100%-1.5rem)] max-w-[calc(48rem-1.5rem)] items-center gap-2 pt-3 pe-2 pb-1 ps-2">251 <div className="flex w-full items-center gap-2 border-t border-border/60 px-3 py-2">
252 {isMobile ? (252 {isMobile ? (
253 <MobileRunContextSelector253 <MobileRunContextSelector
254 envLocked={envLocked}254 envLocked={envLocked}
apps/web/src/components/ChatView.tsx+117-117
...@@ -5569,125 +5569,125 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5569,125 +5569,125 @@ function ChatViewContent(props: ChatViewProps) {
5569 : undefined5569 : undefined
5570 }5570 }
5571 >5571 >
5572 <div5572 <div className="chat-composer-glass-host mx-auto w-full max-w-3xl overflow-hidden rounded-[22px]">
5573 ref={attachDraftHeroComposerAnchorRef}5573 <div ref={attachDraftHeroComposerAnchorRef} className="relative z-10 isolate">
5574 className="relative z-10 isolate mx-auto w-full max-w-3xl before:pointer-events-none before:absolute before:inset-x-0 before:bottom-0 before:-z-10 before:h-2 before:rounded-b-[22px] before:bg-background"5574 <ChatComposer
5575 >5575 composerRef={composerRef}
5576 <ChatComposer5576 composerDraftTarget={composerDraftTarget}
5577 composerRef={composerRef}5577 environmentId={environmentId}
5578 composerDraftTarget={composerDraftTarget}5578 routeKind={routeKind}
5579 environmentId={environmentId}5579 routeThreadRef={routeThreadRef}
5580 routeKind={routeKind}5580 draftId={draftId}
5581 routeThreadRef={routeThreadRef}5581 activeThreadId={activeThreadId}
5582 draftId={draftId}5582 activeThreadEnvironmentId={activeThread?.environmentId}
5583 activeThreadId={activeThreadId}5583 activeThread={activeThread}
5584 activeThreadEnvironmentId={activeThread?.environmentId}5584 isServerThread={isServerThread}
5585 activeThread={activeThread}5585 isLocalDraftThread={isLocalDraftThread}
5586 isServerThread={isServerThread}5586 forceExpandedOnMobile={forceExpandedMobileComposer && isDraftHeroState}
5587 isLocalDraftThread={isLocalDraftThread}5587 projectSelectionRequired={isLocalDraftThread && activeProject === null}
5588 forceExpandedOnMobile={forceExpandedMobileComposer && isDraftHeroState}5588 phase={phase}
5589 projectSelectionRequired={isLocalDraftThread && activeProject === null}5589 isConnecting={isConnecting}
5590 phase={phase}5590 isSendBusy={isSendBusy}
5591 isConnecting={isConnecting}5591 isPreparingWorktree={isPreparingWorktree}
5592 isSendBusy={isSendBusy}5592 environmentUnavailable={activeEnvironmentUnavailableState}
5593 isPreparingWorktree={isPreparingWorktree}5593 activePendingApproval={activePendingApproval}
5594 environmentUnavailable={activeEnvironmentUnavailableState}5594 pendingApprovals={pendingApprovals}
5595 activePendingApproval={activePendingApproval}5595 pendingUserInputs={pendingUserInputs}
5596 pendingApprovals={pendingApprovals}5596 activePendingProgress={activePendingProgress}
5597 pendingUserInputs={pendingUserInputs}5597 activePendingResolvedAnswers={activePendingResolvedAnswers}
5598 activePendingProgress={activePendingProgress}5598 activePendingIsResponding={activePendingIsResponding}
5599 activePendingResolvedAnswers={activePendingResolvedAnswers}5599 activePendingDraftAnswers={activePendingDraftAnswers}
5600 activePendingIsResponding={activePendingIsResponding}5600 activePendingQuestionIndex={activePendingQuestionIndex}
5601 activePendingDraftAnswers={activePendingDraftAnswers}5601 respondingRequestIds={respondingRequestIds}
5602 activePendingQuestionIndex={activePendingQuestionIndex}5602 showPlanFollowUpPrompt={showPlanFollowUpPrompt}
5603 respondingRequestIds={respondingRequestIds}5603 activeProposedPlan={activeProposedPlan}
5604 showPlanFollowUpPrompt={showPlanFollowUpPrompt}5604 activePlan={activePlan as { turnId?: TurnId } | null}
5605 activeProposedPlan={activeProposedPlan}5605 sidebarProposedPlan={sidebarProposedPlan as { turnId?: TurnId } | null}
5606 activePlan={activePlan as { turnId?: TurnId } | null}5606 planSidebarLabel={planSidebarLabel}
5607 sidebarProposedPlan={sidebarProposedPlan as { turnId?: TurnId } | null}5607 planSidebarOpen={planSidebarOpen}
5608 planSidebarLabel={planSidebarLabel}5608 runtimeMode={runtimeMode}
5609 planSidebarOpen={planSidebarOpen}5609 interactionMode={interactionMode}
5610 runtimeMode={runtimeMode}5610 lockedProvider={lockedProvider}
5611 interactionMode={interactionMode}5611 providerStatuses={providerStatuses as ServerProvider[]}
5612 lockedProvider={lockedProvider}5612 activeProjectDefaultModelSelection={activeProject?.defaultModelSelection}
5613 providerStatuses={providerStatuses as ServerProvider[]}5613 activeThreadModelSelection={activeThread?.modelSelection}
5614 activeProjectDefaultModelSelection={activeProject?.defaultModelSelection}5614 activeThreadActivities={activeThread?.activities}
5615 activeThreadModelSelection={activeThread?.modelSelection}5615 resolvedTheme={resolvedTheme}
5616 activeThreadActivities={activeThread?.activities}5616 settings={settings}
5617 resolvedTheme={resolvedTheme}5617 keybindings={keybindings}
5618 settings={settings}5618 terminalOpen={Boolean(terminalUiState.terminalOpen)}
5619 keybindings={keybindings}5619 gitCwd={gitCwd}
5620 terminalOpen={Boolean(terminalUiState.terminalOpen)}5620 promptRef={promptRef}
5621 gitCwd={gitCwd}5621 composerImagesRef={composerImagesRef}
5622 promptRef={promptRef}5622 composerTerminalContextsRef={composerTerminalContextsRef}
5623 composerImagesRef={composerImagesRef}5623 composerElementContextsRef={composerElementContextsRef}
5624 composerTerminalContextsRef={composerTerminalContextsRef}5624 onSend={onSend}
5625 composerElementContextsRef={composerElementContextsRef}5625 onInterrupt={onInterrupt}
5626 onSend={onSend}5626 onImplementPlanInNewThread={onImplementPlanInNewThread}
5627 onInterrupt={onInterrupt}5627 onRespondToApproval={onRespondToApproval}
5628 onImplementPlanInNewThread={onImplementPlanInNewThread}5628 onSelectActivePendingUserInputOption={
5629 onRespondToApproval={onRespondToApproval}5629 onSelectActivePendingUserInputOption
5630 onSelectActivePendingUserInputOption={onSelectActivePendingUserInputOption}5630 }
5631 onAdvanceActivePendingUserInput={onAdvanceActivePendingUserInput}5631 onAdvanceActivePendingUserInput={onAdvanceActivePendingUserInput}
5632 onPreviousActivePendingUserInputQuestion={5632 onPreviousActivePendingUserInputQuestion={
5633 onPreviousActivePendingUserInputQuestion5633 onPreviousActivePendingUserInputQuestion
5634 }5634 }
5635 onChangeActivePendingUserInputCustomAnswer={5635 onChangeActivePendingUserInputCustomAnswer={
5636 onChangeActivePendingUserInputCustomAnswer5636 onChangeActivePendingUserInputCustomAnswer
5637 }5637 }
5638 onProviderModelSelect={onProviderModelSelect}5638 onProviderModelSelect={onProviderModelSelect}
5639 getModelDisabledReason={getModelDisabledReason}5639 getModelDisabledReason={getModelDisabledReason}
5640 toggleInteractionMode={toggleInteractionMode}5640 toggleInteractionMode={toggleInteractionMode}
5641 handleRuntimeModeChange={handleRuntimeModeChange}5641 handleRuntimeModeChange={handleRuntimeModeChange}
5642 handleInteractionModeChange={handleInteractionModeChange}5642 handleInteractionModeChange={handleInteractionModeChange}
5643 togglePlanSidebar={togglePlanSidebar}5643 togglePlanSidebar={togglePlanSidebar}
5644 focusComposer={focusComposer}5644 focusComposer={focusComposer}
5645 scheduleComposerFocus={scheduleComposerFocus}5645 scheduleComposerFocus={scheduleComposerFocus}
5646 setThreadError={setThreadError}5646 setThreadError={setThreadError}
5647 onExpandImage={onExpandTimelineImage}5647 onExpandImage={onExpandTimelineImage}
5648 />5648 />
5649 </div>5649 </div>
5650 <div5650 <div className="min-h-0">
5651 className={cn(5651 <div
5652 "min-h-0",5652 data-terminal-open={terminalUiState.terminalOpen ? "true" : undefined}
5653 isDraftHeroState ? "absolute inset-x-0 top-full" : null,5653 className="relative z-0"
5654 )}5654 >
5655 >5655 {isGitRepo && (
5656 <div5656 <div className="pointer-events-auto">
5657 data-terminal-open={terminalUiState.terminalOpen ? "true" : undefined}5657 <BranchToolbar
5658 className="chat-composer-lower-chrome relative z-0 pb-[calc(env(safe-area-inset-bottom)+1rem)] sm:pb-[calc(env(safe-area-inset-bottom)+1.25rem)]"5658 environmentId={activeThread.environmentId}
5659 >5659 threadId={activeThread.id}
5660 {isGitRepo && (5660 {...(routeKind === "draft" && draftId ? { draftId } : {})}
5661 <div className="pointer-events-auto">5661 onEnvModeChange={onEnvModeChange}
5662 <BranchToolbar5662 startFromOrigin={startFromOrigin}
5663 environmentId={activeThread.environmentId}5663 onStartFromOriginChange={onStartFromOriginChange}
5664 threadId={activeThread.id}5664 {...(canOverrideServerThreadEnvMode
5665 {...(routeKind === "draft" && draftId ? { draftId } : {})}5665 ? { effectiveEnvModeOverride: envMode }
5666 onEnvModeChange={onEnvModeChange}5666 : {})}
5667 startFromOrigin={startFromOrigin}5667 {...(canOverrideServerThreadEnvMode
5668 onStartFromOriginChange={onStartFromOriginChange}5668 ? {
5669 {...(canOverrideServerThreadEnvMode5669 activeThreadBranchOverride: activeThreadBranch,
5670 ? { effectiveEnvModeOverride: envMode }5670 onActiveThreadBranchOverrideChange:
5671 : {})}5671 setPendingServerThreadBranch,
5672 {...(canOverrideServerThreadEnvMode5672 }
5673 ? {5673 : {})}
5674 activeThreadBranchOverride: activeThreadBranch,5674 envLocked={envLocked}
5675 onActiveThreadBranchOverrideChange:5675 onComposerFocusRequest={scheduleComposerFocus}
5676 setPendingServerThreadBranch,5676 {...(canCheckoutPullRequestIntoThread
5677 }5677 ? { onCheckoutPullRequestRequest: openPullRequestDialog }
5678 : {})}5678 : {})}
5679 envLocked={envLocked}5679 {...(hasMultipleEnvironments ? { onEnvironmentChange } : {})}
5680 onComposerFocusRequest={scheduleComposerFocus}5680 availableEnvironments={logicalProjectEnvironments}
5681 {...(canCheckoutPullRequestIntoThread5681 />
5682 ? { onCheckoutPullRequestRequest: openPullRequestDialog }5682 </div>
5683 : {})}5683 )}
5684 {...(hasMultipleEnvironments ? { onEnvironmentChange } : {})}5684 </div>
5685 availableEnvironments={logicalProjectEnvironments}
5686 />
5687 </div>
5688 )}
5689 </div>5685 </div>
5690 </div>5686 </div>
5687 <div
5688 aria-hidden
5689 className="h-[calc(env(safe-area-inset-bottom)+1rem)] sm:h-[calc(env(safe-area-inset-bottom)+1.25rem)]"
5690 />
5691 </div>5691 </div>
5692 </div>5692 </div>
5693 </div>5693 </div>
apps/web/src/components/ProjectScriptsControl.tsx+3-6
...@@ -276,7 +276,7 @@ export default function ProjectScriptsControl({...@@ -276,7 +276,7 @@ export default function ProjectScriptsControl({
276 >276 >
277 <ChevronDownIcon className="size-4" />277 <ChevronDownIcon className="size-4" />
278 </MenuTrigger>278 </MenuTrigger>
279 <MenuPopup align="end" className="command-menu-surface">279 <MenuPopup align="end">
280 {scripts.map((script) => {280 {scripts.map((script) => {
281 const shortcutLabel = shortcutLabelForCommand(281 const shortcutLabel = shortcutLabelForCommand(
282 keybindings,282 keybindings,
...@@ -364,10 +364,7 @@ export default function ProjectScriptsControl({...@@ -364,10 +364,7 @@ export default function ProjectScriptsControl({
364 }}364 }}
365 open={dialogOpen}365 open={dialogOpen}
366 >366 >
367 <DialogPopup367 <DialogPopup className="before:hidden">
368 backdropClassName="bg-black/40 backdrop-blur-none"
369 className="command-palette-dialog-surface workflow-dialog-surface before:hidden"
370 >
371 <DialogHeader>368 <DialogHeader>
372 <DialogTitle>{isEditing ? "Edit Action" : "Add Action"}</DialogTitle>369 <DialogTitle>{isEditing ? "Edit Action" : "Add Action"}</DialogTitle>
373 <DialogDescription>370 <DialogDescription>
...@@ -392,7 +389,7 @@ export default function ProjectScriptsControl({...@@ -392,7 +389,7 @@ export default function ProjectScriptsControl({
392 >389 >
393 <ScriptIcon icon={icon} className="size-4.5" />390 <ScriptIcon icon={icon} className="size-4.5" />
394 </PopoverTrigger>391 </PopoverTrigger>
395 <PopoverPopup align="start" className="command-menu-surface">392 <PopoverPopup align="start">
396 <div className="grid grid-cols-3 gap-2">393 <div className="grid grid-cols-3 gap-2">
397 {SCRIPT_ICONS.map((entry) => {394 {SCRIPT_ICONS.map((entry) => {
398 const isSelected = entry.id === icon;395 const isSelected = entry.id === icon;
apps/web/src/components/chat/ChatComposer.tsx+1-1
...@@ -2154,7 +2154,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2154,7 +2154,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2154 ref={composerSurfaceRef}2154 ref={composerSurfaceRef}
2155 data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"}2155 data-chat-composer-mobile-collapsed={isComposerCollapsedMobile ? "true" : "false"}
2156 className={cn(2156 className={cn(
2157 "chat-composer-glass rounded-[20px] transition-[background-color] duration-200",2157 "rounded-[20px] transition-[background-color] duration-200",
2158 isDragOverComposer ? "bg-accent/45 ring-1 ring-primary/70" : null,2158 isDragOverComposer ? "bg-accent/45 ring-1 ring-primary/70" : null,
2159 projectSelectionRequired ? "opacity-75" : null,2159 projectSelectionRequired ? "opacity-75" : null,
2160 composerProviderState.composerSurfaceClassName,2160 composerProviderState.composerSurfaceClassName,
apps/web/src/components/settings/AddProviderInstanceDialog.tsx+4-4
...@@ -222,8 +222,8 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns...@@ -222,8 +222,8 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns
222 return (222 return (
223 <Dialog open={open} onOpenChange={onOpenChange}>223 <Dialog open={open} onOpenChange={onOpenChange}>
224 <DialogPopup className="max-w-xl overflow-hidden">224 <DialogPopup className="max-w-xl overflow-hidden">
225 <div className="flex min-h-0 flex-col overflow-hidden border-foreground/10 bg-background shadow-2xl">225 <div className="flex min-h-0 flex-col overflow-hidden">
226 <DialogHeader className="border-b border-border/70 bg-background">226 <DialogHeader className="border-b border-border/70">
227 <DialogTitle>Add provider instance</DialogTitle>227 <DialogTitle>Add provider instance</DialogTitle>
228 <DialogDescription>228 <DialogDescription>
229 Configure an additional provider instance — for example, a second Codex install229 Configure an additional provider instance — for example, a second Codex install
...@@ -239,7 +239,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns...@@ -239,7 +239,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns
239239
240 <div240 <div
241 data-slot="dialog-panel"241 data-slot="dialog-panel"
242 className="space-y-4 border-b border-border/70 bg-muted/20 px-6 py-5"242 className="space-y-4 border-b border-border/70 bg-foreground/[0.025] px-6 py-5"
243 >243 >
244 <AnimatedHeight>244 <AnimatedHeight>
245 <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}>245 <div className={cn("grid gap-2", wizardStep !== 0 && "hidden")}>
...@@ -409,7 +409,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns...@@ -409,7 +409,7 @@ export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderIns
409 </AnimatedHeight>409 </AnimatedHeight>
410 </div>410 </div>
411411
412 <DialogFooter className="border-t bg-background">412 <DialogFooter className="border-t bg-transparent">
413 <Button413 <Button
414 variant="outline"414 variant="outline"
415 size="sm"415 size="sm"
apps/web/src/components/settings/SettingsPanels.tsx+53-1
...@@ -18,7 +18,11 @@ import {...@@ -18,7 +18,11 @@ import {
18 settlePromise,18 settlePromise,
19 squashAtomCommandFailure,19 squashAtomCommandFailure,
20} from "@t3tools/client-runtime/state/runtime";20} from "@t3tools/client-runtime/state/runtime";
21import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";21import {
22 DEFAULT_UNIFIED_SETTINGS,
23 MAX_GLASS_OPACITY,
24 MIN_GLASS_OPACITY,
25} from "@t3tools/contracts/settings";
22import { createModelSelection } from "@t3tools/shared/model";26import { createModelSelection } from "@t3tools/shared/model";
23import * as Arr from "effect/Array";27import * as Arr from "effect/Array";
24import * as Duration from "effect/Duration";28import * as Duration from "effect/Duration";
...@@ -389,6 +393,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -389,6 +393,7 @@ export function useSettingsRestore(onRestored?: () => void) {
389 const changedSettingLabels = useMemo(393 const changedSettingLabels = useMemo(
390 () => [394 () => [
391 ...(theme !== "system" ? ["Theme"] : []),395 ...(theme !== "system" ? ["Theme"] : []),
396 ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []),
392 ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat397 ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat
393 ? ["Time format"]398 ? ["Time format"]
394 : []),399 : []),
...@@ -440,6 +445,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -440,6 +445,7 @@ export function useSettingsRestore(onRestored?: () => void) {
440 settings.defaultThreadEnvMode,445 settings.defaultThreadEnvMode,
441 settings.newWorktreesStartFromOrigin,446 settings.newWorktreesStartFromOrigin,
442 settings.diffIgnoreWhitespace,447 settings.diffIgnoreWhitespace,
448 settings.glassOpacity,
443 settings.automaticGitFetchInterval,449 settings.automaticGitFetchInterval,
444 settings.enableAssistantStreaming,450 settings.enableAssistantStreaming,
445 settings.enableProviderUpdateChecks,451 settings.enableProviderUpdateChecks,
...@@ -465,6 +471,7 @@ export function useSettingsRestore(onRestored?: () => void) {...@@ -465,6 +471,7 @@ export function useSettingsRestore(onRestored?: () => void) {
465 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,471 timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat,
466 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,472 wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap,
467 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,473 diffIgnoreWhitespace: DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace,
474 glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity,
468 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,475 sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount,
469 autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,476 autoOpenPlanSidebar: DEFAULT_UNIFIED_SETTINGS.autoOpenPlanSidebar,
470 enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,477 enableAssistantStreaming: DEFAULT_UNIFIED_SETTINGS.enableAssistantStreaming,
...@@ -559,6 +566,51 @@ export function GeneralSettingsPanel() {...@@ -559,6 +566,51 @@ export function GeneralSettingsPanel() {
559 }566 }
560 />567 />
561568
569 <SettingsRow
570 title="Glass opacity"
571 description="Control how transparent glass surfaces are. Higher values make menus, dialogs, and the composer more solid."
572 resetAction={
573 settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? (
574 <SettingResetButton
575 label="glass opacity"
576 onClick={() =>
577 updateSettings({ glassOpacity: DEFAULT_UNIFIED_SETTINGS.glassOpacity })
578 }
579 />
580 ) : null
581 }
582 control={
583 <div className="flex w-full items-center gap-3 sm:w-52">
584 <input
585 aria-label="Glass opacity"
586 className="glass-opacity-slider min-w-0 flex-1"
587 id="glass-opacity"
588 max={MAX_GLASS_OPACITY}
589 min={MIN_GLASS_OPACITY}
590 onChange={(event) => {
591 const glassOpacity = Number(event.currentTarget.value);
592 if (
593 Number.isInteger(glassOpacity) &&
594 glassOpacity >= MIN_GLASS_OPACITY &&
595 glassOpacity <= MAX_GLASS_OPACITY
596 ) {
597 updateSettings({ glassOpacity });
598 }
599 }}
600 step={5}
601 type="range"
602 value={settings.glassOpacity}
603 />
604 <output
605 className="w-10 text-right font-mono text-xs tabular-nums text-muted-foreground"
606 htmlFor="glass-opacity"
607 >
608 {settings.glassOpacity}%
609 </output>
610 </div>
611 }
612 />
613
562 <SettingsRow614 <SettingsRow
563 title="Time format"615 title="Time format"
564 description="System default follows your browser or OS clock preference."616 description="System default follows your browser or OS clock preference."
apps/web/src/components/ui/alert-dialog.tsx+2-2
...@@ -19,7 +19,7 @@ function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backd...@@ -19,7 +19,7 @@ function AlertDialogBackdrop({ className, ...props }: AlertDialogPrimitive.Backd
19 <AlertDialogPrimitive.Backdrop19 <AlertDialogPrimitive.Backdrop
20 forceRender20 forceRender
21 className={cn(21 className={cn(
22 "fixed inset-0 z-50 bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",22 "fixed inset-0 z-50 bg-black/20 backdrop-blur-[2px] transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:bg-black/55",
23 className,23 className,
24 )}24 )}
25 data-slot="alert-dialog-backdrop"25 data-slot="alert-dialog-backdrop"
...@@ -56,7 +56,7 @@ function AlertDialogPopup({...@@ -56,7 +56,7 @@ function AlertDialogPopup({
56 >56 >
57 <AlertDialogPrimitive.Popup57 <AlertDialogPrimitive.Popup
58 className={cn(58 className={cn(
59 "-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",59 "dialog-glass -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border border-border/70 text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
60 bottomStickOnMobile &&60 bottomStickOnMobile &&
61 "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none",61 "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none",
62 className,62 className,
apps/web/src/components/ui/command.tsx+1-1
...@@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B...@@ -30,7 +30,7 @@ function CommandDialogBackdrop({ className, ...props }: CommandDialogPrimitive.B
30 return (30 return (
31 <CommandDialogPrimitive.Backdrop31 <CommandDialogPrimitive.Backdrop
32 className={cn(32 className={cn(
33 "fixed inset-0 z-50 bg-black/40 transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",33 "fixed inset-0 z-50 bg-black/20 backdrop-blur-[2px] transition-opacity duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:bg-black/55",
34 className,34 className,
35 )}35 )}
36 data-slot="command-dialog-backdrop"36 data-slot="command-dialog-backdrop"
apps/web/src/components/ui/dialog.tsx+2-2
...@@ -25,7 +25,7 @@ function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props)...@@ -25,7 +25,7 @@ function DialogBackdrop({ className, ...props }: DialogPrimitive.Backdrop.Props)
25 <DialogPrimitive.Backdrop25 <DialogPrimitive.Backdrop
26 forceRender26 forceRender
27 className={cn(27 className={cn(
28 "fixed inset-0 z-50 bg-background/60 backdrop-blur-xs transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",28 "fixed inset-0 z-50 bg-black/20 backdrop-blur-[2px] transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:bg-black/55",
29 className,29 className,
30 )}30 )}
31 data-slot="dialog-backdrop"31 data-slot="dialog-backdrop"
...@@ -67,7 +67,7 @@ function DialogPopup({...@@ -67,7 +67,7 @@ function DialogPopup({
67 >67 >
68 <DialogPrimitive.Popup68 <DialogPrimitive.Popup
69 className={cn(69 className={cn(
70 "-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",70 "dialog-glass -translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border border-border/70 text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
71 bottomStickOnMobile &&71 bottomStickOnMobile &&
72 "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none",72 "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none",
73 className,73 className,
apps/web/src/components/ui/menu.tsx+1-1
...@@ -42,7 +42,7 @@ function MenuPopup({...@@ -42,7 +42,7 @@ function MenuPopup({
42 align={align}42 align={align}
43 alignOffset={alignOffset}43 alignOffset={alignOffset}
44 anchor={anchor}44 anchor={anchor}
45 className="z-50"45 className="z-[60]"
46 data-slot="menu-positioner"46 data-slot="menu-positioner"
47 side={side}47 side={side}
48 sideOffset={sideOffset}48 sideOffset={sideOffset}
apps/web/src/components/ui/popover.tsx+2-2
...@@ -42,14 +42,14 @@ function PopoverPopup({...@@ -42,14 +42,14 @@ function PopoverPopup({
42 align={align}42 align={align}
43 alignOffset={alignOffset}43 alignOffset={alignOffset}
44 anchor={anchor}44 anchor={anchor}
45 className="z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none"45 className="z-[60] h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-transform data-instant:transition-none"
46 data-slot="popover-positioner"46 data-slot="popover-positioner"
47 side={side}47 side={side}
48 sideOffset={sideOffset}48 sideOffset={sideOffset}
49 >49 >
50 <PopoverPrimitive.Popup50 <PopoverPrimitive.Popup
51 className={cn(51 className={cn(
52 "relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 outline-none transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] has-data-[slot=calendar]:rounded-xl has-data-[slot=calendar]:before:rounded-[calc(var(--radius-xl)-1px)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",52 "dropdown-glass relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg text-popover-foreground outline-none transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] has-data-[slot=calendar]:rounded-xl has-data-[slot=calendar]:before:rounded-[calc(var(--radius-xl)-1px)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
53 tooltipStyle &&53 tooltipStyle &&
54 "w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]",54 "w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]",
55 className,55 className,
apps/web/src/index.css+35-51
...@@ -77,6 +77,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -77,6 +77,7 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
7777
78:root {78:root {
79 --app-scrollbar-width: 6px;79 --app-scrollbar-width: 6px;
80 --glass-opacity: 80%;
80 --desktop-window-right-resize-inset: 0px;81 --desktop-window-right-resize-inset: 0px;
81 --workspace-topbar-height: 52px;82 --workspace-topbar-height: 52px;
82 --workspace-controls-top: 0px;83 --workspace-controls-top: 0px;
...@@ -362,32 +363,29 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -362,32 +363,29 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
362 }363 }
363364
364 .chat-composer-glass {365 .chat-composer-glass {
365 background: var(--secondary);366 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
366 -webkit-backdrop-filter: blur(16px);367 -webkit-backdrop-filter: blur(16px);
367 backdrop-filter: blur(16px);368 backdrop-filter: blur(16px);
368 }369 }
369370
370 .chat-composer-context-strip {371 .chat-composer-glass-host {
371 position: relative;372 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
372 isolation: isolate;
373 }
374
375 .chat-composer-context-strip::before {
376 position: absolute;
377 z-index: -1;
378 inset: 0;
379 border-radius: 0 0 16px 16px;
380 background: color-mix(in srgb, var(--card) 20%, transparent);
381 box-shadow: 0 12px 28px -18px rgb(0 0 0 / 40%);373 box-shadow: 0 12px 28px -18px rgb(0 0 0 / 40%);
382 content: "";
383 -webkit-backdrop-filter: blur(16px);374 -webkit-backdrop-filter: blur(16px);
384 backdrop-filter: blur(16px);375 backdrop-filter: blur(16px);
385 }376 }
386377
387 .dropdown-glass {378 .dropdown-glass,
388 background: var(--secondary);379 .dialog-glass,
380 .workflow-dialog-surface,
381 .command-palette-dialog-surface {
382 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
389 -webkit-backdrop-filter: blur(16px);383 -webkit-backdrop-filter: blur(16px);
390 backdrop-filter: blur(16px);384 backdrop-filter: blur(16px);
385 }
386
387 .dropdown-glass {
388 border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent);
391 box-shadow: 0 16px 40px -18px rgb(0 0 0 / 55%);389 box-shadow: 0 16px 40px -18px rgb(0 0 0 / 55%);
392 }390 }
393391
...@@ -396,30 +394,25 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -396,30 +394,25 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
396 }394 }
397395
398 .command-menu-surface {396 .command-menu-surface {
399 background: var(--popover);397 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent);
400 -webkit-backdrop-filter: none;398 -webkit-backdrop-filter: blur(16px);
401 backdrop-filter: none;399 backdrop-filter: blur(16px);
402 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);400 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);
403 }401 }
404402
405 .workflow-dialog-surface {403 .workflow-dialog-surface {
406 border-color: transparent;404 border-color: color-mix(in srgb, var(--foreground) 10%, transparent);
407 background: var(--secondary);
408 -webkit-backdrop-filter: blur(16px);
409 backdrop-filter: blur(16px);
410 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);405 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%);
411 }406 }
412407
413 .command-palette-dialog-surface {408 .command-palette-dialog-surface {
414 border-color: transparent !important;409 border-color: color-mix(in srgb, var(--foreground) 10%, transparent) !important;
415 background: var(--secondary) !important;410 background: color-mix(in srgb, var(--background) var(--glass-opacity), transparent) !important;
416 -webkit-backdrop-filter: blur(16px);
417 backdrop-filter: blur(16px);
418 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%) !important;411 box-shadow: 0 24px 64px -24px rgb(0 0 0 / 65%) !important;
419 }412 }
420413
421 .dark .dropdown-glass {414 .dark .dropdown-glass,
422 background: color-mix(in srgb, var(--secondary) 80%, var(--background));415 .dark .dialog-glass {
423 box-shadow: 0 18px 44px -18px rgb(0 0 0 / 80%);416 box-shadow: 0 18px 44px -18px rgb(0 0 0 / 80%);
424 }417 }
425418
...@@ -433,12 +426,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -433,12 +426,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
433 box-shadow: none;426 box-shadow: none;
434 }427 }
435428
436 .dark .workflow-dialog-surface {
437 background: var(--secondary);
438 -webkit-backdrop-filter: blur(16px);
439 backdrop-filter: blur(16px);
440 }
441
442 .dark .command-palette-dialog-surface {429 .dark .command-palette-dialog-surface {
443 box-shadow: none !important;430 box-shadow: none !important;
444 }431 }
...@@ -462,15 +449,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -462,15 +449,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
462 box-shadow: none !important;449 box-shadow: none !important;
463 }450 }
464451
465 .dark .chat-composer-context-strip::before {452 .glass-opacity-slider {
466 background: color-mix(in srgb, var(--card) 45%, transparent);453 accent-color: var(--primary);
467 box-shadow: 0 14px 32px -18px rgb(0 0 0 / 75%);454 cursor: pointer;
468 }455 }
469456
470 .chat-composer-lower-chrome {457 .glass-opacity-slider:focus-visible {
471 margin-top: -1px;458 border-radius: 9999px;
472 margin-inline-end: var(--app-scrollbar-width);459 outline: 2px solid var(--ring);
473 padding-top: 1px;460 outline-offset: 3px;
474 }461 }
475462
476 @media (min-width: 40rem) {463 @media (min-width: 40rem) {
...@@ -485,18 +472,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -485,18 +472,15 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
485 }472 }
486473
487 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {474 @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
488 .chat-composer-glass {475 .chat-composer-glass,
489 background: var(--secondary);476 .chat-composer-glass-host {
490 }477 background: var(--background);
491
492 .chat-composer-context-strip::before {
493 background: var(--card);
494 }
495
496 .dropdown-glass {
497 background: color-mix(in srgb, var(--popover) 88%, black);
498 }478 }
499479
480 .dialog-glass,
481 .dropdown-glass,
482 .command-menu-surface,
483 .command-palette-dialog-surface,
500 .workflow-dialog-surface {484 .workflow-dialog-surface {
501 background: var(--popover);485 background: var(--popover);
502 }486 }
apps/web/src/routes/__root.tsx+11
...@@ -127,6 +127,7 @@ function RootRouteView() {...@@ -127,6 +127,7 @@ function RootRouteView() {
127 <ToastProvider>127 <ToastProvider>
128 <AnchoredToastProvider>128 <AnchoredToastProvider>
129 <DocumentTitleSync />129 <DocumentTitleSync />
130 <GlassAppearanceSync />
130 {primaryEnvironmentAuthenticated ? <AuthenticatedTracingBootstrap /> : null}131 {primaryEnvironmentAuthenticated ? <AuthenticatedTracingBootstrap /> : null}
131 <RelayClientInstallDialog />132 <RelayClientInstallDialog />
132 <ConnectOnboardingDialog />133 <ConnectOnboardingDialog />
...@@ -141,6 +142,16 @@ function RootRouteView() {...@@ -141,6 +142,16 @@ function RootRouteView() {
141 );142 );
142}143}
143144
145function GlassAppearanceSync() {
146 const glassOpacity = useClientSettings((settings) => settings.glassOpacity);
147
148 useEffect(() => {
149 document.documentElement.style.setProperty("--glass-opacity", `${glassOpacity}%`);
150 }, [glassOpacity]);
151
152 return null;
153}
154
144function DocumentTitleSync() {155function DocumentTitleSync() {
145 const primaryServerVersion =156 const primaryServerVersion =
146 useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null;157 useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null;
packages/contracts/src/settings.test.ts+16
...@@ -33,6 +33,22 @@ describe("ClientSettings word wrap", () => {...@@ -33,6 +33,22 @@ describe("ClientSettings word wrap", () => {
33 });33 });
34});34});
3535
36describe("ClientSettings glass opacity", () => {
37 it("defaults to a readable translucent surface", () => {
38 expect(decodeClientSettings({}).glassOpacity).toBe(80);
39 });
40
41 it.each([49, 101, 72.5])("rejects an invalid glass opacity: %s", (value) => {
42 expect(() => decodeClientSettings({ glassOpacity: value })).toThrow();
43 expect(() => decodeClientSettingsPatch({ glassOpacity: value })).toThrow();
44 });
45
46 it.each([50, 75, 100])("accepts a glass opacity within the supported range: %s", (value) => {
47 expect(decodeClientSettings({ glassOpacity: value }).glassOpacity).toBe(value);
48 expect(decodeClientSettingsPatch({ glassOpacity: value }).glassOpacity).toBe(value);
49 });
50});
51
36describe("ClientSettings sidebar v2", () => {52describe("ClientSettings sidebar v2", () => {
37 it("defaults the beta off with a three-day auto-settle threshold", () => {53 it("defaults the beta off with a three-day auto-settle threshold", () => {
38 const settings = decodeClientSettings({});54 const settings = decodeClientSettings({});
packages/contracts/src/settings.ts+14
...@@ -48,6 +48,16 @@ export const SidebarAutoSettleAfterDays = Schema.Number.check(...@@ -48,6 +48,16 @@ export const SidebarAutoSettleAfterDays = Schema.Number.check(
48);48);
49export type SidebarAutoSettleAfterDays = typeof SidebarAutoSettleAfterDays.Type;49export type SidebarAutoSettleAfterDays = typeof SidebarAutoSettleAfterDays.Type;
50export const DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS: SidebarAutoSettleAfterDays = 3;50export const DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS: SidebarAutoSettleAfterDays = 3;
51export const MIN_GLASS_OPACITY = 50;
52export const MAX_GLASS_OPACITY = 100;
53export const GlassOpacity = Schema.Int.check(
54 Schema.isBetween({
55 minimum: MIN_GLASS_OPACITY,
56 maximum: MAX_GLASS_OPACITY,
57 }),
58);
59export type GlassOpacity = typeof GlassOpacity.Type;
60export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80;
5161
52export const ClientSettingsSchema = Schema.Struct({62export const ClientSettingsSchema = Schema.Struct({
53 autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),63 autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
...@@ -57,6 +67,9 @@ export const ClientSettingsSchema = Schema.Struct({...@@ -57,6 +67,9 @@ export const ClientSettingsSchema = Schema.Struct({
57 Schema.withDecodingDefault(Effect.succeed([])),67 Schema.withDecodingDefault(Effect.succeed([])),
58 ),68 ),
59 diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),69 diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
70 glassOpacity: GlassOpacity.pipe(
71 Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)),
72 ),
60 // Model favorites. Historically keyed by provider kind, now73 // Model favorites. Historically keyed by provider kind, now
61 // widened to `ProviderInstanceId` so users can favorite a specific model74 // widened to `ProviderInstanceId` so users can favorite a specific model
62 // on a custom provider instance (e.g. "Codex Personal · gpt-5") without75 // on a custom provider instance (e.g. "Codex Personal · gpt-5") without
...@@ -560,6 +573,7 @@ export const ClientSettingsPatch = Schema.Struct({...@@ -560,6 +573,7 @@ export const ClientSettingsPatch = Schema.Struct({
560 confirmThreadArchive: Schema.optionalKey(Schema.Boolean),573 confirmThreadArchive: Schema.optionalKey(Schema.Boolean),
561 confirmThreadDelete: Schema.optionalKey(Schema.Boolean),574 confirmThreadDelete: Schema.optionalKey(Schema.Boolean),
562 diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),575 diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean),
576 glassOpacity: Schema.optionalKey(GlassOpacity),
563 favorites: Schema.optionalKey(577 favorites: Schema.optionalKey(
564 Schema.Array(578 Schema.Array(
565 Schema.Struct({579 Schema.Struct({