Key workspace status and ownership by ID, not directory
Multiple workspaces can share one checkout directory. Agent and terminal status, sidebar tabs, and ownership now attribute strictly by workspace ID, so a running or blocked agent in one workspace no longer shows up on its same-directory siblings. A one-time startup migration backfills workspace IDs onto legacy agents, and every agent and terminal is created with one thereafter. Directory lookups remain only for genuinely folder-derived git facts (branch, diff) and the archive-by-path adapter.
This commit is contained in:
@@ -75,7 +75,6 @@ import {
|
||||
type OpenFileDisposition,
|
||||
type WorkspaceFileOpenRequest,
|
||||
} from "@/workspace/file-open";
|
||||
import { resolveWorkspaceIdByDirectory } from "@/utils/workspace-identity";
|
||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
@@ -276,13 +275,9 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
);
|
||||
|
||||
const workspaceRoot = agent.cwd?.trim() || "";
|
||||
const workspaceId = resolveWorkspaceIdByDirectory({
|
||||
workspaces: useSessionStore.getState().sessions[resolvedServerId]?.workspaces?.values(),
|
||||
workspaceDirectory: workspaceRoot,
|
||||
});
|
||||
const { requestDirectoryListing } = useFileExplorerActions({
|
||||
serverId: resolvedServerId,
|
||||
workspaceId: workspaceId ?? undefined,
|
||||
workspaceId: agent.workspaceId,
|
||||
workspaceRoot,
|
||||
});
|
||||
const { isLoadingOlder, hasOlder, loadOlder } = useLoadOlderAgentHistory({
|
||||
@@ -334,10 +329,10 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaceId) {
|
||||
if (agent.workspaceId) {
|
||||
navigateToPreparedWorkspaceTab({
|
||||
serverId: resolvedServerId,
|
||||
workspaceId,
|
||||
workspaceId: agent.workspaceId,
|
||||
target: createWorkspaceFileTabTarget(location),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useEffect, useRef } from "react";
|
||||
import { useLocalSearchParams, usePathname, useRouter, type Href } from "expo-router";
|
||||
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useResolveWorkspaceIdByCwd } from "@/stores/session-store-hooks";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||
import { resolveWorkspaceIdByDirectory } from "@/utils/workspace-identity";
|
||||
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
|
||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||
|
||||
export default function HostAgentReadyRoute() {
|
||||
@@ -28,16 +27,16 @@ function HostAgentReadyRouteContent() {
|
||||
const agentId = typeof params.agentId === "string" ? params.agentId : "";
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const agentCwd = useSessionStore((state) => {
|
||||
const agentWorkspaceId = useSessionStore((state) => {
|
||||
if (!serverId || !agentId) {
|
||||
return null;
|
||||
}
|
||||
return state.sessions[serverId]?.agents?.get(agentId)?.cwd ?? null;
|
||||
return state.sessions[serverId]?.agents?.get(agentId)?.workspaceId ?? null;
|
||||
});
|
||||
const hasHydratedWorkspaces = useSessionStore((state) =>
|
||||
serverId ? (state.sessions[serverId]?.hasHydratedWorkspaces ?? false) : false,
|
||||
);
|
||||
const resolvedWorkspaceId = useResolveWorkspaceIdByCwd(serverId, agentCwd);
|
||||
const resolvedWorkspaceId = normalizeWorkspaceOpaqueId(agentWorkspaceId);
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectedRef.current) {
|
||||
@@ -67,14 +66,14 @@ function HostAgentReadyRouteContent() {
|
||||
if (!serverId || !agentId) {
|
||||
return;
|
||||
}
|
||||
if (agentCwd?.trim() && !hasHydratedWorkspaces) {
|
||||
if (agentWorkspaceId && !hasHydratedWorkspaces) {
|
||||
return;
|
||||
}
|
||||
if (!client || !isConnected) {
|
||||
redirectedRef.current = true;
|
||||
router.replace(buildHostRootRoute(serverId));
|
||||
}
|
||||
}, [agentCwd, agentId, client, hasHydratedWorkspaces, isConnected, router, serverId]);
|
||||
}, [agentWorkspaceId, agentId, client, hasHydratedWorkspaces, isConnected, router, serverId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectedRef.current) {
|
||||
@@ -91,15 +90,7 @@ function HostAgentReadyRouteContent() {
|
||||
if (cancelled || redirectedRef.current) {
|
||||
return;
|
||||
}
|
||||
const cwd = result?.agent?.cwd?.trim();
|
||||
const workspaces = useSessionStore.getState().sessions[serverId]?.workspaces;
|
||||
const workspaceId = resolveWorkspaceIdByDirectory({
|
||||
workspaces: workspaces?.values(),
|
||||
workspaceDirectory: cwd,
|
||||
});
|
||||
if (!workspaceId && !hasHydratedWorkspaces) {
|
||||
return;
|
||||
}
|
||||
const workspaceId = normalizeWorkspaceOpaqueId(result?.agent?.workspaceId);
|
||||
redirectedRef.current = true;
|
||||
if (workspaceId) {
|
||||
navigateToPreparedWorkspaceTab({
|
||||
@@ -124,7 +115,7 @@ function HostAgentReadyRouteContent() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [agentId, client, hasHydratedWorkspaces, isConnected, pathname, router, serverId]);
|
||||
}, [agentId, client, isConnected, pathname, router, serverId]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ function WorkspaceStatusIndicator({
|
||||
|
||||
if (bucket === "needs_input") {
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot}>
|
||||
<View style={styles.workspaceStatusDot} testID="workspace-status-indicator-needs_input">
|
||||
<ThemedCircleAlert size={14} uniProps={amberColorMapping} />
|
||||
</View>
|
||||
);
|
||||
@@ -222,7 +222,7 @@ function WorkspaceStatusIndicator({
|
||||
? EMPHASIZED_STATUS_DOT_OFFSET
|
||||
: DEFAULT_STATUS_DOT_OFFSET;
|
||||
return (
|
||||
<View style={styles.workspaceStatusDot}>
|
||||
<View style={styles.workspaceStatusDot} testID={`workspace-status-indicator-${bucket}`}>
|
||||
<KindIcon size={14} uniProps={foregroundMutedColorMapping} />
|
||||
{dotColorStyle ? (
|
||||
<StatusDotOverlay
|
||||
|
||||
@@ -28,11 +28,10 @@ function workspace(input?: Partial<WorkspaceDescriptor>): WorkspaceDescriptor {
|
||||
}
|
||||
|
||||
describe("workspace archive pending suppression", () => {
|
||||
it("tracks a locally pending workspace archive by id and directory", () => {
|
||||
it("tracks a locally pending workspace archive by id", () => {
|
||||
markWorkspaceArchivePending({
|
||||
serverId: "server-1",
|
||||
workspaceId: "/repo/worktree",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -41,12 +40,6 @@ describe("workspace archive pending suppression", () => {
|
||||
workspaceId: "/repo/worktree",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isWorkspaceArchivePending({
|
||||
serverId: "server-1",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldSuppressWorkspaceForLocalArchive({
|
||||
serverId: "server-1",
|
||||
@@ -68,19 +61,34 @@ describe("workspace archive pending suppression", () => {
|
||||
markWorkspaceArchivePending({
|
||||
serverId: "server-1",
|
||||
workspaceId: "/repo/worktree",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
});
|
||||
|
||||
expect(
|
||||
shouldSuppressWorkspaceForLocalArchive({
|
||||
serverId: "server-1",
|
||||
workspace: workspace({ workspaceDirectory: "/repo/worktree" }),
|
||||
workspace: workspace({ archivingAt: null }),
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
clearWorkspaceArchivePending({ serverId: "server-1", workspaceId: "/repo/worktree" });
|
||||
});
|
||||
|
||||
it("does not suppress a same-cwd sibling whose id is not the one archived", () => {
|
||||
markWorkspaceArchivePending({
|
||||
serverId: "server-1",
|
||||
workspaceId: "/repo/worktree",
|
||||
});
|
||||
|
||||
expect(
|
||||
shouldSuppressWorkspaceForLocalArchive({
|
||||
serverId: "server-1",
|
||||
workspace: workspace({ id: "sibling", workspaceDirectory: "/repo/worktree" }),
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
clearWorkspaceArchivePending({ serverId: "server-1", workspaceId: "/repo/worktree" });
|
||||
});
|
||||
|
||||
it("allows upserts when this client did not start the archive", () => {
|
||||
expect(
|
||||
shouldSuppressWorkspaceForLocalArchive({
|
||||
@@ -94,7 +102,6 @@ describe("workspace archive pending suppression", () => {
|
||||
markWorkspaceArchivePending({
|
||||
serverId: "server-1",
|
||||
workspaceId: "/repo/worktree",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
});
|
||||
|
||||
expect(
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { normalizeWorkspaceOpaqueId, normalizeWorkspacePath } from "@/utils/workspace-identity";
|
||||
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
|
||||
|
||||
interface PendingWorkspaceArchive {
|
||||
workspaceId: string;
|
||||
workspaceDirectory: string | null;
|
||||
}
|
||||
|
||||
const pendingWorkspaceArchivesByServer = new Map<string, Map<string, PendingWorkspaceArchive>>();
|
||||
const pendingWorkspaceArchivesByServer = new Map<string, Set<string>>();
|
||||
|
||||
function pendingArchiveKey(input: { serverId: string; workspaceId: string }): string {
|
||||
return `${input.serverId.trim()}::${input.workspaceId.trim()}`;
|
||||
@@ -15,7 +10,6 @@ function pendingArchiveKey(input: { serverId: string; workspaceId: string }): st
|
||||
export function markWorkspaceArchivePending(input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
workspaceDirectory?: string | null;
|
||||
}): void {
|
||||
const serverId = input.serverId.trim();
|
||||
const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
|
||||
@@ -23,11 +17,8 @@ export function markWorkspaceArchivePending(input: {
|
||||
return;
|
||||
}
|
||||
|
||||
const archives = pendingWorkspaceArchivesByServer.get(serverId) ?? new Map();
|
||||
archives.set(pendingArchiveKey({ serverId, workspaceId }), {
|
||||
workspaceId,
|
||||
workspaceDirectory: normalizeWorkspacePath(input.workspaceDirectory),
|
||||
});
|
||||
const archives = pendingWorkspaceArchivesByServer.get(serverId) ?? new Set<string>();
|
||||
archives.add(pendingArchiveKey({ serverId, workspaceId }));
|
||||
pendingWorkspaceArchivesByServer.set(serverId, archives);
|
||||
}
|
||||
|
||||
@@ -54,34 +45,15 @@ export function clearWorkspaceArchivePending(input: {
|
||||
export function isWorkspaceArchivePending(input: {
|
||||
serverId: string;
|
||||
workspaceId?: string | null;
|
||||
workspaceDirectory?: string | null;
|
||||
}): boolean {
|
||||
const serverId = input.serverId.trim();
|
||||
if (!serverId) {
|
||||
const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
|
||||
if (!serverId || !workspaceId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const archives = pendingWorkspaceArchivesByServer.get(serverId);
|
||||
if (!archives) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
|
||||
if (workspaceId && archives.has(pendingArchiveKey({ serverId, workspaceId }))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const workspaceDirectory = normalizeWorkspacePath(input.workspaceDirectory);
|
||||
if (!workspaceDirectory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const archive of archives.values()) {
|
||||
if (archive.workspaceDirectory === workspaceDirectory) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return archives?.has(pendingArchiveKey({ serverId, workspaceId })) ?? false;
|
||||
}
|
||||
|
||||
export function shouldSuppressWorkspaceForLocalArchive(input: {
|
||||
@@ -91,6 +63,5 @@ export function shouldSuppressWorkspaceForLocalArchive(input: {
|
||||
return isWorkspaceArchivePending({
|
||||
serverId: input.serverId,
|
||||
workspaceId: input.workspace.id,
|
||||
workspaceDirectory: input.workspace.workspaceDirectory,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ describe("checkout-git-actions-store", () => {
|
||||
|
||||
const archive = useCheckoutGitActionsStore
|
||||
.getState()
|
||||
.archiveWorktree({ serverId, cwd, worktreePath: cwd });
|
||||
.archiveWorktree({ serverId, cwd, worktreePath: cwd, workspaceId });
|
||||
|
||||
expect(useSessionStore.getState().sessions[serverId]?.workspaces.has(workspaceId)).toBe(false);
|
||||
expect(useSessionStore.getState().sessions[serverId]?.workspaces.has(cwd)).toBe(false);
|
||||
@@ -371,7 +371,9 @@ describe("checkout-git-actions-store", () => {
|
||||
appQueryClient.setQueryData(["sidebarPaseoWorktreeList", serverId, "/tmp"], listSnapshot);
|
||||
|
||||
await expect(
|
||||
useCheckoutGitActionsStore.getState().archiveWorktree({ serverId, cwd, worktreePath: cwd }),
|
||||
useCheckoutGitActionsStore
|
||||
.getState()
|
||||
.archiveWorktree({ serverId, cwd, worktreePath: cwd, workspaceId }),
|
||||
).rejects.toThrow("archive failed");
|
||||
|
||||
expect(useSessionStore.getState().sessions[serverId]?.workspaces.get(workspaceId)).toEqual(
|
||||
|
||||
@@ -13,10 +13,7 @@ import {
|
||||
clearWorkspaceArchivePending,
|
||||
markWorkspaceArchivePending,
|
||||
} from "@/contexts/session-workspace-upserts";
|
||||
import {
|
||||
resolveWorkspaceIdByDirectory,
|
||||
resolveWorkspaceMapKeyByIdentity,
|
||||
} from "@/utils/workspace-identity";
|
||||
import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity";
|
||||
import { invalidateCheckoutGitQueriesForClient } from "@/git/query-keys";
|
||||
import { i18n } from "@/i18n/i18next";
|
||||
|
||||
@@ -155,15 +152,11 @@ function isWorktreeListQuery(input: { queryKey: QueryKey; serverId: string }): b
|
||||
|
||||
function snapshotWorktreeArchiveState(input: {
|
||||
serverId: string;
|
||||
worktreePath: string;
|
||||
workspaceId: string | undefined;
|
||||
}): WorktreeArchiveSnapshot {
|
||||
const workspaces = useSessionStore.getState().sessions[input.serverId]?.workspaces;
|
||||
const workspaceId = resolveWorkspaceIdByDirectory({
|
||||
workspaces: workspaces?.values(),
|
||||
workspaceDirectory: input.worktreePath,
|
||||
});
|
||||
const workspaceKey = workspaceId
|
||||
? resolveWorkspaceMapKeyByIdentity({ workspaces, workspaceId })
|
||||
const workspaceKey = input.workspaceId
|
||||
? resolveWorkspaceMapKeyByIdentity({ workspaces, workspaceId: input.workspaceId })
|
||||
: null;
|
||||
return {
|
||||
workspace: workspaceKey ? (workspaces?.get(workspaceKey) ?? null) : null,
|
||||
@@ -506,16 +499,16 @@ export const useCheckoutGitActionsStore = create<CheckoutGitActionsStoreState>()
|
||||
actionId: "archive-worktree",
|
||||
run: async () => {
|
||||
const client = resolveClient(serverId);
|
||||
const snapshot = snapshotWorktreeArchiveState({ serverId, worktreePath });
|
||||
const snapshot = snapshotWorktreeArchiveState({ serverId, workspaceId });
|
||||
// The server archive is keyed by worktreePath and must always run. The
|
||||
// optimistic client-side updates are keyed by workspace id, so they only
|
||||
// apply when the workspace is resolved in the local store.
|
||||
// apply when the caller passes the workspace id and it resolves in the
|
||||
// local store.
|
||||
const workspace = snapshot.workspace;
|
||||
if (workspace) {
|
||||
markWorkspaceArchivePending({
|
||||
serverId,
|
||||
workspaceId: workspace.id,
|
||||
workspaceDirectory: workspace.workspaceDirectory,
|
||||
});
|
||||
removeWorktreeFromSessionStore({ serverId, workspaceId: workspace.id });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useCallback, useEffect, useMemo, type ReactElement } from "react";
|
||||
import { router, type Href } from "expo-router";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { type CheckoutGitActionStatus, useCheckoutGitActionsStore } from "@/git/actions-store";
|
||||
@@ -15,9 +14,8 @@ import type { CheckoutPrMergeMethod } from "@getpaseo/protocol/messages";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { resolveWorkspaceIdByDirectory } from "@/utils/workspace-identity";
|
||||
import { buildWorkspaceArchiveRedirectRoute } from "@/utils/workspace-archive-navigation";
|
||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect";
|
||||
import {
|
||||
confirmRiskyWorktreeArchive,
|
||||
type WorktreeArchiveWarningLabels,
|
||||
@@ -61,6 +59,9 @@ function formatBaseRefLabel(baseRef: string | undefined, fallbackLabel: string):
|
||||
function isLastWorktreeReference(workspaces: WorkspaceDescriptor[], worktreePath: string): boolean {
|
||||
let references = 0;
|
||||
for (const candidate of workspaces) {
|
||||
// Git-fact: counting how many workspaces still reference the worktree
|
||||
// directory on disk, not attributing ownership. Same-cwd siblings keep the
|
||||
// directory alive, so the disk-deletion offer must compare directories.
|
||||
if (candidate.workspaceDirectory === worktreePath) {
|
||||
references += 1;
|
||||
}
|
||||
@@ -227,6 +228,7 @@ interface UseGitActionsResult {
|
||||
export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): UseGitActionsResult {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||
const [postShipArchiveSuggested, setPostShipArchiveSuggested] = useState(false);
|
||||
const [shipDefault, setShipDefault] = useState<"merge" | "pr">("pr");
|
||||
|
||||
@@ -533,27 +535,25 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
||||
|
||||
const runArchiveWorktreeRecord = useCallback(
|
||||
(worktreePath: string, deleteWorktreeFromDisk: boolean) => {
|
||||
const workspaces = useSessionStore.getState().sessions[serverId]?.workspaces;
|
||||
const workspaceList = Array.from(workspaces?.values() ?? []);
|
||||
const archivedWorkspaceId = resolveWorkspaceIdByDirectory({
|
||||
workspaces: workspaceList,
|
||||
workspaceDirectory: worktreePath,
|
||||
});
|
||||
const redirectRoute = archivedWorkspaceId
|
||||
? buildWorkspaceArchiveRedirectRoute({
|
||||
serverId,
|
||||
archivedWorkspaceId,
|
||||
workspaces: workspaceList,
|
||||
})
|
||||
: buildHostRootRoute(serverId);
|
||||
router.replace(redirectRoute as Href);
|
||||
// These git actions only ever target the workspace the screen is showing,
|
||||
// so the redirect is sourced from the active workspace selection (the
|
||||
// screen's own route context), not from any cwd→workspace inference here.
|
||||
if (activeWorkspaceSelection) {
|
||||
redirectIfArchivingActiveWorkspace({
|
||||
serverId,
|
||||
workspaceId: activeWorkspaceSelection.workspaceId,
|
||||
activeWorkspaceSelection,
|
||||
});
|
||||
}
|
||||
// The server archive is keyed by worktreePath; the daemon owns the
|
||||
// directory→workspace resolution for archive-by-path.
|
||||
void runArchiveWorktree({ serverId, cwd, worktreePath, deleteWorktreeFromDisk }).catch(
|
||||
(err) => {
|
||||
toastActionError(err, t("workspace.git.actions.toasts.failedArchive"));
|
||||
},
|
||||
);
|
||||
},
|
||||
[cwd, runArchiveWorktree, serverId, t, toastActionError],
|
||||
[activeWorkspaceSelection, cwd, runArchiveWorktree, serverId, t, toastActionError],
|
||||
);
|
||||
|
||||
const worktreeDeletePrompt = useWorktreeDeletePrompt(runArchiveWorktreeRecord);
|
||||
@@ -567,9 +567,12 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use
|
||||
|
||||
const workspaces = useSessionStore.getState().sessions[serverId]?.workspaces;
|
||||
const workspaceList = Array.from(workspaces?.values() ?? []);
|
||||
const workspace = workspaceList.find(
|
||||
(candidate) => candidate.workspaceDirectory === worktreePath,
|
||||
);
|
||||
// Git-fact lookup: find the workspace backing this worktree directory to show
|
||||
// its name and diff stat in the confirmation. This reads display data by
|
||||
// directory, the same as the disk-deletion reference counting, and never
|
||||
// derives an id used to key the archive (the archive runs by path).
|
||||
const workspace =
|
||||
workspaceList.find((candidate) => candidate.workspaceDirectory === worktreePath) ?? null;
|
||||
const confirmed = await confirmRiskyWorktreeArchive(
|
||||
{
|
||||
worktreeName: workspace?.name ?? branchLabel,
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface AgentScreenAgent {
|
||||
id: string;
|
||||
status: "initializing" | "idle" | "running" | "error" | "closed";
|
||||
cwd: string;
|
||||
workspaceId?: string;
|
||||
capabilities?: AgentCapabilityFlags;
|
||||
lastError?: string | null;
|
||||
projectPlacement?: {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useCreateFlowStore, type PendingCreateAttempt } from "@/stores/create-f
|
||||
import { useSessionStore, type Agent, type WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { selectWorkspace, workspaceEqualityFns } from "@/stores/session-store-hooks/selectors";
|
||||
import { deriveSidebarStateBucket } from "@/utils/sidebar-agent-state";
|
||||
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
|
||||
import { selectPrHintFromStatus } from "@/git/use-pr-status-query";
|
||||
import { useHostProjects } from "@/projects/host-projects";
|
||||
import { fetchAllWorkspaceDescriptors } from "@/projects/workspace-fetching";
|
||||
@@ -132,15 +131,10 @@ function getRootAgentWorkspaceActivity(input: {
|
||||
workspace: WorkspaceDescriptor;
|
||||
agents: Map<string, Agent> | undefined;
|
||||
}): WorkspaceAgentActivity | null {
|
||||
const workspaceDirectory = normalizeWorkspacePath(input.workspace.workspaceDirectory);
|
||||
if (!workspaceDirectory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let latest: WorkspaceAgentActivity | null = null;
|
||||
for (const agent of input.agents?.values() ?? []) {
|
||||
if (agent.archivedAt || agent.parentAgentId) continue;
|
||||
if (normalizeWorkspacePath(agent.cwd) !== workspaceDirectory) continue;
|
||||
if (agent.workspaceId !== input.workspace.id) continue;
|
||||
const status = deriveSidebarStateBucket({
|
||||
status: agent.status,
|
||||
pendingPermissionCount: agent.pendingPermissions.length,
|
||||
|
||||
@@ -254,6 +254,21 @@ describe("translation resources", () => {
|
||||
expect(en.openProject.tiles.addProject.title).toBe("Add a project");
|
||||
});
|
||||
|
||||
it("resolves the worktree delete prompt keys used by the sidebar prompt", () => {
|
||||
const strings = flattenStrings(en);
|
||||
|
||||
expect(strings["sidebar.workspace.confirmations.deleteWorktreePrompt.title"]).toBe(
|
||||
"Archive workspace",
|
||||
);
|
||||
expect(strings["sidebar.workspace.confirmations.deleteWorktreePrompt.message"]).toBe(
|
||||
"Also remove the worktree from disk?",
|
||||
);
|
||||
expect(strings["sidebar.workspace.confirmations.deleteWorktreePrompt.keep"]).toBe(
|
||||
"Keep on disk",
|
||||
);
|
||||
expect(strings["sidebar.workspace.confirmations.deleteWorktreePrompt.delete"]).toBe("Delete");
|
||||
});
|
||||
|
||||
it("includes provider selector and pairing keys for the Batch 4D migration", () => {
|
||||
expect(en.modelSelector.title).toBe("Select provider");
|
||||
expect(en.modelSelector.favorites).toBe("Favorites");
|
||||
|
||||
@@ -811,12 +811,12 @@ export const ar: TranslationResources = {
|
||||
'إخفاء "{{workspaceName}}" من الشريط الجانبي؟\n\n لن يتم تغيير الملفات الموجودة على القرص.',
|
||||
hideConfirm: "يخفي",
|
||||
cancel: "يلغي",
|
||||
},
|
||||
deleteWorktreePrompt: {
|
||||
title: "أرشفة مساحة العمل",
|
||||
message: "هل تريد أيضًا إزالة شجرة العمل من القرص؟",
|
||||
keep: "الاحتفاظ على القرص",
|
||||
delete: "حذف",
|
||||
deleteWorktreePrompt: {
|
||||
title: "أرشفة مساحة العمل",
|
||||
message: "هل تريد أيضًا إزالة شجرة العمل من القرص؟",
|
||||
keep: "الاحتفاظ على القرص",
|
||||
delete: "حذف",
|
||||
},
|
||||
},
|
||||
rename: {
|
||||
title: "إعادة تسمية مساحة العمل",
|
||||
|
||||
@@ -817,12 +817,12 @@ export const en = {
|
||||
'Hide "{{workspaceName}}" from the sidebar?\n\nFiles on disk will not be changed.',
|
||||
hideConfirm: "Hide",
|
||||
cancel: "Cancel",
|
||||
},
|
||||
deleteWorktreePrompt: {
|
||||
title: "Archive workspace",
|
||||
message: "Also remove the worktree from disk?",
|
||||
keep: "Keep on disk",
|
||||
delete: "Delete",
|
||||
deleteWorktreePrompt: {
|
||||
title: "Archive workspace",
|
||||
message: "Also remove the worktree from disk?",
|
||||
keep: "Keep on disk",
|
||||
delete: "Delete",
|
||||
},
|
||||
},
|
||||
rename: {
|
||||
title: "Rename workspace",
|
||||
|
||||
@@ -837,12 +837,12 @@ export const es: TranslationResources = {
|
||||
'¿Ocultar "{{workspaceName}}" de la barra lateral?\n\nLos archivos en el disco no se cambiarán.',
|
||||
hideConfirm: "Esconder",
|
||||
cancel: "Cancelar",
|
||||
},
|
||||
deleteWorktreePrompt: {
|
||||
title: "Archivar espacio de trabajo",
|
||||
message: "¿También eliminar el worktree del disco?",
|
||||
keep: "Conservar en disco",
|
||||
delete: "Eliminar",
|
||||
deleteWorktreePrompt: {
|
||||
title: "Archivar espacio de trabajo",
|
||||
message: "¿También eliminar el worktree del disco?",
|
||||
keep: "Conservar en disco",
|
||||
delete: "Eliminar",
|
||||
},
|
||||
},
|
||||
rename: {
|
||||
title: "Cambiar nombre del espacio de trabajo",
|
||||
|
||||
@@ -836,12 +836,12 @@ export const fr: TranslationResources = {
|
||||
"Masquer «{{workspaceName}}» dans la barre latérale?\n\nLes fichiers sur le disque ne seront pas modifiés.",
|
||||
hideConfirm: "Cacher",
|
||||
cancel: "Annuler",
|
||||
},
|
||||
deleteWorktreePrompt: {
|
||||
title: "Archiver l'espace de travail",
|
||||
message: "Supprimer aussi le worktree du disque?",
|
||||
keep: "Conserver sur le disque",
|
||||
delete: "Supprimer",
|
||||
deleteWorktreePrompt: {
|
||||
title: "Archiver l'espace de travail",
|
||||
message: "Supprimer aussi le worktree du disque?",
|
||||
keep: "Conserver sur le disque",
|
||||
delete: "Supprimer",
|
||||
},
|
||||
},
|
||||
rename: {
|
||||
title: "Renommer l'espace de travail",
|
||||
|
||||
@@ -829,12 +829,12 @@ export const ru: TranslationResources = {
|
||||
"Скрыть «{{workspaceName}}» на боковой панели?\n\n Файлы на диске не будут изменены.",
|
||||
hideConfirm: "Скрывать",
|
||||
cancel: "Отмена",
|
||||
},
|
||||
deleteWorktreePrompt: {
|
||||
title: "Архивировать рабочее пространство",
|
||||
message: "Также удалить рабочее дерево с диска?",
|
||||
keep: "Оставить на диске",
|
||||
delete: "Удалить",
|
||||
deleteWorktreePrompt: {
|
||||
title: "Архивировать рабочее пространство",
|
||||
message: "Также удалить рабочее дерево с диска?",
|
||||
keep: "Оставить на диске",
|
||||
delete: "Удалить",
|
||||
},
|
||||
},
|
||||
rename: {
|
||||
title: "Переименовать рабочую область",
|
||||
|
||||
@@ -802,12 +802,12 @@ export const zhCN: TranslationResources = {
|
||||
hideMessage: "从侧边栏隐藏「{{workspaceName}}」?\n\n磁盘上的文件不会被更改。",
|
||||
hideConfirm: "隐藏",
|
||||
cancel: "取消",
|
||||
},
|
||||
deleteWorktreePrompt: {
|
||||
title: "归档 workspace",
|
||||
message: "同时从磁盘删除 worktree?",
|
||||
keep: "保留在磁盘上",
|
||||
delete: "删除",
|
||||
deleteWorktreePrompt: {
|
||||
title: "归档 workspace",
|
||||
message: "同时从磁盘删除 worktree?",
|
||||
keep: "保留在磁盘上",
|
||||
delete: "删除",
|
||||
},
|
||||
},
|
||||
rename: {
|
||||
title: "重命名 workspace",
|
||||
|
||||
@@ -93,6 +93,7 @@ interface ChatAgentStateShape {
|
||||
id: string | null;
|
||||
status: Agent["status"] | null;
|
||||
cwd: string | null;
|
||||
workspaceId?: string;
|
||||
capabilities?: Agent["capabilities"];
|
||||
lastError?: Agent["lastError"] | null;
|
||||
}
|
||||
@@ -136,6 +137,7 @@ function selectChatAgentState(
|
||||
id: agent.id,
|
||||
status: agent.status,
|
||||
cwd: agent.cwd,
|
||||
workspaceId: agent.workspaceId,
|
||||
capabilities: agent.capabilities,
|
||||
lastError: agent.lastError ?? null,
|
||||
archivedAt: agent.archivedAt ?? null,
|
||||
@@ -156,6 +158,7 @@ function buildChatAgentFromState(
|
||||
id: state.id,
|
||||
status: state.status,
|
||||
cwd: state.cwd,
|
||||
workspaceId: state.workspaceId,
|
||||
capabilities: state.capabilities,
|
||||
lastError: state.lastError ?? null,
|
||||
projectPlacement,
|
||||
@@ -545,15 +548,13 @@ function AgentPanelBody({
|
||||
);
|
||||
const agentState = useSessionStore(
|
||||
useShallow((state) => {
|
||||
const session = state.sessions[serverId];
|
||||
const agent = agentId
|
||||
? (session?.agents?.get(agentId) ?? session?.agentDetails?.get(agentId) ?? null)
|
||||
: null;
|
||||
const agent = resolveChatAgentFromSession(state, serverId, agentId);
|
||||
return {
|
||||
serverId: agent?.serverId ?? null,
|
||||
id: agent?.id ?? null,
|
||||
status: agent?.status ?? null,
|
||||
cwd: agent?.cwd ?? null,
|
||||
workspaceId: agent?.workspaceId,
|
||||
lastError: agent?.lastError ?? null,
|
||||
archivedAt: agent?.archivedAt ?? null,
|
||||
};
|
||||
@@ -646,6 +647,7 @@ function AgentPanelBody({
|
||||
id: agentState.id,
|
||||
status: agentState.status,
|
||||
cwd: agentState.cwd,
|
||||
workspaceId: agentState.workspaceId,
|
||||
lastError: agentState.lastError ?? null,
|
||||
projectPlacement,
|
||||
}
|
||||
|
||||
@@ -75,6 +75,21 @@ function resolveCheckoutRequest(
|
||||
};
|
||||
}
|
||||
|
||||
function buildFirstAgentContext(input: {
|
||||
prompt: string;
|
||||
attachments: AgentAttachment[];
|
||||
}): { prompt?: string; attachments?: AgentAttachment[] } | undefined {
|
||||
const trimmedPrompt = input.prompt.trim();
|
||||
if (!trimmedPrompt && input.attachments.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
|
||||
attachments: input.attachments,
|
||||
};
|
||||
}
|
||||
|
||||
interface NewWorkspaceScreenProps {
|
||||
serverId: string;
|
||||
sourceDirectory?: string;
|
||||
@@ -779,6 +794,8 @@ async function createMultiplicityWorkspace(input: {
|
||||
selectedItem: PickerItem | null;
|
||||
currentBranch: string | null;
|
||||
withInitialAgent: boolean;
|
||||
prompt: string;
|
||||
attachments: AgentAttachment[];
|
||||
mergeWorkspaces: (
|
||||
serverId: string,
|
||||
workspaces: ReturnType<typeof normalizeWorkspaceDescriptor>[],
|
||||
@@ -790,6 +807,10 @@ async function createMultiplicityWorkspace(input: {
|
||||
const baseBranch = isWorktree
|
||||
? (resolveCheckoutRequest(input.selectedItem, input.currentBranch)?.refName ?? undefined)
|
||||
: undefined;
|
||||
const firstAgentContext = buildFirstAgentContext({
|
||||
prompt: input.prompt,
|
||||
attachments: input.attachments,
|
||||
});
|
||||
const payload = await input.client.createWorkspace({
|
||||
source: isWorktree
|
||||
? {
|
||||
@@ -804,6 +825,7 @@ async function createMultiplicityWorkspace(input: {
|
||||
path: input.project.iconWorkingDir,
|
||||
projectId: input.project.projectKey,
|
||||
},
|
||||
...(firstAgentContext ? { firstAgentContext } : {}),
|
||||
});
|
||||
if (payload.error || !payload.workspace) {
|
||||
throw new Error(payload.error ?? input.createFailedMessage);
|
||||
@@ -1292,21 +1314,13 @@ export function NewWorkspaceScreen({
|
||||
throw new Error("Choose a project");
|
||||
}
|
||||
const checkoutRequest = resolveCheckoutRequest(selectedItem, currentBranch);
|
||||
const trimmedPrompt = input.prompt.trim();
|
||||
const hasFirstAgentContext = trimmedPrompt.length > 0 || input.attachments.length > 0;
|
||||
const firstAgentContext = buildFirstAgentContext(input);
|
||||
|
||||
return {
|
||||
cwd: selectedProject.iconWorkingDir,
|
||||
projectId: selectedProject.projectKey,
|
||||
worktreeSlug: createNameId(),
|
||||
...(hasFirstAgentContext
|
||||
? {
|
||||
firstAgentContext: {
|
||||
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
|
||||
...(input.attachments.length > 0 ? { attachments: input.attachments } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(firstAgentContext ? { firstAgentContext } : {}),
|
||||
...checkoutRequest,
|
||||
};
|
||||
},
|
||||
@@ -1334,6 +1348,8 @@ export function NewWorkspaceScreen({
|
||||
selectedItem,
|
||||
currentBranch,
|
||||
withInitialAgent: input.withInitialAgent,
|
||||
prompt: input.prompt,
|
||||
attachments: input.attachments,
|
||||
mergeWorkspaces,
|
||||
serverId,
|
||||
createFailedMessage: t("newWorkspace.errors.createWorktreeFailed"),
|
||||
|
||||
@@ -186,17 +186,18 @@ export function useWorkspaceTerminals(input: UseWorkspaceTerminalsInput) {
|
||||
const paneWorkspaceId = normalizedWorkspaceId || undefined;
|
||||
|
||||
const unsubscribeChanged = client.on("terminals_changed", (message) => {
|
||||
// The terminal subscription is keyed by cwd at the protocol level, so this
|
||||
// gate only routes the event to the pane watching that cwd; it is not an
|
||||
// ownership decision.
|
||||
if (message.payload.cwd !== workspaceDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Two workspaces can share a cwd, so the push can carry terminals from a
|
||||
// sibling workspace. Keep only the ones whose workspaceId matches this
|
||||
// pane; terminals without a workspaceId predate Model B and belong to
|
||||
// whichever pane is watching the cwd.
|
||||
// sibling workspace. A terminal belongs to a workspace only by its
|
||||
// workspaceId, so keep only the ones whose workspaceId matches this pane.
|
||||
const matchingTerminals = message.payload.terminals.filter(
|
||||
(terminal) =>
|
||||
terminal.workspaceId === undefined || terminal.workspaceId === paneWorkspaceId,
|
||||
(terminal) => terminal.workspaceId === paneWorkspaceId,
|
||||
);
|
||||
|
||||
queryClient.setQueryData<ListTerminalsPayload>(queryKey, (current) => ({
|
||||
|
||||
@@ -1791,7 +1791,6 @@ function WorkspaceScreenContent({
|
||||
sessionAgents: state.sessions[normalizedServerId]?.agents,
|
||||
agentDetails: state.sessions[normalizedServerId]?.agentDetails,
|
||||
workspaceId: normalizedWorkspaceId,
|
||||
workspaceDirectory,
|
||||
}),
|
||||
workspaceAgentVisibilityEqual,
|
||||
);
|
||||
|
||||
@@ -77,6 +77,7 @@ describe("workspace navigation", () => {
|
||||
const agent = {
|
||||
id: "agent-1",
|
||||
cwd: "/repo/workspace-a",
|
||||
workspaceId: "workspace-a",
|
||||
requiresAttention: true,
|
||||
attentionReason: "permission",
|
||||
} as unknown as Agent;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
} from "@/utils/host-routes";
|
||||
import {
|
||||
resolveWorkspaceIdByDirectory,
|
||||
normalizeWorkspaceOpaqueId,
|
||||
resolveWorkspaceMapKeyByIdentity,
|
||||
} from "@/utils/workspace-identity";
|
||||
import type { ActiveWorkspaceSelection } from "@/stores/last-workspace-selection";
|
||||
@@ -76,11 +76,7 @@ export function navigateToWorkspace(
|
||||
});
|
||||
const workspaceAgents = resolvedWorkspaceId
|
||||
? Array.from(deps.getSessionAgents(serverId)).filter(
|
||||
(agent) =>
|
||||
resolveWorkspaceIdByDirectory({
|
||||
workspaces: workspaces?.values(),
|
||||
workspaceDirectory: agent.cwd,
|
||||
}) === resolvedWorkspaceId,
|
||||
(agent) => normalizeWorkspaceOpaqueId(agent.workspaceId) === resolvedWorkspaceId,
|
||||
)
|
||||
: [];
|
||||
const attentionAgentId = pickAttentionAgent(workspaceAgents);
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
selectHasWorkspaces,
|
||||
selectProjectOrder,
|
||||
selectRecommendedProjectPaths,
|
||||
selectResolveWorkspaceIdByCwd,
|
||||
selectWorkspace,
|
||||
selectWorkspaceDirectory,
|
||||
selectWorkspaceExists,
|
||||
@@ -135,17 +134,6 @@ export function useHasWorkspaces(serverId: string | null): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function useResolveWorkspaceIdByCwd(
|
||||
serverId: string | null,
|
||||
cwd: string | null | undefined,
|
||||
): string | null {
|
||||
return useStoreWithEqualityFn(
|
||||
useSessionStore,
|
||||
(state) => selectResolveWorkspaceIdByCwd(state, serverId, cwd),
|
||||
workspaceEqualityFns.identity,
|
||||
);
|
||||
}
|
||||
|
||||
export function useWorkspaceStatusesForBadges(): DesktopBadgeWorkspaceStatus[] {
|
||||
return useStoreWithEqualityFn(
|
||||
useSessionStore,
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
selectHasWorkspaces,
|
||||
selectProjectOrder,
|
||||
selectRecommendedProjectPaths,
|
||||
selectResolveWorkspaceIdByCwd,
|
||||
selectWorkspace,
|
||||
selectWorkspaceDirectory,
|
||||
selectWorkspaceFields,
|
||||
@@ -385,33 +384,6 @@ describe("selectHasWorkspaces", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectResolveWorkspaceIdByCwd", () => {
|
||||
it("resolves by cwd and stays stable under unrelated updates", () => {
|
||||
const workspaceA = createWorkspace({
|
||||
id: "workspace-a",
|
||||
workspaceDirectory: "/repo/a",
|
||||
});
|
||||
const workspaceB = createWorkspace({
|
||||
id: "workspace-b",
|
||||
workspaceDirectory: "/repo/b",
|
||||
});
|
||||
initializeWorkspaces([workspaceA, workspaceB]);
|
||||
|
||||
const tracked = trackSelector(
|
||||
useSessionStore,
|
||||
(state) => selectResolveWorkspaceIdByCwd(state, SERVER_ID, "/repo/a"),
|
||||
workspaceEqualityFns.identity,
|
||||
);
|
||||
const before = tracked.current;
|
||||
expect(before).toBe("workspace-a");
|
||||
|
||||
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspaceB, status: "running" }]);
|
||||
expect(tracked.current).toBe(before);
|
||||
|
||||
tracked.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectWorkspaceStatusesForBadges", () => {
|
||||
it("tracks status changes without changing for no-ops or unrelated descriptor updates", () => {
|
||||
const workspaceA = createWorkspace({ id: "workspace-a", status: "done" });
|
||||
|
||||
@@ -5,10 +5,7 @@ import {
|
||||
type WorkspaceStructureProject,
|
||||
} from "@/projects/workspace-structure";
|
||||
import type { DesktopBadgeWorkspaceStatus } from "@/utils/desktop-badge-state";
|
||||
import {
|
||||
resolveWorkspaceIdByDirectory,
|
||||
resolveWorkspaceMapKeyByIdentity,
|
||||
} from "@/utils/workspace-identity";
|
||||
import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity";
|
||||
import type { EmptyProjectDescriptor, WorkspaceDescriptor } from "../session-store";
|
||||
|
||||
export type { DesktopBadgeWorkspaceStatus } from "@/utils/desktop-badge-state";
|
||||
@@ -250,21 +247,6 @@ export function selectHasWorkspaces(state: SessionsSnapshot, serverId: string |
|
||||
return (state.sessions[serverId]?.workspaces?.size ?? 0) > 0;
|
||||
}
|
||||
|
||||
export function selectResolveWorkspaceIdByCwd(
|
||||
state: SessionsSnapshot,
|
||||
serverId: string | null,
|
||||
cwd: string | null | undefined,
|
||||
): string | null {
|
||||
if (!serverId || !cwd) {
|
||||
return null;
|
||||
}
|
||||
const workspaces = state.sessions[serverId]?.workspaces;
|
||||
return resolveWorkspaceIdByDirectory({
|
||||
workspaces: workspaces?.values(),
|
||||
workspaceDirectory: cwd,
|
||||
});
|
||||
}
|
||||
|
||||
export function selectWorkspaceStatusesForBadges(
|
||||
state: SessionsSnapshot,
|
||||
): DesktopBadgeWorkspaceStatus[] {
|
||||
|
||||
@@ -56,6 +56,7 @@ const AGENT_DEFAULTS: Agent = {
|
||||
lastError: null,
|
||||
title: "Agent",
|
||||
cwd: WORKSPACE_DIRECTORY,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
model: null,
|
||||
features: undefined,
|
||||
thinkingOptionId: undefined,
|
||||
@@ -91,7 +92,7 @@ function deriveVisibilityFromSession(): WorkspaceAgentVisibility {
|
||||
const sessionAgents = useSessionStore.getState().sessions[SERVER_ID]?.agents ?? new Map();
|
||||
return deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
workspaceDirectory: WORKSPACE_DIRECTORY,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ export function navigateToAgent(input: NavigateToAgentInput): string {
|
||||
const session = useSessionStore.getState().sessions[serverId];
|
||||
const agent = session?.agents.get(agentId) ?? session?.agentDetails.get(agentId);
|
||||
return {
|
||||
workspaces: session?.workspaces.values(),
|
||||
agentCwd: agent?.cwd,
|
||||
agentWorkspaceId: agent?.workspaceId,
|
||||
};
|
||||
},
|
||||
navigateToHostAgent: (route) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import {
|
||||
resolveNavigateToAgent,
|
||||
type AgentNavTarget,
|
||||
@@ -11,24 +10,6 @@ const SERVER_ID = "server-1";
|
||||
const WORKSPACE_ID = "workspace-1";
|
||||
const AGENT_ID = "agent-1";
|
||||
|
||||
function createWorkspace(): WorkspaceDescriptor {
|
||||
return {
|
||||
id: WORKSPACE_ID,
|
||||
projectId: "project-1",
|
||||
projectDisplayName: "Project",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
projectKind: "git",
|
||||
workspaceKind: "local_checkout",
|
||||
name: "worktree",
|
||||
status: "done",
|
||||
archivingAt: null,
|
||||
statusEnteredAt: null,
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
};
|
||||
}
|
||||
|
||||
interface RecordedHostNav {
|
||||
route: string;
|
||||
}
|
||||
@@ -59,10 +40,9 @@ function createFakeNavigators(target: AgentNavTarget): {
|
||||
}
|
||||
|
||||
describe("resolveNavigateToAgent", () => {
|
||||
it("opens the resolved workspace tab when the agent's cwd matches a workspace", () => {
|
||||
it("opens the workspace tab carried by the agent's workspaceId", () => {
|
||||
const { deps, hostNavigations, tabNavigations } = createFakeNavigators({
|
||||
workspaces: [createWorkspace()],
|
||||
agentCwd: "/repo/worktree",
|
||||
agentWorkspaceId: WORKSPACE_ID,
|
||||
});
|
||||
|
||||
const route = resolveNavigateToAgent(
|
||||
@@ -83,10 +63,9 @@ describe("resolveNavigateToAgent", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back to the host agent route when the workspace is unknown", () => {
|
||||
it("falls back to the host agent route when the agent has no workspaceId", () => {
|
||||
const { deps, hostNavigations, tabNavigations } = createFakeNavigators({
|
||||
workspaces: [],
|
||||
agentCwd: null,
|
||||
agentWorkspaceId: null,
|
||||
});
|
||||
|
||||
const route = resolveNavigateToAgent({ serverId: SERVER_ID, agentId: "missing-agent" }, deps);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
||||
import { resolveWorkspaceIdByDirectory } from "@/utils/workspace-identity";
|
||||
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
|
||||
import type { NavigateToPreparedWorkspaceTabInput } from "@/utils/prepare-workspace-tab";
|
||||
|
||||
export interface NavigateToAgentInput {
|
||||
@@ -11,8 +10,7 @@ export interface NavigateToAgentInput {
|
||||
}
|
||||
|
||||
export interface AgentNavTarget {
|
||||
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
||||
agentCwd: string | null | undefined;
|
||||
agentWorkspaceId: string | null | undefined;
|
||||
}
|
||||
|
||||
export interface NavigateToAgentDeps {
|
||||
@@ -25,14 +23,11 @@ export function resolveNavigateToAgent(
|
||||
input: NavigateToAgentInput,
|
||||
deps: NavigateToAgentDeps,
|
||||
): string {
|
||||
const { workspaces, agentCwd } = deps.readAgentNavTarget({
|
||||
const { agentWorkspaceId } = deps.readAgentNavTarget({
|
||||
serverId: input.serverId,
|
||||
agentId: input.agentId,
|
||||
});
|
||||
const workspaceId = resolveWorkspaceIdByDirectory({
|
||||
workspaces,
|
||||
workspaceDirectory: agentCwd,
|
||||
});
|
||||
const workspaceId = normalizeWorkspaceOpaqueId(agentWorkspaceId);
|
||||
|
||||
if (!workspaceId) {
|
||||
const route = buildHostAgentDetailRoute(input.serverId, input.agentId);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import {
|
||||
resolveWorkspaceIdByDirectory,
|
||||
resolveWorkspaceMapKeyByIdentity,
|
||||
resolveWorkspaceRouteId,
|
||||
} from "./workspace-identity";
|
||||
import { resolveWorkspaceMapKeyByIdentity, resolveWorkspaceRouteId } from "./workspace-identity";
|
||||
|
||||
function createWorkspace(
|
||||
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
|
||||
@@ -38,42 +34,6 @@ describe("resolveWorkspaceRouteId", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveWorkspaceIdByDirectory", () => {
|
||||
it("matches workspace directories", () => {
|
||||
const workspaces = [
|
||||
createWorkspace({
|
||||
id: "workspace-1",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
resolveWorkspaceIdByDirectory({
|
||||
workspaces,
|
||||
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||
}),
|
||||
).toBe("workspace-1");
|
||||
});
|
||||
|
||||
it("does not match project root metadata", () => {
|
||||
const workspaces = [
|
||||
createWorkspace({
|
||||
id: "workspace-1",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
resolveWorkspaceIdByDirectory({
|
||||
workspaces,
|
||||
workspaceDirectory: "/repo",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveWorkspaceMapKeyByIdentity", () => {
|
||||
it("returns the existing map key when the identity already matches a key", () => {
|
||||
const workspaces = new Map<string, WorkspaceDescriptor>([
|
||||
|
||||
@@ -31,26 +31,6 @@ export function resolveWorkspaceRouteId(input: {
|
||||
return normalizeWorkspaceOpaqueId(input.routeWorkspaceId);
|
||||
}
|
||||
|
||||
// Single approved cwd→workspaceId inference site.
|
||||
// Do not add cwd-to-id inference elsewhere; prefer agent.workspaceId when the daemon provides it.
|
||||
export function resolveWorkspaceIdByDirectory(input: {
|
||||
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
||||
workspaceDirectory: string | null | undefined;
|
||||
}): string | null {
|
||||
const normalizedWorkspaceDirectory = normalizeWorkspacePath(input.workspaceDirectory);
|
||||
if (!normalizedWorkspaceDirectory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const workspace of input.workspaces ?? []) {
|
||||
if (normalizeWorkspacePath(workspace.workspaceDirectory) === normalizedWorkspaceDirectory) {
|
||||
return workspace.id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveWorkspaceMapKeyByIdentity(input: {
|
||||
workspaces: Map<string, WorkspaceDescriptor> | null | undefined;
|
||||
workspaceId: string | null | undefined;
|
||||
|
||||
@@ -57,16 +57,19 @@ function makeAgent(input: {
|
||||
};
|
||||
}
|
||||
|
||||
const WORKSPACE_ID = "ws-1";
|
||||
|
||||
describe("workspace agent visibility", () => {
|
||||
it("keeps subagents active and known while excluding them from auto-open", () => {
|
||||
const workspaceDirectory = "/repo/worktree";
|
||||
const parent = makeAgent({
|
||||
id: "parent-agent",
|
||||
cwd: workspaceDirectory,
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
const child = makeAgent({
|
||||
id: "child-agent",
|
||||
cwd: workspaceDirectory,
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
parentAgentId: "parent-agent",
|
||||
});
|
||||
|
||||
@@ -75,7 +78,7 @@ describe("workspace agent visibility", () => {
|
||||
[parent.id, parent],
|
||||
[child.id, child],
|
||||
]),
|
||||
workspaceDirectory,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set(["parent-agent", "child-agent"]));
|
||||
@@ -84,17 +87,17 @@ describe("workspace agent visibility", () => {
|
||||
});
|
||||
|
||||
it("keeps archived subagents known but excludes them from active and auto-open", () => {
|
||||
const workspaceDirectory = "/repo/worktree";
|
||||
const archivedChild = makeAgent({
|
||||
id: "archived-child",
|
||||
cwd: workspaceDirectory,
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
parentAgentId: "parent-agent",
|
||||
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||
});
|
||||
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents: new Map<string, Agent>([[archivedChild.id, archivedChild]]),
|
||||
workspaceDirectory,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set<string>());
|
||||
@@ -103,15 +106,16 @@ describe("workspace agent visibility", () => {
|
||||
});
|
||||
|
||||
it("excludes a child from auto-open even when its snapshot arrives before the parent", () => {
|
||||
const workspaceDirectory = "/repo/worktree";
|
||||
const child = makeAgent({
|
||||
id: "child-agent",
|
||||
cwd: workspaceDirectory,
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
parentAgentId: "parent-agent",
|
||||
});
|
||||
const parent = makeAgent({
|
||||
id: "parent-agent",
|
||||
cwd: workspaceDirectory,
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
@@ -119,7 +123,7 @@ describe("workspace agent visibility", () => {
|
||||
[child.id, child],
|
||||
[parent.id, parent],
|
||||
]),
|
||||
workspaceDirectory,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set(["child-agent", "parent-agent"]));
|
||||
@@ -128,21 +132,23 @@ describe("workspace agent visibility", () => {
|
||||
});
|
||||
|
||||
it("keeps archived agents out of activeAgentIds but present in knownAgentIds", () => {
|
||||
const workspaceDirectory = "/repo/worktree";
|
||||
const visible = makeAgent({
|
||||
id: "visible-agent",
|
||||
cwd: workspaceDirectory,
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
createdAt: new Date("2026-03-04T00:00:00.000Z"),
|
||||
});
|
||||
const archived = makeAgent({
|
||||
id: "archived-agent",
|
||||
cwd: workspaceDirectory,
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||
createdAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||
});
|
||||
const otherWorkspace = makeAgent({
|
||||
id: "other-workspace-agent",
|
||||
cwd: "/repo/other",
|
||||
workspaceId: "ws-other",
|
||||
});
|
||||
|
||||
const sessionAgents = new Map<string, Agent>([
|
||||
@@ -153,7 +159,7 @@ describe("workspace agent visibility", () => {
|
||||
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
workspaceDirectory,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set(["visible-agent"]));
|
||||
@@ -164,18 +170,22 @@ describe("workspace agent visibility", () => {
|
||||
});
|
||||
|
||||
it("treats lazy historical details as known without making them active", () => {
|
||||
const workspaceDirectory = "/repo/worktree";
|
||||
const active = makeAgent({ id: "active-agent", cwd: workspaceDirectory });
|
||||
const active = makeAgent({
|
||||
id: "active-agent",
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
const historicalDetail = makeAgent({
|
||||
id: "historical-agent",
|
||||
cwd: workspaceDirectory,
|
||||
cwd: "/repo/worktree",
|
||||
workspaceId: WORKSPACE_ID,
|
||||
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||
});
|
||||
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents: new Map([[active.id, active]]),
|
||||
agentDetails: new Map([[historicalDetail.id, historicalDetail]]),
|
||||
workspaceDirectory,
|
||||
workspaceId: WORKSPACE_ID,
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set(["active-agent"]));
|
||||
@@ -226,49 +236,6 @@ describe("workspace agent visibility", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("matches workspace agents when cwd and route workspace differ only by trailing slash", () => {
|
||||
const sessionAgents = new Map<string, Agent>([
|
||||
[
|
||||
"slash-agent",
|
||||
makeAgent({
|
||||
id: "slash-agent",
|
||||
cwd: "/Users/moboudra/dev/paseo/.dev/paseo-home/worktrees/1luy0po7/normal-squid/",
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
workspaceDirectory:
|
||||
"/Users/moboudra/dev/paseo/.dev/paseo-home/worktrees/1luy0po7/normal-squid",
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set(["slash-agent"]));
|
||||
expect(result.autoOpenAgentIds).toEqual(new Set(["slash-agent"]));
|
||||
expect(result.knownAgentIds.has("slash-agent")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches workspace agents using the workspace directory even when the route uses a numeric workspace id", () => {
|
||||
const sessionAgents = new Map<string, Agent>([
|
||||
[
|
||||
"recent-agent",
|
||||
makeAgent({
|
||||
id: "recent-agent",
|
||||
cwd: "/tmp/workspace-lifecycle-main",
|
||||
}),
|
||||
],
|
||||
]);
|
||||
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
workspaceDirectory: "/tmp/workspace-lifecycle-main",
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set(["recent-agent"]));
|
||||
expect(result.autoOpenAgentIds).toEqual(new Set(["recent-agent"]));
|
||||
expect(result.knownAgentIds).toEqual(new Set(["recent-agent"]));
|
||||
});
|
||||
|
||||
it("matches agents by workspaceId regardless of cwd", () => {
|
||||
const sessionAgents = new Map<string, Agent>([
|
||||
[
|
||||
@@ -284,7 +251,6 @@ describe("workspace agent visibility", () => {
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
workspaceId: "ws-1",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set(["stamped-agent"]));
|
||||
@@ -306,26 +272,24 @@ describe("workspace agent visibility", () => {
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
workspaceId: "ws-1",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set<string>());
|
||||
expect(result.knownAgentIds).toEqual(new Set<string>());
|
||||
});
|
||||
|
||||
it("falls back to cwd matching for legacy agents without a workspaceId", () => {
|
||||
it("excludes agents without a workspaceId", () => {
|
||||
const sessionAgents = new Map<string, Agent>([
|
||||
["legacy-agent", makeAgent({ id: "legacy-agent", cwd: "/repo/worktree" })],
|
||||
["ownerless-agent", makeAgent({ id: "ownerless-agent", cwd: "/repo/worktree" })],
|
||||
]);
|
||||
|
||||
const result = deriveWorkspaceAgentVisibility({
|
||||
sessionAgents,
|
||||
workspaceId: "ws-1",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
});
|
||||
|
||||
expect(result.activeAgentIds).toEqual(new Set(["legacy-agent"]));
|
||||
expect(result.knownAgentIds).toEqual(new Set(["legacy-agent"]));
|
||||
expect(result.activeAgentIds).toEqual(new Set<string>());
|
||||
expect(result.knownAgentIds).toEqual(new Set<string>());
|
||||
});
|
||||
|
||||
it("builds the tab reconciliation snapshot without callers unpacking agent visibility", () => {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { Agent } from "@/stores/session-store";
|
||||
import type { WorkspaceTabSnapshot } from "@/stores/workspace-layout-actions";
|
||||
import { shouldAutoOpenAgentTab } from "@/subagents/policies";
|
||||
import { normalizeWorkspaceOpaqueId, normalizeWorkspacePath } from "@/utils/workspace-identity";
|
||||
|
||||
function normalizeWorkspaceDirectory(value: string | null | undefined): string {
|
||||
return normalizeWorkspacePath(value) ?? "";
|
||||
}
|
||||
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
|
||||
|
||||
export interface WorkspaceAgentVisibility {
|
||||
activeAgentIds: Set<string>;
|
||||
@@ -13,31 +9,18 @@ export interface WorkspaceAgentVisibility {
|
||||
knownAgentIds: Set<string>;
|
||||
}
|
||||
|
||||
function agentBelongsToWorkspace(input: {
|
||||
agent: Agent;
|
||||
workspaceId: string | null;
|
||||
normalizedWorkspaceDirectory: string;
|
||||
}): boolean {
|
||||
const agentWorkspaceId = normalizeWorkspaceOpaqueId(input.agent.workspaceId);
|
||||
if (agentWorkspaceId) {
|
||||
return input.workspaceId !== null && agentWorkspaceId === input.workspaceId;
|
||||
}
|
||||
// COMPAT(workspaceOwnership): legacy agents predate workspaceId stamping. Fall
|
||||
// back to the single approved cwd→id inference by comparing the agent's cwd to
|
||||
// this workspace's directory. Drop when the daemon floor always stamps workspaceId.
|
||||
return normalizeWorkspaceDirectory(input.agent.cwd) === input.normalizedWorkspaceDirectory;
|
||||
function agentBelongsToWorkspace(agent: Agent, workspaceId: string): boolean {
|
||||
return normalizeWorkspaceOpaqueId(agent.workspaceId) === workspaceId;
|
||||
}
|
||||
|
||||
export function deriveWorkspaceAgentVisibility(input: {
|
||||
sessionAgents: Map<string, Agent> | undefined;
|
||||
agentDetails?: Map<string, Agent> | undefined;
|
||||
workspaceId?: string | null | undefined;
|
||||
workspaceDirectory: string | null | undefined;
|
||||
workspaceId: string | null | undefined;
|
||||
}): WorkspaceAgentVisibility {
|
||||
const { sessionAgents, agentDetails, workspaceDirectory } = input;
|
||||
const { sessionAgents, agentDetails } = input;
|
||||
const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
|
||||
const normalizedWorkspaceDirectory = normalizeWorkspaceDirectory(workspaceDirectory);
|
||||
if ((!sessionAgents && !agentDetails) || (!workspaceId && !normalizedWorkspaceDirectory)) {
|
||||
if ((!sessionAgents && !agentDetails) || !workspaceId) {
|
||||
return {
|
||||
activeAgentIds: new Set<string>(),
|
||||
autoOpenAgentIds: new Set<string>(),
|
||||
@@ -49,7 +32,7 @@ export function deriveWorkspaceAgentVisibility(input: {
|
||||
const autoOpenAgentIds = new Set<string>();
|
||||
const knownAgentIds = new Set<string>();
|
||||
for (const agent of sessionAgents?.values() ?? []) {
|
||||
if (!agentBelongsToWorkspace({ agent, workspaceId, normalizedWorkspaceDirectory })) {
|
||||
if (!agentBelongsToWorkspace(agent, workspaceId)) {
|
||||
continue;
|
||||
}
|
||||
knownAgentIds.add(agent.id);
|
||||
@@ -61,7 +44,7 @@ export function deriveWorkspaceAgentVisibility(input: {
|
||||
}
|
||||
}
|
||||
for (const agent of agentDetails?.values() ?? []) {
|
||||
if (!agentBelongsToWorkspace({ agent, workspaceId, normalizedWorkspaceDirectory })) {
|
||||
if (!agentBelongsToWorkspace(agent, workspaceId)) {
|
||||
continue;
|
||||
}
|
||||
knownAgentIds.add(agent.id);
|
||||
|
||||
@@ -32,6 +32,9 @@ export function useIsLastWorktreeReference(workspace: SidebarWorkspaceEntry): bo
|
||||
if (candidate.id === workspace.workspaceId) {
|
||||
continue;
|
||||
}
|
||||
// Git-fact: comparing directories to detect a sibling worktree reference on
|
||||
// disk, not attributing ownership. The disk-deletion decision is about the
|
||||
// backing directory, which same-cwd siblings genuinely share.
|
||||
if (normalizeWorkspacePath(candidate.workspaceDirectory) === directory) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ function target(input?: Partial<WorkspaceArchiveTarget>): WorkspaceArchiveTarget
|
||||
return {
|
||||
serverId: SERVER_ID,
|
||||
workspaceId: base.id,
|
||||
workspaceDirectory: base.workspaceDirectory,
|
||||
...input,
|
||||
};
|
||||
}
|
||||
@@ -107,7 +106,6 @@ describe("archiveWorkspaceOptimistically", () => {
|
||||
isWorkspaceArchivePending({
|
||||
serverId: SERVER_ID,
|
||||
workspaceId: archived.id,
|
||||
workspaceDirectory: archived.workspaceDirectory,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
@@ -178,10 +176,7 @@ describe("archiveWorkspacesOptimistically", () => {
|
||||
|
||||
const failures = await archiveWorkspacesOptimistically({
|
||||
client,
|
||||
workspaces: [
|
||||
target({ workspaceId: first.id, workspaceDirectory: first.workspaceDirectory }),
|
||||
target({ workspaceId: second.id, workspaceDirectory: second.workspaceDirectory }),
|
||||
],
|
||||
workspaces: [target({ workspaceId: first.id }), target({ workspaceId: second.id })],
|
||||
});
|
||||
|
||||
expect(failures).toHaveLength(1);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity";
|
||||
export interface WorkspaceArchiveTarget {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
workspaceDirectory?: string | null;
|
||||
}
|
||||
|
||||
interface WorkspaceArchiveClient {
|
||||
@@ -46,7 +45,6 @@ function hideWorkspaceOptimistically(
|
||||
markWorkspaceArchivePending({
|
||||
serverId: workspace.serverId,
|
||||
workspaceId: workspace.workspaceId,
|
||||
workspaceDirectory: workspace.workspaceDirectory,
|
||||
});
|
||||
useSessionStore.getState().removeWorkspace(workspace.serverId, workspace.workspaceId);
|
||||
return { workspace: snapshot };
|
||||
|
||||
Reference in New Issue
Block a user