| author | |
| committer | |
| log | e9db39ce05c706122bb747fd5e7e70e1aac78935 |
| tree | d5dbca717364b4a89f842f5fb9de98b978724649 |
| parent | 941acb4f919bcef337d4d9e1623101bdc30d3616 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>8 files changed, 82 insertions(+), 69 deletions(-)
apps/web/src/components/ChatView.tsx+16-25| ... | @@ -25,10 +25,7 @@ import { | ... | @@ -25,10 +25,7 @@ import { |
| 25 | RuntimeMode, | 25 | RuntimeMode, |
| 26 | TerminalOpenInput, | 26 | TerminalOpenInput, |
| 27 | } from "@t3tools/contracts"; | 27 | } from "@t3tools/contracts"; |
| 28 | import { | 28 | import { type EnvironmentConnectionPresentation } from "@t3tools/client-runtime/connection"; |
| 29 | connectionStatusTitle, | ||
| 30 | type EnvironmentConnectionPresentation, | ||
| 31 | } from "@t3tools/client-runtime/connection"; | ||
| 32 | import { wasBootstrapThreadDeleted } from "@t3tools/client-runtime/errors"; | 29 | import { wasBootstrapThreadDeleted } from "@t3tools/client-runtime/errors"; |
| 33 | import { type CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates"; | 30 | import { type CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates"; |
| 34 | import { effectiveSnoozed, threadWokeAt } from "@t3tools/client-runtime/state/thread-settled"; | 31 | import { effectiveSnoozed, threadWokeAt } from "@t3tools/client-runtime/state/thread-settled"; |
| ... | @@ -2267,21 +2264,20 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -2267,21 +2264,20 @@ function ChatViewContent(props: ChatViewProps) { |
| 2267 | /> | 2264 | /> |
| 2268 | ), | 2265 | ), |
| 2269 | title: `${unavailableConnection.phase === "connecting" ? "Connecting" : "Reconnecting"} to ${activeEnvironmentUnavailableState.label}`, | 2266 | title: `${unavailableConnection.phase === "connecting" ? "Connecting" : "Reconnecting"} to ${activeEnvironmentUnavailableState.label}`, |
| 2270 | description: "It may be finishing an update. One moment.", | 2267 | description: "Finishing an update", |
| 2271 | }); | 2268 | }); |
| 2272 | } else { | 2269 | } else { |
| 2273 | items.push({ | 2270 | items.push({ |
| 2274 | id: `environment-unavailable:${activeEnvironmentUnavailableState.environmentId}`, | 2271 | id: `environment-unavailable:${activeEnvironmentUnavailableState.environmentId}`, |
| 2275 | variant: unavailableConnection.phase === "error" ? "error" : "warning", | 2272 | variant: unavailableConnection.phase === "error" ? "error" : "warning", |
| 2276 | icon: <WifiOffIcon />, | 2273 | icon: <WifiOffIcon />, |
| 2277 | title: `${activeEnvironmentUnavailableState.label}: ${connectionStatusTitle(unavailableConnection)}`, | 2274 | title: `${activeEnvironmentUnavailableState.label} is ${environmentReconnecting ? "reconnecting" : "offline"}`, |
| 2278 | description: | 2275 | description: environmentReconnecting ? "Trying again" : "Reconnect to continue", |
| 2279 | unavailableConnection.error ?? | ||
| 2280 | "Reconnect this environment before sending messages or running actions.", | ||
| 2281 | actions: ( | 2276 | actions: ( |
| 2282 | <> | 2277 | <> |
| 2283 | <Button | 2278 | <Button |
| 2284 | size="xs" | 2279 | size="xs" |
| 2280 | variant="ghost" | ||
| 2285 | disabled={environmentReconnecting} | 2281 | disabled={environmentReconnecting} |
| 2286 | onClick={() => | 2282 | onClick={() => |
| 2287 | void handleReconnectActiveEnvironment( | 2283 | void handleReconnectActiveEnvironment( |
| ... | @@ -2293,7 +2289,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -2293,7 +2289,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 2293 | </Button> | 2289 | </Button> |
| 2294 | <Button | 2290 | <Button |
| 2295 | size="xs" | 2291 | size="xs" |
| 2296 | variant="outline" | 2292 | variant="ghost" |
| 2297 | onClick={() => void navigate({ to: "/settings/connections" })} | 2293 | onClick={() => void navigate({ to: "/settings/connections" })} |
| 2298 | > | 2294 | > |
| 2299 | Connections | 2295 | Connections |
| ... | @@ -2344,13 +2340,10 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -2344,13 +2340,10 @@ function ChatViewContent(props: ChatViewProps) { |
| 2344 | description: | 2340 | description: |
| 2345 | !updateInProgress && | 2341 | !updateInProgress && |
| 2346 | !updateFailed && | 2342 | !updateFailed && |
| 2347 | versionMismatchSelfUpdate === "desktop-managed" && | 2343 | versionMismatchSelfUpdate !== null && |
| 2348 | !versionMismatchDesktopAppUpdate | 2344 | (versionMismatchSelfUpdate !== "desktop-managed" || !versionMismatchDesktopAppUpdate) |
| 2349 | ? serverUpdateGuidance(versionMismatchSelfUpdate, versionMismatchServerLabel) | 2345 | ? serverUpdateGuidance(versionMismatchSelfUpdate) |
| 2350 | : undefined, | 2346 | : undefined, |
| 2351 | // The desktop-managed guidance is already the description; the action | ||
| 2352 | // slot would only repeat it. When the desktop app accepts remote | ||
| 2353 | // update requests, the action button takes over instead. | ||
| 2354 | actions: | 2347 | actions: |
| 2355 | updateInProgress || | 2348 | updateInProgress || |
| 2356 | !versionMismatch || | 2349 | !versionMismatch || |
| ... | @@ -2363,6 +2356,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -2363,6 +2356,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 2363 | desktopAppUpdate={versionMismatchDesktopAppUpdate} | 2356 | desktopAppUpdate={versionMismatchDesktopAppUpdate} |
| 2364 | targetVersion={versionMismatch.clientVersion} | 2357 | targetVersion={versionMismatch.clientVersion} |
| 2365 | label={updateFailed ? "Retry" : "Update"} | 2358 | label={updateFailed ? "Retry" : "Update"} |
| 2359 | variant="ghost" | ||
| 2366 | /> | 2360 | /> |
| 2367 | ), | 2361 | ), |
| 2368 | ...(updateInProgress || (!updateFailed && !versionMismatchDismissKey) | 2362 | ...(updateInProgress || (!updateFailed && !versionMismatchDismissKey) |
| ... | @@ -5000,8 +4994,8 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5000,8 +4994,8 @@ function ChatViewContent(props: ChatViewProps) { |
| 5000 | id: `thread-woke:${activeThread?.id ?? "unknown"}`, | 4994 | id: `thread-woke:${activeThread?.id ?? "unknown"}`, |
| 5001 | variant: "info", | 4995 | variant: "info", |
| 5002 | icon: <AlarmClockIcon />, | 4996 | icon: <AlarmClockIcon />, |
| 5003 | title: "This thread woke from snooze", | 4997 | title: "Thread woke from snooze", |
| 5004 | description: "Dismiss to clear the Woke indicator, or send a message to keep going.", | 4998 | description: "Send a message to continue", |
| 5005 | dismissLabel: "Dismiss Woke notification", | 4999 | dismissLabel: "Dismiss Woke notification", |
| 5006 | onDismiss: acknowledgeActiveThreadWoke, | 5000 | onDismiss: acknowledgeActiveThreadWoke, |
| 5007 | }; | 5001 | }; |
| ... | @@ -5016,13 +5010,11 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5016,13 +5010,11 @@ function ChatViewContent(props: ChatViewProps) { |
| 5016 | variant: "info", | 5010 | variant: "info", |
| 5017 | icon: isSnoozed ? <AlarmClockIcon /> : <CheckCircle2Icon />, | 5011 | icon: isSnoozed ? <AlarmClockIcon /> : <CheckCircle2Icon />, |
| 5018 | title: `This thread is ${isSnoozed ? "snoozed" : "settled"}`, | 5012 | title: `This thread is ${isSnoozed ? "snoozed" : "settled"}`, |
| 5019 | description: isSnoozed | 5013 | description: `Send a message to ${isSnoozed ? "wake" : "unsettle"}`, |
| 5020 | ? "Sending a message wakes it and moves it back to Active in the sidebar." | ||
| 5021 | : "Sending a message moves it back to Active in the sidebar.", | ||
| 5022 | actions: ( | 5014 | actions: ( |
| 5023 | <Button | 5015 | <Button |
| 5024 | size="xs" | 5016 | size="xs" |
| 5025 | variant="outline" | 5017 | variant="ghost" |
| 5026 | disabled={isSnoozed ? isUnsnoozing : isUnsettling} | 5018 | disabled={isSnoozed ? isUnsnoozing : isUnsettling} |
| 5027 | onClick={() => | 5019 | onClick={() => |
| 5028 | void (isSnoozed ? handleUnsnoozeActiveThread() : handleUnsettleActiveThread()) | 5020 | void (isSnoozed ? handleUnsnoozeActiveThread() : handleUnsettleActiveThread()) |
| ... | @@ -5106,7 +5098,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5106,7 +5098,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5106 | const compactAction = ( | 5098 | const compactAction = ( |
| 5107 | <Button | 5099 | <Button |
| 5108 | size="xs" | 5100 | size="xs" |
| 5109 | variant="outline" | 5101 | variant="ghost" |
| 5110 | disabled={compactDisabled} | 5102 | disabled={compactDisabled} |
| 5111 | onClick={() => { | 5103 | onClick={() => { |
| 5112 | if (compactDisabled) return; | 5104 | if (compactDisabled) return; |
| ... | @@ -5121,7 +5113,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5121,7 +5113,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 5121 | variant: "info", | 5113 | variant: "info", |
| 5122 | icon: <Minimize2Icon />, | 5114 | icon: <Minimize2Icon />, |
| 5123 | title: "Resume with less context", | 5115 | title: "Resume with less context", |
| 5124 | description: `${formatContextWindowTokens(activeContextWindow.usedTokens)} tokens from an older session`, | 5116 | description: `${formatContextWindowTokens(activeContextWindow.usedTokens)} tokens from earlier`, |
| 5125 | actions: compactDisabledReason ? ( | 5117 | actions: compactDisabledReason ? ( |
| 5126 | <Tooltip> | 5118 | <Tooltip> |
| 5127 | <TooltipTrigger render={<span className="inline-flex">{compactAction}</span>} /> | 5119 | <TooltipTrigger render={<span className="inline-flex">{compactAction}</span>} /> |
| ... | @@ -5198,7 +5190,6 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5198,7 +5190,6 @@ function ChatViewContent(props: ChatViewProps) { |
| 5198 | </Tooltip> | 5190 | </Tooltip> |
| 5199 | </span> | 5191 | </span> |
| 5200 | ), | 5192 | ), |
| 5201 | className: "dark:shadow-none", | ||
| 5202 | actions: ( | 5193 | actions: ( |
| 5203 | <Button | 5194 | <Button |
| 5204 | size="xs" | 5195 | size="xs" |
apps/web/src/components/ServerUpdateAction.tsx+5-2| ... | @@ -4,6 +4,7 @@ import { | ... | @@ -4,6 +4,7 @@ import { |
| 4 | isAtomCommandInterrupted, | 4 | isAtomCommandInterrupted, |
| 5 | squashAtomCommandFailure, | 5 | squashAtomCommandFailure, |
| 6 | } from "@t3tools/client-runtime/state/runtime"; | 6 | } from "@t3tools/client-runtime/state/runtime"; |
| 7 | import type { ComponentProps } from "react"; | ||
| 7 | 8 | ||
| 8 | import { requestConfirmDialog } from "~/confirmDialog"; | 9 | import { requestConfirmDialog } from "~/confirmDialog"; |
| 9 | import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; | 10 | import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; |
| ... | @@ -79,6 +80,7 @@ export function ServerUpdateAction({ | ... | @@ -79,6 +80,7 @@ export function ServerUpdateAction({ |
| 79 | desktopAppUpdate = false, | 80 | desktopAppUpdate = false, |
| 80 | targetVersion, | 81 | targetVersion, |
| 81 | label = "Update", | 82 | label = "Update", |
| 83 | variant = "outline", | ||
| 82 | }: { | 84 | }: { |
| 83 | readonly environmentId: EnvironmentId; | 85 | readonly environmentId: EnvironmentId; |
| 84 | readonly serverLabel: string; | 86 | readonly serverLabel: string; |
| ... | @@ -88,6 +90,7 @@ export function ServerUpdateAction({ | ... | @@ -88,6 +90,7 @@ export function ServerUpdateAction({ |
| 88 | readonly desktopAppUpdate?: boolean; | 90 | readonly desktopAppUpdate?: boolean; |
| 89 | readonly targetVersion: string; | 91 | readonly targetVersion: string; |
| 90 | readonly label?: string; | 92 | readonly label?: string; |
| 93 | readonly variant?: ComponentProps<typeof Button>["variant"]; | ||
| 91 | }) { | 94 | }) { |
| 92 | const isDesktopAppUpdate = selfUpdate === "desktop-managed"; | 95 | const isDesktopAppUpdate = selfUpdate === "desktop-managed"; |
| 93 | const updateServer = useAtomCommand(serverEnvironment.updateServer, { | 96 | const updateServer = useAtomCommand(serverEnvironment.updateServer, { |
| ... | @@ -170,14 +173,14 @@ export function ServerUpdateAction({ | ... | @@ -170,14 +173,14 @@ export function ServerUpdateAction({ |
| 170 | if (selfUpdate === null) { | 173 | if (selfUpdate === null) { |
| 171 | const command = manualServerUpdateCommand(targetVersion); | 174 | const command = manualServerUpdateCommand(targetVersion); |
| 172 | return ( | 175 | return ( |
| 173 | <Button size="xs" variant="outline" onClick={() => copyToClipboard(command, { command })}> | 176 | <Button size="xs" variant={variant} onClick={() => copyToClipboard(command, { command })}> |
| 174 | Copy update command | 177 | Copy update command |
| 175 | </Button> | 178 | </Button> |
| 176 | ); | 179 | ); |
| 177 | } | 180 | } |
| 178 | 181 | ||
| 179 | return ( | 182 | return ( |
| 180 | <Button size="xs" variant="outline" onClick={() => void handleUpdate()}> | 183 | <Button size="xs" variant={variant} onClick={() => void handleUpdate()}> |
| 181 | {label} | 184 | {label} |
| 182 | </Button> | 185 | </Button> |
| 183 | ); | 186 | ); |
apps/web/src/components/chat/ComposerBanner.tsx+6-2| ... | @@ -143,11 +143,13 @@ function Column({ className, ...props }: ComponentProps<"div">) { | ... | @@ -143,11 +143,13 @@ function Column({ className, ...props }: ComponentProps<"div">) { |
| 143 | 143 | ||
| 144 | function Root({ | 144 | function Root({ |
| 145 | className, | 145 | className, |
| 146 | density = "default", | ||
| 146 | placement = "attached", | 147 | placement = "attached", |
| 147 | variant = "default", | 148 | variant = "default", |
| 148 | width = "fill", | 149 | width = "fill", |
| 149 | ...props | 150 | ...props |
| 150 | }: ComponentProps<"div"> & { | 151 | }: ComponentProps<"div"> & { |
| 152 | density?: "default" | "comfortable"; | ||
| 151 | placement?: "attached" | "floating"; | 153 | placement?: "attached" | "floating"; |
| 152 | variant?: ComposerBannerVariant; | 154 | variant?: ComposerBannerVariant; |
| 153 | width?: "fill" | "content"; | 155 | width?: "fill" | "content"; |
| ... | @@ -155,7 +157,8 @@ function Root({ | ... | @@ -155,7 +157,8 @@ function Root({ |
| 155 | return ( | 157 | return ( |
| 156 | <Surface | 158 | <Surface |
| 157 | className={cn( | 159 | className={cn( |
| 158 | "min-w-0 p-1 pb-[calc(var(--chat-composer-attachment-overlap)+(--spacing(1)))] text-xs/4 [--composer-banner-icon-column:--spacing(7)] sm:[--composer-banner-icon-column:--spacing(6)]", | 160 | "min-w-0 px-1 pt-(--composer-banner-padding-block) pb-[calc(var(--chat-composer-attachment-overlap)+var(--composer-banner-padding-block))] text-xs/4 [--composer-banner-icon-column:--spacing(7)] [--composer-banner-padding-block:--spacing(1)] sm:[--composer-banner-icon-column:--spacing(6)]", |
| 161 | density === "comfortable" && "[--composer-banner-padding-block:--spacing(1.25)]", | ||
| 159 | width === "content" ? "w-fit max-w-full flex-none" : "@container", | 162 | width === "content" ? "w-fit max-w-full flex-none" : "@container", |
| 160 | className, | 163 | className, |
| 161 | )} | 164 | )} |
| ... | @@ -183,7 +186,7 @@ function Row({ | ... | @@ -183,7 +186,7 @@ function Row({ |
| 183 | "not-has-[>[data-slot=composer-banner-actions]]:grid-cols-[var(--composer-banner-icon-column)_minmax(0,1fr)]", | 186 | "not-has-[>[data-slot=composer-banner-actions]]:grid-cols-[var(--composer-banner-icon-column)_minmax(0,1fr)]", |
| 184 | "[&:is(button)]:cursor-pointer [&:is(button)]:rounded-[0.5rem] [&:is(button)]:focus-visible:outline-2 [&:is(button)]:focus-visible:-outline-offset-2 [&:is(button)]:focus-visible:outline-ring", | 187 | "[&:is(button)]:cursor-pointer [&:is(button)]:rounded-[0.5rem] [&:is(button)]:focus-visible:outline-2 [&:is(button)]:focus-visible:-outline-offset-2 [&:is(button)]:focus-visible:outline-ring", |
| 185 | layout === "wrap-actions" && | 188 | layout === "wrap-actions" && |
| 186 | "@max-[400px]:flex @max-[400px]:flex-wrap @max-[400px]:gap-y-1 @max-[400px]:*:data-[slot=composer-banner-actions]:ms-auto @max-[400px]:*:data-[slot=composer-banner-actions]:max-w-full @max-[400px]:has-[>[data-slot=composer-banner-icon]]:*:data-[slot=composer-banner-actions]:max-w-[calc(100%-var(--composer-banner-icon-column)-(--spacing(1)))] @max-[400px]:*:data-[slot=composer-banner-content]:min-h-(--composer-banner-icon-column) @max-[400px]:*:data-[slot=composer-banner-content]:flex-[1_1_10rem]", | 189 | "@max-[400px]:*:data-[slot=composer-banner-content]:min-h-(--composer-banner-icon-column)", |
| 187 | className, | 190 | className, |
| 188 | ), | 191 | ), |
| 189 | "data-composer-banner-row": "true", | 192 | "data-composer-banner-row": "true", |
| ... | @@ -243,6 +246,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) { | ... | @@ -243,6 +246,7 @@ function Actions({ className, ...props }: ComponentProps<"span">) { |
| 243 | data-slot="composer-banner-actions" | 246 | data-slot="composer-banner-actions" |
| 244 | className={cn( | 247 | className={cn( |
| 245 | "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1", | 248 | "col-start-3 row-start-1 flex flex-wrap items-center justify-end gap-1", |
| 249 | "@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", | ||
| 246 | className, | 250 | className, |
| 247 | )} | 251 | )} |
| 248 | {...props} | 252 | {...props} |
apps/web/src/components/chat/ComposerBannerStack.tsx+42-14| ... | @@ -1,6 +1,9 @@ | ... | @@ -1,6 +1,9 @@ |
| 1 | import { InfoIcon } from "lucide-react"; | ||
| 1 | import { useEffect, useId, useLayoutEffect, useRef, useState, type ReactNode } from "react"; | 2 | import { useEffect, useId, useLayoutEffect, useRef, useState, type ReactNode } from "react"; |
| 2 | 3 | ||
| 3 | import { cn } from "~/lib/utils"; | 4 | import { cn } from "~/lib/utils"; |
| 5 | import { Button } from "../ui/button"; | ||
| 6 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; | ||
| 4 | import { ComposerBanner, type ComposerBannerVariant } from "./ComposerBanner"; | 7 | import { ComposerBanner, type ComposerBannerVariant } from "./ComposerBanner"; |
| 5 | 8 | ||
| 6 | // Match the duration-220 exit transition before removing a dismissed notice. | 9 | // Match the duration-220 exit transition before removing a dismissed notice. |
| ... | @@ -15,14 +18,13 @@ export interface ComposerBannerStackItem { | ... | @@ -15,14 +18,13 @@ export interface ComposerBannerStackItem { |
| 15 | readonly description?: ReactNode; | 18 | readonly description?: ReactNode; |
| 16 | readonly children?: ReactNode; | 19 | readonly children?: ReactNode; |
| 17 | readonly actions?: ReactNode; | 20 | readonly actions?: ReactNode; |
| 18 | readonly className?: string; | ||
| 19 | readonly dismissLabel?: string; | 21 | readonly dismissLabel?: string; |
| 20 | readonly onDismiss?: () => void; | 22 | readonly onDismiss?: () => void; |
| 21 | } | 23 | } |
| 22 | 24 | ||
| 23 | export type ComposerBannerStackContent = Pick< | 25 | export type ComposerBannerStackContent = Pick< |
| 24 | ComposerBannerStackItem, | 26 | ComposerBannerStackItem, |
| 25 | "id" | "variant" | "priority" | "className" | 27 | "id" | "variant" | "priority" |
| 26 | > & { readonly content: ReactNode }; | 28 | > & { readonly content: ReactNode }; |
| 27 | 29 | ||
| 28 | type ComposerBannerStackEntry = ComposerBannerStackItem | ComposerBannerStackContent; | 30 | type ComposerBannerStackEntry = ComposerBannerStackItem | ComposerBannerStackContent; |
| ... | @@ -252,37 +254,63 @@ function ComposerBannerStackAlert({ | ... | @@ -252,37 +254,63 @@ function ComposerBannerStackAlert({ |
| 252 | if ("content" in item) { | 254 | if ("content" in item) { |
| 253 | return ( | 255 | return ( |
| 254 | <ComposerBanner.Root | 256 | <ComposerBanner.Root |
| 257 | density="comfortable" | ||
| 255 | placement={attached ? "attached" : "floating"} | 258 | placement={attached ? "attached" : "floating"} |
| 256 | variant={item.variant} | 259 | variant={item.variant} |
| 257 | className={item.className} | ||
| 258 | > | 260 | > |
| 259 | {item.content} | 261 | {item.content} |
| 260 | </ComposerBanner.Root> | 262 | </ComposerBanner.Root> |
| 261 | ); | 263 | ); |
| 262 | } | 264 | } |
| 263 | |||
| 264 | return ( | 265 | return ( |
| 265 | <ComposerBanner.Root | 266 | <ComposerBanner.Root |
| 266 | role="alert" | 267 | role="alert" |
| 267 | placement={attached ? "attached" : "floating"} | 268 | placement={attached ? "attached" : "floating"} |
| 268 | variant={item.variant} | 269 | variant={item.variant} |
| 269 | className={item.className} | 270 | density="comfortable" |
| 270 | > | 271 | > |
| 271 | <ComposerBanner.Row layout="wrap-actions"> | 272 | <ComposerBanner.Row layout="wrap-actions"> |
| 272 | <ComposerBanner.Icon className={item.description ? "min-h-4 self-start" : undefined}> | 273 | <ComposerBanner.Icon className="h-(--composer-banner-icon-column) self-start"> |
| 273 | {item.icon} | 274 | {item.icon} |
| 274 | </ComposerBanner.Icon> | 275 | </ComposerBanner.Icon> |
| 275 | <ComposerBanner.Content | 276 | <ComposerBanner.Content className="whitespace-nowrap"> |
| 276 | className={item.description ? "flex-col items-start gap-0" : "font-medium"} | 277 | <span |
| 277 | > | 278 | className={cn( |
| 279 | "min-w-0 font-medium leading-7 sm:leading-6", | ||
| 280 | typeof item.title === "string" && "truncate", | ||
| 281 | )} | ||
| 282 | > | ||
| 283 | {item.title} | ||
| 284 | </span> | ||
| 278 | {item.description ? ( | 285 | {item.description ? ( |
| 279 | <> | 286 | <> |
| 280 | <span className="font-medium">{item.title}</span> | 287 | <span className="min-w-0 shrink-[9999] truncate text-muted-foreground @max-[400px]:sr-only"> |
| 281 | <span className="text-muted-foreground">{item.description}</span> | 288 | {item.description} |
| 289 | </span> | ||
| 290 | <Popover> | ||
| 291 | <PopoverTrigger | ||
| 292 | openOnHover | ||
| 293 | render={ | ||
| 294 | <Button | ||
| 295 | size="icon-xs" | ||
| 296 | variant="ghost" | ||
| 297 | aria-label="Show notice details" | ||
| 298 | className="hidden flex-none text-muted-foreground hover:text-foreground @max-[400px]:inline-flex" | ||
| 299 | /> | ||
| 300 | } | ||
| 301 | > | ||
| 302 | <InfoIcon className="size-3.5" /> | ||
| 303 | </PopoverTrigger> | ||
| 304 | <PopoverPopup | ||
| 305 | tooltipStyle | ||
| 306 | side="top" | ||
| 307 | className="max-w-72 whitespace-normal text-pretty" | ||
| 308 | > | ||
| 309 | {item.description} | ||
| 310 | </PopoverPopup> | ||
| 311 | </Popover> | ||
| 282 | </> | 312 | </> |
| 283 | ) : ( | 313 | ) : null} |
| 284 | item.title | ||
| 285 | )} | ||
| 286 | </ComposerBanner.Content> | 314 | </ComposerBanner.Content> |
| 287 | {item.actions || item.onDismiss ? ( | 315 | {item.actions || item.onDismiss ? ( |
| 288 | <ComposerBanner.Actions> | 316 | <ComposerBanner.Actions> |
apps/web/src/components/chat/ComposerStashBadge.tsx+6-1| ... | @@ -34,7 +34,12 @@ export const ComposerStashBadge = memo(function ComposerStashBadge(props: { | ... | @@ -34,7 +34,12 @@ export const ComposerStashBadge = memo(function ComposerStashBadge(props: { |
| 34 | ); | 34 | ); |
| 35 | 35 | ||
| 36 | return ( | 36 | return ( |
| 37 | <ComposerBanner.Root width="content" data-composer-shoulder-tab className="ml-auto"> | 37 | <ComposerBanner.Root |
| 38 | density="comfortable" | ||
| 39 | width="content" | ||
| 40 | data-composer-shoulder-tab | ||
| 41 | className="ml-auto" | ||
| 42 | > | ||
| 38 | <ComposerBanner.Row | 43 | <ComposerBanner.Row |
| 39 | render={<button type="button" />} | 44 | render={<button type="button" />} |
| 40 | data-prompt-stash-badge="true" | 45 | data-prompt-stash-badge="true" |
apps/web/src/components/chat/ComposerTasksBadge.tsx+3-1| ... | @@ -123,7 +123,9 @@ export const ComposerTasksBadge = memo(function ComposerTasksBadge({ | ... | @@ -123,7 +123,9 @@ export const ComposerTasksBadge = memo(function ComposerTasksBadge({ |
| 123 | return placement === "inline" ? ( | 123 | return placement === "inline" ? ( |
| 124 | row | 124 | row |
| 125 | ) : ( | 125 | ) : ( |
| 126 | <ComposerBanner.Root data-composer-shoulder-tab>{row}</ComposerBanner.Root> | 126 | <ComposerBanner.Root density="comfortable" data-composer-shoulder-tab> |
| 127 | {row} | ||
| 128 | </ComposerBanner.Root> | ||
| 127 | ); | 129 | ); |
| 128 | }); | 130 | }); |
| 129 | 131 |
apps/web/src/versionSkew.test.ts+2-9| ... | @@ -215,14 +215,7 @@ describe("versionSkew", () => { | ... | @@ -215,14 +215,7 @@ describe("versionSkew", () => { |
| 215 | }); | 215 | }); |
| 216 | 216 | ||
| 217 | it("matches version-drift guidance to the advertised update path", () => { | 217 | it("matches version-drift guidance to the advertised update path", () => { |
| 218 | expect(serverUpdateGuidance("respawn", "Remote server")).toBe( | 218 | expect(serverUpdateGuidance("respawn")).toBe("Update to stay in sync"); |
| 219 | "Update the Remote server so they stay in sync.", | 219 | expect(serverUpdateGuidance("desktop-managed")).toBe("Update the desktop app"); |
| 220 | ); | ||
| 221 | expect(serverUpdateGuidance("desktop-managed", "Desktop server")).toBe( | ||
| 222 | "Update the desktop app that runs the Desktop server.", | ||
| 223 | ); | ||
| 224 | expect(serverUpdateGuidance(null, "Local server")).toBe( | ||
| 225 | "Relaunch the Local server with the copied command to sync them.", | ||
| 226 | ); | ||
| 227 | }); | 220 | }); |
| 228 | }); | 221 | }); |
apps/web/src/versionSkew.ts+2-15| ... | @@ -111,21 +111,8 @@ export function manualServerUpdateCommand(targetVersion: string): string { | ... | @@ -111,21 +111,8 @@ export function manualServerUpdateCommand(targetVersion: string): string { |
| 111 | return `npx t3@${targetVersion}`; | 111 | return `npx t3@${targetVersion}`; |
| 112 | } | 112 | } |
| 113 | 113 | ||
| 114 | /** One sentence telling the user how to resolve version skew for a server, | 114 | export function serverUpdateGuidance(capability: ServerSelfUpdateCapability): string { |
| 115 | matched to the update path it offers. */ | 115 | return capability === "desktop-managed" ? "Update the desktop app" : "Update to stay in sync"; |
| 116 | export function serverUpdateGuidance( | ||
| 117 | capability: ServerSelfUpdateCapability | null, | ||
| 118 | serverLabel: string, | ||
| 119 | ): string { | ||
| 120 | switch (capability) { | ||
| 121 | case "boot-service": | ||
| 122 | case "respawn": | ||
| 123 | return `Update the ${serverLabel} so they stay in sync.`; | ||
| 124 | case "desktop-managed": | ||
| 125 | return `Update the desktop app that runs the ${serverLabel}.`; | ||
| 126 | default: | ||
| 127 | return `Relaunch the ${serverLabel} with the copied command to sync them.`; | ||
| 128 | } | ||
| 129 | } | 116 | } |
| 130 | 117 | ||
| 131 | export function buildVersionMismatchDismissalKey( | 118 | export function buildVersionMismatchDismissalKey( |