diff --git a/packages/app/e2e/new-workspace-entry.spec.ts b/packages/app/e2e/new-workspace-entry.spec.ts index a77a5a56a..550d4df3b 100644 --- a/packages/app/e2e/new-workspace-entry.spec.ts +++ b/packages/app/e2e/new-workspace-entry.spec.ts @@ -74,6 +74,37 @@ test.describe("New workspace entry points", () => { } }); + test("the New Workspace screen hides the host selector when there is only one host", async ({ + page, + }) => { + const seeded: SeededWorkspace = await seedWorkspace({ repoPrefix: "entry-single-host-" }); + + try { + await seedSavedSettingsHosts(page, [ + { + serverId: getServerId(), + label: "localhost", + endpoint: `127.0.0.1:${getE2EDaemonPort()}`, + }, + ]); + + await gotoAppShell(page); + await waitForSidebarHydration(page); + await expect( + page.getByTestId(`sidebar-workspace-row-${getServerId()}:${seeded.workspaceId}`), + ).toBeVisible({ timeout: 30_000 }); + + await openGlobalNewWorkspaceComposer(page); + + await expect(page.getByTestId("new-workspace-project-picker-trigger")).toBeVisible({ + timeout: 30_000, + }); + await expect(page.getByTestId("host-picker-trigger")).toHaveCount(0); + } finally { + await seeded.cleanup(); + } + }); + test("each project's row icon preselects that project, and the reused screen resets a stale manual choice across projects", async ({ page, }) => { diff --git a/packages/app/e2e/sidebar-workspace.spec.ts b/packages/app/e2e/sidebar-workspace.spec.ts index bc01852a8..ed6940203 100644 --- a/packages/app/e2e/sidebar-workspace.spec.ts +++ b/packages/app/e2e/sidebar-workspace.spec.ts @@ -132,6 +132,25 @@ test.describe("Sidebar workspace list", () => { await workspace.cleanup(); } }); + + test("workspace hover card shows host as metadata", async ({ page }) => { + const workspace = await seedWorkspace({ repoPrefix: "sidebar-hover-host-" }); + + try { + await gotoAppShell(page); + await waitForSidebarProject(page, path.basename(workspace.repoPath)); + + const row = await waitForSidebarWorkspace(page, workspace.workspaceId); + await row.hover(); + + const hoverCard = page.getByTestId("workspace-hover-card"); + await expect(hoverCard).toBeVisible({ timeout: 30_000 }); + await expect(page.getByTestId("hover-card-workspace-host")).toHaveText("localhost"); + await expect(hoverCard).not.toContainText(/\b(Online|Connecting|Offline|Error|Idle)\b/); + } finally { + await workspace.cleanup(); + } + }); }); test.describe("Mobile sidebar panelState transition", () => { diff --git a/packages/app/src/components/hosts/host-picker.tsx b/packages/app/src/components/hosts/host-picker.tsx index 5eac4e84f..86101d857 100644 --- a/packages/app/src/components/hosts/host-picker.tsx +++ b/packages/app/src/components/hosts/host-picker.tsx @@ -100,7 +100,6 @@ export function HostPickerOption({ trailingSlot={trailingSlot} selected={selected} active={active} - interactiveFeedback={false} onPress={onPress} testID={testID} /> @@ -134,7 +133,6 @@ function SystemHostPickerOption({ leadingSlot={leadingSlot} selected={selected} active={active} - interactiveFeedback={false} onPress={onPress} testID={testID} /> diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx index cad13d764..edc92b10c 100644 --- a/packages/app/src/components/ui/combobox.tsx +++ b/packages/app/src/components/ui/combobox.tsx @@ -230,7 +230,6 @@ export interface ComboboxItemProps { disabled?: boolean; /** When true, bumps hover/pressed colors up one surface level (for items on elevated backgrounds). */ elevated?: boolean; - interactiveFeedback?: boolean; onPress: () => void; testID?: string; } @@ -245,7 +244,6 @@ export function ComboboxItem({ active, disabled, elevated, - interactiveFeedback = true, onPress, testID, }: ComboboxItemProps): ReactElement { @@ -271,16 +269,12 @@ export function ComboboxItem({ const itemPressableStyle = useCallback( ({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.comboboxItem, - interactiveFeedback && - hovered && - (elevated ? styles.comboboxItemHoveredElevated : styles.comboboxItemHovered), - interactiveFeedback && - pressed && - (elevated ? styles.comboboxItemPressedElevated : styles.comboboxItemPressed), - interactiveFeedback && active && styles.comboboxItemActive, + hovered && (elevated ? styles.comboboxItemHoveredElevated : styles.comboboxItemHovered), + pressed && (elevated ? styles.comboboxItemPressedElevated : styles.comboboxItemPressed), + active && styles.comboboxItemActive, disabled && styles.comboboxItemDisabled, ], - [elevated, active, disabled, interactiveFeedback], + [elevated, active, disabled], ); const itemContentStyle = useMemo( diff --git a/packages/app/src/components/workspace-hover-card.tsx b/packages/app/src/components/workspace-hover-card.tsx index f0d724036..8bbd2318c 100644 --- a/packages/app/src/components/workspace-hover-card.tsx +++ b/packages/app/src/components/workspace-hover-card.tsx @@ -21,6 +21,7 @@ import { ExternalLink, Folder, GitBranch, + Server, } from "lucide-react-native"; import { GitHubIcon } from "@/components/icons/github-icon"; import type { Theme } from "@/styles/theme"; @@ -39,8 +40,7 @@ import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone"; import { useIsCompactFormFactor } from "@/constants/layout"; import { FloatingSurface } from "@/components/ui/floating"; import { isWeb } from "@/constants/platform"; -import { useHosts, useHostRuntimeSnapshot } from "@/runtime/host-runtime"; -import { formatConnectionStatus } from "@/utils/daemons"; +import { useHosts } from "@/runtime/host-runtime"; interface Rect { x: number; @@ -330,20 +330,16 @@ function WorkspaceHoverCardContent({ const ThemedGitBranch = withUnistyles(GitBranch); const ThemedFolder = withUnistyles(Folder); +const ThemedServer = withUnistyles(Server); + +type CardInfoIcon = React.ComponentType>; function HostRow({ serverId }: { serverId: string }): ReactElement | null { const hosts = useHosts(); const host = hosts.find((h) => h.serverId === serverId); - const snapshot = useHostRuntimeSnapshot(serverId); const label = host?.label?.trim() || serverId; - const status = formatConnectionStatus(snapshot?.connectionStatus ?? "idle"); - return ( - - {label} - {status} - - ); + return ; } const ThemedExternalLink = withUnistyles(ExternalLink); @@ -360,6 +356,25 @@ const successColorMapping = (theme: Theme) => ({ color: theme.colors.statusSucce const warningColorMapping = (theme: Theme) => ({ color: theme.colors.statusWarning }); const dangerColorMapping = (theme: Theme) => ({ color: theme.colors.statusDanger }); +function InfoRow({ + icon: Icon, + value, + testID, +}: { + icon: CardInfoIcon; + value: string; + testID: string; +}) { + return ( + + + + {value} + + + ); +} + function CopyableInfoRow({ icon: Icon, value, @@ -367,7 +382,7 @@ function CopyableInfoRow({ copyLabel, testID, }: { - icon: React.ComponentType>; + icon: CardInfoIcon; value: string; copyValue: string; copyLabel: string; @@ -567,22 +582,6 @@ const styles = StyleSheet.create((theme) => ({ flex: 1, minWidth: 0, }, - hostRow: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - gap: theme.spacing[2], - paddingHorizontal: theme.spacing[3], - paddingBottom: theme.spacing[2], - }, - hostRowLabel: { - fontSize: theme.fontSize.xs, - color: theme.colors.foregroundMuted, - }, - hostRowStatus: { - fontSize: theme.fontSize.xs, - color: theme.colors.foregroundMuted, - }, cardMetaRow: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index d1074f716..3a9dd72ea 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -1196,6 +1196,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme const selectedHostLabel = host.allHosts.find((h) => h.serverId === host.selectedServerId)?.label ?? "Host"; + const showHostControl = host.allHosts.length > 1; const isolationTriggerLabel = isolationLabel(t, isolation.effectiveIsolation); const badgePressableStyle = useCallback( @@ -1242,7 +1243,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme ); - const hostControl = ( + const hostControl = showHostControl ? ( - ); + ) : null; const isolationControl = isolation.canCreateWorktree ? ( @@ -1334,7 +1335,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme return isCompact ? ( {projectControl} - {hostControl} + {hostControl ? {hostControl} : null} {/* Keep fixed row height when git-only controls are hidden. */} {isolationControl ? ( {isolationControl}