Confirm risky worktree archive (#994)
This commit is contained in:
+1
-1
@@ -216,7 +216,7 @@ The bespoke pills in `packages/app/src/screens/settings/host-page.tsx:97-116`, `
|
|||||||
- Raw DOM APIs without an `isWeb` guard.
|
- Raw DOM APIs without an `isWeb` guard.
|
||||||
- Spacing values outside the scale. `padding: 20` and `gap: 10` are wrong.
|
- Spacing values outside the scale. `padding: 20` and `gap: 10` are wrong.
|
||||||
- Color changes for disabled state. Opacity only.
|
- Color changes for disabled state. Opacity only.
|
||||||
- Destructive actions without `confirmDialog`. Restart, remove, archive, and any future destructive action are confirmed.
|
- Destructive actions without `confirmDialog`. Restart, remove, and future destructive actions are confirmed. Worktree archive is confirmed only when git runtime reports uncommitted changes or unpushed commits; clean pushed worktrees archive immediately.
|
||||||
- Bespoke status pills. `<StatusBadge>` is the pill primitive.
|
- Bespoke status pills. `<StatusBadge>` is the pill primitive.
|
||||||
- Raw `Modal` for a focused task. `<AdaptiveModalSheet>` is the modal primitive.
|
- Raw `Modal` for a focused task. `<AdaptiveModalSheet>` is the modal primitive.
|
||||||
- Importing `ActivityIndicator` directly. `<LoadingSpinner>` is the loading primitive.
|
- Importing `ActivityIndicator` directly. `<LoadingSpinner>` is the loading primitive.
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ import {
|
|||||||
requireWorkspaceExecutionDirectory,
|
requireWorkspaceExecutionDirectory,
|
||||||
resolveWorkspaceExecutionDirectory,
|
resolveWorkspaceExecutionDirectory,
|
||||||
} from "@/utils/workspace-execution";
|
} from "@/utils/workspace-execution";
|
||||||
|
import { confirmRiskyWorktreeArchive } from "@/git/worktree-archive-warning";
|
||||||
import {
|
import {
|
||||||
archiveWorkspaceOptimistically,
|
archiveWorkspaceOptimistically,
|
||||||
archiveWorkspacesOptimistically,
|
archiveWorkspacesOptimistically,
|
||||||
@@ -1474,92 +1475,93 @@ function WorkspaceRowWithMenu({
|
|||||||
});
|
});
|
||||||
}, [activeWorkspaceSelection, workspace.serverId, workspace.workspaceId]);
|
}, [activeWorkspaceSelection, workspace.serverId, workspace.workspaceId]);
|
||||||
|
|
||||||
const handleArchiveWorktree = useCallback(() => {
|
const archiveWorktreeAfterConfirmation = useCallback(async () => {
|
||||||
if (isArchiving) {
|
if (isArchiving) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async () => {
|
const confirmed = await confirmRiskyWorktreeArchive({
|
||||||
const confirmed = await confirmDialog({
|
worktreeName: workspace.name,
|
||||||
title: "Archive worktree?",
|
isDirty: workspace.archiveHasUncommittedChanges,
|
||||||
message: `Archive "${workspace.name}"?\n\nThe worktree will be removed from disk, terminals will be stopped, and agents inside will be archived.\n\nYour branch is still accessible if you committed.`,
|
aheadOfOrigin: workspace.archiveUnpushedCommitCount,
|
||||||
confirmLabel: "Archive",
|
diffStat: workspace.diffStat,
|
||||||
cancelLabel: "Cancel",
|
});
|
||||||
destructive: true,
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let archiveDirectory: string;
|
||||||
|
try {
|
||||||
|
archiveDirectory = requireWorkspaceExecutionDirectory({
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
|
workspaceDirectory: workspace.workspaceDirectory,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Workspace path not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!archiveDirectory) {
|
||||||
return;
|
toast.error("Workspace path not available");
|
||||||
}
|
return;
|
||||||
let archiveDirectory: string;
|
}
|
||||||
try {
|
|
||||||
archiveDirectory = requireWorkspaceExecutionDirectory({
|
|
||||||
workspaceId: workspace.workspaceId,
|
|
||||||
workspaceDirectory: workspace.workspaceDirectory,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(error instanceof Error ? error.message : "Workspace path not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!archiveDirectory) {
|
redirectAfterArchive();
|
||||||
toast.error("Workspace path not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectAfterArchive();
|
void archiveWorktree({
|
||||||
|
serverId: workspace.serverId,
|
||||||
void archiveWorktree({
|
cwd: archiveDirectory,
|
||||||
serverId: workspace.serverId,
|
worktreePath: archiveDirectory,
|
||||||
cwd: archiveDirectory,
|
}).catch((error) => {
|
||||||
worktreePath: archiveDirectory,
|
const message = error instanceof Error ? error.message : "Failed to archive worktree";
|
||||||
}).catch((error) => {
|
toast.error(message);
|
||||||
const message = error instanceof Error ? error.message : "Failed to archive worktree";
|
});
|
||||||
toast.error(message);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}, [archiveWorktree, isArchiving, redirectAfterArchive, toast, workspace]);
|
}, [archiveWorktree, isArchiving, redirectAfterArchive, toast, workspace]);
|
||||||
|
|
||||||
const handleArchiveWorkspace = useCallback(() => {
|
const handleArchiveWorktree = useCallback(() => {
|
||||||
|
void archiveWorktreeAfterConfirmation();
|
||||||
|
}, [archiveWorktreeAfterConfirmation]);
|
||||||
|
|
||||||
|
const hideWorkspaceAfterConfirmation = useCallback(async () => {
|
||||||
if (isArchivingWorkspace) {
|
if (isArchivingWorkspace) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void (async () => {
|
const confirmed = await confirmDialog({
|
||||||
const confirmed = await confirmDialog({
|
title: "Hide workspace?",
|
||||||
title: "Hide workspace?",
|
message: `Hide "${workspace.name}" from the sidebar?\n\nFiles on disk will not be changed.`,
|
||||||
message: `Hide "${workspace.name}" from the sidebar?\n\nFiles on disk will not be changed.`,
|
confirmLabel: "Hide",
|
||||||
confirmLabel: "Hide",
|
cancelLabel: "Cancel",
|
||||||
cancelLabel: "Cancel",
|
destructive: true,
|
||||||
destructive: true,
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = getHostRuntimeStore().getClient(workspace.serverId);
|
||||||
|
if (!client) {
|
||||||
|
toast.error("Host is not connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsArchivingWorkspace(true);
|
||||||
|
try {
|
||||||
|
await archiveWorkspaceOptimistically({
|
||||||
|
client,
|
||||||
|
workspace,
|
||||||
|
afterHide: redirectAfterArchive,
|
||||||
});
|
});
|
||||||
if (!confirmed) {
|
} catch (error) {
|
||||||
return;
|
toast.error(error instanceof Error ? error.message : "Failed to hide workspace");
|
||||||
}
|
} finally {
|
||||||
|
setIsArchivingWorkspace(false);
|
||||||
const client = getHostRuntimeStore().getClient(workspace.serverId);
|
}
|
||||||
if (!client) {
|
|
||||||
toast.error("Host is not connected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsArchivingWorkspace(true);
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
await archiveWorkspaceOptimistically({
|
|
||||||
client,
|
|
||||||
workspace,
|
|
||||||
afterHide: redirectAfterArchive,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to hide workspace");
|
|
||||||
} finally {
|
|
||||||
setIsArchivingWorkspace(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
})();
|
|
||||||
}, [isArchivingWorkspace, redirectAfterArchive, toast, workspace]);
|
}, [isArchivingWorkspace, redirectAfterArchive, toast, workspace]);
|
||||||
|
|
||||||
|
const handleArchiveWorkspace = useCallback(() => {
|
||||||
|
void hideWorkspaceAfterConfirmation();
|
||||||
|
}, [hideWorkspaceAfterConfirmation]);
|
||||||
|
|
||||||
const handleCopyPath = useCallback(() => {
|
const handleCopyPath = useCallback(() => {
|
||||||
let copyTargetDirectory: string;
|
let copyTargetDirectory: string;
|
||||||
try {
|
try {
|
||||||
@@ -1589,7 +1591,7 @@ function WorkspaceRowWithMenu({
|
|||||||
priority: 0,
|
priority: 0,
|
||||||
handle: () => {
|
handle: () => {
|
||||||
if (isWorktree) {
|
if (isWorktree) {
|
||||||
handleArchiveWorktree();
|
void archiveWorktreeAfterConfirmation();
|
||||||
} else {
|
} else {
|
||||||
handleArchiveWorkspace();
|
handleArchiveWorkspace();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useToast } from "@/contexts/toast-context";
|
|||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { buildWorkspaceArchiveRedirectRoute } from "@/utils/workspace-archive-navigation";
|
import { buildWorkspaceArchiveRedirectRoute } from "@/utils/workspace-archive-navigation";
|
||||||
|
import { confirmRiskyWorktreeArchive } from "@/git/worktree-archive-warning";
|
||||||
|
|
||||||
export type { GitActionId, GitAction, GitActions } from "@/git/policy";
|
export type { GitActionId, GitAction, GitActions } from "@/git/policy";
|
||||||
|
|
||||||
@@ -169,6 +170,11 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
cwd,
|
cwd,
|
||||||
enabled: isGit,
|
enabled: isGit,
|
||||||
});
|
});
|
||||||
|
const baseRefLabel = useMemo(() => formatBaseRefLabel(baseRef), [baseRef]);
|
||||||
|
const branchLabel = resolveBranchLabel({
|
||||||
|
currentBranch: gitStatus?.currentBranch,
|
||||||
|
notGit,
|
||||||
|
});
|
||||||
|
|
||||||
// Ship default persistence
|
// Ship default persistence
|
||||||
const shipDefaultStorageKey = useMemo(() => {
|
const shipDefaultStorageKey = useMemo(() => {
|
||||||
@@ -389,31 +395,59 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
});
|
});
|
||||||
}, [baseRef, cwd, runMergeFromBase, serverId, toast, toastActionError, toastActionSuccess]);
|
}, [baseRef, cwd, runMergeFromBase, serverId, toast, toastActionError, toastActionSuccess]);
|
||||||
|
|
||||||
const handleArchiveWorktree = useCallback(() => {
|
const archiveWorktreeAfterConfirmation = useCallback(async () => {
|
||||||
const worktreePath = status?.cwd;
|
const worktreePath = status?.cwd;
|
||||||
if (!worktreePath) {
|
if (!worktreePath) {
|
||||||
toast.error("Worktree path unavailable");
|
toast.error("Worktree path unavailable");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaces = useSessionStore.getState().sessions[serverId]?.workspaces;
|
const workspaces = useSessionStore.getState().sessions[serverId]?.workspaces;
|
||||||
|
const workspaceList = Array.from(workspaces?.values() ?? []);
|
||||||
|
const workspace = workspaceList.find(
|
||||||
|
(candidate) => candidate.workspaceDirectory === worktreePath,
|
||||||
|
);
|
||||||
|
const confirmed = await confirmRiskyWorktreeArchive({
|
||||||
|
worktreeName: workspace?.name ?? branchLabel,
|
||||||
|
isDirty: gitStatus?.isDirty,
|
||||||
|
aheadOfOrigin: gitStatus?.aheadOfOrigin,
|
||||||
|
diffStat: workspace?.diffStat ?? null,
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const archivedWorkspaceId =
|
const archivedWorkspaceId =
|
||||||
resolveWorkspaceIdByExecutionDirectory({
|
resolveWorkspaceIdByExecutionDirectory({
|
||||||
workspaces: workspaces?.values(),
|
workspaces: workspaceList,
|
||||||
workspaceDirectory: worktreePath,
|
workspaceDirectory: worktreePath,
|
||||||
}) ?? worktreePath;
|
}) ?? worktreePath;
|
||||||
router.replace(
|
router.replace(
|
||||||
buildWorkspaceArchiveRedirectRoute({
|
buildWorkspaceArchiveRedirectRoute({
|
||||||
serverId,
|
serverId,
|
||||||
archivedWorkspaceId,
|
archivedWorkspaceId,
|
||||||
workspaces: workspaces?.values() ?? [],
|
workspaces: workspaceList,
|
||||||
}) as Href,
|
}) as Href,
|
||||||
);
|
);
|
||||||
void runArchiveWorktree({ serverId, cwd, worktreePath }).catch((err) => {
|
void runArchiveWorktree({ serverId, cwd, worktreePath }).catch((err) => {
|
||||||
toastActionError(err, "Failed to archive worktree");
|
toastActionError(err, "Failed to archive worktree");
|
||||||
});
|
});
|
||||||
}, [cwd, runArchiveWorktree, serverId, status, toast, toastActionError]);
|
}, [
|
||||||
|
branchLabel,
|
||||||
|
cwd,
|
||||||
|
gitStatus?.aheadOfOrigin,
|
||||||
|
gitStatus?.isDirty,
|
||||||
|
runArchiveWorktree,
|
||||||
|
serverId,
|
||||||
|
status?.cwd,
|
||||||
|
toast,
|
||||||
|
toastActionError,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleArchiveWorktree = useCallback(() => {
|
||||||
|
void archiveWorktreeAfterConfirmation();
|
||||||
|
}, [archiveWorktreeAfterConfirmation]);
|
||||||
|
|
||||||
const baseRefLabel = useMemo(() => formatBaseRefLabel(baseRef), [baseRef]);
|
|
||||||
const derived = deriveGitActionsState({
|
const derived = deriveGitActionsState({
|
||||||
isGit,
|
isGit,
|
||||||
status,
|
status,
|
||||||
@@ -437,11 +471,6 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
|||||||
shouldPromoteArchive,
|
shouldPromoteArchive,
|
||||||
} = derived;
|
} = derived;
|
||||||
|
|
||||||
const branchLabel = resolveBranchLabel({
|
|
||||||
currentBranch: gitStatus?.currentBranch,
|
|
||||||
notGit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlePrAction = useCallback(() => {
|
const handlePrAction = useCallback(() => {
|
||||||
if (prStatus?.url) {
|
if (prStatus?.url) {
|
||||||
openURLInNewTab(prStatus.url);
|
openURLInNewTab(prStatus.url);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildWorktreeArchiveConfirmationMessage,
|
||||||
|
buildWorktreeArchiveRiskReasons,
|
||||||
|
} from "@/git/worktree-archive-warning";
|
||||||
|
|
||||||
|
describe("worktree archive warning", () => {
|
||||||
|
it("does not require a confirmation for clean and pushed worktrees", () => {
|
||||||
|
expect(
|
||||||
|
buildWorktreeArchiveConfirmationMessage({
|
||||||
|
worktreeName: "feature",
|
||||||
|
isDirty: false,
|
||||||
|
aheadOfOrigin: 0,
|
||||||
|
diffStat: null,
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("explains uncommitted line changes", () => {
|
||||||
|
expect(
|
||||||
|
buildWorktreeArchiveRiskReasons({
|
||||||
|
isDirty: true,
|
||||||
|
aheadOfOrigin: 0,
|
||||||
|
diffStat: { additions: 12, deletions: 1 },
|
||||||
|
}),
|
||||||
|
).toEqual(["Uncommitted changes (12 added lines, 1 deleted line)"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats nonzero diff stats as dirty when dirty state is missing", () => {
|
||||||
|
expect(
|
||||||
|
buildWorktreeArchiveRiskReasons({
|
||||||
|
isDirty: undefined,
|
||||||
|
aheadOfOrigin: 0,
|
||||||
|
diffStat: { additions: 4, deletions: 0 },
|
||||||
|
}),
|
||||||
|
).toEqual(["Uncommitted changes (4 added lines)"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("explains unpushed commits", () => {
|
||||||
|
expect(
|
||||||
|
buildWorktreeArchiveRiskReasons({
|
||||||
|
isDirty: false,
|
||||||
|
aheadOfOrigin: 2,
|
||||||
|
diffStat: null,
|
||||||
|
}),
|
||||||
|
).toEqual(["2 unpushed commits"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes every archive risk in the confirmation copy", () => {
|
||||||
|
expect(
|
||||||
|
buildWorktreeArchiveConfirmationMessage({
|
||||||
|
worktreeName: "risky-feature",
|
||||||
|
isDirty: true,
|
||||||
|
aheadOfOrigin: 1,
|
||||||
|
diffStat: { additions: 1, deletions: 3 },
|
||||||
|
}),
|
||||||
|
).toBe("Uncommitted changes (1 added line, 3 deleted lines)\n1 unpushed commit");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||||
|
|
||||||
|
export interface WorktreeArchiveRisk {
|
||||||
|
isDirty?: boolean | null;
|
||||||
|
aheadOfOrigin?: number | null;
|
||||||
|
diffStat?: { additions: number; deletions: number } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorktreeArchiveConfirmationInput extends WorktreeArchiveRisk {
|
||||||
|
worktreeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluralize(count: number, singular: string, plural = `${singular}s`): string {
|
||||||
|
return count === 1 ? singular : plural;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDiffStat(diffStat: WorktreeArchiveRisk["diffStat"]): string | null {
|
||||||
|
if (!diffStat) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (diffStat.additions > 0) {
|
||||||
|
parts.push(`${diffStat.additions} added ${pluralize(diffStat.additions, "line")}`);
|
||||||
|
}
|
||||||
|
if (diffStat.deletions > 0) {
|
||||||
|
parts.push(`${diffStat.deletions} deleted ${pluralize(diffStat.deletions, "line")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(", ") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWorktreeArchiveRiskReasons(input: WorktreeArchiveRisk): string[] {
|
||||||
|
const reasons: string[] = [];
|
||||||
|
const diffStat = input.diffStat;
|
||||||
|
const hasDiffStatChanges = diffStat ? diffStat.additions > 0 || diffStat.deletions > 0 : false;
|
||||||
|
const hasUncommittedChanges =
|
||||||
|
input.isDirty === true || (input.isDirty == null && hasDiffStatChanges);
|
||||||
|
|
||||||
|
if (hasUncommittedChanges) {
|
||||||
|
const diffStatLabel = formatDiffStat(diffStat);
|
||||||
|
reasons.push(diffStatLabel ? `Uncommitted changes (${diffStatLabel})` : "Uncommitted changes");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((input.aheadOfOrigin ?? 0) > 0) {
|
||||||
|
const aheadOfOrigin = input.aheadOfOrigin ?? 0;
|
||||||
|
reasons.push(`${aheadOfOrigin} unpushed ${pluralize(aheadOfOrigin, "commit")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWorktreeArchiveConfirmationMessage(
|
||||||
|
input: WorktreeArchiveConfirmationInput,
|
||||||
|
): string | null {
|
||||||
|
const reasons = buildWorktreeArchiveRiskReasons(input);
|
||||||
|
if (reasons.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reasons.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmRiskyWorktreeArchive(
|
||||||
|
input: WorktreeArchiveConfirmationInput,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const message = buildWorktreeArchiveConfirmationMessage(input);
|
||||||
|
if (!message) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await confirmDialog({
|
||||||
|
title: `Archive "${input.worktreeName}"?`,
|
||||||
|
message,
|
||||||
|
confirmLabel: "Archive",
|
||||||
|
cancelLabel: "Cancel",
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ export interface SidebarWorkspaceEntry {
|
|||||||
statusBucket: SidebarStateBucket;
|
statusBucket: SidebarStateBucket;
|
||||||
archivingAt: string | null;
|
archivingAt: string | null;
|
||||||
diffStat: { additions: number; deletions: number } | null;
|
diffStat: { additions: number; deletions: number } | null;
|
||||||
|
archiveHasUncommittedChanges: boolean | null;
|
||||||
|
archiveUnpushedCommitCount: number | null;
|
||||||
scripts: WorkspaceDescriptor["scripts"];
|
scripts: WorkspaceDescriptor["scripts"];
|
||||||
hasRunningScripts: boolean;
|
hasRunningScripts: boolean;
|
||||||
}
|
}
|
||||||
@@ -69,6 +71,8 @@ function createStructuralWorkspaceEntry(input: {
|
|||||||
statusBucket: "done",
|
statusBucket: "done",
|
||||||
archivingAt: null,
|
archivingAt: null,
|
||||||
diffStat: null,
|
diffStat: null,
|
||||||
|
archiveHasUncommittedChanges: null,
|
||||||
|
archiveUnpushedCommitCount: null,
|
||||||
scripts: [],
|
scripts: [],
|
||||||
hasRunningScripts: false,
|
hasRunningScripts: false,
|
||||||
};
|
};
|
||||||
@@ -91,6 +95,8 @@ export function createSidebarWorkspaceEntry(input: {
|
|||||||
statusBucket: input.workspace.status,
|
statusBucket: input.workspace.status,
|
||||||
archivingAt: input.workspace.archivingAt,
|
archivingAt: input.workspace.archivingAt,
|
||||||
diffStat: input.workspace.diffStat,
|
diffStat: input.workspace.diffStat,
|
||||||
|
archiveHasUncommittedChanges: input.workspace.gitRuntime?.isDirty ?? null,
|
||||||
|
archiveUnpushedCommitCount: input.workspace.gitRuntime?.aheadOfOrigin ?? null,
|
||||||
scripts: input.workspace.scripts,
|
scripts: input.workspace.scripts,
|
||||||
hasRunningScripts: input.workspace.scripts.some((script) => script.lifecycle === "running"),
|
hasRunningScripts: input.workspace.scripts.some((script) => script.lifecycle === "running"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorks
|
|||||||
name: "paseo",
|
name: "paseo",
|
||||||
statusBucket: "done",
|
statusBucket: "done",
|
||||||
diffStat: null,
|
diffStat: null,
|
||||||
|
archiveHasUncommittedChanges: null,
|
||||||
|
archiveUnpushedCommitCount: null,
|
||||||
scripts: [],
|
scripts: [],
|
||||||
hasRunningScripts: false,
|
hasRunningScripts: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ function workspace(input: {
|
|||||||
statusBucket: "done",
|
statusBucket: "done",
|
||||||
archivingAt: null,
|
archivingAt: null,
|
||||||
diffStat: null,
|
diffStat: null,
|
||||||
|
archiveHasUncommittedChanges: null,
|
||||||
|
archiveUnpushedCommitCount: null,
|
||||||
scripts: [],
|
scripts: [],
|
||||||
hasRunningScripts: false,
|
hasRunningScripts: false,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user