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
88 changes: 87 additions & 1 deletion apps/web/src/components/BranchToolbar.logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { EnvironmentId, type VcsRef } from "@t3tools/contracts";
import { EnvironmentId, ProjectId, type VcsRef } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";
import {
buildCheckoutWorkspaceChoices,
dedupeCheckoutOptions,
dedupeRemoteBranchesWithLocalMatches,
deriveLocalBranchNameFromRemoteRef,
resolveEnvironmentOptionLabel,
Expand Down Expand Up @@ -106,6 +108,90 @@ describe("resolvePreviousWorktreeLabel", () => {
});
});

describe("buildCheckoutWorkspaceChoices", () => {
it("keeps every same-repository clone selectable as a checkout or worktree base", () => {
const firstProjectId = ProjectId.make("project-first-clone");
const secondProjectId = ProjectId.make("project-second-clone");

expect(
buildCheckoutWorkspaceChoices([
{
projectId: firstProjectId,
title: "t3code",
workspaceRoot: "/src/t3code",
},
{
projectId: secondProjectId,
title: "t3code-3",
workspaceRoot: "/src/t3code-3",
},
]).map(({ projectId, mode }) => ({ projectId, mode })),
).toEqual([
{ projectId: firstProjectId, mode: "local" },
{ projectId: firstProjectId, mode: "worktree" },
{ projectId: secondProjectId, mode: "local" },
{ projectId: secondProjectId, mode: "worktree" },
]);
});
});

describe("dedupeCheckoutOptions", () => {
it("keeps the active project when duplicate normalized roots exist", () => {
const staleProjectId = ProjectId.make("project-stale");
const activeProjectId = ProjectId.make("project-active");

expect(
dedupeCheckoutOptions(
[
{
projectId: staleProjectId,
title: "Stale checkout",
workspaceRoot: "C:\\src\\t3code",
},
{
projectId: activeProjectId,
title: "Active checkout",
workspaceRoot: "C:/src/t3code",
},
],
activeProjectId,
),
).toEqual([
{
projectId: activeProjectId,
title: "Active checkout",
workspaceRoot: "C:/src/t3code",
},
]);
});

it("preserves checkouts that differ only by path casing", () => {
const lowerProjectId = ProjectId.make("project-lower");
const upperProjectId = ProjectId.make("project-upper");
const checkouts = [
{ projectId: lowerProjectId, title: "Lowercase", workspaceRoot: "/src/t3code" },
{ projectId: upperProjectId, title: "Uppercase", workspaceRoot: "/src/T3Code" },
];

expect(dedupeCheckoutOptions(checkouts, lowerProjectId)).toEqual(checkouts);
});

it("deduplicates paths that differ only by trailing separators", () => {
const staleProjectId = ProjectId.make("project-trailing-slash");
const activeProjectId = ProjectId.make("project-no-trailing-slash");

expect(
dedupeCheckoutOptions(
[
{ projectId: staleProjectId, title: "Stale", workspaceRoot: "/src/t3code/" },
{ projectId: activeProjectId, title: "Active", workspaceRoot: "/src/t3code" },
],
activeProjectId,
).map((checkout) => checkout.projectId),
).toEqual([activeProjectId]);
});
});

describe("resolveDraftEnvModeAfterBranchChange", () => {
it("switches to local mode when returning from an existing worktree to the main worktree", () => {
expect(
Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { EnvironmentId, VcsRef, ProjectId } from "@t3tools/contracts";
import { normalizeProjectPathForComparison } from "@t3tools/shared/path";
import * as Schema from "effect/Schema";
import { toSortableTimestamp } from "../lib/threadSort";
export {
Expand All @@ -13,9 +14,50 @@ export interface EnvironmentOption {
isPrimary: boolean;
}

export interface CheckoutOption {
projectId: ProjectId;
title: string;
workspaceRoot: string;
}

export interface CheckoutWorkspaceChoice {
projectId: ProjectId;
mode: EnvMode;
value: string;
}

export function dedupeCheckoutOptions(
checkouts: readonly CheckoutOption[],
activeProjectId: ProjectId,
): CheckoutOption[] {
const activeCheckout = checkouts.find((checkout) => checkout.projectId === activeProjectId);
const orderedCheckouts = activeCheckout
? [activeCheckout, ...checkouts.filter((checkout) => checkout.projectId !== activeProjectId)]
: checkouts;
const seenWorkspaceRoots = new Set<string>();
return orderedCheckouts.filter((checkout) => {
const workspaceRoot = normalizeProjectPathForComparison(checkout.workspaceRoot);
if (seenWorkspaceRoots.has(workspaceRoot)) return false;
seenWorkspaceRoots.add(workspaceRoot);
return true;
});
Comment thread
juliusmarminge marked this conversation as resolved.
}

export const EnvMode = Schema.Literals(["local", "worktree"]);
export type EnvMode = typeof EnvMode.Type;

export function buildCheckoutWorkspaceChoices(
checkouts: readonly CheckoutOption[],
): CheckoutWorkspaceChoice[] {
return checkouts.flatMap((checkout) =>
(["local", "worktree"] as const).map((mode) => ({
projectId: checkout.projectId,
mode,
value: JSON.stringify([checkout.projectId, mode]),
})),
);
}

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

function normalizeDisplayLabel(value: string | null | undefined): string | null {
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useComposerDraftStore, type DraftId } from "../composerDraftStore";
import { useProject, useThread, useThreadShellsForProjectRefs } from "../state/entities";
import { useIsMobile } from "../hooks/useMediaQuery";
import {
type CheckoutOption,
type EnvMode,
type EnvironmentOption,
resolveCurrentWorkspaceLabel,
Expand Down Expand Up @@ -56,6 +57,8 @@ interface BranchToolbarProps {
onComposerFocusRequest?: () => void;
availableEnvironments?: readonly EnvironmentOption[];
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
availableCheckouts?: readonly CheckoutOption[];
onWorkspaceChange?: (projectId: CheckoutOption["projectId"], mode: EnvMode) => void;
}

interface MobileRunContextSelectorProps {
Expand Down Expand Up @@ -229,6 +232,8 @@ export const BranchToolbar = memo(function BranchToolbar({
onComposerFocusRequest,
availableEnvironments,
onEnvironmentChange,
availableCheckouts,
onWorkspaceChange,
}: BranchToolbarProps) {
const threadRef = useMemo(
() => scopeThreadRef(environmentId, threadId),
Expand Down Expand Up @@ -340,6 +345,9 @@ export const BranchToolbar = memo(function BranchToolbar({
onEnvModeChange={onEnvModeChange}
previousWorktreeLabel={previousWorktreeLabel}
onUsePreviousWorktree={onUsePreviousWorktree}
activeProjectId={activeProject.id}
availableCheckouts={availableCheckouts ?? []}
{...(onWorkspaceChange ? { onWorkspaceChange } : {})}
/>
</div>
)}
Expand Down
Loading
Loading