Skip to content
Open
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
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
181 changes: 181 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import {
dedupeRemoteBranchesWithLocalMatches,
deriveLocalBranchNameFromRemoteRef,
resolveEnvironmentOptionLabel,
resolveAvailableBranchToolbarPicker,
resolveBranchToolbarRunContextShortcutTarget,
resolveBranchSelectionTarget,
resolveBranchPickerQueryForOpenState,
resolveBranchPickerShortcutOpenState,
resolveBranchToolbarPickerOpenChange,
resolveBranchToolbarPickerToggle,
resolveCurrentWorkspaceLabel,
resolveDraftEnvModeAfterBranchChange,
resolveEffectiveEnvMode,
Expand All @@ -14,10 +20,185 @@ import {
resolveLocalCheckoutBranchMismatch,
resolvePreviousWorktreeLabel,
resolvePreviousWorktreeSeed,
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("resolveBranchToolbarPickerToggle", () => {
it("uses each preceding result when shortcuts repeat before a render", () => {
let current = resolveBranchToolbarPickerToggle(null, "environment");
current = resolveBranchToolbarPickerToggle(current, "environment");
expect(current).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("resolveBranchPickerShortcutOpenState", () => {
it("always permits closing an already-open picker", () => {
expect(resolveBranchPickerShortcutOpenState({ open: true, unavailable: true })).toBe(false);
});

it("only permits opening while the picker is available", () => {
expect(resolveBranchPickerShortcutOpenState({ open: false, unavailable: false })).toBe(true);
expect(resolveBranchPickerShortcutOpenState({ open: false, unavailable: true })).toBeNull();
});

it("uses each preceding result when shortcuts repeat before a render", () => {
let open = resolveBranchPickerShortcutOpenState({ open: false, unavailable: false }) ?? false;
open = resolveBranchPickerShortcutOpenState({ open, unavailable: false }) ?? open;
expect(open).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
87 changes: 87 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,93 @@ 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 resolveBranchPickerShortcutOpenState(input: {
readonly open: boolean;
readonly unavailable: boolean;
}): boolean | null {
if (input.open) return false;
return input.unavailable ? null : true;
}

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

export function resolveBranchToolbarPickerToggle(
current: BranchToolbarPicker | null,
picker: BranchToolbarPicker,
): BranchToolbarPicker | null {
return current === picker ? null : picker;
}

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