Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
resolveBranchToolbarValue,
resolveLockedWorkspaceLabel,
shouldIncludeBranchPickerItem,
shouldShowEnvironmentIndicator,
} from "./BranchToolbar.logic";

const localEnvironmentId = EnvironmentId.make("environment-local");
Expand Down Expand Up @@ -119,6 +120,44 @@ describe("resolveEnvironmentOptionLabel", () => {
});
});

describe("shouldShowEnvironmentIndicator", () => {
it("shows the indicator whenever multiple environments are pickable", () => {
expect(
shouldShowEnvironmentIndicator({
activeEnvironment: { isPrimary: true },
canPickEnvironment: true,
}),
).toBe(true);
});

it("shows a sole remote environment so the user knows where the project runs", () => {
expect(
shouldShowEnvironmentIndicator({
activeEnvironment: { isPrimary: false },
canPickEnvironment: false,
}),
).toBe(true);
});

it("hides a sole primary (this-device) environment", () => {
expect(
shouldShowEnvironmentIndicator({
activeEnvironment: { isPrimary: true },
canPickEnvironment: false,
}),
).toBe(false);
});

it("hides the indicator when the active environment is unknown", () => {
expect(
shouldShowEnvironmentIndicator({
activeEnvironment: null,
canPickEnvironment: false,
}),
).toBe(false);
});
});

describe("resolveEffectiveEnvMode", () => {
it("treats draft threads already attached to a worktree as current-checkout mode", () => {
expect(
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export function resolveEnvironmentOptionLabel(input: {
return runtimeLabel ?? savedLabel ?? input.environmentId;
}

// A remote (non-primary) environment is always surfaced, even when it is the
// only environment available: with a single connected machine there is nothing
// to pick, but the user still needs to see where the project runs.
export function shouldShowEnvironmentIndicator(input: {
activeEnvironment: Pick<EnvironmentOption, "isPrimary"> | null;
canPickEnvironment: boolean;
}): boolean {
if (input.canPickEnvironment) return true;
return input.activeEnvironment !== null && !input.activeEnvironment.isPrimary;
}

export function resolveEnvModeLabel(mode: EnvMode): string {
return mode === "worktree" ? "New worktree" : "Current checkout";
}
Expand Down
18 changes: 14 additions & 4 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
resolveEnvModeLabel,
resolveEffectiveEnvMode,
resolveLockedWorkspaceLabel,
shouldShowEnvironmentIndicator,
} from "./BranchToolbar.logic";
import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector";
import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector";
Expand Down Expand Up @@ -60,6 +61,7 @@ interface MobileRunContextSelectorProps {
environmentId: EnvironmentId;
availableEnvironments: readonly EnvironmentOption[] | undefined;
showEnvironmentPicker: boolean;
showEnvironmentIndicator: boolean;
onEnvironmentChange: ((environmentId: EnvironmentId) => void) | undefined;
effectiveEnvMode: EnvMode;
activeWorktreePath: string | null;
Expand All @@ -72,6 +74,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
environmentId,
availableEnvironments,
showEnvironmentPicker,
showEnvironmentIndicator,
onEnvironmentChange,
effectiveEnvMode,
activeWorktreePath,
Expand All @@ -94,7 +97,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
: resolveCurrentWorkspaceLabel(activeWorktreePath);
const isLocked = envLocked || envModeLocked;
const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : CloudIcon;
const icon = showEnvironmentPicker ? (
const icon = showEnvironmentIndicator ? (
// Button's base styles apply `-mx-0.5` to descendant SVGs, which eats 4px
// out of whatever gap we set. mx-0! cancels that so gap-0.5 reads as 2px.
<span className="inline-flex shrink-0 items-center gap-0.5">
Expand All @@ -108,7 +111,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
<>
{icon}
<span className="min-w-0 truncate">
{showEnvironmentPicker ? (activeEnvironment?.label ?? "Run on") : workspaceLabel}
{showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel}
</span>
</>
);
Expand Down Expand Up @@ -234,6 +237,12 @@ export const BranchToolbar = memo(function BranchToolbar({
const showEnvironmentPicker = Boolean(
availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange,
);
const activeEnvironmentOption =
availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null;
const showEnvironmentIndicator = shouldShowEnvironmentIndicator({
activeEnvironment: activeEnvironmentOption,
canPickEnvironment: showEnvironmentPicker,
});
const isMobile = useIsMobile();

if (!hasActiveThread || !activeProject) return null;
Expand All @@ -247,20 +256,21 @@ export const BranchToolbar = memo(function BranchToolbar({
environmentId={environmentId}
availableEnvironments={availableEnvironments}
showEnvironmentPicker={showEnvironmentPicker}
showEnvironmentIndicator={showEnvironmentIndicator}
onEnvironmentChange={onEnvironmentChange}
effectiveEnvMode={effectiveEnvMode}
activeWorktreePath={activeWorktreePath}
onEnvModeChange={onEnvModeChange}
/>
) : (
<div className="flex min-w-0 shrink-0 items-center gap-1">
{showEnvironmentPicker && availableEnvironments && onEnvironmentChange && (
{showEnvironmentIndicator && availableEnvironments && (
<>
<BranchToolbarEnvironmentSelector
envLocked={envLocked}
environmentId={environmentId}
availableEnvironments={availableEnvironments}
onEnvironmentChange={onEnvironmentChange}
{...(showEnvironmentPicker && onEnvironmentChange ? { onEnvironmentChange } : {})}
/>
<Separator orientation="vertical" className="mx-0.5 h-3.5!" />
</>
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/BranchToolbarEnvironmentSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ interface BranchToolbarEnvironmentSelectorProps {
envLocked: boolean;
environmentId: EnvironmentId;
availableEnvironments: readonly EnvironmentOption[];
onEnvironmentChange: (environmentId: EnvironmentId) => void;
// Absent when there is only one environment to show: the indicator still
// renders (as a static label) so remote projects are always identifiable.
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
}

export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvironmentSelector({
Expand All @@ -39,7 +41,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir
[availableEnvironments],
);

if (envLocked) {
if (envLocked || onEnvironmentChange === undefined) {
return (
<span className="inline-flex items-center gap-1 border border-transparent px-[calc(--spacing(3)-1px)] text-sm font-medium text-muted-foreground/70 sm:text-xs">
{activeEnvironment?.isPrimary ? (
Expand Down
Loading