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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user