| ... | @@ -308,6 +308,8 @@ import { | ... | @@ -308,6 +308,8 @@ import { |
| 308 | } from "../lib/composerContextRecords"; | 308 | } from "../lib/composerContextRecords"; |
| 309 | import { type ReviewCommentContext } from "../reviewCommentContext"; | 309 | import { type ReviewCommentContext } from "../reviewCommentContext"; |
| 310 | import { environmentCatalog } from "../connection/catalog"; | 310 | import { environmentCatalog } from "../connection/catalog"; |
| | 311 | import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; |
| | 312 | import { useEnvironmentDisconnectDelay } from "../hooks/useEnvironmentDisconnectDelay"; |
| 311 | import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; | 313 | import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; |
| 312 | import { useKnownTerminalSessions, useThreadRunningTerminalIds } from "../state/terminalSessions"; | 314 | import { useKnownTerminalSessions, useThreadRunningTerminalIds } from "../state/terminalSessions"; |
| 313 | import { useEnvironmentQuery } from "../state/query"; | 315 | import { useEnvironmentQuery } from "../state/query"; |
| ... | @@ -1497,6 +1499,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1497,6 +1499,9 @@ export default function ChatView(props: ChatViewProps) { |
| 1497 | const { environments } = useEnvironments(); | 1499 | const { environments } = useEnvironments(); |
| 1498 | const primaryEnvironment = usePrimaryEnvironment(); | 1500 | const primaryEnvironment = usePrimaryEnvironment(); |
| 1499 | const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, { reportFailure: false }); | 1501 | const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, { reportFailure: false }); |
| | 1502 | const setEnvironmentEnabled = useAtomCommand(environmentCatalog.setEnabled, { |
| | 1503 | reportFailure: false, |
| | 1504 | }); |
| 1500 | const environmentById = useMemo( | 1505 | const environmentById = useMemo( |
| 1501 | () => new Map(environments.map((environment) => [environment.environmentId, environment])), | 1506 | () => new Map(environments.map((environment) => [environment.environmentId, environment])), |
| 1502 | [environments], | 1507 | [environments], |
| ... | @@ -2208,6 +2213,37 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2208,6 +2213,37 @@ export default function ChatView(props: ChatViewProps) { |
| 2208 | }, | 2213 | }, |
| 2209 | [retryEnvironment], | 2214 | [retryEnvironment], |
| 2210 | ); | 2215 | ); |
| | 2216 | const disconnectDelayElapsed = useEnvironmentDisconnectDelay( |
| | 2217 | activeEnvironmentUnavailable ? activeEnvironment.environmentId : null, |
| | 2218 | ); |
| | 2219 | const canDisconnectActiveEnvironment = |
| | 2220 | disconnectDelayElapsed && |
| | 2221 | activeEnvironment !== null && |
| | 2222 | activeEnvironment.entry.target._tag !== "PrimaryConnectionTarget" && |
| | 2223 | !isDesktopLocalConnectionTarget(activeEnvironment.entry.target); |
| | 2224 | const [disconnectingEnvironment, setDisconnectingEnvironment] = useState(false); |
| | 2225 | const handleDisconnectActiveEnvironment = useCallback( |
| | 2226 | async (environmentId: EnvironmentId) => { |
| | 2227 | setDisconnectingEnvironment(true); |
| | 2228 | const result = await setEnvironmentEnabled({ environmentId, enabled: false }); |
| | 2229 | setDisconnectingEnvironment(false); |
| | 2230 | if (result._tag === "Failure") { |
| | 2231 | if (!isAtomCommandInterrupted(result)) { |
| | 2232 | const error = squashAtomCommandFailure(result); |
| | 2233 | toastManager.add( |
| | 2234 | stackedThreadToast({ |
| | 2235 | type: "error", |
| | 2236 | title: "Could not disconnect server", |
| | 2237 | description: error instanceof Error ? error.message : "Failed to disconnect.", |
| | 2238 | }), |
| | 2239 | ); |
| | 2240 | } |
| | 2241 | return; |
| | 2242 | } |
| | 2243 | void navigate({ to: "/", replace: true }); |
| | 2244 | }, |
| | 2245 | [navigate, setEnvironmentEnabled], |
| | 2246 | ); |
| 2211 | const logicalProjectEnvironments = useMemo(() => { | 2247 | const logicalProjectEnvironments = useMemo(() => { |
| 2212 | if (!activeProject) return []; | 2248 | if (!activeProject) return []; |
| 2213 | const logicalKey = deriveLogicalProjectKeyFromSettings(activeProject, projectGroupingSettings); | 2249 | const logicalKey = deriveLogicalProjectKeyFromSettings(activeProject, projectGroupingSettings); |
| ... | @@ -2448,6 +2484,20 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2448,6 +2484,20 @@ export default function ChatView(props: ChatViewProps) { |
| 2448 | const items: ComposerBannerStackItem[] = []; | 2484 | const items: ComposerBannerStackItem[] = []; |
| 2449 | const updateRunning = serverUpdateState.status === "running"; | 2485 | const updateRunning = serverUpdateState.status === "running"; |
| 2450 | const unavailableConnection = activeEnvironmentUnavailableState?.connection ?? null; | 2486 | const unavailableConnection = activeEnvironmentUnavailableState?.connection ?? null; |
| | 2487 | const disconnectAction = |
| | 2488 | canDisconnectActiveEnvironment && activeEnvironmentUnavailableState ? ( |
| | 2489 | <Button |
| | 2490 | size="xs" |
| | 2491 | variant="ghost" |
| | 2492 | disabled={disconnectingEnvironment} |
| | 2493 | title="Hide this server's threads. Switch it on again in Connections." |
| | 2494 | onClick={() => |
| | 2495 | void handleDisconnectActiveEnvironment(activeEnvironmentUnavailableState.environmentId) |
| | 2496 | } |
| | 2497 | > |
| | 2498 | Disconnect server |
| | 2499 | </Button> |
| | 2500 | ) : undefined; |
| 2451 | const environmentReconnecting = | 2501 | const environmentReconnecting = |
| 2452 | unavailableConnection !== null && | 2502 | unavailableConnection !== null && |
| 2453 | (unavailableConnection.phase === "connecting" || | 2503 | (unavailableConnection.phase === "connecting" || |
| ... | @@ -2481,6 +2531,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2481,6 +2531,7 @@ export default function ChatView(props: ChatViewProps) { |
| 2481 | ), | 2531 | ), |
| 2482 | title: `${unavailableConnection.phase === "connecting" ? "Connecting" : "Reconnecting"} to ${activeEnvironmentUnavailableState.label}`, | 2532 | title: `${unavailableConnection.phase === "connecting" ? "Connecting" : "Reconnecting"} to ${activeEnvironmentUnavailableState.label}`, |
| 2483 | description: "Finishing an update", | 2533 | description: "Finishing an update", |
| | 2534 | actions: disconnectAction, |
| 2484 | }); | 2535 | }); |
| 2485 | } else { | 2536 | } else { |
| 2486 | items.push({ | 2537 | items.push({ |
| ... | @@ -2488,28 +2539,22 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2488,28 +2539,22 @@ export default function ChatView(props: ChatViewProps) { |
| 2488 | variant: unavailableConnection.phase === "error" ? "error" : "warning", | 2539 | variant: unavailableConnection.phase === "error" ? "error" : "warning", |
| 2489 | icon: <WifiOffIcon />, | 2540 | icon: <WifiOffIcon />, |
| 2490 | title: `${activeEnvironmentUnavailableState.label} is ${environmentReconnecting ? "reconnecting" : "offline"}`, | 2541 | title: `${activeEnvironmentUnavailableState.label} is ${environmentReconnecting ? "reconnecting" : "offline"}`, |
| 2491 | description: environmentReconnecting ? "Trying again" : "Reconnect to continue", | | |
| 2492 | actions: ( | 2542 | actions: ( |
| 2493 | <> | 2543 | <> |
| 2494 | <Button | 2544 | {!environmentReconnecting ? ( |
| 2495 | size="xs" | 2545 | <Button |
| 2496 | variant="ghost" | 2546 | size="xs" |
| 2497 | disabled={environmentReconnecting} | 2547 | variant="ghost" |
| 2498 | onClick={() => | 2548 | onClick={() => |
| 2499 | void handleReconnectActiveEnvironment( | 2549 | void handleReconnectActiveEnvironment( |
| 2500 | activeEnvironmentUnavailableState.environmentId, | 2550 | activeEnvironmentUnavailableState.environmentId, |
| 2501 | ) | 2551 | ) |
| 2502 | } | 2552 | } |
| 2503 | > | 2553 | > |
| 2504 | {environmentReconnecting ? "Reconnecting..." : "Reconnect"} | 2554 | Reconnect |
| 2505 | </Button> | 2555 | </Button> |
| 2506 | <Button | 2556 | ) : null} |
| 2507 | size="xs" | 2557 | {disconnectAction} |
| 2508 | variant="ghost" | | |
| 2509 | onClick={() => void navigate({ to: "/settings/connections" })} | | |
| 2510 | > | | |
| 2511 | Connections | | |
| 2512 | </Button> | | |
| 2513 | </> | 2558 | </> |
| 2514 | ), | 2559 | ), |
| 2515 | }); | 2560 | }); |
| ... | @@ -2564,22 +2609,22 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2564,22 +2609,22 @@ export default function ChatView(props: ChatViewProps) { |
| 2564 | (versionMismatchSelfUpdate !== "desktop-managed" || !versionMismatchDesktopAppUpdate) | 2609 | (versionMismatchSelfUpdate !== "desktop-managed" || !versionMismatchDesktopAppUpdate) |
| 2565 | ? serverUpdateGuidance(versionMismatchSelfUpdate) | 2610 | ? serverUpdateGuidance(versionMismatchSelfUpdate) |
| 2566 | : undefined, | 2611 | : undefined, |
| 2567 | actions: | 2612 | actions: updateInProgress ? ( |
| 2568 | updateInProgress || | 2613 | disconnectAction |
| 2569 | !versionMismatch || | 2614 | ) : !versionMismatch || |
| 2570 | (versionMismatchSelfUpdate === "desktop-managed" && | 2615 | (versionMismatchSelfUpdate === "desktop-managed" && |
| 2571 | !versionMismatchDesktopAppUpdate) ? undefined : ( | 2616 | !versionMismatchDesktopAppUpdate) ? undefined : ( |
| 2572 | <ServerUpdateAction | 2617 | <ServerUpdateAction |
| 2573 | environmentId={serverUpdateEnvironmentId} | 2618 | environmentId={serverUpdateEnvironmentId} |
| 2574 | serverLabel={versionMismatchServerLabel} | 2619 | serverLabel={versionMismatchServerLabel} |
| 2575 | selfUpdate={versionMismatchSelfUpdate} | 2620 | selfUpdate={versionMismatchSelfUpdate} |
| 2576 | desktopAppUpdate={versionMismatchDesktopAppUpdate} | 2621 | desktopAppUpdate={versionMismatchDesktopAppUpdate} |
| 2577 | threadContinuation={versionMismatchThreadContinuation} | 2622 | threadContinuation={versionMismatchThreadContinuation} |
| 2578 | targetVersion={versionMismatch.clientVersion} | 2623 | targetVersion={versionMismatch.clientVersion} |
| 2579 | label={updateFailed ? "Retry" : "Update"} | 2624 | label={updateFailed ? "Retry" : "Update"} |
| 2580 | variant="ghost" | 2625 | variant="ghost" |
| 2581 | /> | 2626 | /> |
| 2582 | ), | 2627 | ), |
| 2583 | ...(updateInProgress || (!updateFailed && !versionMismatchDismissKey) | 2628 | ...(updateInProgress || (!updateFailed && !versionMismatchDismissKey) |
| 2584 | ? {} | 2629 | ? {} |
| 2585 | : { | 2630 | : { |
| ... | @@ -2603,7 +2648,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2603,7 +2648,9 @@ export default function ChatView(props: ChatViewProps) { |
| 2603 | activeEnvironmentUnavailableState, | 2648 | activeEnvironmentUnavailableState, |
| 2604 | reconnectWarningGraceElapsed, | 2649 | reconnectWarningGraceElapsed, |
| 2605 | handleReconnectActiveEnvironment, | 2650 | handleReconnectActiveEnvironment, |
| 2606 | navigate, | 2651 | canDisconnectActiveEnvironment, |
| | 2652 | disconnectingEnvironment, |
| | 2653 | handleDisconnectActiveEnvironment, |
| 2607 | setDismissedVersionMismatchKey, | 2654 | setDismissedVersionMismatchKey, |
| 2608 | showVersionMismatchBanner, | 2655 | showVersionMismatchBanner, |
| 2609 | serverUpdateFailureDismissed, | 2656 | serverUpdateFailureDismissed, |