From 1a09a63a289a79f6da74a2f2aec02e2cd568cc0d Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Sat, 15 Aug 2026 03:48:16 -0400 Subject: [PATCH 1/3] Clean up the Connections pane so signed-out and connected states read as one story. Drop the repeated alarms, jargon version warnings, and chrome that made the pane look unfinished, and keep unreadable sessions distinct so Repair stays the safe action. Co-authored-by: Cursor --- .../remoteRuntime/remoteBootstrap.test.ts | 4 +- .../services/remoteRuntime/remoteBootstrap.ts | 15 +- .../components/account/AccountPage.tsx | 2 +- .../components/app/ConnectionsPanel.test.tsx | 32 ++++- .../components/app/ConnectionsPanel.tsx | 18 +-- .../remoteTargets/RemoteTargetList.test.tsx | 72 ++++++++-- .../remoteTargets/RemoteTargetList.tsx | 72 +++++----- .../remoteTargets/SavedMachineRow.tsx | 135 ++++++++++++------ .../remoteTargets/remoteMachineModel.test.ts | 56 ++++++++ .../remoteTargets/remoteMachineModel.ts | 26 +++- .../remoteTargets/remoteTargetListStyles.ts | 23 ++- .../settings/SyncDevicesSection.test.tsx | 61 +++++--- .../settings/SyncDevicesSection.tsx | 65 ++++----- .../settings/accountDirectorySummary.ts | 13 +- apps/desktop/src/renderer/lib/account.test.ts | 27 ++++ apps/desktop/src/renderer/lib/account.ts | 28 +++- .../__tests__/connectionsPane.test.tsx | 2 +- docs/features/remote-runtime/README.md | 2 +- 18 files changed, 464 insertions(+), 189 deletions(-) diff --git a/apps/desktop/src/main/services/remoteRuntime/remoteBootstrap.test.ts b/apps/desktop/src/main/services/remoteRuntime/remoteBootstrap.test.ts index 81278b9287..08d9371b66 100644 --- a/apps/desktop/src/main/services/remoteRuntime/remoteBootstrap.test.ts +++ b/apps/desktop/src/main/services/remoteRuntime/remoteBootstrap.test.ts @@ -402,7 +402,7 @@ describe("validateRemoteRuntimeInitializeResult", () => { }); expect(info.version).toBe("0.9.0"); expect(info.capabilities.machineProjects.browseDirectories).toBe(false); - expect(info.compatibilityWarnings.join("\n")).toMatch(/local ADE is 1\.0\.0/i); + expect(info.compatibilityWarnings.join("\n")).not.toMatch(/This machine is on ADE/i); expect(info.compatibilityWarnings.join("\n")).toMatch(/missing project capabilities/i); }); @@ -424,7 +424,7 @@ describe("validateRemoteRuntimeInitializeResult", () => { }, }, }, - }).compatibilityWarnings.join("\n")).toMatch(/reported 0\.9\.0/i); + }).compatibilityWarnings.join("\n")).not.toMatch(/This machine is on ADE|Remote ADE service reported/i); }); }); diff --git a/apps/desktop/src/main/services/remoteRuntime/remoteBootstrap.ts b/apps/desktop/src/main/services/remoteRuntime/remoteBootstrap.ts index 34c1a18692..6d3e31fb77 100644 --- a/apps/desktop/src/main/services/remoteRuntime/remoteBootstrap.ts +++ b/apps/desktop/src/main/services/remoteRuntime/remoteBootstrap.ts @@ -151,18 +151,9 @@ export function validateRemoteRuntimeInitializeResult(args: { ? runtimeInfo.version.trim() : null; const compatibilityWarnings: string[] = []; - if (args.expectedVersion && version !== args.expectedVersion) { - const expected = args.expectedVersion; - const actual = version ?? "unknown"; - // A version difference alone is not a fault in either direction — the - // capability checks below are what decide whether anything is actually - // missing. Say so plainly instead of raising an alarm the user cannot act - // on; "remote is newer" in particular is the normal state for a machine on - // the release channel seen from an alpha desktop. - compatibilityWarnings.push( - `Remote ADE service reported ${actual}; local ADE is ${expected}. The versions differ but their RPC capabilities match, so ADE connected normally — nothing to do.`, - ); - } + // Version-only skew is not a fault. The Connections row already has both + // versions and formats a quiet note there. Do not push a string the UI then + // has to sniff back out of the yellow warning list. const missing = MACHINE_PROJECT_CAPABILITIES.filter((capability) => machineProjects[capability] !== true); if (missing.length) { compatibilityWarnings.push( diff --git a/apps/desktop/src/renderer/components/account/AccountPage.tsx b/apps/desktop/src/renderer/components/account/AccountPage.tsx index 3c19d307a7..1c00693a8b 100644 --- a/apps/desktop/src/renderer/components/account/AccountPage.tsx +++ b/apps/desktop/src/renderer/components/account/AccountPage.tsx @@ -154,7 +154,7 @@ export function SignInCard({ One source for the state's own words: `unreadable` and `expired` both take their title from the label table, so the header never drifts from the notice under it. `signed_out` keeps the call to - action -- the table's "Not signed in" describes the state, but + action -- the table's "Signed out" describes the state, but this card is where you act on it. */} {unreadable || expired ? accountSessionTitle(sessionState) : "Sign in to ADE"} diff --git a/apps/desktop/src/renderer/components/app/ConnectionsPanel.test.tsx b/apps/desktop/src/renderer/components/app/ConnectionsPanel.test.tsx index 11912693fc..098e4f0875 100644 --- a/apps/desktop/src/renderer/components/app/ConnectionsPanel.test.tsx +++ b/apps/desktop/src/renderer/components/app/ConnectionsPanel.test.tsx @@ -14,6 +14,13 @@ import type { // two data sources directly. const syncRef = { current: { devices: [] as SyncDeviceRuntimeState[] } }; const snapshotRef = { current: { connections: [], connectedCount: 0, updatedAt: 0 } as RemoteRuntimeConnectionSnapshot }; +const accountRef = { + current: { + signedIn: true, + email: "ada@example.com" as string | null, + sessionState: undefined as undefined | "signed_out" | "expired" | "unreadable" | "active", + }, +}; vi.mock("../settings/SyncDevicesSection", () => ({ useSyncConnections: () => syncRef.current, @@ -31,7 +38,12 @@ vi.mock("../../lib/account", async () => { return { ...actual, useAccountStatus: () => ({ - status: { ...actual.SIGNED_OUT_ACCOUNT, signedIn: true, email: "ada@example.com" }, + status: { + ...actual.SIGNED_OUT_ACCOUNT, + signedIn: accountRef.current.signedIn, + email: accountRef.current.email, + sessionState: accountRef.current.sessionState, + }, loading: false, refresh: vi.fn(), }), @@ -66,6 +78,7 @@ describe("ConnectionsPanel tab dots", () => { beforeEach(() => { syncRef.current = { devices: [] }; snapshotRef.current = { connections: [], connectedCount: 0, updatedAt: 0 }; + accountRef.current = { signedIn: true, email: "ada@example.com", sessionState: undefined }; window.ade = { github: { getStatus: vi.fn(async () => ({ connected: false })) }, remoteRuntime: { @@ -150,4 +163,21 @@ describe("ConnectionsPanel tab dots", () => { expect(within(screen.getByRole("tab", { name: /Phone/ })).queryByTitle("Active connection")).toBeNull(); }); + + it("offers Sign in when the account is signed out", async () => { + accountRef.current = { signedIn: false, email: null, sessionState: "signed_out" }; + renderPanel(); + expect(await screen.findByRole("button", { name: "Sign in" })).toBeTruthy(); + expect(screen.getByText("Signed out")).toBeTruthy(); + expect(screen.getByText("Sign in to easily connect your machines")).toBeTruthy(); + expect(screen.queryByRole("button", { name: "Manage account" })).toBeNull(); + }); + + it("keeps unreadable sessions distinct from signed out", async () => { + accountRef.current = { signedIn: false, email: null, sessionState: "unreadable" }; + renderPanel(); + expect(await screen.findByRole("button", { name: "Fix sign-in" })).toBeTruthy(); + expect(screen.getByText("Can't read your sign-in")).toBeTruthy(); + expect(screen.queryByText("Signed out")).toBeNull(); + }); }); diff --git a/apps/desktop/src/renderer/components/app/ConnectionsPanel.tsx b/apps/desktop/src/renderer/components/app/ConnectionsPanel.tsx index 321bcceb12..0050a6e547 100644 --- a/apps/desktop/src/renderer/components/app/ConnectionsPanel.tsx +++ b/apps/desktop/src/renderer/components/app/ConnectionsPanel.tsx @@ -27,6 +27,8 @@ import { ConfirmDialog, useConfirmDialog } from "../shared/InlineDialogs"; import { accountAvatarImage, accountInitials, + accountSessionConnectionsAction, + accountSessionConnectionsActionAria, accountSessionConnectionsSubtitle, accountSessionState, accountSessionTitle, @@ -133,13 +135,7 @@ function AccountHeader({ cursor: "pointer", textAlign: "left", }} - aria-label={ - sessionState === "active" - ? "Manage account" - : sessionState === "unreadable" - ? "Fix your sign-in" - : "Sign in to ADE" - } + aria-label={accountSessionConnectionsActionAria(sessionState)} > {avatarImage && !imgBroken ? ( @@ -203,7 +199,7 @@ function AccountHeader({ cursor: "pointer", }} > - Manage account + {accountSessionConnectionsAction(sessionState)} ); @@ -324,7 +320,11 @@ export function ConnectionsPanel({
- +
{ }, }); - render(); - expect(await screen.findByText(/route publish failing for 5 min/)).toBeTruthy(); + render(); + expect(await screen.findByText(/couldn't publish it for 5 min/)).toBeTruthy(); expect(screen.queryByRole("button", { name: "Repair" })).toBeNull(); }); + it("hides a publish-failing banner while signed out", async () => { + remoteRuntimeMock.listTargets.mockResolvedValue([]); + remoteRuntimeMock.listDiscoveredMachines.mockResolvedValue({ machines: [], diagnostics: [] }); + installAdeMock(); + appMock.getInfo.mockResolvedValue({ + localRuntime: { + publishHealth: { + state: "http_error", + failingSinceMs: Date.now() - 5 * 60_000, + lastLegDurations: { snapshot: null, token: null, http: null }, + }, + }, + }); + + render(); + await screen.findByRole("button", { name: "Add machine" }); + expect(screen.queryByText(/couldn't publish it/)).toBeNull(); + expect(screen.queryByText(/route publish failing/)).toBeNull(); + }); + it("pairs a discovered ADE machine with its 6-digit code instead of creating an SSH target", async () => { remoteRuntimeMock.listTargets.mockResolvedValue([]); remoteRuntimeMock.listDiscoveredMachines.mockResolvedValue({ @@ -405,9 +425,9 @@ describe("RemoteTargetList", () => { remoteRuntimeMock.connect.mockResolvedValue({ target, arch: "darwin-arm64", - version: "1.0.0", + version: "0.9.0", compatibilityWarnings: [ - "Remote ADE service reported 0.9.0; local ADE is 1.0.0. ADE will connect because the RPC capabilities are compatible.", + "This machine is on ADE 1.0.0. The other machine is on 0.9.0. They can still connect — update the other machine when you can.", ], projects: [project], }); @@ -427,9 +447,12 @@ describe("RemoteTargetList", () => { await waitFor(() => expect(remoteRuntimeMock.connect).toHaveBeenCalledWith("target-1"), ); - expect(screen.getByText("Connected")).toBeTruthy(); - expect(screen.getByText(/RPC capabilities are compatible/i)).toBeTruthy(); expect(screen.getByRole("button", { name: "Disconnect" })).toBeTruthy(); + expect(screen.queryByText(/Never connected/)).toBeNull(); + expect(screen.getByText(/Last connected/)).toBeTruthy(); + expect( + screen.getByText("This machine is on ADE 1.0.0. Mac Studio is on 0.9.0. They can still connect — update Mac Studio when you can."), + ).toBeTruthy(); expect(screen.queryByText("/remote/ADE")).toBeNull(); expect(screen.queryByRole("button", { name: "Open" })).toBeNull(); @@ -439,7 +462,7 @@ describe("RemoteTargetList", () => { manual: true, }), ); - expect(screen.getByText("Not connected")).toBeTruthy(); + expect(screen.getByText(/Not connected · Last connected/)).toBeTruthy(); }); it("does not let an older event overwrite a local connection-setting snapshot", async () => { @@ -571,10 +594,12 @@ describe("RemoteTargetList", () => { fireEvent.click(screen.getByRole("button", { name: "Disconnect" })); await waitFor(() => - expect(onDisconnectRequested).toHaveBeenCalledWith(target), + expect(onDisconnectRequested).toHaveBeenCalledWith( + expect.objectContaining({ id: "target-1", name: "Mac Studio" }), + ), ); expect(remoteRuntimeMock.disconnect).not.toHaveBeenCalled(); - expect(screen.getByText("Connected")).toBeTruthy(); + expect(screen.getByRole("button", { name: "Disconnect" })).toBeTruthy(); }); it("toggles the saved machine edit details from the Edit button", async () => { @@ -1055,7 +1080,7 @@ describe("RemoteTargetList", () => { await waitFor(() => expect(screen.getAllByText("Connected Mac").length).toBeGreaterThan(0), ); - expect(screen.getByText("CONNECTED")).toBeTruthy(); + expect(screen.getByText("Connected")).toBeTruthy(); expect(screen.getByRole("button", { name: "Disconnect" })).toBeTruthy(); openAddMode("Find nearby computers"); @@ -1213,7 +1238,7 @@ describe("RemoteTargetList", () => { // A brand-new user's first read is the call to action, not an explanation // of optional software they never asked about. expect( - await screen.findByText("No computers yet. Choose Add machine to connect one."), + await screen.findByText("No computers yet. Add a machine to connect one."), ).toBeTruthy(); expect(screen.queryByText("Tailscale not installed — LAN discovery only.")).toBeNull(); @@ -1247,7 +1272,7 @@ describe("RemoteTargetList", () => { screen.getByText("Tailscale discovery failed; LAN discovery still ran."), ); expect(warning.querySelector("svg")).not.toBeNull(); - expect(screen.getByText("No computers yet. Choose Add machine to connect one.")).toBeTruthy(); + expect(screen.getByText("No computers yet. Add a machine to connect one.")).toBeTruthy(); }); it("adopts a desktop account machine as paired-only instead of saving a broken SSH target", async () => { @@ -1463,11 +1488,32 @@ describe("RemoteTargetList", () => { ); await waitFor(() => - expect(screen.getByText(/couldn't reach your ADE account/i)).toBeTruthy(), + expect(screen.getByText(/Couldn't load machines from your account/i)).toBeTruthy(), ); expect(screen.queryByText(/No computers yet/i)).toBeNull(); }); + it("does not repeat the account-load failure while signed out", async () => { + remoteRuntimeMock.listTargets.mockResolvedValue([]); + remoteRuntimeMock.listDiscoveredMachines.mockResolvedValue({ + machines: [], + diagnostics: [], + }); + installAdeMock(); + + render( + , + ); + + await screen.findByRole("button", { name: "Add machine" }); + expect(screen.queryByText(/Couldn't load machines from your account/i)).toBeNull(); + expect(screen.queryByText(/couldn't reach your ADE account/i)).toBeNull(); + }); + it("shows an account connect failure only on the machine that failed", async () => { remoteRuntimeMock.listTargets.mockResolvedValue([]); remoteRuntimeMock.listDiscoveredMachines.mockResolvedValue({ diff --git a/apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.tsx b/apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.tsx index 5d501f568d..3802badbcc 100644 --- a/apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.tsx +++ b/apps/desktop/src/renderer/components/remoteTargets/RemoteTargetList.tsx @@ -1,8 +1,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { + ArrowClockwise, CaretLeft, CaretRight, DesktopTower, + Plus, TerminalWindow, UserCircle, Warning, @@ -16,7 +18,6 @@ import { MONO_FONT, SANS_FONT, outlineButton, - primaryButton, } from "../lanes/laneDesignTokens"; import { isBrainAccountSessionFailure } from "../../../shared/types"; import type { @@ -57,6 +58,7 @@ import { DiscoveredMachineRow } from "./DiscoveredMachineRow"; import { AccountMachineRow } from "./AccountMachineRow"; import { helperTextStyle, + iconActionButtonStyle, inlineDetailStyle, panelStyle, sectionHeaderStyle, @@ -150,9 +152,9 @@ function joinDiagnosticMessages( } const SECTION_LABELS: Record = { - connected: "CONNECTED", - available: "AVAILABLE", - unavailable: "UNAVAILABLE", + connected: "Connected", + available: "Available", + unavailable: "Unavailable", }; export function RemoteTargetList({ @@ -562,10 +564,14 @@ export function RemoteTargetList({ if (!trusted) return null; } const result = await window.ade.remoteRuntime.connect(targetId); - setConnected(result); + const connectedTarget = { + ...result.target, + lastConnectedAt: result.target.lastConnectedAt ?? Date.now(), + }; + setConnected({ ...result, target: connectedTarget }); setTargets((current) => current.map((target) => - target.id === result.target.id ? result.target : target, + target.id === connectedTarget.id ? connectedTarget : target, ), ); setConnectionSnapshot((current) => { @@ -581,7 +587,7 @@ export function RemoteTargetList({ })); const existing = current?.connections ?? fallbackConnections; const connectedEntry: RemoteRuntimeConnectionStatus = { - target: result.target, + target: connectedTarget, state: "connected", arch: result.arch, version: result.version, @@ -591,7 +597,7 @@ export function RemoteTargetList({ projects: result.projects, lastError: null, lastAttemptedAt: Date.now(), - connectedAt: result.target.lastConnectedAt ?? Date.now(), + connectedAt: connectedTarget.lastConnectedAt, }; const connections = existing.some( (entry) => entry.target.id === result.target.id, @@ -912,20 +918,21 @@ export function RemoteTargetList({ } }, [nextLocalConnectionSnapshotUpdatedAt]); - const connectedCount = - connectionSnapshot?.connectedCount ?? (connected ? 1 : 0); - const totalRows = sections.connected.length + sections.available.length + sections.unavailable.length; // "The account list did not arrive" — distinct from "the account has none". + // Signed-out / expired already have a header line; don't repeat it here. const accountMachinesLoadFailed = Boolean( - accountMachinesState + accountSignedIn + && accountMachinesState && accountMachinesState !== "ok" && accountMachinesState !== "signed_out", ); + const showPublishFailure = publishHealthDisplay.kind === "failing" + && (accountSignedIn || isBrainAccountSessionFailure(localPublishHealth?.state)); const nearbyPairingByAccountMachineKey = useMemo(() => { const matches = new Map(); @@ -1054,6 +1061,7 @@ export function RemoteTargetList({ } updating={updatingTargetId === row.target.id} updateStatus={updateResultByTargetId[row.target.id] ?? null} + localAdeVersion={updateSnapshot.currentVersion} onUpdateAndRestart={(targetVersion) => void updateAndRestartTarget( row.target.id, @@ -1220,7 +1228,7 @@ export function RemoteTargetList({ gap: 12, }} > -
+
- + Machines
-
{connectedCount} connected
- {publishHealthDisplay.kind === "healthy" ? ( -
Routes fresh
- ) : null} - {publishHealthDisplay.kind === "failing" ? ( + {showPublishFailure && publishHealthDisplay.kind === "failing" ? (
- Other devices may not reach this computer — route publish failing for{" "} + Other machines may not find this one — ADE couldn't publish it for{" "} {publishHealthDisplay.minutes} min {showRepair ? : null}
) : null}
-
+
@@ -1427,7 +1429,7 @@ export function RemoteTargetList({
{accountMachinesState === "not_configured" ? "Account computers aren't available yet. Saved and nearby computers still work." - : "We couldn't reach your ADE account, so computers linked to it aren't listed. Saved and nearby computers still work — close and reopen this panel to try again."} + : "Couldn't load machines from your account. Saved and nearby machines still work."}
) : null} @@ -1440,7 +1442,7 @@ export function RemoteTargetList({ && !addMode && !loadingDiscovered ? (
- No computers yet. Choose Add machine to connect one. + No computers yet. Add a machine to connect one.
) : null} {loadingDiscovered ? ( diff --git a/apps/desktop/src/renderer/components/remoteTargets/SavedMachineRow.tsx b/apps/desktop/src/renderer/components/remoteTargets/SavedMachineRow.tsx index 0f51596206..b313d2853a 100644 --- a/apps/desktop/src/renderer/components/remoteTargets/SavedMachineRow.tsx +++ b/apps/desktop/src/renderer/components/remoteTargets/SavedMachineRow.tsx @@ -1,9 +1,11 @@ +import { useEffect, useState } from "react"; import { ArrowClockwise, - CaretDown, - CaretUp, CheckCircle, + PencilSimple, + Plugs, PlugsConnected, + Pulse, Trash, Warning, } from "@phosphor-icons/react"; @@ -25,6 +27,8 @@ import { HostKeyTrustCard } from "./HostKeyTrustCard"; import { connectionStateLabel, formatLastSeen, + formatVersionSkewNote, + isVersionSkewWarning, selectMachineErrorCard, type MachineSection, type SavedMachineRow as SavedMachineRowModel, @@ -36,6 +40,7 @@ import { } from "./RemoteTargetForm"; import { helperTextStyle, + iconActionButtonStyle, inlineDetailStyle, inlineErrorTextStyle, inlineSuccessTextStyle, @@ -44,6 +49,21 @@ import { subTextStyle, } from "./remoteTargetListStyles"; +const CONNECTING_STALE_MS = 20_000; + +function useStaleConnecting(connecting: boolean): boolean { + const [stale, setStale] = useState(false); + useEffect(() => { + if (!connecting) { + setStale(false); + return; + } + const timer = window.setTimeout(() => setStale(true), CONNECTING_STALE_MS); + return () => window.clearTimeout(timer); + }, [connecting]); + return stale; +} + type SavedMachineRowProps = { row: SavedMachineRowModel; section: MachineSection; @@ -64,6 +84,8 @@ type SavedMachineRowProps = { updating?: boolean; /** Outcome of the last update run on this machine. */ updateStatus?: { ok: boolean; message: string } | null; + /** ADE version running on this computer, used for the quiet skew note. */ + localAdeVersion?: string | null; onUpdateAndRestart?: (targetVersion: string | null) => void; hostKeyTrust: RemoteRuntimeSshHostKeyTrustStatus | null; trustingHostKey: boolean; @@ -96,6 +118,7 @@ export function SavedMachineRow({ updateTargetVersion = null, updating = false, updateStatus = null, + localAdeVersion = null, onUpdateAndRestart, hostKeyTrust, trustingHostKey, @@ -113,17 +136,33 @@ export function SavedMachineRow({ const { target, status } = row; const targetConnecting = busyId === target.id || status?.state === "connecting"; - const statusLabel = connectionStateLabel( - status ?? null, - connected?.target.id === target.id, + // A live connect (busyId set) can take minutes for SSH bootstrap. Only treat + // snapshot-stuck "connecting" with no in-flight action as hung. + const connectingStale = useStaleConnecting( + status?.state === "connecting" && busyId !== target.id && !row.connected, ); - const warnings = selected + const statusLabel = connectingStale + ? "Can't reach" + : connectionStateLabel( + status ?? null, + connected?.target.id === target.id, + ); + const versionNote = formatVersionSkewNote({ + localVersion: localAdeVersion, + remoteVersion: row.version, + remoteName: target.name, + }); + const compatibilityWarnings = selected ? (status?.compatibilityWarnings ?? (connected?.target.id === target.id ? connected.compatibilityWarnings : []) ?? []) : (status?.compatibilityWarnings ?? []); + const warnings = compatibilityWarnings.filter((warning) => !isVersionSkewWarning(warning)); + const rawSkewWarning = compatibilityWarnings.find(isVersionSkewWarning) ?? null; + const displayedVersionNote = versionNote + ?? (localAdeVersion && row.version ? null : rawSkewWarning); const errorCard = selectMachineErrorCard({ errorInfo: status?.state === "error" ? status.lastErrorInfo : null, rawError: status?.state === "error" ? status.lastError : null, @@ -178,18 +217,22 @@ export function SavedMachineRow({
{section === "unavailable" && row.unavailableReason ? ( {row.unavailableReason} + ) : row.connected ? ( + {formatLastSeen(target.lastConnectedAt)} ) : ( - <> - {statusLabel} - {` · ${formatLastSeen(target.lastConnectedAt)}`} - + + {target.lastConnectedAt || statusLabel !== "Not connected" + ? `${statusLabel} · ${formatLastSeen(target.lastConnectedAt)}` + : formatLastSeen(null)} + )}
onUpdateAndRestart(updateTargetVersion)} style={outlineButton({ - height: 30, + height: 28, padding: "0 10px", fontSize: 11, })} @@ -212,15 +255,17 @@ export function SavedMachineRow({ {row.connected ? ( ) : section !== "unavailable" ? ( <> @@ -229,63 +274,63 @@ export function SavedMachineRow({ disabled={busyId != null} onClick={() => onConnect(target.id)} style={primaryButton({ - height: 30, + height: 28, padding: "0 10px", fontSize: 11, })} > - {targetConnecting ? "Connecting…" : "Connect"} + {connectingStale ? "Retry" : targetConnecting ? "Connecting…" : "Connect"} ) : null} {section !== "unavailable" ? ( ) : null}
@@ -328,6 +373,10 @@ export function SavedMachineRow({ omittedAttemptCount={omittedAttemptCount} /> + {displayedVersionNote ? ( +
{displayedVersionNote}
+ ) : null} + {warnings.length > 0 ? (
{ }); }); +describe("formatLastSeen", () => { + it("uses a relative last-connected phrase", () => { + expect(formatLastSeen(null)).toBe("Never connected"); + expect(formatLastSeen(Date.now() - 2 * 60 * 60_000)).toBe("Last connected 2h ago"); + }); +}); + +describe("formatVersionSkewNote", () => { + it("names both machines and points the update at the older one", () => { + expect( + formatVersionSkewNote({ + localVersion: "1.2.59", + remoteVersion: "1.2.57", + remoteName: "Arul's Mac Studio", + }), + ).toBe( + "This machine is on ADE 1.2.59. Arul's Mac Studio is on 1.2.57. They can still connect — update Arul's Mac Studio when you can.", + ); + expect( + formatVersionSkewNote({ + localVersion: "1.2.57", + remoteVersion: "1.2.59", + remoteName: "Arul's Mac Studio", + }), + ).toBe( + "This machine is on ADE 1.2.57. Arul's Mac Studio is on 1.2.59. They can still connect — update this machine when you can.", + ); + expect( + formatVersionSkewNote({ + localVersion: "1.2.59", + remoteVersion: "1.2.59", + remoteName: "Studio", + }), + ).toBeNull(); + }); + + it("recognizes both the old jargon and the new version-skew copy", () => { + expect( + isVersionSkewWarning( + "Remote ADE service reported 1.2.57; local ADE is 1.2.59. The versions differ but their RPC capabilities match, so ADE connected normally — nothing to do.", + ), + ).toBe(true); + expect( + isVersionSkewWarning( + "This machine is on ADE 1.2.59. The other machine is on 1.2.57. They can still connect — update the other machine when you can.", + ), + ).toBe(true); + expect(isVersionSkewWarning("Remote ADE service is missing project capabilities: create.")).toBe( + false, + ); + }); +}); + describe("formatRemoteTargetError", () => { it("does not blame sshd for a refused paired-sync port", () => { expect(formatRemoteTargetError("ECONNREFUSED")).toBe( diff --git a/apps/desktop/src/renderer/components/remoteTargets/remoteMachineModel.ts b/apps/desktop/src/renderer/components/remoteTargets/remoteMachineModel.ts index c30751b523..f4b2f00234 100644 --- a/apps/desktop/src/renderer/components/remoteTargets/remoteMachineModel.ts +++ b/apps/desktop/src/renderer/components/remoteTargets/remoteMachineModel.ts @@ -271,7 +271,31 @@ export function formatLastSeen(value: number | null): string { if (!value) return "Never connected"; const date = new Date(value); if (!Number.isFinite(date.getTime())) return "Last connection unknown"; - return `Last connected ${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`; + const phrase = relativeLastSeenPhrase(value); + return phrase ? `Last connected ${phrase}` : "Last connection unknown"; +} + +/** Quiet version-skew note. Yellow warnings are reserved for real breakage. */ +export function formatVersionSkewNote(args: { + localVersion: string | null | undefined; + remoteVersion: string | null | undefined; + remoteName: string; +}): string | null { + const local = args.localVersion?.trim(); + const remote = args.remoteVersion?.trim(); + if (!local || !remote || local === remote) return null; + const advice = isMachineVersionOutdated(remote, local) + ? `update ${args.remoteName} when you can` + : isMachineVersionOutdated(local, remote) + ? "update this machine when you can" + : "nothing to do"; + return `This machine is on ADE ${local}. ${args.remoteName} is on ${remote}. They can still connect — ${advice}.`; +} + +export function isVersionSkewWarning(warning: string): boolean { + return /Remote ADE service reported|RPC capabilities (?:are compatible|match)|versions differ but their RPC|They can still connect — update the other machine/i.test( + warning, + ); } /** diff --git a/apps/desktop/src/renderer/components/remoteTargets/remoteTargetListStyles.ts b/apps/desktop/src/renderer/components/remoteTargets/remoteTargetListStyles.ts index b5d3a13317..f5f8d788c2 100644 --- a/apps/desktop/src/renderer/components/remoteTargets/remoteTargetListStyles.ts +++ b/apps/desktop/src/renderer/components/remoteTargets/remoteTargetListStyles.ts @@ -1,5 +1,5 @@ import type { CSSProperties } from "react"; -import { COLORS, MONO_FONT, SANS_FONT } from "../lanes/laneDesignTokens"; +import { COLORS, SANS_FONT } from "../lanes/laneDesignTokens"; export const panelStyle: CSSProperties = { display: "grid", @@ -49,14 +49,13 @@ export const inlineSuccessTextStyle: CSSProperties = { export const sectionHeaderStyle: CSSProperties = { color: COLORS.textMuted, fontFamily: SANS_FONT, - fontSize: 10.5, + fontSize: 11, fontWeight: 600, - letterSpacing: "0.08em", }; export const nameStyle: CSSProperties = { color: COLORS.textPrimary, - fontFamily: MONO_FONT, + fontFamily: SANS_FONT, fontSize: 13, fontWeight: 700, overflow: "hidden", @@ -66,9 +65,23 @@ export const nameStyle: CSSProperties = { export const subTextStyle: CSSProperties = { color: COLORS.textMuted, - fontFamily: MONO_FONT, + fontFamily: SANS_FONT, fontSize: 12, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }; + +export const iconActionButtonStyle: CSSProperties = { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + width: 28, + height: 28, + padding: 0, + border: "none", + borderRadius: 7, + background: "transparent", + color: COLORS.textSecondary, + cursor: "pointer", +}; diff --git a/apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx b/apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx index ad7a44414f..573227b3c9 100644 --- a/apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx +++ b/apps/desktop/src/renderer/components/settings/SyncDevicesSection.test.tsx @@ -159,7 +159,7 @@ const autoConfirm = async () => true; * every test that touches them has to open it first. */ function openPairing() { - fireEvent.click(screen.getByRole("button", { name: /^Pairing code/ })); + fireEvent.click(screen.getByRole("button", { name: /^Manual pairing code/ })); } describe("ThisMacCard", () => { @@ -192,8 +192,7 @@ describe("ThisMacCard", () => { ); // Connecting runs through the ADE account now; no code is a normal state. expect(screen.queryByText(/Set a pairing code/)).toBeNull(); - expect(screen.getByRole("button", { name: /^Pairing code/ }).textContent) - .toContain("Not set"); + expect(screen.getByRole("button", { name: /^Manual pairing code/ })).toBeTruthy(); }); it("renames this computer and mirrors the name into the account directory", async () => { @@ -254,11 +253,27 @@ describe("ThisMacCard", () => { ); // The alert above carries the runtime's full explanation; the status line // does not repeat a one-liner version of it. - expect(screen.queryByRole("button", { name: /^Pairing code/ })).toBeNull(); + expect(screen.queryByRole("button", { name: /^Manual pairing code/ })).toBeNull(); expect(screen.queryByRole("button", { name: /Generate code/i })).toBeNull(); expect(screen.queryByRole("button", { name: /Remove/i })).toBeNull(); }); + it("still offers Repair on a cold unreadable read while signed out", () => { + (globalThis.window as any).ade = { app: { restartBackgroundService: vi.fn() } }; + render( + , + ); + expect(screen.getByRole("button", { name: "Repair" })).toBeTruthy(); + expect( + screen.getByText("Your session is still there — open your account to fix it"), + ).toBeTruthy(); + expect(screen.queryByText(/Not signed in/)).toBeNull(); + }); + it("restarts the brain and re-reads health when repairing an unreadable session", async () => { // The main process resolves only once the replacement brain answers, so the // hook re-reads health the moment the call settles — no renderer-side sleep. @@ -323,7 +338,7 @@ describe("ThisMacCard", () => { it("explains nearby fallback when signed out", () => { render(); expect( - screen.getByText("Not signed in — nearby devices can still connect with the pairing code"), + screen.getByText("Not signed in — you can still connect to other machines manually"), ).toBeTruthy(); }); @@ -375,12 +390,12 @@ describe("ThisMacCard", () => { it("reopens the pairing disclosure closed every time, never remembering it", () => { const { unmount } = render(); openPairing(); - expect(screen.getByRole("button", { name: /^Pairing code/ }).getAttribute("aria-expanded")) + expect(screen.getByRole("button", { name: /^Manual pairing code/ }).getAttribute("aria-expanded")) .toBe("true"); unmount(); render(); - expect(screen.getByRole("button", { name: /^Pairing code/ }).getAttribute("aria-expanded")) + expect(screen.getByRole("button", { name: /^Manual pairing code/ }).getAttribute("aria-expanded")) .toBe("false"); }); @@ -396,16 +411,14 @@ describe("ThisMacCard", () => { expect(await screen.findByRole("img", { name: accessibleName })).toBeTruthy(); // The logo is the platform statement, so the version line never repeats it. - expect(await screen.findByText("ADE 1.2.28")).toBeTruthy(); + expect(await screen.findByText("This machine — ADE 1.2.28")).toBeTruthy(); expect(screen.queryByText(new RegExp(`ADE 1\\.2\\.28.*${accessibleName}`))).toBeNull(); }); - it("labels this computer with a chip", () => { + it("labels this pane as this machine", () => { render(); - // Composed from THIS_MACHINE_NAME, never spelled out: this badge was - // macOS-only copy ("This Mac") until ADE shipped on Windows, and pinning the - // literal here is what let Settings and Account miss the rename. - expect(screen.getByText(THIS_MACHINE_NAME)).toBeTruthy(); + expect(screen.getByText("This machine")).toBeTruthy(); + expect(screen.queryByText(THIS_MACHINE_NAME)).toBeNull(); }); it("swaps the version line for the fault while the listener is down", async () => { @@ -421,8 +434,7 @@ describe("ThisMacCard", () => { render(); expect(await screen.findByText("Port 8787 is already in use.")).toBeTruthy(); - // One slot, so the card never grows a line in the unhappy path. - expect(screen.queryByText("ADE 1.2.28")).toBeNull(); + expect(screen.getByText("This machine — ADE 1.2.28")).toBeTruthy(); }); it("no longer embeds a Connect-a-phone disclosure — the Phone tab owns pairing", () => { @@ -768,18 +780,27 @@ describe("useSyncConnections local scoping", () => { }); describe("accountDirectorySummary", () => { - it("reflects whether signed-out nearby pairing has a configured code", () => { + it("keeps an unreadable session distinct from signed out", () => { + const status = { pairingPinConfigured: true } as SyncRoleSnapshot; + expect(accountDirectorySummary(status, false, "unreadable")).toEqual({ + label: "Your session is still there — open your account to fix it", + healthy: false, + }); + }); + + it("uses one signed-out line whether a pairing code is set or not", () => { const status = { pairingPinConfigured: false } as SyncRoleSnapshot; expect(accountDirectorySummary(status, false)).toEqual({ - label: "Not signed in — set a pairing code so nearby devices can connect", + label: "Not signed in — you can still connect to other machines manually", healthy: false, }); status.pairingPinConfigured = true; - expect(accountDirectorySummary(status, false).label).toContain( - "nearby devices can still connect with the pairing code", - ); + expect(accountDirectorySummary(status, false)).toEqual({ + label: "Not signed in — you can still connect to other machines manually", + healthy: false, + }); }); const summaryForState = ( diff --git a/apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx b/apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx index 2a4e1bbe40..f6710e0ebc 100644 --- a/apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx +++ b/apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx @@ -16,12 +16,13 @@ import { import { accountDirectorySummary } from "./accountDirectorySummary"; import { QRCodeSVG } from "qrcode.react"; import { createPortal } from "react-dom"; -import { isBrainAccountSessionFailure } from "../../../shared/types"; -import type { - SyncDeviceRuntimeState, - SyncPeerDeviceType, - SyncPairingConnectInfo, - SyncRoleSnapshot, +import { + isBrainAccountSessionFailure, + type AdeAccountSessionState, + type SyncDeviceRuntimeState, + type SyncPeerDeviceType, + type SyncPairingConnectInfo, + type SyncRoleSnapshot, } from "../../../shared/types"; import { buildPairingQrPayload, encodePairingQrUrl } from "../../../shared/pairingQr"; import { THIS_MACHINE_NAME } from "../../../shared/machineIdentity"; @@ -42,7 +43,6 @@ import { SANS_FONT, cardStyle, dangerButton, - inlineBadge, outlineButton, primaryButton, } from "../lanes/laneDesignTokens"; @@ -140,7 +140,7 @@ function isCrdtSyncUnavailable(status: SyncRoleSnapshot): boolean { // laptop glyph *and* the "· Windows" suffix that used to trail the version — the // logo is the platform statement, so spelling it out again is pure duplication. function PlatformGlyph({ platform }: { platform: string | undefined }) { - const props = { size: 17, weight: "duotone" as const, color: COLORS.accent }; + const props = { size: 18, weight: "regular" as const, color: COLORS.textSecondary }; switch (platform) { case "darwin": return ; @@ -190,9 +190,11 @@ function useAppInfoLine(): { version: string; platform: string } | null { export function ThisMacCard({ sync, accountSignedIn, + sessionState = accountSignedIn ? "active" : "signed_out", }: { sync: SyncConnections; accountSignedIn: boolean; + sessionState?: AdeAccountSessionState; }) { const { status, busy, error, notice, isRemoteBound, boundMachineName } = sync; const appInfo = useAppInfoLine(); @@ -254,25 +256,26 @@ export function ThisMacCard({ // the reader nothing they could act on, and a missing pairing code is a // normal state now that the account is the primary way to connect. const problem = connectionProblem(status, host); - const directorySummary = accountDirectorySummary(status, accountSignedIn); + const directorySummary = accountDirectorySummary(status, accountSignedIn, sessionState); // A brain-side unreadable account session is the one directory failure a // restart clears — same test RemoteTargetList runs on its publish health. - const showRepair = accountSignedIn - && isBrainAccountSessionFailure(status.routeHealth?.accountDirectory?.state) + // Repair stays available on a cold unreadable read even when signedIn is false. + const showRepair = isBrainAccountSessionFailure(status.routeHealth?.accountDirectory?.state) && repair.available; - return ( + return (
- - {THIS_MACHINE_NAME} - + {appInfo ? `This machine — ADE ${appInfo.version}` : "This machine"} +
@@ -323,8 +323,6 @@ export function ThisMacCard({ {showRepair ? : null}
- {/* Third line is one slot: the fault takes the version's place while - something is wrong, so the card never grows a line. */} {problem ? (
- ) : appInfo ? ( -
ADE {appInfo.version}
) : null}
@@ -365,7 +361,7 @@ export function ThisMacCard({ ) : null} {host && !crdtUnavailable ? ( - + {isRemoteBound ? ( - Pairing code - - · {pinConfigured ? "Set" : "Not set"} - + Manual pairing code {open ? (
diff --git a/apps/desktop/src/renderer/components/settings/accountDirectorySummary.ts b/apps/desktop/src/renderer/components/settings/accountDirectorySummary.ts index 6f6278a678..b2ab7a2759 100644 --- a/apps/desktop/src/renderer/components/settings/accountDirectorySummary.ts +++ b/apps/desktop/src/renderer/components/settings/accountDirectorySummary.ts @@ -1,8 +1,10 @@ import { describeUnpublishedAccountDirectory, + type AdeAccountSessionState, type SyncAccountDirectoryState, type SyncRoleSnapshot, } from "../../../shared/types"; +import { accountSessionConnectionsSubtitle } from "../../lib/account"; export type AccountDirectorySummary = { label: string; @@ -29,12 +31,17 @@ function unpublishedMachineLabel(state: SyncAccountDirectoryState): string { export function accountDirectorySummary( status: SyncRoleSnapshot, accountSignedIn: boolean, + sessionState: AdeAccountSessionState = accountSignedIn ? "active" : "signed_out", ): AccountDirectorySummary { + if (sessionState === "unreadable") { + return { + label: accountSessionConnectionsSubtitle("unreadable"), + healthy: false, + }; + } if (!accountSignedIn) { return { - label: status.pairingPinConfigured - ? "Not signed in — nearby devices can still connect with the pairing code" - : "Not signed in — set a pairing code so nearby devices can connect", + label: "Not signed in — you can still connect to other machines manually", healthy: false, }; } diff --git a/apps/desktop/src/renderer/lib/account.test.ts b/apps/desktop/src/renderer/lib/account.test.ts index cfa6b3ba22..87b665c8f6 100644 --- a/apps/desktop/src/renderer/lib/account.test.ts +++ b/apps/desktop/src/renderer/lib/account.test.ts @@ -198,6 +198,33 @@ describe("account session state", () => { ).toBe("unreadable"); }); + it("uses one signed-out Connections header for expired and signed-out", async () => { + const { accountSessionTitle, accountSessionConnectionsSubtitle } = await import("./account"); + + expect(accountSessionTitle("signed_out")).toBe("Signed out"); + expect(accountSessionTitle("expired")).toBe("Signed out"); + expect(accountSessionTitle("unreadable")).toBe("Can't read your sign-in"); + expect(accountSessionConnectionsSubtitle("signed_out")).toBe( + "Sign in to easily connect your machines", + ); + expect(accountSessionConnectionsSubtitle("expired")).toBe( + "Sign in to easily connect your machines", + ); + }); + + it("keeps Connections header actions in the session label table", async () => { + const { + accountSessionConnectionsAction, + accountSessionConnectionsActionAria, + } = await import("./account"); + + expect(accountSessionConnectionsAction("signed_out")).toBe("Sign in"); + expect(accountSessionConnectionsAction("expired")).toBe("Sign in"); + expect(accountSessionConnectionsAction("unreadable")).toBe("Fix sign-in"); + expect(accountSessionConnectionsAction("active")).toBe("Manage account"); + expect(accountSessionConnectionsActionAria("unreadable")).toBe("Fix your sign-in"); + }); + it("only prompts a fresh sign-in for the state where it is safe", async () => { const { accountSessionNotice } = await import("./account"); diff --git a/apps/desktop/src/renderer/lib/account.ts b/apps/desktop/src/renderer/lib/account.ts index 085f528cfe..9eb2b48325 100644 --- a/apps/desktop/src/renderer/lib/account.ts +++ b/apps/desktop/src/renderer/lib/account.ts @@ -106,6 +106,8 @@ const ACCOUNT_SESSION_LABELS: Record< title: string; shortLabel: string; connectionsSubtitle: string; + connectionsAction: string; + connectionsActionAria: string; } > = { active: { @@ -113,18 +115,24 @@ const ACCOUNT_SESSION_LABELS: Record< title: "Signed in to ADE", shortLabel: "Account", connectionsSubtitle: "Manage your account", + connectionsAction: "Manage account", + connectionsActionAria: "Manage account", }, signed_out: { notice: null, - title: "Not signed in", + title: "Signed out", shortLabel: "Signed out", - connectionsSubtitle: "Sign in to connect your devices", + connectionsSubtitle: "Sign in to easily connect your machines", + connectionsAction: "Sign in", + connectionsActionAria: "Sign in to ADE", }, expired: { notice: "Your ADE sign-in expired — sign in again.", - title: "Sign-in expired", - shortLabel: "Sign-in expired", - connectionsSubtitle: "Sign in again to connect your devices", + title: "Signed out", + shortLabel: "Signed out", + connectionsSubtitle: "Sign in to easily connect your machines", + connectionsAction: "Sign in", + connectionsActionAria: "Sign in to ADE", }, unreadable: { notice: @@ -132,6 +140,8 @@ const ACCOUNT_SESSION_LABELS: Record< title: "Can't read your sign-in", shortLabel: "Sign-in unavailable", connectionsSubtitle: "Your session is still there — open your account to fix it", + connectionsAction: "Fix sign-in", + connectionsActionAria: "Fix your sign-in", }, }; @@ -151,6 +161,14 @@ export function accountSessionConnectionsSubtitle(state: AdeAccountSessionState) return ACCOUNT_SESSION_LABELS[state].connectionsSubtitle; } +export function accountSessionConnectionsAction(state: AdeAccountSessionState): string { + return ACCOUNT_SESSION_LABELS[state].connectionsAction; +} + +export function accountSessionConnectionsActionAria(state: AdeAccountSessionState): string { + return ACCOUNT_SESSION_LABELS[state].connectionsActionAria; +} + export async function fetchAccountStatus(options?: { force?: boolean }): Promise { const api = accountApi(); if (!api?.status) return SIGNED_OUT_ACCOUNT; diff --git a/apps/desktop/src/renderer/webclient/adapter/__tests__/connectionsPane.test.tsx b/apps/desktop/src/renderer/webclient/adapter/__tests__/connectionsPane.test.tsx index 10073ec27c..4e7c05d7e5 100644 --- a/apps/desktop/src/renderer/webclient/adapter/__tests__/connectionsPane.test.tsx +++ b/apps/desktop/src/renderer/webclient/adapter/__tests__/connectionsPane.test.tsx @@ -62,7 +62,7 @@ describe("hosted Connections pane", () => { , ); - expect(await screen.findByText("No computers yet. Choose Add machine to connect one.")).toBeTruthy(); + expect(await screen.findByText("No computers yet. Add a machine to connect one.")).toBeTruthy(); await expect(window.ade.remoteRuntime.getConnectionSnapshot()).resolves.toEqual({ connections: [], connectedCount: 0, diff --git a/docs/features/remote-runtime/README.md b/docs/features/remote-runtime/README.md index 309f77e146..d3c6f8c87d 100644 --- a/docs/features/remote-runtime/README.md +++ b/docs/features/remote-runtime/README.md @@ -760,7 +760,7 @@ won the same connection attempt; nothing is wrong. Version skew and capability skew no longer fail the connect outright. The bootstrap performs the JSON-RPC `ade/initialize` handshake, normalizes the `capabilities.machineProjects` flags returned by the remote runtime, and reports the result as `RemoteRuntimeCapabilities` plus a `compatibilityWarnings` array on the `RemoteRuntimeConnectResult`. The renderer's remote target panel displays each warning inline under the connection chip. Warnings cover: -- Runtime version mismatch (`Remote ADE service reported X; local ADE is Y. ADE will connect because the RPC capabilities are compatible.`). +- Runtime version mismatch, shown as a quiet Connections-row note from the two versions (`This machine is on ADE X. {name} is on Y. They can still connect — update the older machine when you can.`). - Remote package channel mismatch (e.g. desktop is `beta`, remote runtime advertises `stable`). - Missing `machineProjects` capabilities — `browseDirectories`, `getDetail`, `getWorkSummary`, `getDefaultParentDir`, `create`, `clone`, `listMyGitHubRepos`. These map to the `projects.*` RPCs the renderer uses for the project picker / new-project / clone flows. Missing capabilities do not block connect, but the connection pool refuses the matching call with a self-describing error when the renderer attempts it (e.g. `Remote ADE service 0.7.2 does not support cloning remote projects.`). - The bootstrap fell back to a different ADE home (`Using remote runtime home .ade-beta because .ade did not contain an ADE service for darwin-arm64.`). From 722e6b5326d8a6d21128db6ffe5547fbb1bb5d81 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Sat, 15 Aug 2026 04:03:19 -0400 Subject: [PATCH 2/3] Update the TopBar Connections test to the new signed-out subtitle. Co-authored-by: Cursor --- apps/desktop/src/renderer/components/app/TopBar.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/components/app/TopBar.test.tsx b/apps/desktop/src/renderer/components/app/TopBar.test.tsx index 462ef60cc6..010ba82263 100644 --- a/apps/desktop/src/renderer/components/app/TopBar.test.tsx +++ b/apps/desktop/src/renderer/components/app/TopBar.test.tsx @@ -724,7 +724,7 @@ describe("TopBar", () => { }); expect(screen.getByRole("dialog", { name: "Connections" })).toBeTruthy(); - expect(screen.getByText("Sign in to connect your devices")).toBeTruthy(); + expect(screen.getByText("Sign in to easily connect your machines")).toBeTruthy(); expect(mobileTab.getAttribute("aria-selected")).toBe("true"); expect(screen.getByTestId("sync-devices-section").getAttribute("data-variant")).toBe("phone"); expect(connectionsButton.getAttribute("aria-expanded")).toBe("true"); From 4fc9c175a2b65dc0465a9f0fd3a71ce478d8b498 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Sat, 15 Aug 2026 04:42:03 -0400 Subject: [PATCH 3/3] ship: apply post-rebase quality revalidation Make sessionState the only This-machine input so an unreadable session cannot collapse to signed-out, and flatten the new nested ternaries. Co-authored-by: Cursor --- .../components/app/ConnectionsPanel.tsx | 7 ++- .../remoteTargets/SavedMachineRow.tsx | 8 ++- .../remoteTargets/remoteMachineModel.ts | 11 ++-- .../settings/SyncDevicesSection.test.tsx | 53 +++++++++---------- .../settings/SyncDevicesSection.tsx | 9 ++-- .../settings/accountDirectorySummary.ts | 5 +- 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/apps/desktop/src/renderer/components/app/ConnectionsPanel.tsx b/apps/desktop/src/renderer/components/app/ConnectionsPanel.tsx index 0050a6e547..e06aac5621 100644 --- a/apps/desktop/src/renderer/components/app/ConnectionsPanel.tsx +++ b/apps/desktop/src/renderer/components/app/ConnectionsPanel.tsx @@ -321,10 +321,9 @@ export function ConnectionsPanel({
+ sync={sync} + sessionState={accountSessionState(accountStatus)} + />
{ @@ -280,7 +286,7 @@ export function SavedMachineRow({ })} > - {connectingStale ? "Retry" : targetConnecting ? "Connecting…" : "Connect"} + {connectButtonLabel(connectingStale, targetConnecting)}