Skip to content

Commit e034689

Browse files
authored
Merge branch 'main' into agent/right-panel-close-button
2 parents e1ba7ac + c0bb237 commit e034689

4 files changed

Lines changed: 68 additions & 6 deletions

File tree

apps/web/src/components/BranchToolbar.logic.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
resolveBranchToolbarValue,
1313
resolveLockedWorkspaceLabel,
1414
shouldIncludeBranchPickerItem,
15+
shouldShowEnvironmentIndicator,
1516
} from "./BranchToolbar.logic";
1617

1718
const localEnvironmentId = EnvironmentId.make("environment-local");
@@ -119,6 +120,44 @@ describe("resolveEnvironmentOptionLabel", () => {
119120
});
120121
});
121122

123+
describe("shouldShowEnvironmentIndicator", () => {
124+
it("shows the indicator whenever multiple environments are pickable", () => {
125+
expect(
126+
shouldShowEnvironmentIndicator({
127+
activeEnvironment: { isPrimary: true },
128+
canPickEnvironment: true,
129+
}),
130+
).toBe(true);
131+
});
132+
133+
it("shows a sole remote environment so the user knows where the project runs", () => {
134+
expect(
135+
shouldShowEnvironmentIndicator({
136+
activeEnvironment: { isPrimary: false },
137+
canPickEnvironment: false,
138+
}),
139+
).toBe(true);
140+
});
141+
142+
it("hides a sole primary (this-device) environment", () => {
143+
expect(
144+
shouldShowEnvironmentIndicator({
145+
activeEnvironment: { isPrimary: true },
146+
canPickEnvironment: false,
147+
}),
148+
).toBe(false);
149+
});
150+
151+
it("hides the indicator when the active environment is unknown", () => {
152+
expect(
153+
shouldShowEnvironmentIndicator({
154+
activeEnvironment: null,
155+
canPickEnvironment: false,
156+
}),
157+
).toBe(false);
158+
});
159+
});
160+
122161
describe("resolveEffectiveEnvMode", () => {
123162
it("treats draft threads already attached to a worktree as current-checkout mode", () => {
124163
expect(

apps/web/src/components/BranchToolbar.logic.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ export function resolveEnvironmentOptionLabel(input: {
4242
return runtimeLabel ?? savedLabel ?? input.environmentId;
4343
}
4444

45+
// A remote (non-primary) environment is always surfaced, even when it is the
46+
// only environment available: with a single connected machine there is nothing
47+
// to pick, but the user still needs to see where the project runs.
48+
export function shouldShowEnvironmentIndicator(input: {
49+
activeEnvironment: Pick<EnvironmentOption, "isPrimary"> | null;
50+
canPickEnvironment: boolean;
51+
}): boolean {
52+
if (input.canPickEnvironment) return true;
53+
return input.activeEnvironment !== null && !input.activeEnvironment.isPrimary;
54+
}
55+
4556
export function resolveEnvModeLabel(mode: EnvMode): string {
4657
return mode === "worktree" ? "New worktree" : "Current checkout";
4758
}

apps/web/src/components/BranchToolbar.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
resolveEnvModeLabel,
2121
resolveEffectiveEnvMode,
2222
resolveLockedWorkspaceLabel,
23+
shouldShowEnvironmentIndicator,
2324
} from "./BranchToolbar.logic";
2425
import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector";
2526
import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector";
@@ -60,6 +61,7 @@ interface MobileRunContextSelectorProps {
6061
environmentId: EnvironmentId;
6162
availableEnvironments: readonly EnvironmentOption[] | undefined;
6263
showEnvironmentPicker: boolean;
64+
showEnvironmentIndicator: boolean;
6365
onEnvironmentChange: ((environmentId: EnvironmentId) => void) | undefined;
6466
effectiveEnvMode: EnvMode;
6567
activeWorktreePath: string | null;
@@ -72,6 +74,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
7274
environmentId,
7375
availableEnvironments,
7476
showEnvironmentPicker,
77+
showEnvironmentIndicator,
7578
onEnvironmentChange,
7679
effectiveEnvMode,
7780
activeWorktreePath,
@@ -94,7 +97,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
9497
: resolveCurrentWorkspaceLabel(activeWorktreePath);
9598
const isLocked = envLocked || envModeLocked;
9699
const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : CloudIcon;
97-
const icon = showEnvironmentPicker ? (
100+
const icon = showEnvironmentIndicator ? (
98101
// Button's base styles apply `-mx-0.5` to descendant SVGs, which eats 4px
99102
// out of whatever gap we set. mx-0! cancels that so gap-0.5 reads as 2px.
100103
<span className="inline-flex shrink-0 items-center gap-0.5">
@@ -108,7 +111,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
108111
<>
109112
{icon}
110113
<span className="min-w-0 truncate">
111-
{showEnvironmentPicker ? (activeEnvironment?.label ?? "Run on") : workspaceLabel}
114+
{showEnvironmentIndicator ? (activeEnvironment?.label ?? "Run on") : workspaceLabel}
112115
</span>
113116
</>
114117
);
@@ -234,6 +237,12 @@ export const BranchToolbar = memo(function BranchToolbar({
234237
const showEnvironmentPicker = Boolean(
235238
availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange,
236239
);
240+
const activeEnvironmentOption =
241+
availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null;
242+
const showEnvironmentIndicator = shouldShowEnvironmentIndicator({
243+
activeEnvironment: activeEnvironmentOption,
244+
canPickEnvironment: showEnvironmentPicker,
245+
});
237246
const isMobile = useIsMobile();
238247

239248
if (!hasActiveThread || !activeProject) return null;
@@ -247,20 +256,21 @@ export const BranchToolbar = memo(function BranchToolbar({
247256
environmentId={environmentId}
248257
availableEnvironments={availableEnvironments}
249258
showEnvironmentPicker={showEnvironmentPicker}
259+
showEnvironmentIndicator={showEnvironmentIndicator}
250260
onEnvironmentChange={onEnvironmentChange}
251261
effectiveEnvMode={effectiveEnvMode}
252262
activeWorktreePath={activeWorktreePath}
253263
onEnvModeChange={onEnvModeChange}
254264
/>
255265
) : (
256266
<div className="flex min-w-0 shrink-0 items-center gap-1">
257-
{showEnvironmentPicker && availableEnvironments && onEnvironmentChange && (
267+
{showEnvironmentIndicator && availableEnvironments && (
258268
<>
259269
<BranchToolbarEnvironmentSelector
260270
envLocked={envLocked}
261271
environmentId={environmentId}
262272
availableEnvironments={availableEnvironments}
263-
onEnvironmentChange={onEnvironmentChange}
273+
{...(showEnvironmentPicker && onEnvironmentChange ? { onEnvironmentChange } : {})}
264274
/>
265275
<Separator orientation="vertical" className="mx-0.5 h-3.5!" />
266276
</>

apps/web/src/components/BranchToolbarEnvironmentSelector.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ interface BranchToolbarEnvironmentSelectorProps {
1717
envLocked: boolean;
1818
environmentId: EnvironmentId;
1919
availableEnvironments: readonly EnvironmentOption[];
20-
onEnvironmentChange: (environmentId: EnvironmentId) => void;
20+
// Absent when there is only one environment to show: the indicator still
21+
// renders (as a static label) so remote projects are always identifiable.
22+
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
2123
}
2224

2325
export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvironmentSelector({
@@ -39,7 +41,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir
3941
[availableEnvironments],
4042
);
4143

42-
if (envLocked) {
44+
if (envLocked || onEnvironmentChange === undefined) {
4345
return (
4446
<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">
4547
{activeEnvironment?.isPrimary ? (

0 commit comments

Comments
 (0)