diff --git a/apps/app/src/components/machines/MachineStatusDot.tsx b/apps/app/src/components/machines/MachineStatusDot.tsx index df1d6d0295..622c7fe3fb 100644 --- a/apps/app/src/components/machines/MachineStatusDot.tsx +++ b/apps/app/src/components/machines/MachineStatusDot.tsx @@ -1,12 +1,4 @@ -import type { ReactNode } from "react"; -import { Icon } from "@bb/shared-ui/icon"; import { cn } from "@bb/shared-ui/lib/utils"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@bb/shared-ui/tooltip"; /** * Connection-status dot for a machine (host): filled success dot when the @@ -31,36 +23,3 @@ export function MachineStatusDot({ /> ); } - -/** Accessible connection state for surfaces where a dot alone is ambiguous. */ -export function MachineStatusIcon({ - connected, - tooltip, - className, -}: { - connected: boolean; - tooltip?: ReactNode; - className?: string; -}) { - const label = connected ? "Online" : "Offline"; - const icon = connected ? "Cloud" : "CloudOff"; - return ( - - - - - - - - {tooltip ?? label} - - - ); -} diff --git a/apps/app/src/components/settings/MachinesSettingsSection.test.tsx b/apps/app/src/components/settings/MachinesSettingsSection.test.tsx index 137d3468e3..81c73f22d2 100644 --- a/apps/app/src/components/settings/MachinesSettingsSection.test.tsx +++ b/apps/app/src/components/settings/MachinesSettingsSection.test.tsx @@ -152,7 +152,7 @@ afterEach(() => { }); describe("MachinesSettingsSection", () => { - it("renders machine status, project, and permission metadata as accessible icons", async () => { + it("renders machine status, project, and permission metadata as visible text", async () => { vi.mocked(sdk.system.config).mockResolvedValue(systemConfig()); vi.mocked(sdk.hosts.list).mockResolvedValue([primaryHost, offlineHost]); stubSidebarBootstrapFetch(); @@ -163,27 +163,17 @@ describe("MachinesSettingsSection", () => { expect(screen.getByText("dev-vm")).toBeDefined(); expect(screen.getByText("this machine")).toBeDefined(); expect(screen.getByText("primary")).toBeDefined(); - await waitFor(() => { - expect(screen.getByRole("img", { name: "Online" })).toBeDefined(); - }); - expect( - screen - .getByRole("img", { name: "Online" }) - .querySelector('[data-icon="Cloud"]'), - ).not.toBeNull(); + expect(screen.getByText("Online")).toBeDefined(); + expect(screen.getByText(/^Offline · last seen/u)).toBeDefined(); + expect(screen.getByText("2 projects")).toBeDefined(); + expect(screen.getByText("1 project")).toBeDefined(); + expect(screen.getAllByText("Full Access")).toHaveLength(2); + expect(screen.getByText("macOS")).toBeDefined(); expect( screen - .getByRole("img", { name: "Offline" }) - .querySelector('[data-icon="CloudOff"]'), - ).not.toBeNull(); - const primaryProjects = screen.getByRole("img", { name: "2 projects" }); - expect( - primaryProjects.querySelector('[data-icon="FolderGit"]'), - ).not.toBeNull(); - expect(screen.getByRole("img", { name: "1 project" })).toBeDefined(); - expect(screen.getAllByRole("img", { name: "Full Access" })).toHaveLength(2); - expect(screen.getByText("macOS")).toBeDefined(); - expect(screen.queryByText(/Online ·/u)).toBeNull(); + .getByRole("link", { name: "Open MacBook Pro" }) + .querySelector("[data-icon]"), + ).toBeNull(); }); it("distinguishes the client-local daemon from the primary machine", async () => { @@ -255,11 +245,11 @@ describe("MachinesSettingsSection", () => { renderSection(); - expect( - await screen.findByRole("img", { - name: `Needs update · daemon protocol ${HOST_DAEMON_PROTOCOL_VERSION - 1} · server protocol ${HOST_DAEMON_PROTOCOL_VERSION}`, - }), - ).toBeDefined(); + const updateStatus = await screen.findByText( + `Needs update · daemon protocol ${HOST_DAEMON_PROTOCOL_VERSION - 1} · server protocol ${HOST_DAEMON_PROTOCOL_VERSION}`, + ); + expect(updateStatus.className).toContain("min-w-0"); + expect(updateStatus.className).not.toContain("shrink-0"); // The action lives in the row menu so the rows keep one shape. await openHostMenu("dev-vm"); const renameItem = await screen.findByRole("menuitem", { name: "Rename" }); @@ -309,7 +299,7 @@ describe("MachinesSettingsSection", () => { }); }); - it("uses a compact accessible Add machine action", async () => { + it("uses a labeled Add a machine action", async () => { vi.mocked(sdk.system.config).mockResolvedValue(systemConfig()); vi.mocked(sdk.hosts.list).mockResolvedValue([primaryHost, offlineHost]); stubSidebarBootstrapFetch(); @@ -317,15 +307,14 @@ describe("MachinesSettingsSection", () => { renderSection(); const addMachine = await screen.findByRole("button", { - name: "Add machine", + name: "Add a machine", }); - expect(addMachine.textContent).toBe(""); - expect(addMachine.className).toContain("size-7"); + expect(addMachine.textContent).toBe("Add a machine"); expect(addMachine.querySelector('[data-icon="Plus"]')).not.toBeNull(); - fireEvent.pointerMove(addMachine); - expect((await screen.findByRole("tooltip")).textContent).toBe( - "Add machine", - ); + const action = addMachine.parentElement; + expect(action?.className).toContain("self-start"); + expect(action?.parentElement?.className).toContain("flex-col"); + expect(action?.parentElement?.className).toContain("sm:flex-row"); fireEvent.click(addMachine); expect( await screen.findByRole("heading", { name: "Add a machine" }), @@ -359,7 +348,7 @@ describe("MachinesSettingsSection", () => { }); }); - it("keeps permission metadata left-aligned and reserves a hover caret", async () => { + it("shows permission metadata as text and reserves a hover caret", async () => { vi.mocked(sdk.system.config).mockResolvedValue(systemConfig()); vi.mocked(sdk.hosts.list).mockResolvedValue([ primaryHost, @@ -369,14 +358,8 @@ describe("MachinesSettingsSection", () => { renderSection(); - expect( - await screen.findByRole("img", { - name: "Accept Edits", - }), - ).toBeDefined(); - expect(screen.getByRole("img", { name: "Full Access" })).toBeDefined(); - expect(screen.queryByText("Accept Edits")).toBeNull(); - expect(screen.queryByText("Full Access")).toBeNull(); + expect(await screen.findByText("Accept Edits")).toBeDefined(); + expect(screen.getByText("Full Access")).toBeDefined(); // The control itself lives on the machine page. expect( screen.queryByRole("button", { name: /Permission limit for/ }), @@ -390,12 +373,6 @@ describe("MachinesSettingsSection", () => { expect(row?.className).toContain("focus-within:bg-state-hover"); expect(row?.className).toContain("px-2"); expect(row?.className).toContain("py-2"); - const permission = screen.getByRole("img", { name: "Accept Edits" }); - expect(permission.querySelector('[data-icon="FolderEdit"]')).not.toBeNull(); - fireEvent.pointerMove(permission); - expect((await screen.findByRole("tooltip")).textContent).toBe( - "Accept Edits", - ); const caret = row?.querySelector('[data-icon="ChevronRight"]'); expect(caret?.classList.contains("opacity-0")).toBe(true); expect(caret?.classList.contains("size-3.5")).toBe(true); diff --git a/apps/app/src/components/settings/MachinesSettingsSection.tsx b/apps/app/src/components/settings/MachinesSettingsSection.tsx index 70f42e1612..1afd5371c9 100644 --- a/apps/app/src/components/settings/MachinesSettingsSection.tsx +++ b/apps/app/src/components/settings/MachinesSettingsSection.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, type ReactNode } from "react"; +import { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import type { Host, PermissionMode } from "@bb/domain"; import { RETRY_ACTION_ICON } from "@bb/domain/update-state"; @@ -16,7 +16,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@bb/shared-ui/dropdown-menu"; -import { Icon, type IconName } from "@bb/shared-ui/icon"; +import { Icon } from "@bb/shared-ui/icon"; import { cn } from "@bb/shared-ui/lib/utils"; import { ResourceRowDetailChevron } from "@bb/shared-ui/resource-list"; import { @@ -28,7 +28,7 @@ import { import { AddMachineDialog } from "@/components/dialogs/AddMachineDialog"; import { ConfirmDeleteDialog } from "@/components/dialogs/ConfirmDeleteDialog"; import { appToast } from "@/components/ui/app-toast"; -import { MachineStatusIcon } from "@/components/machines/MachineStatusDot"; +import { MachineStatusDot } from "@/components/machines/MachineStatusDot"; import { MachineRenameDialog } from "@/components/settings/MachineRenameDialog"; import { SettingsBadge, @@ -45,7 +45,6 @@ import { useHosts } from "@/hooks/queries/host-queries"; import { useSidebarNavigation } from "@/hooks/queries/sidebar-navigation-query"; import { useSystemConfig } from "@/hooks/queries/system-queries"; import { useHostDaemon } from "@/hooks/useHostDaemon"; -import { PersistentHostIconName } from "@/lib/host-display"; import { getSettingsMachineRoutePath } from "@/lib/route-paths"; import { PERMISSION_MODE_OPTIONS } from "@/lib/permission-mode-options"; import { getMutationErrorMessage } from "@/lib/mutation-errors"; @@ -76,39 +75,6 @@ const PLATFORM_LABELS: Record = { unknown: null, }; -function MachineMetadataIcon({ - icon, - label, - children, - className, -}: { - icon: IconName; - label: string; - children?: ReactNode; - className?: string; -}) { - return ( - - - - - - {children} - - - {label} - - - ); -} - interface MachineRowProps { host: Host; isPrimary: boolean; @@ -138,10 +104,12 @@ function MachineRow({ }: MachineRowProps) { const permission = PERMISSION_MODE_PRESENTATION[host.maxPermissionMode]; const projectLabel = `${projectCount} ${projectCount === 1 ? "project" : "projects"}`; - const offlineTooltip = - host.lastSeenAt === null - ? "Offline" - : `Offline · last seen ${formatRelativeTime({ timestamp: host.lastSeenAt, now })}`; + const connectionLabel = + host.status === "connected" + ? "Online" + : host.lastSeenAt === null + ? "Offline" + : `Offline · last seen ${formatRelativeTime({ timestamp: host.lastSeenAt, now })}`; const updateStatus = formatHostUpdateStatus(host); const removeItem = ( -
@@ -189,35 +153,27 @@ function MachineRow({ ) : null} {showPrimaryBadge ? primary : null}
-
- +
+ + + {connectionLabel} + {platformLabel === null ? null : ( {platformLabel} )} - - {projectCount} - - + {projectLabel} + + {permission.label} + {updateStatus === null ? null : ( - + + {updateStatus} + )}
@@ -315,22 +271,14 @@ export function MachinesSettingsSection() { title="Machines" description={MACHINES_SECTION_DESCRIPTION} action={ - - - - - - Add machine - - + } > {hosts === undefined ? ( diff --git a/apps/app/src/components/ui/settings-section.tsx b/apps/app/src/components/ui/settings-section.tsx index 3d385d9668..ecc3e28e6d 100644 --- a/apps/app/src/components/ui/settings-section.tsx +++ b/apps/app/src/components/ui/settings-section.tsx @@ -27,8 +27,8 @@ export function SettingsSection({
@@ -44,7 +44,7 @@ export function SettingsSection({

) : null}
- {action ?
{action}
: null} + {action ?
{action}
: null}
{ renderView(); - expect( - await screen.findByRole("heading", { name: /dev-vm/u }), - ).toBeDefined(); + const machineHeading = await screen.findByRole("heading", { + name: /dev-vm/u, + }); + expect(machineHeading.tagName).toBe("H1"); const checkedByMode = Object.fromEntries( (await screen.findAllByRole("radio")).map((option) => [ option.textContent?.startsWith("Accept Edits") @@ -197,21 +198,23 @@ describe("MachineSettingsView", () => { }); expect( screen - .getByRole("radio", { name: /Accept Edits/u }) - .querySelector('[data-icon="FolderEdit"]'), - ).not.toBeNull(); + .getAllByRole("radio") + .every((option) => option.querySelector("[data-icon]") === null), + ).toBe(true); + const machineSubtitle = screen.getByText(/^Online ·/u); + expect(machineSubtitle.closest("section")).toBeNull(); + expect(screen.queryByRole("img", { name: "Online" })).toBeNull(); expect( screen - .getByRole("radio", { name: /Approve for me/u }) - .querySelector('[data-icon="SecurityCheck"]'), - ).not.toBeNull(); + .getByRole("heading", { name: /dev-vm/u }) + .querySelector("[data-icon]"), + ).toBeNull(); expect( screen - .getByRole("radio", { name: /Full Access/u }) - .querySelector('[data-icon="SquareUnlock02"]'), - ).not.toBeNull(); - expect(screen.getByRole("img", { name: "Online" })).toBeDefined(); - expect(document.querySelector('[data-icon="FolderGit"]')).not.toBeNull(); + .getByRole("heading", { name: "Machine information" }) + .closest("section") + ?.querySelector("[data-icon]"), + ).toBeNull(); expect( document.querySelector('[data-provider-icon="codex"]'), ).not.toBeNull(); @@ -226,6 +229,15 @@ describe("MachineSettingsView", () => { .getByRole("heading", { name: "Provider CLIs" }) .querySelector("[data-icon]"), ).toBeNull(); + const installedLabel = screen.getByText("Installed"); + expect(installedLabel.parentElement?.className).toContain("flex-col"); + expect(installedLabel.parentElement?.className).toContain("sm:flex-row"); + expect(installedLabel.nextElementSibling?.className).toContain( + "justify-start", + ); + expect(installedLabel.nextElementSibling?.className).toContain( + "sm:justify-end", + ); // The page exists so the modes can explain themselves. expect(screen.getByText(/No sandbox and no approvals/u)).toBeDefined(); }); @@ -247,7 +259,7 @@ describe("MachineSettingsView", () => { expect(screen.queryByRole("button", { name: "Rename" })).toBeNull(); }); - it("names an offline machine's status icon", async () => { + it("shows an offline machine's status as text", async () => { vi.mocked(sdk.system.config).mockResolvedValue(systemConfig()); vi.mocked(sdk.hosts.list).mockResolvedValue([ host({ status: "disconnected", lastSeenAt: Date.now() - 60_000 }), @@ -256,8 +268,8 @@ describe("MachineSettingsView", () => { renderView(); - expect(await screen.findByRole("img", { name: "Offline" })).toBeDefined(); - expect(screen.queryByText(/^Offline ·/u)).toBeNull(); + expect(await screen.findByText(/^Offline · last seen/u)).toBeDefined(); + expect(screen.queryByRole("img", { name: "Offline" })).toBeNull(); }); it("links update issues to Updates in a warning pill", async () => { diff --git a/apps/app/src/views/MachineSettingsView.tsx b/apps/app/src/views/MachineSettingsView.tsx index 9dfa27efe9..f7836ff486 100644 --- a/apps/app/src/views/MachineSettingsView.tsx +++ b/apps/app/src/views/MachineSettingsView.tsx @@ -1,11 +1,6 @@ import { useMemo, useState, type ReactNode } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; -// Route views render icons outside the shell's core set. Importing the -// extended registry here ships it as a static dependency of this route chunk, -// so those icons never flash blank waiting for an on-demand load. -import "@bb/shared-ui/icon-extended"; import type { Host, PermissionMode } from "@bb/domain"; -import { UPDATE_ACTION_ICON } from "@bb/domain/update-state"; import { providerCliKeyValues, type HostPlatform, @@ -14,12 +9,12 @@ import { import { Button } from "@bb/shared-ui/button"; import { DialogFooter, DialogHeader, DialogTitle } from "@bb/shared-ui/dialog"; import { DialogDescription } from "@bb/shared-ui/dialog"; -import { Icon, type IconName } from "@bb/shared-ui/icon"; +import { Icon } from "@bb/shared-ui/icon"; import { cn } from "@bb/shared-ui/lib/utils"; import { Pill } from "@bb/shared-ui/pill"; import { ResourceOverflowMenu } from "@bb/shared-ui/resource-list"; import { ConfirmDeleteDialog } from "@/components/dialogs/ConfirmDeleteDialog"; -import { MachineStatusIcon } from "@/components/machines/MachineStatusDot"; +import { MachineStatusDot } from "@/components/machines/MachineStatusDot"; import { PageShell } from "@/components/ui/page-shell.js"; import { SettingsBadge, @@ -46,7 +41,6 @@ import { hostCanRetryUpdate, } from "@/lib/host-update-status"; import { getMutationErrorMessage } from "@/lib/mutation-errors"; -import { PersistentHostIconName } from "@/lib/host-display"; import { PERMISSION_MODE_OPTIONS } from "@/lib/permission-mode-options"; import { formatRelativeTime } from "@/lib/relative-time"; import { @@ -90,7 +84,7 @@ function headerMeta({ platformLabel: string | null; now: number; }): string { - const parts: string[] = []; + const parts: string[] = [host.status === "connected" ? "Online" : "Offline"]; if (host.status !== "connected" && host.lastSeenAt !== null) { parts.push( `last seen ${formatRelativeTime({ timestamp: host.lastSeenAt, now })}`, @@ -145,14 +139,6 @@ function PermissionLimitCards({ ) : null} - {option.label} @@ -174,24 +160,14 @@ function PermissionLimitCards({ interface DetailRowProps { label: string; - icon?: IconName; children: ReactNode; } -function DetailRow({ label, icon, children }: DetailRowProps) { +function DetailRow({ label, children }: DetailRowProps) { return ( - - - {icon ? ( - - ) : null} - {label} - -
+ + {label} +
{children}
@@ -308,48 +284,40 @@ export function MachineSettingsView() { Machines - - - {host.name} +
+
+
+

+ {host.name} +

{isThisMachine ? ( This machine ) : null} {showMachineIdentityBadges && isPrimary ? ( Primary ) : null} - - } - titleAction={ - { - renameHost.reset(); - setRenameOpen(true); - }, - }, - ]} - /> - } - > - - - +
+
+

{headerMeta({ host, platformLabel, now })}

- - - +
+
+ { + renameHost.reset(); + setRenameOpen(true); + }, + }, + ]} + /> +
{installedProviders.length > 0 ? ( - + {installedProviders.map((entry) => ( - + {projects.length === 0 ? ( None ) : ( @@ -457,7 +425,7 @@ export function MachineSettingsView() { )} - + {updateStatus ?? "Up to date"} {hostCanRetryUpdate(host) ? (