import type { EnvironmentId, VcsRef, ProjectId } from "@t3tools/contracts";import * as Schema from "effect/Schema";import { toSortableTimestamp } from "../lib/threadSort";export { dedupeRemoteBranchesWithLocalMatches, deriveLocalBranchNameFromRemoteRef,} from "@t3tools/shared/git"; export interface EnvironmentOption { environmentId: EnvironmentId; projectId: ProjectId; label: string; isPrimary: boolean;} export const EnvMode = Schema.Literals(["local", "worktree"]);export type EnvMode = typeof EnvMode.Type; const GENERIC_LOCAL_ENVIRONMENT_LABELS = new Set(["local", "local environment"]); function normalizeDisplayLabel(value: string | null | undefined): string | null { const trimmed = value?.trim(); return trimmed && trimmed.length > 0 ? trimmed : null;} export function resolveEnvironmentOptionLabel(input: { isPrimary: boolean; environmentId: EnvironmentId; runtimeLabel?: string | null; savedLabel?: string | null;}): string { const runtimeLabel = normalizeDisplayLabel(input.runtimeLabel); const savedLabel = normalizeDisplayLabel(input.savedLabel); if (input.isPrimary) { const preferredLocalLabel = [runtimeLabel, savedLabel].find((label) => { if (!label) return false; return !GENERIC_LOCAL_ENVIRONMENT_LABELS.has(label.toLowerCase()); }); return preferredLocalLabel ?? "This device"; } 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 shouldShowComposerContextStrip(input: { hasActiveProject: boolean; isGitRepo: boolean; showEnvironmentIndicator: boolean;}): boolean { return input.hasActiveProject && (input.isGitRepo || input.showEnvironmentIndicator);} export function resolveEnvModeLabel(mode: EnvMode): string { return mode === "worktree" ? "New worktree" : "Current checkout";} export function resolveCurrentWorkspaceLabel(activeWorktreePath: string | null): string { return activeWorktreePath ? "Current worktree" : resolveEnvModeLabel("local");} export function resolveLockedWorkspaceLabel(activeWorktreePath: string | null): string { return activeWorktreePath ? "Worktree" : "Local checkout";} export interface PreviousWorktreeSeed { branch: string | null; worktreePath: string;} // The most recently touched worktree in the project that the composer isn't// already pointing at. Backs the "Previous worktree" entry in the workspace// selector so a follow-up thread can hop back into the worktree you just// worked in without hunting for its branch. Archived threads don't compete —// the rest of the UI hides them, so their worktrees shouldn't resurface here.export function resolvePreviousWorktreeSeed(input: { threads: ReadonlyArray<{ branch: string | null; worktreePath: string | null; updatedAt: string; archivedAt?: string | null; }>; currentWorktreePath: string | null;}): PreviousWorktreeSeed | null { let latest: { branch: string | null; worktreePath: string; updatedAt: number } | null = null; for (const thread of input.threads) { if ( !thread.worktreePath || thread.worktreePath === input.currentWorktreePath || (thread.archivedAt ?? null) !== null ) { continue; } const updatedAt = toSortableTimestamp(thread.updatedAt); if (updatedAt === null) { continue; } if (latest === null || updatedAt > latest.updatedAt) { latest = { branch: thread.branch, worktreePath: thread.worktreePath, updatedAt, }; } } return latest === null ? null : { branch: latest.branch, worktreePath: latest.worktreePath };} export function resolvePreviousWorktreeLabel(seed: PreviousWorktreeSeed): string { return seed.branch ? `Previous worktree (${seed.branch})` : "Previous worktree";} export function resolveEffectiveEnvMode(input: { activeWorktreePath: string | null; hasServerThread: boolean; draftThreadEnvMode: EnvMode | undefined;}): EnvMode { const { activeWorktreePath, hasServerThread, draftThreadEnvMode } = input; if (!hasServerThread) { if (activeWorktreePath) { return "local"; } return draftThreadEnvMode === "worktree" ? "worktree" : "local"; } return activeWorktreePath ? "worktree" : "local";} export function resolveDraftEnvModeAfterBranchChange(input: { nextWorktreePath: string | null; currentWorktreePath: string | null; effectiveEnvMode: EnvMode;}): EnvMode { const { nextWorktreePath, currentWorktreePath, effectiveEnvMode } = input; if (nextWorktreePath) { return "worktree"; } if (effectiveEnvMode === "worktree" && !currentWorktreePath) { return "worktree"; } return "local";} export function resolveBranchToolbarValue(input: { envMode: EnvMode; activeWorktreePath: string | null; activeThreadBranch: string | null; currentGitBranch: string | null;}): string | null { const { envMode, activeWorktreePath, activeThreadBranch, currentGitBranch } = input; if (envMode === "worktree" && !activeWorktreePath) { return activeThreadBranch ?? currentGitBranch; } return currentGitBranch ?? activeThreadBranch;} export function resolveBranchTriggerLabel(input: { activeWorktreePath: string | null; effectiveEnvMode: EnvMode; resolvedActiveBranch: string | null; resolvedActiveBranchIsRemote: boolean | null; startFromOrigin: boolean;}): string { const { activeWorktreePath, effectiveEnvMode, resolvedActiveBranch, resolvedActiveBranchIsRemote, startFromOrigin, } = input; if (!resolvedActiveBranch) { return "Select ref"; } if (effectiveEnvMode === "worktree" && !activeWorktreePath) { const baseRef = startFromOrigin && resolvedActiveBranchIsRemote === false ? `origin/${resolvedActiveBranch}` : resolvedActiveBranch; return `From ${baseRef}`; } return resolvedActiveBranch;} export function resolveBranchToolbarPrBranch(input: { activeThreadBranch: string | null; resolvedActiveBranch: string | null;}): string | null { return input.activeThreadBranch === input.resolvedActiveBranch ? input.activeThreadBranch : null;} export function resolveLocalCheckoutBranchMismatch(input: { effectiveEnvMode: EnvMode; activeWorktreePath: string | null; activeThreadBranch: string | null; currentGitBranch: string | null;}): { threadBranch: string; currentBranch: string } | null { const { effectiveEnvMode, activeWorktreePath, activeThreadBranch, currentGitBranch } = input; if (effectiveEnvMode !== "local" || activeWorktreePath !== null) { return null; } if (!activeThreadBranch || !currentGitBranch || activeThreadBranch === currentGitBranch) { return null; } return { threadBranch: activeThreadBranch, currentBranch: currentGitBranch };} export function resolveBranchSelectionTarget(input: { activeProjectCwd: string; activeWorktreePath: string | null; refName: Pick<VcsRef, "isDefault" | "worktreePath">;}): { checkoutCwd: string; nextWorktreePath: string | null; reuseExistingWorktree: boolean;} { const { activeProjectCwd, activeWorktreePath, refName } = input; if (refName.worktreePath) { return { checkoutCwd: refName.worktreePath, nextWorktreePath: refName.worktreePath === activeProjectCwd ? null : refName.worktreePath, reuseExistingWorktree: true, }; } const nextWorktreePath = activeWorktreePath !== null && refName.isDefault ? null : activeWorktreePath; return { checkoutCwd: nextWorktreePath ?? activeProjectCwd, nextWorktreePath, reuseExistingWorktree: false, };} export function shouldIncludeBranchPickerItem(input: { itemValue: string; normalizedQuery: string; createBranchItemValue: string | null; checkoutPullRequestItemValue: string | null;}): boolean { const { itemValue, normalizedQuery, createBranchItemValue, checkoutPullRequestItemValue } = input; if (normalizedQuery.length === 0) { return true; } if (createBranchItemValue && itemValue === createBranchItemValue) { return true; } if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) { return true; } return itemValue.toLowerCase().includes(normalizedQuery);}
import type { EnvironmentId, VcsRef, ProjectId } from "@t3tools/contracts";
import * as Schema from "effect/Schema";
import { toSortableTimestamp } from "../lib/threadSort";
export {
dedupeRemoteBranchesWithLocalMatches,
deriveLocalBranchNameFromRemoteRef,
} from "@t3tools/shared/git";
export interface EnvironmentOption {
environmentId: EnvironmentId;
projectId: ProjectId;
label: string;
isPrimary: boolean;
}
export const EnvMode = Schema.Literals(["local", "worktree"]);
export type EnvMode = typeof EnvMode.Type;
const GENERIC_LOCAL_ENVIRONMENT_LABELS = new Set(["local", "local environment"]);
function normalizeDisplayLabel(value: string | null | undefined): string | null {
const trimmed = value?.trim();
return trimmed && trimmed.length > 0 ? trimmed : null;
}
export function resolveEnvironmentOptionLabel(input: {
isPrimary: boolean;
environmentId: EnvironmentId;
runtimeLabel?: string | null;
savedLabel?: string | null;
}): string {
const runtimeLabel = normalizeDisplayLabel(input.runtimeLabel);
const savedLabel = normalizeDisplayLabel(input.savedLabel);
if (input.isPrimary) {
const preferredLocalLabel = [runtimeLabel, savedLabel].find((label) => {
if (!label) return false;
return !GENERIC_LOCAL_ENVIRONMENT_LABELS.has(label.toLowerCase());
});
return preferredLocalLabel ?? "This device";
}
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 shouldShowComposerContextStrip(input: {
hasActiveProject: boolean;
isGitRepo: boolean;
showEnvironmentIndicator: boolean;
}): boolean {
return input.hasActiveProject && (input.isGitRepo || input.showEnvironmentIndicator);
}
export function resolveEnvModeLabel(mode: EnvMode): string {
return mode === "worktree" ? "New worktree" : "Current checkout";
}
export function resolveCurrentWorkspaceLabel(activeWorktreePath: string | null): string {
return activeWorktreePath ? "Current worktree" : resolveEnvModeLabel("local");
}
export function resolveLockedWorkspaceLabel(activeWorktreePath: string | null): string {
return activeWorktreePath ? "Worktree" : "Local checkout";
}
export interface PreviousWorktreeSeed {
branch: string | null;
worktreePath: string;
}
// The most recently touched worktree in the project that the composer isn't
// already pointing at. Backs the "Previous worktree" entry in the workspace
// selector so a follow-up thread can hop back into the worktree you just
// worked in without hunting for its branch. Archived threads don't compete —
// the rest of the UI hides them, so their worktrees shouldn't resurface here.
export function resolvePreviousWorktreeSeed(input: {
threads: ReadonlyArray<{
branch: string | null;
worktreePath: string | null;
updatedAt: string;
archivedAt?: string | null;
}>;
currentWorktreePath: string | null;
}): PreviousWorktreeSeed | null {
let latest: { branch: string | null; worktreePath: string; updatedAt: number } | null = null;
for (const thread of input.threads) {
if (
!thread.worktreePath ||
thread.worktreePath === input.currentWorktreePath ||
(thread.archivedAt ?? null) !== null
) {
continue;
}
const updatedAt = toSortableTimestamp(thread.updatedAt);
if (updatedAt === null) {
continue;
}
if (latest === null || updatedAt > latest.updatedAt) {
latest = {
branch: thread.branch,
worktreePath: thread.worktreePath,
updatedAt,
};
}
}
return latest === null ? null : { branch: latest.branch, worktreePath: latest.worktreePath };
}
export function resolvePreviousWorktreeLabel(seed: PreviousWorktreeSeed): string {
return seed.branch ? `Previous worktree (${seed.branch})` : "Previous worktree";
}
export function resolveEffectiveEnvMode(input: {
activeWorktreePath: string | null;
hasServerThread: boolean;
draftThreadEnvMode: EnvMode | undefined;
}): EnvMode {
const { activeWorktreePath, hasServerThread, draftThreadEnvMode } = input;
if (!hasServerThread) {
if (activeWorktreePath) {
return "local";
}
return draftThreadEnvMode === "worktree" ? "worktree" : "local";
}
return activeWorktreePath ? "worktree" : "local";
}
export function resolveDraftEnvModeAfterBranchChange(input: {
nextWorktreePath: string | null;
currentWorktreePath: string | null;
effectiveEnvMode: EnvMode;
}): EnvMode {
const { nextWorktreePath, currentWorktreePath, effectiveEnvMode } = input;
if (nextWorktreePath) {
return "worktree";
}
if (effectiveEnvMode === "worktree" && !currentWorktreePath) {
return "worktree";
}
return "local";
}
export function resolveBranchToolbarValue(input: {
envMode: EnvMode;
activeWorktreePath: string | null;
activeThreadBranch: string | null;
currentGitBranch: string | null;
}): string | null {
const { envMode, activeWorktreePath, activeThreadBranch, currentGitBranch } = input;
if (envMode === "worktree" && !activeWorktreePath) {
return activeThreadBranch ?? currentGitBranch;
}
return currentGitBranch ?? activeThreadBranch;
}
export function resolveBranchTriggerLabel(input: {
activeWorktreePath: string | null;
effectiveEnvMode: EnvMode;
resolvedActiveBranch: string | null;
resolvedActiveBranchIsRemote: boolean | null;
startFromOrigin: boolean;
}): string {
const {
activeWorktreePath,
effectiveEnvMode,
resolvedActiveBranch,
resolvedActiveBranchIsRemote,
startFromOrigin,
} = input;
if (!resolvedActiveBranch) {
return "Select ref";
}
if (effectiveEnvMode === "worktree" && !activeWorktreePath) {
const baseRef =
startFromOrigin && resolvedActiveBranchIsRemote === false
? `origin/${resolvedActiveBranch}`
: resolvedActiveBranch;
return `From ${baseRef}`;
}
return resolvedActiveBranch;
}
export function resolveBranchToolbarPrBranch(input: {
activeThreadBranch: string | null;
resolvedActiveBranch: string | null;
}): string | null {
return input.activeThreadBranch === input.resolvedActiveBranch ? input.activeThreadBranch : null;
}
export function resolveLocalCheckoutBranchMismatch(input: {
effectiveEnvMode: EnvMode;
activeWorktreePath: string | null;
activeThreadBranch: string | null;
currentGitBranch: string | null;
}): { threadBranch: string; currentBranch: string } | null {
const { effectiveEnvMode, activeWorktreePath, activeThreadBranch, currentGitBranch } = input;
if (effectiveEnvMode !== "local" || activeWorktreePath !== null) {
return null;
}
if (!activeThreadBranch || !currentGitBranch || activeThreadBranch === currentGitBranch) {
return null;
}
return { threadBranch: activeThreadBranch, currentBranch: currentGitBranch };
}
export function resolveBranchSelectionTarget(input: {
activeProjectCwd: string;
activeWorktreePath: string | null;
refName: Pick<VcsRef, "isDefault" | "worktreePath">;
}): {
checkoutCwd: string;
nextWorktreePath: string | null;
reuseExistingWorktree: boolean;
} {
const { activeProjectCwd, activeWorktreePath, refName } = input;
if (refName.worktreePath) {
return {
checkoutCwd: refName.worktreePath,
nextWorktreePath: refName.worktreePath === activeProjectCwd ? null : refName.worktreePath,
reuseExistingWorktree: true,
};
}
const nextWorktreePath =
activeWorktreePath !== null && refName.isDefault ? null : activeWorktreePath;
return {
checkoutCwd: nextWorktreePath ?? activeProjectCwd,
nextWorktreePath,
reuseExistingWorktree: false,
};
}
export function shouldIncludeBranchPickerItem(input: {
itemValue: string;
normalizedQuery: string;
createBranchItemValue: string | null;
checkoutPullRequestItemValue: string | null;
}): boolean {
const { itemValue, normalizedQuery, createBranchItemValue, checkoutPullRequestItemValue } = input;
if (normalizedQuery.length === 0) {
return true;
}
if (createBranchItemValue && itemValue === createBranchItemValue) {
return true;
}
if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) {
return true;
}
return itemValue.toLowerCase().includes(normalizedQuery);
}