fix(app): clean up host selection affordances
Single-host users should not see host-selection chrome, while multi-host surfaces keep shared combobox feedback and consistent hover-card metadata rows.
This commit is contained in:
@@ -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 ({
|
test("each project's row icon preselects that project, and the reused screen resets a stale manual choice across projects", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -132,6 +132,25 @@ test.describe("Sidebar workspace list", () => {
|
|||||||
await workspace.cleanup();
|
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", () => {
|
test.describe("Mobile sidebar panelState transition", () => {
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ export function HostPickerOption({
|
|||||||
trailingSlot={trailingSlot}
|
trailingSlot={trailingSlot}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
active={active}
|
active={active}
|
||||||
interactiveFeedback={false}
|
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
/>
|
/>
|
||||||
@@ -134,7 +133,6 @@ function SystemHostPickerOption({
|
|||||||
leadingSlot={leadingSlot}
|
leadingSlot={leadingSlot}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
active={active}
|
active={active}
|
||||||
interactiveFeedback={false}
|
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -230,7 +230,6 @@ export interface ComboboxItemProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
/** When true, bumps hover/pressed colors up one surface level (for items on elevated backgrounds). */
|
/** When true, bumps hover/pressed colors up one surface level (for items on elevated backgrounds). */
|
||||||
elevated?: boolean;
|
elevated?: boolean;
|
||||||
interactiveFeedback?: boolean;
|
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
testID?: string;
|
testID?: string;
|
||||||
}
|
}
|
||||||
@@ -245,7 +244,6 @@ export function ComboboxItem({
|
|||||||
active,
|
active,
|
||||||
disabled,
|
disabled,
|
||||||
elevated,
|
elevated,
|
||||||
interactiveFeedback = true,
|
|
||||||
onPress,
|
onPress,
|
||||||
testID,
|
testID,
|
||||||
}: ComboboxItemProps): ReactElement {
|
}: ComboboxItemProps): ReactElement {
|
||||||
@@ -271,16 +269,12 @@ export function ComboboxItem({
|
|||||||
const itemPressableStyle = useCallback(
|
const itemPressableStyle = useCallback(
|
||||||
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||||
styles.comboboxItem,
|
styles.comboboxItem,
|
||||||
interactiveFeedback &&
|
hovered && (elevated ? styles.comboboxItemHoveredElevated : styles.comboboxItemHovered),
|
||||||
hovered &&
|
pressed && (elevated ? styles.comboboxItemPressedElevated : styles.comboboxItemPressed),
|
||||||
(elevated ? styles.comboboxItemHoveredElevated : styles.comboboxItemHovered),
|
active && styles.comboboxItemActive,
|
||||||
interactiveFeedback &&
|
|
||||||
pressed &&
|
|
||||||
(elevated ? styles.comboboxItemPressedElevated : styles.comboboxItemPressed),
|
|
||||||
interactiveFeedback && active && styles.comboboxItemActive,
|
|
||||||
disabled && styles.comboboxItemDisabled,
|
disabled && styles.comboboxItemDisabled,
|
||||||
],
|
],
|
||||||
[elevated, active, disabled, interactiveFeedback],
|
[elevated, active, disabled],
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemContentStyle = useMemo(
|
const itemContentStyle = useMemo(
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
Folder,
|
Folder,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
|
Server,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { GitHubIcon } from "@/components/icons/github-icon";
|
import { GitHubIcon } from "@/components/icons/github-icon";
|
||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
@@ -39,8 +40,7 @@ import { useHoverSafeZone } from "@/hooks/use-hover-safe-zone";
|
|||||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { FloatingSurface } from "@/components/ui/floating";
|
import { FloatingSurface } from "@/components/ui/floating";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
import { useHosts, useHostRuntimeSnapshot } from "@/runtime/host-runtime";
|
import { useHosts } from "@/runtime/host-runtime";
|
||||||
import { formatConnectionStatus } from "@/utils/daemons";
|
|
||||||
|
|
||||||
interface Rect {
|
interface Rect {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -330,20 +330,16 @@ function WorkspaceHoverCardContent({
|
|||||||
|
|
||||||
const ThemedGitBranch = withUnistyles(GitBranch);
|
const ThemedGitBranch = withUnistyles(GitBranch);
|
||||||
const ThemedFolder = withUnistyles(Folder);
|
const ThemedFolder = withUnistyles(Folder);
|
||||||
|
const ThemedServer = withUnistyles(Server);
|
||||||
|
|
||||||
|
type CardInfoIcon = React.ComponentType<React.ComponentProps<typeof ThemedGitBranch>>;
|
||||||
|
|
||||||
function HostRow({ serverId }: { serverId: string }): ReactElement | null {
|
function HostRow({ serverId }: { serverId: string }): ReactElement | null {
|
||||||
const hosts = useHosts();
|
const hosts = useHosts();
|
||||||
const host = hosts.find((h) => h.serverId === serverId);
|
const host = hosts.find((h) => h.serverId === serverId);
|
||||||
const snapshot = useHostRuntimeSnapshot(serverId);
|
|
||||||
const label = host?.label?.trim() || serverId;
|
const label = host?.label?.trim() || serverId;
|
||||||
const status = formatConnectionStatus(snapshot?.connectionStatus ?? "idle");
|
|
||||||
|
|
||||||
return (
|
return <InfoRow icon={ThemedServer} value={label} testID="hover-card-workspace-host" />;
|
||||||
<View style={styles.hostRow}>
|
|
||||||
<Text style={styles.hostRowLabel}>{label}</Text>
|
|
||||||
<Text style={styles.hostRowStatus}>{status}</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemedExternalLink = withUnistyles(ExternalLink);
|
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 warningColorMapping = (theme: Theme) => ({ color: theme.colors.statusWarning });
|
||||||
const dangerColorMapping = (theme: Theme) => ({ color: theme.colors.statusDanger });
|
const dangerColorMapping = (theme: Theme) => ({ color: theme.colors.statusDanger });
|
||||||
|
|
||||||
|
function InfoRow({
|
||||||
|
icon: Icon,
|
||||||
|
value,
|
||||||
|
testID,
|
||||||
|
}: {
|
||||||
|
icon: CardInfoIcon;
|
||||||
|
value: string;
|
||||||
|
testID: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View style={styles.cardInfoRow}>
|
||||||
|
<Icon size={12} uniProps={foregroundMutedColorMapping} />
|
||||||
|
<Text style={styles.cardInfoText} numberOfLines={1} testID={testID}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CopyableInfoRow({
|
function CopyableInfoRow({
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
value,
|
value,
|
||||||
@@ -367,7 +382,7 @@ function CopyableInfoRow({
|
|||||||
copyLabel,
|
copyLabel,
|
||||||
testID,
|
testID,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ComponentType<React.ComponentProps<typeof ThemedGitBranch>>;
|
icon: CardInfoIcon;
|
||||||
value: string;
|
value: string;
|
||||||
copyValue: string;
|
copyValue: string;
|
||||||
copyLabel: string;
|
copyLabel: string;
|
||||||
@@ -567,22 +582,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
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: {
|
cardMetaRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -1196,6 +1196,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
|||||||
|
|
||||||
const selectedHostLabel =
|
const selectedHostLabel =
|
||||||
host.allHosts.find((h) => h.serverId === host.selectedServerId)?.label ?? "Host";
|
host.allHosts.find((h) => h.serverId === host.selectedServerId)?.label ?? "Host";
|
||||||
|
const showHostControl = host.allHosts.length > 1;
|
||||||
const isolationTriggerLabel = isolationLabel(t, isolation.effectiveIsolation);
|
const isolationTriggerLabel = isolationLabel(t, isolation.effectiveIsolation);
|
||||||
|
|
||||||
const badgePressableStyle = useCallback(
|
const badgePressableStyle = useCallback(
|
||||||
@@ -1242,7 +1243,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
const hostControl = (
|
const hostControl = showHostControl ? (
|
||||||
<View>
|
<View>
|
||||||
<HostPicker
|
<HostPicker
|
||||||
hosts={host.allHosts}
|
hosts={host.allHosts}
|
||||||
@@ -1271,7 +1272,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</HostPicker>
|
</HostPicker>
|
||||||
</View>
|
</View>
|
||||||
);
|
) : null;
|
||||||
|
|
||||||
const isolationControl = isolation.canCreateWorktree ? (
|
const isolationControl = isolation.canCreateWorktree ? (
|
||||||
<View>
|
<View>
|
||||||
@@ -1334,7 +1335,7 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
|||||||
return isCompact ? (
|
return isCompact ? (
|
||||||
<View testID="new-workspace-ref-picker-row" style={styles.formStack}>
|
<View testID="new-workspace-ref-picker-row" style={styles.formStack}>
|
||||||
<FormRow>{projectControl}</FormRow>
|
<FormRow>{projectControl}</FormRow>
|
||||||
<FormRow>{hostControl}</FormRow>
|
{hostControl ? <FormRow>{hostControl}</FormRow> : null}
|
||||||
{/* Keep fixed row height when git-only controls are hidden. */}
|
{/* Keep fixed row height when git-only controls are hidden. */}
|
||||||
{isolationControl ? (
|
{isolationControl ? (
|
||||||
<FormRow>{isolationControl}</FormRow>
|
<FormRow>{isolationControl}</FormRow>
|
||||||
|
|||||||
Reference in New Issue
Block a user