Skip to content
Open
6 changes: 6 additions & 0 deletions apps/server/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
assert.equal(defaultsByCommand.get("terminal.splitVertical"), "mod+shift+d");
assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1");
assert.equal(defaultsByCommand.get("modelPicker.jump.9"), "mod+9");
assert.equal(defaultsByCommand.get("modelOptionsPicker.toggle"), "mod+shift+e");
assert.equal(defaultsByCommand.get("runtimeModePicker.toggle"), "mod+shift+a");
assert.equal(defaultsByCommand.get("planMode.toggle"), "mod+shift+p");
assert.equal(defaultsByCommand.get("environmentPicker.toggle"), "mod+shift+v");
assert.equal(defaultsByCommand.get("envModePicker.toggle"), "mod+shift+l");
assert.equal(defaultsByCommand.get("branchPicker.toggle"), "mod+shift+b");
}),
);

Expand Down
154 changes: 154 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,172 @@ import {
dedupeRemoteBranchesWithLocalMatches,
deriveLocalBranchNameFromRemoteRef,
resolveEnvironmentOptionLabel,
resolveAvailableBranchToolbarPicker,
resolveBranchToolbarRunContextShortcutTarget,
resolveBranchSelectionTarget,
resolveBranchPickerQueryForOpenState,
resolveBranchToolbarPickerOpenChange,
resolveCurrentWorkspaceLabel,
resolveDraftEnvModeAfterBranchChange,
resolveEffectiveEnvMode,
resolveEnvModeLabel,
resolveBranchToolbarValue,
resolveLockedWorkspaceLabel,
resolveLocalCheckoutBranchMismatch,
shouldShowBranchPickerShortcutHint,
shouldIncludeBranchPickerItem,
shouldShowEnvironmentIndicator,
} from "./BranchToolbar.logic";

describe("resolveBranchToolbarRunContextShortcutTarget", () => {
const available = {
isRendered: true,
environmentPickerAvailable: true,
envLocked: false,
envModeLocked: false,
};

it("routes either run-context shortcut to the combined picker on mobile", () => {
expect(
resolveBranchToolbarRunContextShortcutTarget({
...available,
control: "environment",
isMobile: true,
}),
).toBe("mobile-run-context");
expect(
resolveBranchToolbarRunContextShortcutTarget({
...available,
control: "env-mode",
isMobile: true,
}),
).toBe("mobile-run-context");
});

it("keeps the mobile environment shortcut available when only workspace mode is locked", () => {
expect(
resolveBranchToolbarRunContextShortcutTarget({
...available,
control: "environment",
isMobile: true,
envModeLocked: true,
}),
).toBe("mobile-run-context");
});

it("keeps the mobile workspace shortcut available when only environment is locked", () => {
expect(
resolveBranchToolbarRunContextShortcutTarget({
...available,
control: "env-mode",
isMobile: true,
envLocked: true,
}),
).toBe("mobile-run-context");
});

it("rejects only the shortcut whose specific control is unavailable", () => {
expect(
resolveBranchToolbarRunContextShortcutTarget({
...available,
control: "environment",
isMobile: true,
environmentPickerAvailable: false,
}),
).toBeNull();
expect(
resolveBranchToolbarRunContextShortcutTarget({
...available,
control: "env-mode",
isMobile: true,
envModeLocked: true,
}),
).toBeNull();
});
});

describe("resolveBranchToolbarPickerOpenChange", () => {
it("replaces the active picker instead of stacking keyboard-opened overlays", () => {
expect(resolveBranchToolbarPickerOpenChange("environment", "env-mode", true)).toBe("env-mode");
expect(resolveBranchToolbarPickerOpenChange("env-mode", "branch", true)).toBe("branch");
});

it("ignores a stale close event from a picker that is no longer active", () => {
expect(resolveBranchToolbarPickerOpenChange("branch", "environment", false)).toBe("branch");
expect(resolveBranchToolbarPickerOpenChange("branch", "branch", false)).toBeNull();
});
});

describe("resolveAvailableBranchToolbarPicker", () => {
const allAvailable = {
environment: true,
envMode: true,
mobileRunContext: true,
branch: true,
};

it("preserves a picker while its interactive control is available", () => {
expect(resolveAvailableBranchToolbarPicker("environment", allAvailable)).toBe("environment");
expect(resolveAvailableBranchToolbarPicker("mobile-run-context", allAvailable)).toBe(
"mobile-run-context",
);
});

it("clears each picker when its interactive control stops being available", () => {
expect(
resolveAvailableBranchToolbarPicker("environment", {
...allAvailable,
environment: false,
}),
).toBeNull();
expect(
resolveAvailableBranchToolbarPicker("env-mode", { ...allAvailable, envMode: false }),
).toBeNull();
expect(
resolveAvailableBranchToolbarPicker("mobile-run-context", {
...allAvailable,
mobileRunContext: false,
}),
).toBeNull();
expect(
resolveAvailableBranchToolbarPicker("branch", { ...allAvailable, branch: false }),
).toBeNull();
});
});

describe("shouldShowBranchPickerShortcutHint", () => {
it("only shows a hint while the corresponding shortcut can toggle the picker", () => {
expect(
shouldShowBranchPickerShortcutHint({
shortcutHintLabel: "⌘B",
isInitialBranchesLoadPending: false,
isBranchActionPending: false,
}),
).toBe(true);
expect(
shouldShowBranchPickerShortcutHint({
shortcutHintLabel: "⌘B",
isInitialBranchesLoadPending: true,
isBranchActionPending: false,
}),
).toBe(false);
expect(
shouldShowBranchPickerShortcutHint({
shortcutHintLabel: "⌘B",
isInitialBranchesLoadPending: false,
isBranchActionPending: true,
}),
).toBe(false);
});
});

describe("resolveBranchPickerQueryForOpenState", () => {
it("clears a search when the controlled picker is closed externally", () => {
expect(resolveBranchPickerQueryForOpenState("feature/search", false)).toBe("");
expect(resolveBranchPickerQueryForOpenState("feature/search", true)).toBe("feature/search");
});
});

const localEnvironmentId = EnvironmentId.make("environment-local");
const remoteEnvironmentId = EnvironmentId.make("environment-remote");

Expand Down
72 changes: 72 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,78 @@ export interface EnvironmentOption {
export const EnvMode = Schema.Literals(["local", "worktree"]);
export type EnvMode = typeof EnvMode.Type;

export type BranchToolbarPicker = "environment" | "env-mode" | "mobile-run-context" | "branch";

export type BranchToolbarRunContextControl = "environment" | "env-mode";

export function resolveBranchToolbarRunContextShortcutTarget(input: {
readonly control: BranchToolbarRunContextControl;
readonly isRendered: boolean;
readonly isMobile: boolean;
readonly environmentPickerAvailable: boolean;
readonly envLocked: boolean;
readonly envModeLocked: boolean;
}): BranchToolbarPicker | null {
if (!input.isRendered) return null;

const controlAvailable =
input.control === "environment"
? input.environmentPickerAvailable && !input.envLocked
: !input.envModeLocked;
if (!controlAvailable) return null;

if (input.isMobile) return "mobile-run-context";
return input.control;
}

export interface BranchToolbarPickerAvailability {
readonly environment: boolean;
readonly envMode: boolean;
readonly mobileRunContext: boolean;
readonly branch: boolean;
}

export function resolveAvailableBranchToolbarPicker(
picker: BranchToolbarPicker | null,
availability: BranchToolbarPickerAvailability,
): BranchToolbarPicker | null {
switch (picker) {
case "environment":
return availability.environment ? picker : null;
case "env-mode":
return availability.envMode ? picker : null;
case "mobile-run-context":
return availability.mobileRunContext ? picker : null;
case "branch":
return availability.branch ? picker : null;
case null:
return null;
}
}

export function shouldShowBranchPickerShortcutHint(input: {
readonly shortcutHintLabel: string | null | undefined;
readonly isInitialBranchesLoadPending: boolean;
readonly isBranchActionPending: boolean;
}): boolean {
return Boolean(
input.shortcutHintLabel && !input.isInitialBranchesLoadPending && !input.isBranchActionPending,
);
}

export function resolveBranchToolbarPickerOpenChange(
current: BranchToolbarPicker | null,
picker: BranchToolbarPicker,
open: boolean,
): BranchToolbarPicker | null {
if (open) return picker;
return current === picker ? null : current;
}

export function resolveBranchPickerQueryForOpenState(query: string, open: boolean): string {
return open ? query : "";
}

const GENERIC_LOCAL_ENVIRONMENT_LABELS = new Set(["local", "local environment"]);

function normalizeDisplayLabel(value: string | null | undefined): string | null {
Expand Down
Loading
Loading