refactor: transition to opaque workspace identifiers
This commit is contained in:
@@ -1280,7 +1280,7 @@ function WorkspaceRowWithMenu({
|
|||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const payload = await client.archiveWorkspace(Number(workspace.workspaceId));
|
const payload = await client.archiveWorkspace(workspace.workspaceId);
|
||||||
if (payload.error) {
|
if (payload.error) {
|
||||||
throw new Error(payload.error);
|
throw new Error(payload.error);
|
||||||
}
|
}
|
||||||
@@ -1438,7 +1438,7 @@ function NonGitProjectRowWithMenuContent({
|
|||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const payload = await client.archiveWorkspace(Number(workspace.workspaceId));
|
const payload = await client.archiveWorkspace(workspace.workspaceId);
|
||||||
if (payload.error) {
|
if (payload.error) {
|
||||||
throw new Error(payload.error);
|
throw new Error(payload.error);
|
||||||
}
|
}
|
||||||
@@ -1792,7 +1792,7 @@ function ProjectBlock({
|
|||||||
|
|
||||||
void Promise.allSettled(
|
void Promise.allSettled(
|
||||||
project.workspaces.map(async (ws) => {
|
project.workspaces.map(async (ws) => {
|
||||||
const payload = await client.archiveWorkspace(Number(ws.workspaceId));
|
const payload = await client.archiveWorkspace(ws.workspaceId);
|
||||||
if (payload.error) {
|
if (payload.error) {
|
||||||
throw new Error(payload.error);
|
throw new Error(payload.error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { patchWorkspaceScripts } from "./session-workspace-scripts";
|
|||||||
|
|
||||||
function workspace(input: {
|
function workspace(input: {
|
||||||
id: string;
|
id: string;
|
||||||
|
workspaceDirectory?: string;
|
||||||
scripts?: WorkspaceDescriptor["scripts"];
|
scripts?: WorkspaceDescriptor["scripts"];
|
||||||
}): WorkspaceDescriptor {
|
}): WorkspaceDescriptor {
|
||||||
return {
|
return {
|
||||||
@@ -12,7 +13,7 @@ function workspace(input: {
|
|||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
projectDisplayName: "Project 1",
|
projectDisplayName: "Project 1",
|
||||||
projectRootPath: "/repo",
|
projectRootPath: "/repo",
|
||||||
workspaceDirectory: input.id,
|
workspaceDirectory: input.workspaceDirectory ?? "/repo/main",
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
workspaceKind: "checkout",
|
workspaceKind: "checkout",
|
||||||
name: "main",
|
name: "main",
|
||||||
@@ -35,58 +36,54 @@ const runningScript: WorkspaceScriptPayload = {
|
|||||||
|
|
||||||
describe("patchWorkspaceScripts", () => {
|
describe("patchWorkspaceScripts", () => {
|
||||||
it("patches only the matching workspace scripts", () => {
|
it("patches only the matching workspace scripts", () => {
|
||||||
const other = workspace({ id: "/repo/other", scripts: [] });
|
const other = workspace({ id: "ws-other", workspaceDirectory: "/repo/other", scripts: [] });
|
||||||
const current = new Map<string, WorkspaceDescriptor>([
|
const current = new Map<string, WorkspaceDescriptor>([
|
||||||
["/repo/main", workspace({ id: "/repo/main", scripts: [] })],
|
["ws-main", workspace({ id: "ws-main", workspaceDirectory: "/repo/main", scripts: [] })],
|
||||||
[other.id, other],
|
[other.id, other],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const next = patchWorkspaceScripts(current, {
|
const next = patchWorkspaceScripts(current, {
|
||||||
workspaceId: "/repo/main",
|
workspaceId: "ws-main",
|
||||||
scripts: [runningScript],
|
scripts: [runningScript],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(next).not.toBe(current);
|
expect(next).not.toBe(current);
|
||||||
expect(next.get("/repo/main")?.scripts).toEqual([runningScript]);
|
expect(next.get("ws-main")?.scripts).toEqual([runningScript]);
|
||||||
expect(next.get("/repo/other")).toBe(other);
|
expect(next.get("ws-other")).toBe(other);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("patches the matching workspace when the update uses workspace directory identity", () => {
|
it("patches the matching workspace when the map key differs from the workspace id", () => {
|
||||||
const current = new Map<string, WorkspaceDescriptor>([
|
const current = new Map<string, WorkspaceDescriptor>([
|
||||||
[
|
[
|
||||||
"42",
|
"workspace-record-42",
|
||||||
workspace({
|
workspace({
|
||||||
id: "42",
|
id: "ws-main",
|
||||||
|
workspaceDirectory: "C:\\repo\\main\\",
|
||||||
scripts: [],
|
scripts: [],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
current.set("42", {
|
|
||||||
...current.get("42")!,
|
|
||||||
workspaceDirectory: "C:\\repo\\main\\",
|
|
||||||
});
|
|
||||||
|
|
||||||
const next = patchWorkspaceScripts(current, {
|
const next = patchWorkspaceScripts(current, {
|
||||||
workspaceId: "C:/repo/main",
|
workspaceId: "ws-main",
|
||||||
scripts: [runningScript],
|
scripts: [runningScript],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(next).not.toBe(current);
|
expect(next).not.toBe(current);
|
||||||
expect(next.get("42")?.scripts).toEqual([runningScript]);
|
expect(next.get("workspace-record-42")?.scripts).toEqual([runningScript]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores updates for unknown workspaces", () => {
|
it("ignores updates for unknown workspaces", () => {
|
||||||
const current = new Map<string, WorkspaceDescriptor>([
|
const current = new Map<string, WorkspaceDescriptor>([
|
||||||
["/repo/main", workspace({ id: "/repo/main", scripts: [] })],
|
["ws-main", workspace({ id: "ws-main", workspaceDirectory: "/repo/main", scripts: [] })],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const next = patchWorkspaceScripts(current, {
|
const next = patchWorkspaceScripts(current, {
|
||||||
workspaceId: "/repo/missing",
|
workspaceId: "ws-missing",
|
||||||
scripts: [runningScript],
|
scripts: [runningScript],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(next).toBe(current);
|
expect(next).toBe(current);
|
||||||
expect(next.get("/repo/main")?.scripts).toEqual([]);
|
expect(next.get("ws-main")?.scripts).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function patchWorkspaceScripts(
|
|||||||
): Map<string, WorkspaceDescriptor> {
|
): Map<string, WorkspaceDescriptor> {
|
||||||
const workspaceKey = resolveWorkspaceMapKeyByIdentity({
|
const workspaceKey = resolveWorkspaceMapKeyByIdentity({
|
||||||
workspaces,
|
workspaces,
|
||||||
workspaceIdentity: update.workspaceId,
|
workspaceId: update.workspaceId,
|
||||||
});
|
});
|
||||||
if (!workspaceKey) {
|
if (!workspaceKey) {
|
||||||
return workspaces;
|
return workspaces;
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ function compareWorkspaceBaseline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return left.workspaceId.localeCompare(right.workspaceId, undefined, {
|
return left.workspaceId.localeCompare(right.workspaceId, undefined, {
|
||||||
numeric: true,
|
|
||||||
sensitivity: "base",
|
sensitivity: "base",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,15 +54,15 @@ function makeAgent(input: {
|
|||||||
|
|
||||||
describe("workspace agent visibility", () => {
|
describe("workspace agent visibility", () => {
|
||||||
it("keeps archived agents out of activeAgentIds but present in knownAgentIds", () => {
|
it("keeps archived agents out of activeAgentIds but present in knownAgentIds", () => {
|
||||||
const workspaceId = "/repo/worktree";
|
const workspaceDirectory = "/repo/worktree";
|
||||||
const visible = makeAgent({
|
const visible = makeAgent({
|
||||||
id: "visible-agent",
|
id: "visible-agent",
|
||||||
cwd: workspaceId,
|
cwd: workspaceDirectory,
|
||||||
createdAt: new Date("2026-03-04T00:00:00.000Z"),
|
createdAt: new Date("2026-03-04T00:00:00.000Z"),
|
||||||
});
|
});
|
||||||
const archived = makeAgent({
|
const archived = makeAgent({
|
||||||
id: "archived-agent",
|
id: "archived-agent",
|
||||||
cwd: workspaceId,
|
cwd: workspaceDirectory,
|
||||||
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
|
archivedAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||||
createdAt: new Date("2026-03-04T00:01:00.000Z"),
|
createdAt: new Date("2026-03-04T00:01:00.000Z"),
|
||||||
});
|
});
|
||||||
@@ -79,7 +79,7 @@ describe("workspace agent visibility", () => {
|
|||||||
|
|
||||||
const result = deriveWorkspaceAgentVisibility({
|
const result = deriveWorkspaceAgentVisibility({
|
||||||
sessionAgents,
|
sessionAgents,
|
||||||
workspaceDirectory: workspaceId,
|
workspaceDirectory,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.activeAgentIds).toEqual(new Set(["visible-agent"]));
|
expect(result.activeAgentIds).toEqual(new Set(["visible-agent"]));
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Agent } from "@/stores/session-store";
|
import type { Agent } from "@/stores/session-store";
|
||||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
|
||||||
|
|
||||||
function normalizeWorkspaceId(value: string | null | undefined): string {
|
function normalizeWorkspaceId(value: string | null | undefined): string {
|
||||||
return normalizeWorkspaceIdentity(value) ?? "";
|
return normalizeWorkspacePath(value) ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceAgentVisibility {
|
export interface WorkspaceAgentVisibility {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import type {
|
|||||||
AgentSnapshotPayload,
|
AgentSnapshotPayload,
|
||||||
WorkspaceDescriptorPayload,
|
WorkspaceDescriptorPayload,
|
||||||
} from "@server/shared/messages";
|
} from "@server/shared/messages";
|
||||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
import { normalizeWorkspaceOpaqueId } from "@/utils/workspace-identity";
|
||||||
import {
|
import {
|
||||||
createAgentLastActivityCoalescer,
|
createAgentLastActivityCoalescer,
|
||||||
type AgentLastActivityCommitter,
|
type AgentLastActivityCommitter,
|
||||||
@@ -130,7 +130,7 @@ export function normalizeWorkspaceDescriptor(
|
|||||||
payload: WorkspaceDescriptorPayload,
|
payload: WorkspaceDescriptorPayload,
|
||||||
): WorkspaceDescriptor {
|
): WorkspaceDescriptor {
|
||||||
return {
|
return {
|
||||||
id: normalizeWorkspaceIdentity(String(payload.id)) ?? String(payload.id),
|
id: normalizeWorkspaceOpaqueId(String(payload.id)) ?? String(payload.id),
|
||||||
projectId: String(payload.projectId),
|
projectId: String(payload.projectId),
|
||||||
projectDisplayName: payload.projectDisplayName,
|
projectDisplayName: payload.projectDisplayName,
|
||||||
projectRootPath: payload.projectRootPath,
|
projectRootPath: payload.projectRootPath,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
} from "@/stores/workspace-layout-store";
|
} from "@/stores/workspace-layout-store";
|
||||||
|
|
||||||
const SERVER_ID = "server-1";
|
const SERVER_ID = "server-1";
|
||||||
const WORKSPACE_ID = "/repo/worktree";
|
const WORKSPACE_ID = "ws-main";
|
||||||
|
|
||||||
function createTab(tabId: string): WorkspaceTab {
|
function createTab(tabId: string): WorkspaceTab {
|
||||||
return {
|
return {
|
||||||
@@ -813,7 +813,7 @@ describe("workspace-layout-store actions", () => {
|
|||||||
const workspaceKey = createWorkspaceKey();
|
const workspaceKey = createWorkspaceKey();
|
||||||
const otherWorkspaceKey = buildWorkspaceTabPersistenceKey({
|
const otherWorkspaceKey = buildWorkspaceTabPersistenceKey({
|
||||||
serverId: SERVER_ID,
|
serverId: SERVER_ID,
|
||||||
workspaceId: "/repo/other-worktree",
|
workspaceId: "ws-other-worktree",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(otherWorkspaceKey).toBeTruthy();
|
expect(otherWorkspaceKey).toBeTruthy();
|
||||||
@@ -849,7 +849,7 @@ describe("workspace-layout-store actions", () => {
|
|||||||
const workspaceKey = createWorkspaceKey();
|
const workspaceKey = createWorkspaceKey();
|
||||||
const otherWorkspaceKey = buildWorkspaceTabPersistenceKey({
|
const otherWorkspaceKey = buildWorkspaceTabPersistenceKey({
|
||||||
serverId: SERVER_ID,
|
serverId: SERVER_ID,
|
||||||
workspaceId: "/repo/other-worktree",
|
workspaceId: "ws-other-worktree",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(otherWorkspaceKey).toBeTruthy();
|
expect(otherWorkspaceKey).toBeTruthy();
|
||||||
|
|||||||
@@ -23,6 +23,17 @@ import {
|
|||||||
const SERVER_ID = "server-1";
|
const SERVER_ID = "server-1";
|
||||||
const WORKSPACE_ID = "/repo/worktree";
|
const WORKSPACE_ID = "/repo/worktree";
|
||||||
|
|
||||||
|
describe("buildWorkspaceTabPersistenceKey", () => {
|
||||||
|
it("preserves opaque workspace ids instead of normalizing them like paths", () => {
|
||||||
|
expect(
|
||||||
|
buildWorkspaceTabPersistenceKey({
|
||||||
|
serverId: SERVER_ID,
|
||||||
|
workspaceId: " setup\\workspace\\ ",
|
||||||
|
}),
|
||||||
|
).toBe("server-1:setup\\workspace\\");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("workspace-tabs-store retargetTab", () => {
|
describe("workspace-tabs-store retargetTab", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useWorkspaceTabsStore.setState({
|
useWorkspaceTabsStore.setState({
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ function trimNonEmpty(value: string | null | undefined): string | null {
|
|||||||
return trimmed.length > 0 ? trimmed : null;
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeWorkspaceId(value: string): string {
|
|
||||||
return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildWorkspaceTabPersistenceKey(input: {
|
export function buildWorkspaceTabPersistenceKey(input: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -41,7 +37,7 @@ export function buildWorkspaceTabPersistenceKey(input: {
|
|||||||
if (!serverId || !workspaceId) {
|
if (!serverId || !workspaceId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return `${serverId}:${normalizeWorkspaceId(workspaceId)}`;
|
return `${serverId}:${workspaceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTabOrder(list: unknown): string[] {
|
function normalizeTabOrder(list: unknown): string[] {
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ describe("parseHostAgentRouteFromPathname", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("workspace route parsing", () => {
|
describe("workspace route parsing", () => {
|
||||||
it("encodes numeric workspace IDs without base64", () => {
|
it("keeps URL-safe workspace IDs unencoded", () => {
|
||||||
expect(encodeWorkspaceIdForPathSegment("164")).toBe("164");
|
expect(encodeWorkspaceIdForPathSegment("164")).toBe("164");
|
||||||
expect(decodeWorkspaceIdFromPathSegment("164")).toBe("164");
|
expect(decodeWorkspaceIdFromPathSegment("164")).toBe("164");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("encodes path-based workspace IDs as base64url (legacy)", () => {
|
it("encodes non-URL-safe workspace IDs as base64url", () => {
|
||||||
expect(encodeWorkspaceIdForPathSegment("/tmp/repo")).toBe("L3RtcC9yZXBv");
|
expect(encodeWorkspaceIdForPathSegment("/tmp/repo")).toBe("b64_L3RtcC9yZXBv");
|
||||||
expect(decodeWorkspaceIdFromPathSegment("L3RtcC9yZXBv")).toBe("/tmp/repo");
|
expect(decodeWorkspaceIdFromPathSegment("L3RtcC9yZXBv")).toBe("/tmp/repo");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ describe("workspace route parsing", () => {
|
|||||||
expect(decodeFilePathFromPathSegment(encoded)).toBe("src/index.ts");
|
expect(decodeFilePathFromPathSegment(encoded)).toBe("src/index.ts");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses workspace route with numeric ID", () => {
|
it("parses workspace route with a plain workspace id", () => {
|
||||||
expect(parseHostWorkspaceRouteFromPathname("/h/local/workspace/164")).toEqual({
|
expect(parseHostWorkspaceRouteFromPathname("/h/local/workspace/164")).toEqual({
|
||||||
serverId: "local",
|
serverId: "local",
|
||||||
workspaceId: "164",
|
workspaceId: "164",
|
||||||
@@ -66,12 +66,14 @@ describe("workspace route parsing", () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds numeric workspace routes without base64", () => {
|
it("builds plain workspace routes for URL-safe ids", () => {
|
||||||
expect(buildHostWorkspaceRoute("local", "164")).toBe("/h/local/workspace/164");
|
expect(buildHostWorkspaceRoute("local", "164")).toBe("/h/local/workspace/164");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds base64url workspace routes for legacy paths", () => {
|
it("builds base64url workspace routes for legacy paths", () => {
|
||||||
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe("/h/local/workspace/L3RtcC9yZXBv");
|
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe(
|
||||||
|
"/h/local/workspace/b64_L3RtcC9yZXBv",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds host root routes", () => {
|
it("builds host root routes", () => {
|
||||||
@@ -115,12 +117,19 @@ describe("workspace route parsing", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("round-trips numeric IDs through encode/decode", () => {
|
it("round-trips URL-safe IDs through encode/decode", () => {
|
||||||
const ids = ["1", "40", "164", "9999"];
|
const ids = ["1", "40", "164", "9999", "workspace-1", "opaque_id.v2~test"];
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const encoded = encodeWorkspaceIdForPathSegment(id);
|
const encoded = encodeWorkspaceIdForPathSegment(id);
|
||||||
const decoded = decodeWorkspaceIdFromPathSegment(encoded);
|
const decoded = decodeWorkspaceIdFromPathSegment(encoded);
|
||||||
expect(decoded).toBe(id);
|
expect(decoded).toBe(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("round-trips opaque IDs with reserved characters through base64 encoding", () => {
|
||||||
|
const id = " team/setup:id#1 ";
|
||||||
|
const encoded = encodeWorkspaceIdForPathSegment(id);
|
||||||
|
expect(encoded).toBe("b64_dGVhbS9zZXR1cDppZCMx");
|
||||||
|
expect(decodeWorkspaceIdFromPathSegment(encoded)).toBe("team/setup:id#1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
|
|
||||||
type NullableString = string | null | undefined;
|
type NullableString = string | null | undefined;
|
||||||
|
const BASE64_WORKSPACE_ID_PREFIX = "b64_";
|
||||||
|
|
||||||
function stripSearchAndHash(pathname: string): string {
|
function stripSearchAndHash(pathname: string): string {
|
||||||
const hashIndex = pathname.indexOf("#");
|
const hashIndex = pathname.indexOf("#");
|
||||||
@@ -87,12 +88,16 @@ function tryDecodeBase64UrlNoPadUtf8(input: string): string | null {
|
|||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPathLikeWorkspaceIdentity(value: string): boolean {
|
function normalizeWorkspaceId(value: string): string {
|
||||||
return value.includes("/") || value.includes("\\") || /^[A-Za-z]:[\\/]/.test(value);
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeWorkspaceId(value: string): string {
|
function isUrlSafeWorkspaceId(value: string): boolean {
|
||||||
return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
return /^[A-Za-z0-9._~-]+$/.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLegacyPathLikeWorkspaceValue(value: string): boolean {
|
||||||
|
return value.includes("/") || value.includes("\\") || /^[A-Za-z]:[\\/]/.test(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceOpenIntent =
|
export type WorkspaceOpenIntent =
|
||||||
@@ -163,13 +168,11 @@ export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
|
|||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
// Numeric string IDs are URL-safe and don't need encoding.
|
|
||||||
// Legacy path-based IDs still get base64-encoded for safety.
|
|
||||||
const id = normalizeWorkspaceId(normalized);
|
const id = normalizeWorkspaceId(normalized);
|
||||||
if (isPathLikeWorkspaceIdentity(id)) {
|
if (isUrlSafeWorkspaceId(id)) {
|
||||||
return toBase64UrlNoPad(id);
|
return id;
|
||||||
}
|
}
|
||||||
return encodeURIComponent(id);
|
return `${BASE64_WORKSPACE_ID_PREFIX}${toBase64UrlNoPad(id)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeWorkspaceIdFromPathSegment(workspaceIdSegment: string): string | null {
|
export function decodeWorkspaceIdFromPathSegment(workspaceIdSegment: string): string | null {
|
||||||
@@ -178,32 +181,25 @@ export function decodeWorkspaceIdFromPathSegment(workspaceIdSegment: string): st
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode %2F etc first (legacy scheme), but keep the raw segment to decide if base64 applies.
|
|
||||||
const decoded = trimNonEmpty(decodeSegment(normalizedSegment));
|
const decoded = trimNonEmpty(decodeSegment(normalizedSegment));
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy: if it already looks like a path after decoding, keep it.
|
if (decoded.startsWith(BASE64_WORKSPACE_ID_PREFIX)) {
|
||||||
if (decoded.includes("/") || decoded.includes("\\")) {
|
const encodedPayload = decoded.slice(BASE64_WORKSPACE_ID_PREFIX.length);
|
||||||
return normalizeWorkspaceId(decoded);
|
const prefixedDecoded =
|
||||||
}
|
tryDecodeBase64UrlNoPadUtf8(encodedPayload) ?? decodeBase64UrlNoPadUtf8(encodedPayload);
|
||||||
|
return prefixedDecoded ? normalizeWorkspaceId(prefixedDecoded) : null;
|
||||||
// If the segment looks like a plain numeric ID, return it directly.
|
|
||||||
// Do NOT attempt base64 decode on short alphanumeric strings.
|
|
||||||
if (/^\d+$/.test(decoded)) {
|
|
||||||
return decoded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const base64Decoded = tryDecodeBase64UrlNoPadUtf8(decoded);
|
const base64Decoded = tryDecodeBase64UrlNoPadUtf8(decoded);
|
||||||
if (base64Decoded) {
|
if (base64Decoded && isLegacyPathLikeWorkspaceValue(base64Decoded)) {
|
||||||
return normalizeWorkspaceId(base64Decoded);
|
return normalizeWorkspaceId(base64Decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some older links use non-canonical base64url (non-zero pad bits). Accept
|
|
||||||
// decoded values only when they clearly represent filesystem paths.
|
|
||||||
const relaxedBase64Decoded = decodeBase64UrlNoPadUtf8(decoded);
|
const relaxedBase64Decoded = decodeBase64UrlNoPadUtf8(decoded);
|
||||||
if (relaxedBase64Decoded && isPathLikeWorkspaceIdentity(relaxedBase64Decoded)) {
|
if (relaxedBase64Decoded && isLegacyPathLikeWorkspaceValue(relaxedBase64Decoded)) {
|
||||||
return normalizeWorkspaceId(relaxedBase64Decoded);
|
return normalizeWorkspaceId(relaxedBase64Decoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ describe("buildNotificationRoute", () => {
|
|||||||
buildNotificationRoute({
|
buildNotificationRoute({
|
||||||
serverId: "srv-1",
|
serverId: "srv-1",
|
||||||
agentId: "agent-1",
|
agentId: "agent-1",
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-main",
|
||||||
}),
|
}),
|
||||||
).toBe("/h/srv-1/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1");
|
).toBe("/h/srv-1/workspace/ws-main?open=agent%3Aagent-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("routes directly to server-scoped agent path when both ids are present", () => {
|
it("routes directly to server-scoped agent path when both ids are present", () => {
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import type {
|
|||||||
|
|
||||||
function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorkspaceEntry {
|
function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorkspaceEntry {
|
||||||
return {
|
return {
|
||||||
workspaceKey: "srv:/repo",
|
workspaceKey: "srv:ws-root",
|
||||||
serverId: "srv",
|
serverId: "srv",
|
||||||
workspaceId: "/repo",
|
workspaceId: "ws-root",
|
||||||
|
workspaceDirectory: "/repo",
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
workspaceKind: "checkout",
|
workspaceKind: "checkout",
|
||||||
name: "paseo",
|
name: "paseo",
|
||||||
@@ -41,7 +42,7 @@ function project(overrides: Partial<SidebarProjectEntry> = {}): SidebarProjectEn
|
|||||||
describe("buildSidebarProjectRowModel", () => {
|
describe("buildSidebarProjectRowModel", () => {
|
||||||
it("flattens non-git projects with one workspace into a direct workspace row model", () => {
|
it("flattens non-git projects with one workspace into a direct workspace row model", () => {
|
||||||
const flattenedWorkspace = workspace({
|
const flattenedWorkspace = workspace({
|
||||||
workspaceId: "/repo/non-git",
|
workspaceId: "ws-non-git",
|
||||||
workspaceKind: "checkout",
|
workspaceKind: "checkout",
|
||||||
statusBucket: "running",
|
statusBucket: "running",
|
||||||
});
|
});
|
||||||
@@ -66,7 +67,7 @@ describe("buildSidebarProjectRowModel", () => {
|
|||||||
it("marks flattened non-git project rows as selected when their workspace is active", () => {
|
it("marks flattened non-git project rows as selected when their workspace is active", () => {
|
||||||
const flattenedWorkspace = workspace({
|
const flattenedWorkspace = workspace({
|
||||||
serverId: "srv-2",
|
serverId: "srv-2",
|
||||||
workspaceId: "/repo/non-git",
|
workspaceId: "ws-non-git",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = buildSidebarProjectRowModel({
|
const result = buildSidebarProjectRowModel({
|
||||||
@@ -78,7 +79,7 @@ describe("buildSidebarProjectRowModel", () => {
|
|||||||
serverId: "srv-2",
|
serverId: "srv-2",
|
||||||
activeWorkspaceSelection: {
|
activeWorkspaceSelection: {
|
||||||
serverId: "srv-2",
|
serverId: "srv-2",
|
||||||
workspaceId: "/repo/non-git",
|
workspaceId: "ws-non-git",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ describe("buildSidebarProjectRowModel", () => {
|
|||||||
|
|
||||||
it("keeps single-workspace git projects as sections with the new worktree action", () => {
|
it("keeps single-workspace git projects as sections with the new worktree action", () => {
|
||||||
const onlyWorkspace = workspace({
|
const onlyWorkspace = workspace({
|
||||||
workspaceId: "/repo/main",
|
workspaceId: "ws-main",
|
||||||
workspaceKind: "checkout",
|
workspaceKind: "checkout",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,8 +115,8 @@ describe("buildSidebarProjectRowModel", () => {
|
|||||||
project: project({
|
project: project({
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
workspaces: [
|
workspaces: [
|
||||||
workspace({ workspaceId: "/repo/main", workspaceKind: "checkout" }),
|
workspace({ workspaceId: "ws-main", workspaceKind: "checkout" }),
|
||||||
workspace({ workspaceId: "/repo/feature", workspaceKind: "worktree" }),
|
workspace({ workspaceId: "ws-feature", workspaceKind: "worktree" }),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
@@ -144,8 +145,8 @@ describe("isSidebarProjectFlattened", () => {
|
|||||||
isSidebarProjectFlattened(
|
isSidebarProjectFlattened(
|
||||||
project({
|
project({
|
||||||
workspaces: [
|
workspaces: [
|
||||||
workspace({ workspaceId: "/repo/main" }),
|
workspace({ workspaceId: "ws-main" }),
|
||||||
workspace({ workspaceId: "/repo/feat" }),
|
workspace({ workspaceId: "ws-feat" }),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,14 +6,20 @@ import type {
|
|||||||
|
|
||||||
import { buildSidebarShortcutModel } from "./sidebar-shortcuts";
|
import { buildSidebarShortcutModel } from "./sidebar-shortcuts";
|
||||||
|
|
||||||
function workspace(serverId: string, cwd: string): SidebarWorkspaceEntry {
|
function workspace(input: {
|
||||||
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
workspaceDirectory: string;
|
||||||
|
name: string;
|
||||||
|
}): SidebarWorkspaceEntry {
|
||||||
return {
|
return {
|
||||||
workspaceKey: `${serverId}:${cwd}`,
|
workspaceKey: `${input.serverId}:${input.workspaceId}`,
|
||||||
serverId,
|
serverId: input.serverId,
|
||||||
workspaceId: cwd,
|
workspaceId: input.workspaceId,
|
||||||
|
workspaceDirectory: input.workspaceDirectory,
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
workspaceKind: "checkout",
|
workspaceKind: "checkout",
|
||||||
name: cwd,
|
name: input.name,
|
||||||
statusBucket: "done",
|
statusBucket: "done",
|
||||||
diffStat: null,
|
diffStat: null,
|
||||||
scripts: [],
|
scripts: [],
|
||||||
@@ -26,7 +32,7 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
|
|||||||
projectKey,
|
projectKey,
|
||||||
projectName: projectKey,
|
projectName: projectKey,
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
iconWorkingDir: workspaces[0]?.workspaceId ?? "",
|
iconWorkingDir: workspaces[0]?.workspaceDirectory ?? "",
|
||||||
statusBucket: "done",
|
statusBucket: "done",
|
||||||
activeCount: 0,
|
activeCount: 0,
|
||||||
totalWorkspaces: workspaces.length,
|
totalWorkspaces: workspaces.length,
|
||||||
@@ -37,8 +43,34 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
|
|||||||
describe("buildSidebarShortcutModel", () => {
|
describe("buildSidebarShortcutModel", () => {
|
||||||
it("builds shortcut targets in visual order and excludes collapsed projects", () => {
|
it("builds shortcut targets in visual order and excludes collapsed projects", () => {
|
||||||
const projects = [
|
const projects = [
|
||||||
project("p1", [workspace("s1", "/repo/main"), workspace("s1", "/repo/feat-a")]),
|
project("p1", [
|
||||||
project("p2", [workspace("s1", "/repo2/main"), workspace("s1", "/repo2/feat-a")]),
|
workspace({
|
||||||
|
serverId: "s1",
|
||||||
|
workspaceId: "ws-main",
|
||||||
|
workspaceDirectory: "/repo/main",
|
||||||
|
name: "main",
|
||||||
|
}),
|
||||||
|
workspace({
|
||||||
|
serverId: "s1",
|
||||||
|
workspaceId: "ws-feat-a",
|
||||||
|
workspaceDirectory: "/repo/feat-a",
|
||||||
|
name: "feat-a",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
project("p2", [
|
||||||
|
workspace({
|
||||||
|
serverId: "s1",
|
||||||
|
workspaceId: "ws-repo2-main",
|
||||||
|
workspaceDirectory: "/repo2/main",
|
||||||
|
name: "main",
|
||||||
|
}),
|
||||||
|
workspace({
|
||||||
|
serverId: "s1",
|
||||||
|
workspaceId: "ws-repo2-feat-a",
|
||||||
|
workspaceDirectory: "/repo2/feat-a",
|
||||||
|
name: "feat-a",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
const model = buildSidebarShortcutModel({
|
const model = buildSidebarShortcutModel({
|
||||||
@@ -47,21 +79,26 @@ describe("buildSidebarShortcutModel", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(model.visibleTargets).toEqual([
|
expect(model.visibleTargets).toEqual([
|
||||||
{ serverId: "s1", workspaceId: "/repo/main" },
|
{ serverId: "s1", workspaceId: "ws-main" },
|
||||||
{ serverId: "s1", workspaceId: "/repo/feat-a" },
|
{ serverId: "s1", workspaceId: "ws-feat-a" },
|
||||||
]);
|
]);
|
||||||
expect(model.shortcutTargets).toEqual([
|
expect(model.shortcutTargets).toEqual([
|
||||||
{ serverId: "s1", workspaceId: "/repo/main" },
|
{ serverId: "s1", workspaceId: "ws-main" },
|
||||||
{ serverId: "s1", workspaceId: "/repo/feat-a" },
|
{ serverId: "s1", workspaceId: "ws-feat-a" },
|
||||||
]);
|
]);
|
||||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:/repo/main")).toBe(1);
|
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-main")).toBe(1);
|
||||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:/repo/feat-a")).toBe(2);
|
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-feat-a")).toBe(2);
|
||||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:/repo2/main")).toBeUndefined();
|
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-repo2-main")).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("limits shortcuts to 9", () => {
|
it("limits shortcuts to 9", () => {
|
||||||
const workspaces = Array.from({ length: 20 }, (_, index) =>
|
const workspaces = Array.from({ length: 20 }, (_, index) =>
|
||||||
workspace("s", `/repo/w${index + 1}`),
|
workspace({
|
||||||
|
serverId: "s",
|
||||||
|
workspaceId: `ws-${index + 1}`,
|
||||||
|
workspaceDirectory: `/repo/w${index + 1}`,
|
||||||
|
name: `w${index + 1}`,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
const projects = [project("p", workspaces)];
|
const projects = [project("p", workspaces)];
|
||||||
|
|
||||||
@@ -71,14 +108,23 @@ describe("buildSidebarShortcutModel", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(model.visibleTargets).toHaveLength(20);
|
expect(model.visibleTargets).toHaveLength(20);
|
||||||
expect(model.visibleTargets[19]).toEqual({ serverId: "s", workspaceId: "/repo/w20" });
|
expect(model.visibleTargets[19]).toEqual({ serverId: "s", workspaceId: "ws-20" });
|
||||||
expect(model.shortcutTargets).toHaveLength(9);
|
expect(model.shortcutTargets).toHaveLength(9);
|
||||||
expect(model.shortcutTargets[0]).toEqual({ serverId: "s", workspaceId: "/repo/w1" });
|
expect(model.shortcutTargets[0]).toEqual({ serverId: "s", workspaceId: "ws-1" });
|
||||||
expect(model.shortcutTargets[8]).toEqual({ serverId: "s", workspaceId: "/repo/w9" });
|
expect(model.shortcutTargets[8]).toEqual({ serverId: "s", workspaceId: "ws-9" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("still excludes collapsed single-workspace git projects because they are not flattened", () => {
|
it("still excludes collapsed single-workspace git projects because they are not flattened", () => {
|
||||||
const projects = [project("p1", [workspace("s1", "/repo/main")])];
|
const projects = [
|
||||||
|
project("p1", [
|
||||||
|
workspace({
|
||||||
|
serverId: "s1",
|
||||||
|
workspaceId: "ws-main",
|
||||||
|
workspaceDirectory: "/repo/main",
|
||||||
|
name: "main",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
const model = buildSidebarShortcutModel({
|
const model = buildSidebarShortcutModel({
|
||||||
projects,
|
projects,
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ function createWorkspace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("resolveWorkspaceRouteId", () => {
|
describe("resolveWorkspaceRouteId", () => {
|
||||||
it("normalizes route workspace ids", () => {
|
it("trims route workspace ids without path normalization", () => {
|
||||||
expect(resolveWorkspaceRouteId({ routeWorkspaceId: " /tmp/repo/ " })).toBe("/tmp/repo");
|
expect(resolveWorkspaceRouteId({ routeWorkspaceId: " C:\\tmp\\repo\\ " })).toBe(
|
||||||
|
"C:\\tmp\\repo\\",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null for empty values", () => {
|
it("returns null for empty values", () => {
|
||||||
@@ -87,12 +89,12 @@ describe("resolveWorkspaceMapKeyByIdentity", () => {
|
|||||||
expect(
|
expect(
|
||||||
resolveWorkspaceMapKeyByIdentity({
|
resolveWorkspaceMapKeyByIdentity({
|
||||||
workspaces,
|
workspaces,
|
||||||
workspaceIdentity: "workspace-1",
|
workspaceId: "workspace-1",
|
||||||
}),
|
}),
|
||||||
).toBe("workspace-1");
|
).toBe("workspace-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves a workspace directory identity to the canonical map key", () => {
|
it("does not resolve workspace directories when an id is required", () => {
|
||||||
const workspaces = new Map<string, WorkspaceDescriptor>([
|
const workspaces = new Map<string, WorkspaceDescriptor>([
|
||||||
[
|
[
|
||||||
"workspace-1",
|
"workspace-1",
|
||||||
@@ -106,9 +108,9 @@ describe("resolveWorkspaceMapKeyByIdentity", () => {
|
|||||||
expect(
|
expect(
|
||||||
resolveWorkspaceMapKeyByIdentity({
|
resolveWorkspaceMapKeyByIdentity({
|
||||||
workspaces,
|
workspaces,
|
||||||
workspaceIdentity: "C:/repo/feature",
|
workspaceId: "C:/repo/feature",
|
||||||
}),
|
}),
|
||||||
).toBe("workspace-1");
|
).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
import { normalizeWorkspaceOpaqueId, normalizeWorkspacePath } from "@/utils/workspace-identity";
|
||||||
|
|
||||||
export type WorkspaceAuthorityResult = {
|
export type WorkspaceAuthorityResult = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -23,20 +23,20 @@ export type WorkspaceExecutionAuthorityResult =
|
|||||||
export function resolveWorkspaceRouteId(input: {
|
export function resolveWorkspaceRouteId(input: {
|
||||||
routeWorkspaceId: string | null | undefined;
|
routeWorkspaceId: string | null | undefined;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
return normalizeWorkspaceIdentity(input.routeWorkspaceId);
|
return normalizeWorkspaceOpaqueId(input.routeWorkspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveWorkspaceIdByExecutionDirectory(input: {
|
export function resolveWorkspaceIdByExecutionDirectory(input: {
|
||||||
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
||||||
workspaceDirectory: string | null | undefined;
|
workspaceDirectory: string | null | undefined;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
const normalizedWorkspaceDirectory = normalizeWorkspaceIdentity(input.workspaceDirectory);
|
const normalizedWorkspaceDirectory = normalizeWorkspacePath(input.workspaceDirectory);
|
||||||
if (!normalizedWorkspaceDirectory) {
|
if (!normalizedWorkspaceDirectory) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const workspace of input.workspaces ?? []) {
|
for (const workspace of input.workspaces ?? []) {
|
||||||
if (normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedWorkspaceDirectory) {
|
if (normalizeWorkspacePath(workspace.workspaceDirectory) === normalizedWorkspaceDirectory) {
|
||||||
return workspace.id;
|
return workspace.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,10 +46,10 @@ export function resolveWorkspaceIdByExecutionDirectory(input: {
|
|||||||
|
|
||||||
export function resolveWorkspaceMapKeyByIdentity(input: {
|
export function resolveWorkspaceMapKeyByIdentity(input: {
|
||||||
workspaces: Map<string, WorkspaceDescriptor> | null | undefined;
|
workspaces: Map<string, WorkspaceDescriptor> | null | undefined;
|
||||||
workspaceIdentity: string | null | undefined;
|
workspaceId: string | null | undefined;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
const normalizedWorkspaceIdentity = normalizeWorkspaceIdentity(input.workspaceIdentity);
|
const normalizedWorkspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
|
||||||
if (!normalizedWorkspaceIdentity) {
|
if (!normalizedWorkspaceId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,15 +58,12 @@ export function resolveWorkspaceMapKeyByIdentity(input: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workspaces.has(normalizedWorkspaceIdentity)) {
|
if (workspaces.has(normalizedWorkspaceId)) {
|
||||||
return normalizedWorkspaceIdentity;
|
return normalizedWorkspaceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [workspaceKey, workspace] of workspaces) {
|
for (const [workspaceKey, workspace] of workspaces) {
|
||||||
if (
|
if (normalizeWorkspaceOpaqueId(workspace.id) === normalizedWorkspaceId) {
|
||||||
normalizeWorkspaceIdentity(workspace.id) === normalizedWorkspaceIdentity ||
|
|
||||||
normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedWorkspaceIdentity
|
|
||||||
) {
|
|
||||||
return workspaceKey;
|
return workspaceKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +87,7 @@ export function getWorkspaceExecutionAuthority(
|
|||||||
: (() => {
|
: (() => {
|
||||||
const workspaceKey = resolveWorkspaceMapKeyByIdentity({
|
const workspaceKey = resolveWorkspaceMapKeyByIdentity({
|
||||||
workspaces: input.workspaces,
|
workspaces: input.workspaces,
|
||||||
workspaceIdentity: input.workspaceId,
|
workspaceId: input.workspaceId,
|
||||||
});
|
});
|
||||||
if (!workspaceKey) {
|
if (!workspaceKey) {
|
||||||
return null;
|
return null;
|
||||||
@@ -99,7 +96,7 @@ export function getWorkspaceExecutionAuthority(
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
if ("workspaces" in input) {
|
if ("workspaces" in input) {
|
||||||
const normalizedWorkspaceId = normalizeWorkspaceIdentity(input.workspaceId);
|
const normalizedWorkspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
|
||||||
if (!normalizedWorkspaceId) {
|
if (!normalizedWorkspaceId) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -120,7 +117,7 @@ export function getWorkspaceExecutionAuthority(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceDirectory = normalizeWorkspaceIdentity(workspace.workspaceDirectory);
|
const workspaceDirectory = normalizeWorkspacePath(workspace.workspaceDirectory);
|
||||||
if (!workspaceDirectory) {
|
if (!workspaceDirectory) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -156,24 +153,10 @@ export function requireWorkspaceExecutionAuthority(
|
|||||||
return result.authority;
|
return result.authority;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireWorkspaceRecordId(workspaceId: string): number {
|
|
||||||
const normalizedWorkspaceId = normalizeWorkspaceIdentity(workspaceId);
|
|
||||||
if (!normalizedWorkspaceId) {
|
|
||||||
throw new Error("Workspace ID is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedWorkspaceId = Number(normalizedWorkspaceId);
|
|
||||||
if (!Number.isInteger(parsedWorkspaceId)) {
|
|
||||||
throw new Error(`Workspace ID is not a persisted record ID: ${workspaceId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedWorkspaceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveWorkspaceExecutionDirectory(input: {
|
export function resolveWorkspaceExecutionDirectory(input: {
|
||||||
workspaceDirectory: string | null | undefined;
|
workspaceDirectory: string | null | undefined;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
return normalizeWorkspaceIdentity(input.workspaceDirectory);
|
return normalizeWorkspacePath(input.workspaceDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireWorkspaceExecutionDirectory(input: {
|
export function requireWorkspaceExecutionDirectory(input: {
|
||||||
@@ -206,5 +189,3 @@ export function resolveWorkspaceExecutionAuthority(
|
|||||||
const result = getWorkspaceExecutionAuthority(input);
|
const result = getWorkspaceExecutionAuthority(input);
|
||||||
return result.ok ? result.authority : null;
|
return result.ok ? result.authority : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseWorkspaceRecordId = requireWorkspaceRecordId;
|
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ function trimNonEmpty(value: string | null | undefined): string | null {
|
|||||||
return trimmed.length > 0 ? trimmed : null;
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeWorkspaceIdentity(value: string | null | undefined): string | null {
|
export function normalizeWorkspaceOpaqueId(value: string | null | undefined): string | null {
|
||||||
|
return trimNonEmpty(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeWorkspacePath(value: string | null | undefined): string | null {
|
||||||
const trimmed = trimNonEmpty(value);
|
const trimmed = trimNonEmpty(value);
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function normalizeWorkspaceTabTarget(
|
|||||||
}
|
}
|
||||||
if (value.kind === "setup") {
|
if (value.kind === "setup") {
|
||||||
const workspaceId = trimNonEmpty(value.workspaceId);
|
const workspaceId = trimNonEmpty(value.workspaceId);
|
||||||
return workspaceId ? { kind: "setup", workspaceId: workspaceId.replace(/\\/g, "/") } : null;
|
return workspaceId ? { kind: "setup", workspaceId } : null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ describe("DaemonClient", () => {
|
|||||||
wrapSessionMessage({
|
wrapSessionMessage({
|
||||||
type: "workspace_setup_progress",
|
type: "workspace_setup_progress",
|
||||||
payload: {
|
payload: {
|
||||||
workspaceId: "/tmp/project/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
status: "running",
|
status: "running",
|
||||||
detail: {
|
detail: {
|
||||||
type: "worktree_setup",
|
type: "worktree_setup",
|
||||||
@@ -264,9 +264,9 @@ describe("DaemonClient", () => {
|
|||||||
|
|
||||||
expect(events).toContainEqual({
|
expect(events).toContainEqual({
|
||||||
type: "workspace_setup_progress",
|
type: "workspace_setup_progress",
|
||||||
workspaceId: "/tmp/project/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
payload: {
|
payload: {
|
||||||
workspaceId: "/tmp/project/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
status: "running",
|
status: "running",
|
||||||
detail: {
|
detail: {
|
||||||
type: "worktree_setup",
|
type: "worktree_setup",
|
||||||
@@ -309,7 +309,7 @@ describe("DaemonClient", () => {
|
|||||||
const createPromise = client.createAgent({
|
const createPromise = client.createAgent({
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
cwd: "/tmp/project/.paseo/worktrees/feature-a",
|
cwd: "/tmp/project/.paseo/worktrees/feature-a",
|
||||||
workspaceId: "/tmp/project/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
title: "Compat agent",
|
title: "Compat agent",
|
||||||
modeId: "default",
|
modeId: "default",
|
||||||
});
|
});
|
||||||
@@ -326,7 +326,7 @@ describe("DaemonClient", () => {
|
|||||||
expect(request.message).toEqual(
|
expect(request.message).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: "create_agent_request",
|
type: "create_agent_request",
|
||||||
workspaceId: "/tmp/project/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export type CreateAgentRequestOptions = {
|
|||||||
config?: AgentSessionConfig;
|
config?: AgentSessionConfig;
|
||||||
provider?: AgentProvider;
|
provider?: AgentProvider;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
workspaceId?: string | number;
|
workspaceId?: string;
|
||||||
initialPrompt?: string;
|
initialPrompt?: string;
|
||||||
clientMessageId?: string;
|
clientMessageId?: string;
|
||||||
outputSchema?: Record<string, unknown>;
|
outputSchema?: Record<string, unknown>;
|
||||||
@@ -1400,7 +1400,7 @@ export class DaemonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async archiveWorkspace(
|
async archiveWorkspace(
|
||||||
workspaceId: string | number,
|
workspaceId: string,
|
||||||
requestId?: string,
|
requestId?: string,
|
||||||
): Promise<ArchiveWorkspacePayload> {
|
): Promise<ArchiveWorkspacePayload> {
|
||||||
return this.sendCorrelatedSessionRequest({
|
return this.sendCorrelatedSessionRequest({
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ export async function createPaseoDaemon(
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
let boundListenTarget: ListenTarget | null = null;
|
let boundListenTarget: ListenTarget | null = null;
|
||||||
|
let workspaceRegistry: FileBackedWorkspaceRegistry | null = null;
|
||||||
|
|
||||||
const scriptRouteStore = new ScriptRouteStore();
|
const scriptRouteStore = new ScriptRouteStore();
|
||||||
const scriptRuntimeStore = new WorkspaceScriptRuntimeStore();
|
const scriptRuntimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
@@ -252,6 +253,8 @@ export async function createPaseoDaemon(
|
|||||||
routeStore: scriptRouteStore,
|
routeStore: scriptRouteStore,
|
||||||
runtimeStore: scriptRuntimeStore,
|
runtimeStore: scriptRuntimeStore,
|
||||||
daemonPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
|
daemonPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
|
||||||
|
resolveWorkspaceDirectory: async (workspaceId) =>
|
||||||
|
(await workspaceRegistry?.get(workspaceId))?.cwd ?? null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const handleBranchChange = createBranchChangeRouteHandler({
|
const handleBranchChange = createBranchChangeRouteHandler({
|
||||||
@@ -396,7 +399,7 @@ export async function createPaseoDaemon(
|
|||||||
path.join(config.paseoHome, "projects", "projects.json"),
|
path.join(config.paseoHome, "projects", "projects.json"),
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
const workspaceRegistry = new FileBackedWorkspaceRegistry(
|
workspaceRegistry = new FileBackedWorkspaceRegistry(
|
||||||
path.join(config.paseoHome, "projects", "workspaces.json"),
|
path.join(config.paseoHome, "projects", "workspaces.json"),
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function createWorkspaceRepo(options?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildPayloads(input: {
|
function buildPayloads(input: {
|
||||||
|
workspaceId: string;
|
||||||
workspaceDirectory: string;
|
workspaceDirectory: string;
|
||||||
routeStore: ScriptRouteStore;
|
routeStore: ScriptRouteStore;
|
||||||
runtimeStore: WorkspaceScriptRuntimeStore;
|
runtimeStore: WorkspaceScriptRuntimeStore;
|
||||||
@@ -49,6 +50,7 @@ function buildPayloads(input: {
|
|||||||
|
|
||||||
describe("script-status-projection", () => {
|
describe("script-status-projection", () => {
|
||||||
it("projects plain scripts and services differently", () => {
|
it("projects plain scripts and services differently", () => {
|
||||||
|
const workspaceId = "workspace-plain-and-service";
|
||||||
const workspace = createWorkspaceRepo({
|
const workspace = createWorkspaceRepo({
|
||||||
paseoConfig: {
|
paseoConfig: {
|
||||||
scripts: {
|
scripts: {
|
||||||
@@ -60,7 +62,7 @@ describe("script-status-projection", () => {
|
|||||||
const routeStore = new ScriptRouteStore();
|
const routeStore = new ScriptRouteStore();
|
||||||
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
runtimeStore.set({
|
runtimeStore.set({
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "typecheck",
|
scriptName: "typecheck",
|
||||||
type: "script",
|
type: "script",
|
||||||
lifecycle: "stopped",
|
lifecycle: "stopped",
|
||||||
@@ -71,6 +73,7 @@ describe("script-status-projection", () => {
|
|||||||
try {
|
try {
|
||||||
expect(
|
expect(
|
||||||
buildPayloads({
|
buildPayloads({
|
||||||
|
workspaceId,
|
||||||
workspaceDirectory: workspace.repoDir,
|
workspaceDirectory: workspace.repoDir,
|
||||||
routeStore,
|
routeStore,
|
||||||
runtimeStore,
|
runtimeStore,
|
||||||
@@ -104,6 +107,7 @@ describe("script-status-projection", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("overlays runtime, route, and health state for running services", () => {
|
it("overlays runtime, route, and health state for running services", () => {
|
||||||
|
const workspaceId = "workspace-running-service";
|
||||||
const workspace = createWorkspaceRepo({
|
const workspace = createWorkspaceRepo({
|
||||||
branchName: "feature/card",
|
branchName: "feature/card",
|
||||||
paseoConfig: {
|
paseoConfig: {
|
||||||
@@ -116,12 +120,12 @@ describe("script-status-projection", () => {
|
|||||||
routeStore.registerRoute({
|
routeStore.registerRoute({
|
||||||
hostname: "feature-card.web.localhost",
|
hostname: "feature-card.web.localhost",
|
||||||
port: 4321,
|
port: 4321,
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "web",
|
scriptName: "web",
|
||||||
});
|
});
|
||||||
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
runtimeStore.set({
|
runtimeStore.set({
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "web",
|
scriptName: "web",
|
||||||
type: "service",
|
type: "service",
|
||||||
lifecycle: "running",
|
lifecycle: "running",
|
||||||
@@ -132,6 +136,7 @@ describe("script-status-projection", () => {
|
|||||||
try {
|
try {
|
||||||
expect(
|
expect(
|
||||||
buildPayloads({
|
buildPayloads({
|
||||||
|
workspaceId,
|
||||||
workspaceDirectory: workspace.repoDir,
|
workspaceDirectory: workspace.repoDir,
|
||||||
routeStore,
|
routeStore,
|
||||||
runtimeStore,
|
runtimeStore,
|
||||||
@@ -156,6 +161,7 @@ describe("script-status-projection", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("maps internal pending health to null on the wire", () => {
|
it("maps internal pending health to null on the wire", () => {
|
||||||
|
const workspaceId = "workspace-pending-health";
|
||||||
const workspace = createWorkspaceRepo({
|
const workspace = createWorkspaceRepo({
|
||||||
paseoConfig: {
|
paseoConfig: {
|
||||||
scripts: {
|
scripts: {
|
||||||
@@ -167,12 +173,12 @@ describe("script-status-projection", () => {
|
|||||||
routeStore.registerRoute({
|
routeStore.registerRoute({
|
||||||
hostname: "web.localhost",
|
hostname: "web.localhost",
|
||||||
port: 4321,
|
port: 4321,
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "web",
|
scriptName: "web",
|
||||||
});
|
});
|
||||||
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
runtimeStore.set({
|
runtimeStore.set({
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "web",
|
scriptName: "web",
|
||||||
type: "service",
|
type: "service",
|
||||||
lifecycle: "running",
|
lifecycle: "running",
|
||||||
@@ -183,6 +189,7 @@ describe("script-status-projection", () => {
|
|||||||
try {
|
try {
|
||||||
expect(
|
expect(
|
||||||
buildPayloads({
|
buildPayloads({
|
||||||
|
workspaceId,
|
||||||
workspaceDirectory: workspace.repoDir,
|
workspaceDirectory: workspace.repoDir,
|
||||||
routeStore,
|
routeStore,
|
||||||
runtimeStore,
|
runtimeStore,
|
||||||
@@ -207,17 +214,18 @@ describe("script-status-projection", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("includes orphaned running runtime entries even after config removal", () => {
|
it("includes orphaned running runtime entries even after config removal", () => {
|
||||||
|
const workspaceId = "workspace-orphaned-service";
|
||||||
const workspace = createWorkspaceRepo();
|
const workspace = createWorkspaceRepo();
|
||||||
const routeStore = new ScriptRouteStore();
|
const routeStore = new ScriptRouteStore();
|
||||||
routeStore.registerRoute({
|
routeStore.registerRoute({
|
||||||
hostname: "docs.localhost",
|
hostname: "docs.localhost",
|
||||||
port: 3002,
|
port: 3002,
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "docs",
|
scriptName: "docs",
|
||||||
});
|
});
|
||||||
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
runtimeStore.set({
|
runtimeStore.set({
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "docs",
|
scriptName: "docs",
|
||||||
type: "service",
|
type: "service",
|
||||||
lifecycle: "running",
|
lifecycle: "running",
|
||||||
@@ -228,6 +236,7 @@ describe("script-status-projection", () => {
|
|||||||
try {
|
try {
|
||||||
expect(
|
expect(
|
||||||
buildPayloads({
|
buildPayloads({
|
||||||
|
workspaceId,
|
||||||
workspaceDirectory: workspace.repoDir,
|
workspaceDirectory: workspace.repoDir,
|
||||||
routeStore,
|
routeStore,
|
||||||
runtimeStore,
|
runtimeStore,
|
||||||
@@ -251,11 +260,12 @@ describe("script-status-projection", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("projects orphaned plain scripts as scripts instead of services", () => {
|
it("projects orphaned plain scripts as scripts instead of services", () => {
|
||||||
|
const workspaceId = "workspace-orphaned-script";
|
||||||
const workspace = createWorkspaceRepo();
|
const workspace = createWorkspaceRepo();
|
||||||
const routeStore = new ScriptRouteStore();
|
const routeStore = new ScriptRouteStore();
|
||||||
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
runtimeStore.set({
|
runtimeStore.set({
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "typecheck",
|
scriptName: "typecheck",
|
||||||
type: "script",
|
type: "script",
|
||||||
lifecycle: "running",
|
lifecycle: "running",
|
||||||
@@ -266,6 +276,7 @@ describe("script-status-projection", () => {
|
|||||||
try {
|
try {
|
||||||
expect(
|
expect(
|
||||||
buildPayloads({
|
buildPayloads({
|
||||||
|
workspaceId,
|
||||||
workspaceDirectory: workspace.repoDir,
|
workspaceDirectory: workspace.repoDir,
|
||||||
routeStore,
|
routeStore,
|
||||||
runtimeStore,
|
runtimeStore,
|
||||||
@@ -288,7 +299,8 @@ describe("script-status-projection", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("createScriptStatusEmitter overlays health onto the projected workspace script list", () => {
|
it("createScriptStatusEmitter overlays health onto the projected workspace script list", async () => {
|
||||||
|
const workspaceId = "workspace-emitter";
|
||||||
const workspace = createWorkspaceRepo({
|
const workspace = createWorkspaceRepo({
|
||||||
paseoConfig: {
|
paseoConfig: {
|
||||||
scripts: {
|
scripts: {
|
||||||
@@ -301,12 +313,12 @@ describe("script-status-projection", () => {
|
|||||||
routeStore.registerRoute({
|
routeStore.registerRoute({
|
||||||
hostname: "api.localhost",
|
hostname: "api.localhost",
|
||||||
port: 3001,
|
port: 3001,
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "api",
|
scriptName: "api",
|
||||||
});
|
});
|
||||||
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
runtimeStore.set({
|
runtimeStore.set({
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scriptName: "api",
|
scriptName: "api",
|
||||||
type: "service",
|
type: "service",
|
||||||
lifecycle: "running",
|
lifecycle: "running",
|
||||||
@@ -320,10 +332,12 @@ describe("script-status-projection", () => {
|
|||||||
routeStore,
|
routeStore,
|
||||||
runtimeStore,
|
runtimeStore,
|
||||||
daemonPort: 6767,
|
daemonPort: 6767,
|
||||||
|
resolveWorkspaceDirectory: async (workspaceId) =>
|
||||||
|
workspaceId === "workspace-emitter" ? workspace.repoDir : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
emitUpdate(workspace.repoDir, [
|
emitUpdate(workspaceId, [
|
||||||
{
|
{
|
||||||
scriptName: "api",
|
scriptName: "api",
|
||||||
hostname: "api.localhost",
|
hostname: "api.localhost",
|
||||||
@@ -331,11 +345,12 @@ describe("script-status-projection", () => {
|
|||||||
health: "healthy",
|
health: "healthy",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
expect(session.emit).toHaveBeenCalledWith({
|
expect(session.emit).toHaveBeenCalledWith({
|
||||||
type: "script_status_update",
|
type: "script_status_update",
|
||||||
payload: {
|
payload: {
|
||||||
workspaceId: workspace.repoDir,
|
workspaceId,
|
||||||
scripts: [
|
scripts: [
|
||||||
{
|
{
|
||||||
scriptName: "api",
|
scriptName: "api",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type SessionEmitter = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type BuildWorkspaceScriptPayloadsOptions = {
|
type BuildWorkspaceScriptPayloadsOptions = {
|
||||||
|
workspaceId: string;
|
||||||
workspaceDirectory: string;
|
workspaceDirectory: string;
|
||||||
routeStore: ScriptRouteStore;
|
routeStore: ScriptRouteStore;
|
||||||
runtimeStore: WorkspaceScriptRuntimeStore;
|
runtimeStore: WorkspaceScriptRuntimeStore;
|
||||||
@@ -83,17 +84,18 @@ function sortPayloads(payloads: WorkspaceScriptPayload[]): WorkspaceScriptPayloa
|
|||||||
export function buildWorkspaceScriptPayloads(
|
export function buildWorkspaceScriptPayloads(
|
||||||
options: BuildWorkspaceScriptPayloadsOptions,
|
options: BuildWorkspaceScriptPayloadsOptions,
|
||||||
): WorkspaceScriptPayload[] {
|
): WorkspaceScriptPayload[] {
|
||||||
|
const workspaceId = options.workspaceId;
|
||||||
const workspaceDirectory = options.workspaceDirectory;
|
const workspaceDirectory = options.workspaceDirectory;
|
||||||
const branchName = resolveWorkspaceBranchName(workspaceDirectory);
|
const branchName = resolveWorkspaceBranchName(workspaceDirectory);
|
||||||
const scriptConfigs = getScriptConfigs(workspaceDirectory);
|
const scriptConfigs = getScriptConfigs(workspaceDirectory);
|
||||||
const runtimeEntries = new Map(
|
const runtimeEntries = new Map(
|
||||||
options.runtimeStore
|
options.runtimeStore
|
||||||
.listForWorkspace(workspaceDirectory)
|
.listForWorkspace(workspaceId)
|
||||||
.map((entry) => [entry.scriptName, entry] as const),
|
.map((entry) => [entry.scriptName, entry] as const),
|
||||||
);
|
);
|
||||||
const routesByScriptName = new Map(
|
const routesByScriptName = new Map(
|
||||||
options.routeStore
|
options.routeStore
|
||||||
.listRoutesForWorkspace(workspaceDirectory)
|
.listRoutesForWorkspace(workspaceId)
|
||||||
.map((entry) => [entry.scriptName, entry] as const),
|
.map((entry) => [entry.scriptName, entry] as const),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -177,33 +179,43 @@ export function createScriptStatusEmitter({
|
|||||||
routeStore,
|
routeStore,
|
||||||
runtimeStore,
|
runtimeStore,
|
||||||
daemonPort,
|
daemonPort,
|
||||||
|
resolveWorkspaceDirectory,
|
||||||
}: {
|
}: {
|
||||||
sessions: () => SessionEmitter[];
|
sessions: () => SessionEmitter[];
|
||||||
routeStore: ScriptRouteStore;
|
routeStore: ScriptRouteStore;
|
||||||
runtimeStore: WorkspaceScriptRuntimeStore;
|
runtimeStore: WorkspaceScriptRuntimeStore;
|
||||||
daemonPort: number | null | (() => number | null);
|
daemonPort: number | null | (() => number | null);
|
||||||
|
resolveWorkspaceDirectory: (workspaceId: string) => string | null | Promise<string | null>;
|
||||||
}): (workspaceId: string, scripts: ScriptHealthEntry[]) => void {
|
}): (workspaceId: string, scripts: ScriptHealthEntry[]) => void {
|
||||||
return (workspaceId, scripts) => {
|
return (workspaceId, scripts) => {
|
||||||
const resolvedDaemonPort = resolveDaemonPort(daemonPort);
|
void (async () => {
|
||||||
const scriptHealthByHostname = new Map(
|
const workspaceDirectory = await resolveWorkspaceDirectory(workspaceId);
|
||||||
scripts.map((script) => [script.hostname, script.health] as const),
|
if (!workspaceDirectory) {
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const projected = buildWorkspaceScriptPayloads({
|
const resolvedDaemonPort = resolveDaemonPort(daemonPort);
|
||||||
workspaceDirectory: workspaceId,
|
const scriptHealthByHostname = new Map(
|
||||||
routeStore,
|
scripts.map((script) => [script.hostname, script.health] as const),
|
||||||
runtimeStore,
|
);
|
||||||
daemonPort: resolvedDaemonPort,
|
|
||||||
resolveHealth: (hostname) => scriptHealthByHostname.get(hostname) ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = buildScriptStatusUpdateMessage({
|
const projected = buildWorkspaceScriptPayloads({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
scripts: projected,
|
workspaceDirectory,
|
||||||
});
|
routeStore,
|
||||||
|
runtimeStore,
|
||||||
|
daemonPort: resolvedDaemonPort,
|
||||||
|
resolveHealth: (hostname) => scriptHealthByHostname.get(hostname) ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
for (const session of sessions()) {
|
const message = buildScriptStatusUpdateMessage({
|
||||||
session.emit(message);
|
workspaceId,
|
||||||
}
|
scripts: projected,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const session of sessions()) {
|
||||||
|
session.emit(message);
|
||||||
|
}
|
||||||
|
})();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1474,6 +1474,7 @@ export class Session {
|
|||||||
|
|
||||||
private buildPersistedProjectRecord(input: {
|
private buildPersistedProjectRecord(input: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
cwd: string;
|
||||||
placement: ProjectPlacementPayload;
|
placement: ProjectPlacementPayload;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -1481,7 +1482,7 @@ export class Session {
|
|||||||
return createPersistedProjectRecord({
|
return createPersistedProjectRecord({
|
||||||
projectId: input.placement.projectKey,
|
projectId: input.placement.projectKey,
|
||||||
rootPath: deriveProjectRootPath({
|
rootPath: deriveProjectRootPath({
|
||||||
cwd: input.workspaceId,
|
cwd: input.cwd,
|
||||||
checkout: input.placement.checkout,
|
checkout: input.placement.checkout,
|
||||||
}),
|
}),
|
||||||
kind: deriveProjectKind(input.placement.checkout),
|
kind: deriveProjectKind(input.placement.checkout),
|
||||||
@@ -1494,6 +1495,7 @@ export class Session {
|
|||||||
|
|
||||||
private buildPersistedWorkspaceRecord(input: {
|
private buildPersistedWorkspaceRecord(input: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
cwd: string;
|
||||||
placement: ProjectPlacementPayload;
|
placement: ProjectPlacementPayload;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -1501,10 +1503,10 @@ export class Session {
|
|||||||
return createPersistedWorkspaceRecord({
|
return createPersistedWorkspaceRecord({
|
||||||
workspaceId: input.workspaceId,
|
workspaceId: input.workspaceId,
|
||||||
projectId: input.placement.projectKey,
|
projectId: input.placement.projectKey,
|
||||||
cwd: input.workspaceId,
|
cwd: input.cwd,
|
||||||
kind: deriveWorkspaceKind(input.placement.checkout),
|
kind: deriveWorkspaceKind(input.placement.checkout),
|
||||||
displayName: deriveWorkspaceDisplayName({
|
displayName: deriveWorkspaceDisplayName({
|
||||||
cwd: input.workspaceId,
|
cwd: input.cwd,
|
||||||
checkout: input.placement.checkout,
|
checkout: input.placement.checkout,
|
||||||
}),
|
}),
|
||||||
createdAt: input.createdAt,
|
createdAt: input.createdAt,
|
||||||
@@ -1522,15 +1524,6 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveWorkspaceByIdOrDirectory(
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<PersistedWorkspaceRecord | null> {
|
|
||||||
const record = await this.workspaceRegistry.get(workspaceId);
|
|
||||||
if (record) return record;
|
|
||||||
// Fallback: treat as directory path
|
|
||||||
return this.findWorkspaceByDirectory(workspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resolveWorkspaceDirectory(cwd: string): Promise<string> {
|
private async resolveWorkspaceDirectory(cwd: string): Promise<string> {
|
||||||
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
||||||
try {
|
try {
|
||||||
@@ -2980,7 +2973,7 @@ export class Session {
|
|||||||
attachments,
|
attachments,
|
||||||
);
|
);
|
||||||
const resolvedWorkspace = msg.workspaceId
|
const resolvedWorkspace = msg.workspaceId
|
||||||
? await this.resolveWorkspaceByIdOrDirectory(msg.workspaceId)
|
? await this.workspaceRegistry.get(msg.workspaceId)
|
||||||
: ((await this.findWorkspaceByDirectory(sessionConfig.cwd)) ??
|
: ((await this.findWorkspaceByDirectory(sessionConfig.cwd)) ??
|
||||||
(await this.findOrCreateWorkspaceForDirectory(sessionConfig.cwd)));
|
(await this.findOrCreateWorkspaceForDirectory(sessionConfig.cwd)));
|
||||||
if (!resolvedWorkspace) {
|
if (!resolvedWorkspace) {
|
||||||
@@ -4378,26 +4371,26 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async removeWorkspaceGitWatchTarget(cwd: string): Promise<void> {
|
private async removeWorkspaceGitWatchTarget(cwd: string): Promise<void> {
|
||||||
const workspaceId = normalizePersistedWorkspaceId(cwd);
|
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
||||||
const target = this.workspaceGitWatchTargets.get(workspaceId);
|
const target = this.workspaceGitWatchTargets.get(normalizedCwd);
|
||||||
if (target) {
|
if (target) {
|
||||||
this.closeWorkspaceGitWatchTarget(target);
|
this.closeWorkspaceGitWatchTarget(target);
|
||||||
this.workspaceGitWatchTargets.delete(workspaceId);
|
this.workspaceGitWatchTargets.delete(normalizedCwd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeWorkspaceGitSubscription(cwd: string): void {
|
private removeWorkspaceGitSubscription(cwd: string): void {
|
||||||
const workspaceId = normalizePersistedWorkspaceId(cwd);
|
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
||||||
const target = this.workspaceGitWatchTargets.get(workspaceId);
|
const target = this.workspaceGitWatchTargets.get(normalizedCwd);
|
||||||
if (target) {
|
if (target) {
|
||||||
const unsubscribeFetch = this.workspaceGitFetchSubscriptions.get(workspaceId);
|
const unsubscribeFetch = this.workspaceGitFetchSubscriptions.get(normalizedCwd);
|
||||||
unsubscribeFetch?.();
|
unsubscribeFetch?.();
|
||||||
this.workspaceGitFetchSubscriptions.delete(workspaceId);
|
this.workspaceGitFetchSubscriptions.delete(normalizedCwd);
|
||||||
this.closeWorkspaceGitWatchTarget(target);
|
this.closeWorkspaceGitWatchTarget(target);
|
||||||
this.workspaceGitWatchTargets.delete(workspaceId);
|
this.workspaceGitWatchTargets.delete(normalizedCwd);
|
||||||
}
|
}
|
||||||
this.workspaceGitSubscriptions.get(workspaceId)?.();
|
this.workspaceGitSubscriptions.get(normalizedCwd)?.();
|
||||||
this.workspaceGitSubscriptions.delete(workspaceId);
|
this.workspaceGitSubscriptions.delete(normalizedCwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private workspaceGitDescriptorFingerprint(workspace: WorkspaceDescriptorPayload | null): string {
|
private workspaceGitDescriptorFingerprint(workspace: WorkspaceDescriptorPayload | null): string {
|
||||||
@@ -4442,7 +4435,7 @@ export class Session {
|
|||||||
workspaces: Iterable<WorkspaceDescriptorPayload>,
|
workspaces: Iterable<WorkspaceDescriptorPayload>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const workspace of workspaces) {
|
for (const workspace of workspaces) {
|
||||||
const persistedWorkspace = await this.findWorkspaceByDirectory(workspace.id);
|
const persistedWorkspace = await this.workspaceRegistry.get(workspace.id);
|
||||||
if (!persistedWorkspace) {
|
if (!persistedWorkspace) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -4457,20 +4450,20 @@ export class Session {
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
options: { isGit: boolean },
|
options: { isGit: boolean },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const workspaceId = normalizePersistedWorkspaceId(cwd);
|
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
||||||
if (!options.isGit) {
|
if (!options.isGit) {
|
||||||
this.removeWorkspaceGitSubscription(workspaceId);
|
this.removeWorkspaceGitSubscription(normalizedCwd);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.workspaceGitSubscriptions.has(workspaceId)) {
|
if (this.workspaceGitSubscriptions.has(normalizedCwd)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscription = await this.workspaceGitService.subscribe({ cwd: workspaceId }, () => {
|
const subscription = await this.workspaceGitService.subscribe({ cwd: normalizedCwd }, () => {
|
||||||
void this.emitWorkspaceUpdateForCwd(workspaceId);
|
void this.emitWorkspaceUpdateForCwd(normalizedCwd);
|
||||||
});
|
});
|
||||||
this.workspaceGitSubscriptions.set(workspaceId, subscription.unsubscribe);
|
this.workspaceGitSubscriptions.set(normalizedCwd, subscription.unsubscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleSubscribeCheckoutDiffRequest(
|
private async handleSubscribeCheckoutDiffRequest(
|
||||||
@@ -5611,8 +5604,8 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: workspace.cwd,
|
id: workspace.workspaceId,
|
||||||
projectId: resolvedProjectRecord?.rootPath ?? workspace.cwd,
|
projectId: workspace.projectId,
|
||||||
projectDisplayName: resolvedProjectRecord?.displayName ?? String(workspace.projectId),
|
projectDisplayName: resolvedProjectRecord?.displayName ?? String(workspace.projectId),
|
||||||
projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd,
|
projectRootPath: resolvedProjectRecord?.rootPath ?? workspace.cwd,
|
||||||
workspaceDirectory: workspace.cwd,
|
workspaceDirectory: workspace.cwd,
|
||||||
@@ -5625,6 +5618,7 @@ export class Session {
|
|||||||
scripts:
|
scripts:
|
||||||
this.scriptRouteStore && this.scriptRuntimeStore
|
this.scriptRouteStore && this.scriptRuntimeStore
|
||||||
? buildWorkspaceScriptPayloads({
|
? buildWorkspaceScriptPayloads({
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
workspaceDirectory: workspace.cwd,
|
workspaceDirectory: workspace.cwd,
|
||||||
routeStore: this.scriptRouteStore,
|
routeStore: this.scriptRouteStore,
|
||||||
runtimeStore: this.scriptRuntimeStore,
|
runtimeStore: this.scriptRuntimeStore,
|
||||||
@@ -5726,24 +5720,18 @@ export class Session {
|
|||||||
.map((project) => [project.projectId, project] as const),
|
.map((project) => [project.projectId, project] as const),
|
||||||
);
|
);
|
||||||
const descriptorsByWorkspaceId = new Map<string, WorkspaceDescriptorPayload>();
|
const descriptorsByWorkspaceId = new Map<string, WorkspaceDescriptorPayload>();
|
||||||
const workspaceIds = options.workspaceIds
|
const workspaceIds = options.workspaceIds ? new Set(options.workspaceIds) : null;
|
||||||
? new Set(
|
|
||||||
Array.from(options.workspaceIds, (workspaceId) =>
|
|
||||||
normalizePersistedWorkspaceId(workspaceId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
const workspaceIdsByDirectory = new Map(
|
const workspaceIdsByDirectory = new Map(
|
||||||
activeRecords.map((workspace) => [workspace.cwd, workspace.cwd] as const),
|
activeRecords.map((workspace) => [workspace.cwd, workspace.workspaceId] as const),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const workspace of activeRecords) {
|
for (const workspace of activeRecords) {
|
||||||
if (workspaceIds && !workspaceIds.has(workspace.cwd)) {
|
if (workspaceIds && !workspaceIds.has(workspace.workspaceId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const projectRecord = activeProjects.get(workspace.projectId) ?? null;
|
const projectRecord = activeProjects.get(workspace.projectId) ?? null;
|
||||||
descriptorsByWorkspaceId.set(
|
descriptorsByWorkspaceId.set(
|
||||||
workspace.cwd,
|
workspace.workspaceId,
|
||||||
await this.buildWorkspaceDescriptor({
|
await this.buildWorkspaceDescriptor({
|
||||||
workspace,
|
workspace,
|
||||||
projectRecord,
|
projectRecord,
|
||||||
@@ -5785,7 +5773,7 @@ export class Session {
|
|||||||
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
const normalizedCwd = normalizePersistedWorkspaceId(cwd);
|
||||||
const exact = workspaces.find((workspace) => workspace.cwd === normalizedCwd);
|
const exact = workspaces.find((workspace) => workspace.cwd === normalizedCwd);
|
||||||
if (exact) {
|
if (exact) {
|
||||||
return exact.cwd;
|
return exact.workspaceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bestMatch: PersistedWorkspaceRecord | null = null;
|
let bestMatch: PersistedWorkspaceRecord | null = null;
|
||||||
@@ -5799,7 +5787,7 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestMatch?.cwd ?? normalizedCwd;
|
return bestMatch?.workspaceId ?? normalizedCwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async listWorkspaceDescriptors(): Promise<WorkspaceDescriptorPayload[]> {
|
private async listWorkspaceDescriptors(): Promise<WorkspaceDescriptorPayload[]> {
|
||||||
@@ -6140,9 +6128,12 @@ export class Session {
|
|||||||
const workspaceRecord = createPersistedWorkspaceRecord({
|
const workspaceRecord = createPersistedWorkspaceRecord({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
projectId: placement.projectKey,
|
projectId: placement.projectKey,
|
||||||
cwd: workspaceId,
|
cwd: normalizedCwd,
|
||||||
kind: deriveWorkspaceKind(placement.checkout),
|
kind: deriveWorkspaceKind(placement.checkout),
|
||||||
displayName: deriveWorkspaceDisplayName({ cwd: workspaceId, checkout: placement.checkout }),
|
displayName: deriveWorkspaceDisplayName({
|
||||||
|
cwd: normalizedCwd,
|
||||||
|
checkout: placement.checkout,
|
||||||
|
}),
|
||||||
createdAt: timestamp,
|
createdAt: timestamp,
|
||||||
updatedAt: timestamp,
|
updatedAt: timestamp,
|
||||||
});
|
});
|
||||||
@@ -6224,9 +6215,7 @@ export class Session {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueWorkspaceIds = new Set(
|
const uniqueWorkspaceIds = new Set(Array.from(workspaceIds));
|
||||||
Array.from(workspaceIds, (workspaceId) => normalizePersistedWorkspaceId(workspaceId)),
|
|
||||||
);
|
|
||||||
if (uniqueWorkspaceIds.size === 0) {
|
if (uniqueWorkspaceIds.size === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -6292,20 +6281,16 @@ export class Session {
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
options?: { skipReconcile?: boolean; dedupeGitState?: boolean },
|
options?: { skipReconcile?: boolean; dedupeGitState?: boolean },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const activeWorkspaces = (await this.workspaceRegistry.list()).filter(
|
const workspaces = await this.workspaceRegistry.list();
|
||||||
(workspace) => !workspace.archivedAt,
|
const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(cwd, workspaces);
|
||||||
);
|
|
||||||
const workspaceId = this.resolveRegisteredWorkspaceIdForCwd(cwd, activeWorkspaces);
|
|
||||||
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], options);
|
await this.emitWorkspaceUpdatesForWorkspaceIds([workspaceId], options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async emitWorkspaceUpdatesForCwds(cwds: Iterable<string>): Promise<void> {
|
private async emitWorkspaceUpdatesForCwds(cwds: Iterable<string>): Promise<void> {
|
||||||
const activeWorkspaces = (await this.workspaceRegistry.list()).filter(
|
const workspaces = await this.workspaceRegistry.list();
|
||||||
(workspace) => !workspace.archivedAt,
|
|
||||||
);
|
|
||||||
const uniqueWorkspaceIds = new Set<string>();
|
const uniqueWorkspaceIds = new Set<string>();
|
||||||
for (const cwd of cwds) {
|
for (const cwd of cwds) {
|
||||||
uniqueWorkspaceIds.add(this.resolveRegisteredWorkspaceIdForCwd(cwd, activeWorkspaces));
|
uniqueWorkspaceIds.add(this.resolveRegisteredWorkspaceIdForCwd(cwd, workspaces));
|
||||||
}
|
}
|
||||||
await this.emitWorkspaceUpdatesForWorkspaceIds(uniqueWorkspaceIds);
|
await this.emitWorkspaceUpdatesForWorkspaceIds(uniqueWorkspaceIds);
|
||||||
}
|
}
|
||||||
@@ -6484,12 +6469,14 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildWorkspaceScriptPayloadSnapshot(
|
private buildWorkspaceScriptPayloadSnapshot(
|
||||||
|
workspaceId: string,
|
||||||
workspaceDirectory: string,
|
workspaceDirectory: string,
|
||||||
): WorkspaceDescriptorPayload["scripts"] {
|
): WorkspaceDescriptorPayload["scripts"] {
|
||||||
if (!this.scriptRouteStore || !this.scriptRuntimeStore) {
|
if (!this.scriptRouteStore || !this.scriptRuntimeStore) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return buildWorkspaceScriptPayloads({
|
return buildWorkspaceScriptPayloads({
|
||||||
|
workspaceId,
|
||||||
workspaceDirectory,
|
workspaceDirectory,
|
||||||
routeStore: this.scriptRouteStore,
|
routeStore: this.scriptRouteStore,
|
||||||
runtimeStore: this.scriptRuntimeStore,
|
runtimeStore: this.scriptRuntimeStore,
|
||||||
@@ -6498,12 +6485,12 @@ export class Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitWorkspaceScriptStatusUpdate(workspaceDirectory: string): void {
|
private emitWorkspaceScriptStatusUpdate(workspaceId: string, workspaceDirectory: string): void {
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "script_status_update",
|
type: "script_status_update",
|
||||||
payload: {
|
payload: {
|
||||||
workspaceId: workspaceDirectory,
|
workspaceId,
|
||||||
scripts: this.buildWorkspaceScriptPayloadSnapshot(workspaceDirectory),
|
scripts: this.buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -6524,14 +6511,14 @@ export class Session {
|
|||||||
throw new Error("Workspace scripts are not available on this daemon");
|
throw new Error("Workspace scripts are not available on this daemon");
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = await this.resolveWorkspaceByIdOrDirectory(request.workspaceId);
|
const workspace = await this.workspaceRegistry.get(request.workspaceId);
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new Error(`Workspace not found: ${request.workspaceId}`);
|
throw new Error(`Workspace not found: ${request.workspaceId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceResult = await spawnWorkspaceScript({
|
const serviceResult = await spawnWorkspaceScript({
|
||||||
repoRoot: workspace.cwd,
|
repoRoot: workspace.cwd,
|
||||||
workspaceId: workspace.cwd,
|
workspaceId: workspace.workspaceId,
|
||||||
branchName: readGitCommand(workspace.cwd, "git symbolic-ref --short HEAD"),
|
branchName: readGitCommand(workspace.cwd, "git symbolic-ref --short HEAD"),
|
||||||
scriptName: request.scriptName,
|
scriptName: request.scriptName,
|
||||||
daemonPort: this.getDaemonTcpPort?.() ?? null,
|
daemonPort: this.getDaemonTcpPort?.() ?? null,
|
||||||
@@ -6541,11 +6528,11 @@ export class Session {
|
|||||||
terminalManager: this.terminalManager,
|
terminalManager: this.terminalManager,
|
||||||
logger: this.sessionLogger,
|
logger: this.sessionLogger,
|
||||||
onLifecycleChanged: () => {
|
onLifecycleChanged: () => {
|
||||||
this.emitWorkspaceScriptStatusUpdate(workspace.cwd);
|
this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.emitWorkspaceScriptStatusUpdate(workspace.cwd);
|
this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd);
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "start_workspace_script_response",
|
type: "start_workspace_script_response",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -6687,8 +6674,8 @@ export class Session {
|
|||||||
scriptRuntimeStore: this.scriptRuntimeStore,
|
scriptRuntimeStore: this.scriptRuntimeStore,
|
||||||
getDaemonTcpPort: this.getDaemonTcpPort,
|
getDaemonTcpPort: this.getDaemonTcpPort,
|
||||||
getDaemonTcpHost: this.getDaemonTcpHost,
|
getDaemonTcpHost: this.getDaemonTcpHost,
|
||||||
onScriptsChanged: (workspaceDirectory) => {
|
onScriptsChanged: (workspaceId, workspaceDirectory) => {
|
||||||
this.emitWorkspaceScriptStatusUpdate(workspaceDirectory);
|
this.emitWorkspaceScriptStatusUpdate(workspaceId, workspaceDirectory);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
@@ -6702,7 +6689,6 @@ export class Session {
|
|||||||
{
|
{
|
||||||
emit: (message) => this.emit(message),
|
emit: (message) => this.emit(message),
|
||||||
workspaceSetupSnapshots: this.workspaceSetupSnapshots,
|
workspaceSetupSnapshots: this.workspaceSetupSnapshots,
|
||||||
workspaceRegistry: this.workspaceRegistry,
|
|
||||||
},
|
},
|
||||||
request,
|
request,
|
||||||
);
|
);
|
||||||
@@ -6712,7 +6698,7 @@ export class Session {
|
|||||||
request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>,
|
request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const existing = await this.resolveWorkspaceByIdOrDirectory(request.workspaceId);
|
const existing = await this.workspaceRegistry.get(request.workspaceId);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
throw new Error(`Workspace not found: ${request.workspaceId}`);
|
throw new Error(`Workspace not found: ${request.workspaceId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,8 +259,8 @@ describe("workspace git watch targets", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let descriptor = {
|
let descriptor = {
|
||||||
id: "/tmp/repo",
|
id: "ws-10",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-1",
|
||||||
projectDisplayName: "repo",
|
projectDisplayName: "repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
@@ -304,7 +304,7 @@ describe("workspace git watch targets", () => {
|
|||||||
expect(workspaceUpdates[0]?.payload).toMatchObject({
|
expect(workspaceUpdates[0]?.payload).toMatchObject({
|
||||||
kind: "upsert",
|
kind: "upsert",
|
||||||
workspace: {
|
workspace: {
|
||||||
id: "/tmp/repo",
|
id: "ws-10",
|
||||||
name: "renamed-branch",
|
name: "renamed-branch",
|
||||||
diffStat: { additions: 1, deletions: 0 },
|
diffStat: { additions: 1, deletions: 0 },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -899,8 +899,8 @@ describe("workspace aggregation", () => {
|
|||||||
const session = createSessionForWorkspaceTests() as any;
|
const session = createSessionForWorkspaceTests() as any;
|
||||||
session.workspaceRegistry.list = async () => [
|
session.workspaceRegistry.list = async () => [
|
||||||
createPersistedWorkspaceRecord({
|
createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/non-git",
|
workspaceId: "ws-non-git",
|
||||||
projectId: "/tmp/non-git",
|
projectId: "proj-non-git",
|
||||||
cwd: "/tmp/non-git",
|
cwd: "/tmp/non-git",
|
||||||
kind: "directory",
|
kind: "directory",
|
||||||
displayName: "non-git",
|
displayName: "non-git",
|
||||||
@@ -930,8 +930,8 @@ describe("workspace aggregation", () => {
|
|||||||
const session = createSessionForWorkspaceTests() as any;
|
const session = createSessionForWorkspaceTests() as any;
|
||||||
session.workspaceRegistry.list = async () => [
|
session.workspaceRegistry.list = async () => [
|
||||||
createPersistedWorkspaceRecord({
|
createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/repo-branch",
|
workspaceId: "ws-repo-branch",
|
||||||
projectId: "/tmp/repo-branch",
|
projectId: "proj-repo-branch",
|
||||||
cwd: "/tmp/repo-branch",
|
cwd: "/tmp/repo-branch",
|
||||||
kind: "local_checkout",
|
kind: "local_checkout",
|
||||||
displayName: "feature/name-from-server",
|
displayName: "feature/name-from-server",
|
||||||
@@ -973,8 +973,8 @@ describe("workspace aggregation", () => {
|
|||||||
const session = createSessionForWorkspaceTests() as any;
|
const session = createSessionForWorkspaceTests() as any;
|
||||||
session.workspaceRegistry.list = async () => [
|
session.workspaceRegistry.list = async () => [
|
||||||
createPersistedWorkspaceRecord({
|
createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-repo-status",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-repo-status",
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
kind: "local_checkout",
|
kind: "local_checkout",
|
||||||
displayName: "repo",
|
displayName: "repo",
|
||||||
@@ -1017,8 +1017,8 @@ describe("workspace aggregation", () => {
|
|||||||
const session = createSessionForWorkspaceTests() as any;
|
const session = createSessionForWorkspaceTests() as any;
|
||||||
session.workspaceRegistry.list = async () => [
|
session.workspaceRegistry.list = async () => [
|
||||||
createPersistedWorkspaceRecord({
|
createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-repo-subdir",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-repo-subdir",
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
kind: "local_checkout",
|
kind: "local_checkout",
|
||||||
displayName: "main",
|
displayName: "main",
|
||||||
@@ -1042,7 +1042,7 @@ describe("workspace aggregation", () => {
|
|||||||
|
|
||||||
expect(result.entries).toHaveLength(1);
|
expect(result.entries).toHaveLength(1);
|
||||||
expect(result.entries[0]).toMatchObject({
|
expect(result.entries[0]).toMatchObject({
|
||||||
id: "/tmp/repo",
|
id: "ws-repo-subdir",
|
||||||
status: "done",
|
status: "done",
|
||||||
activityAt: null,
|
activityAt: null,
|
||||||
});
|
});
|
||||||
@@ -1128,8 +1128,8 @@ describe("workspace aggregation", () => {
|
|||||||
[
|
[
|
||||||
"/tmp/repo",
|
"/tmp/repo",
|
||||||
{
|
{
|
||||||
id: "/tmp/repo",
|
id: "ws-repo-running",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-repo-running",
|
||||||
projectDisplayName: "repo",
|
projectDisplayName: "repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
projectKind: "non_git",
|
projectKind: "non_git",
|
||||||
@@ -1147,8 +1147,8 @@ describe("workspace aggregation", () => {
|
|||||||
[
|
[
|
||||||
"/tmp/repo",
|
"/tmp/repo",
|
||||||
{
|
{
|
||||||
id: "/tmp/repo",
|
id: "ws-repo-running",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-repo-running",
|
||||||
projectDisplayName: "repo",
|
projectDisplayName: "repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
projectKind: "non_git",
|
projectKind: "non_git",
|
||||||
@@ -1167,8 +1167,8 @@ describe("workspace aggregation", () => {
|
|||||||
expect((workspaceUpdates[1] as any).payload).toEqual({
|
expect((workspaceUpdates[1] as any).payload).toEqual({
|
||||||
kind: "upsert",
|
kind: "upsert",
|
||||||
workspace: {
|
workspace: {
|
||||||
id: "/tmp/repo",
|
id: "ws-repo-running",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-repo-running",
|
||||||
projectDisplayName: "repo",
|
projectDisplayName: "repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
projectKind: "non_git",
|
projectKind: "non_git",
|
||||||
@@ -1280,6 +1280,26 @@ describe("workspace aggregation", () => {
|
|||||||
test("workspace update fanout for multiple cwd values is deduplicated", async () => {
|
test("workspace update fanout for multiple cwd values is deduplicated", async () => {
|
||||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||||
const session = createSessionForWorkspaceTests() as any;
|
const session = createSessionForWorkspaceTests() as any;
|
||||||
|
session.workspaceRegistry.list = async () => [
|
||||||
|
createPersistedWorkspaceRecord({
|
||||||
|
workspaceId: "ws-repo-main",
|
||||||
|
projectId: "proj-repo-main",
|
||||||
|
cwd: "/tmp/repo",
|
||||||
|
kind: "local_checkout",
|
||||||
|
displayName: "main",
|
||||||
|
createdAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
}),
|
||||||
|
createPersistedWorkspaceRecord({
|
||||||
|
workspaceId: "ws-repo-feature",
|
||||||
|
projectId: "proj-repo-main",
|
||||||
|
cwd: "/tmp/repo/worktree",
|
||||||
|
kind: "worktree",
|
||||||
|
displayName: "feature",
|
||||||
|
createdAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
}),
|
||||||
|
];
|
||||||
session.workspaceUpdatesSubscription = {
|
session.workspaceUpdatesSubscription = {
|
||||||
subscriptionId: "sub-dedup",
|
subscriptionId: "sub-dedup",
|
||||||
filter: undefined,
|
filter: undefined,
|
||||||
@@ -1288,14 +1308,14 @@ describe("workspace aggregation", () => {
|
|||||||
lastEmittedByWorkspaceId: new Map(),
|
lastEmittedByWorkspaceId: new Map(),
|
||||||
};
|
};
|
||||||
session.reconcileActiveWorkspaceRecords = async () =>
|
session.reconcileActiveWorkspaceRecords = async () =>
|
||||||
new Set(["/tmp/repo", "/tmp/repo/worktree"]);
|
new Set(["ws-repo-main", "ws-repo-feature"]);
|
||||||
session.buildWorkspaceDescriptorMap = async () =>
|
session.buildWorkspaceDescriptorMap = async () =>
|
||||||
new Map([
|
new Map([
|
||||||
[
|
[
|
||||||
"/tmp/repo",
|
"ws-repo-main",
|
||||||
{
|
{
|
||||||
id: "/tmp/repo",
|
id: "ws-repo-main",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-repo-main",
|
||||||
projectDisplayName: "repo",
|
projectDisplayName: "repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
@@ -1306,10 +1326,10 @@ describe("workspace aggregation", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"/tmp/repo/worktree",
|
"ws-repo-feature",
|
||||||
{
|
{
|
||||||
id: "/tmp/repo/worktree",
|
id: "ws-repo-feature",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-repo-main",
|
||||||
projectDisplayName: "repo",
|
projectDisplayName: "repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
@@ -1333,8 +1353,8 @@ describe("workspace aggregation", () => {
|
|||||||
expect(workspaceUpdates).toHaveLength(2);
|
expect(workspaceUpdates).toHaveLength(2);
|
||||||
expect(workspaceUpdates.map((entry) => entry.payload.kind)).toEqual(["upsert", "upsert"]);
|
expect(workspaceUpdates.map((entry) => entry.payload.kind)).toEqual(["upsert", "upsert"]);
|
||||||
expect(workspaceUpdates.map((entry) => entry.payload.workspace.id).sort()).toEqual([
|
expect(workspaceUpdates.map((entry) => entry.payload.workspace.id).sort()).toEqual([
|
||||||
"/tmp/repo",
|
"ws-repo-feature",
|
||||||
"/tmp/repo/worktree",
|
"ws-repo-main",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1521,8 +1541,8 @@ describe("workspace aggregation", () => {
|
|||||||
const emitted: Array<{ type: string; payload: unknown }> = [];
|
const emitted: Array<{ type: string; payload: unknown }> = [];
|
||||||
const session = createSessionForWorkspaceTests() as any;
|
const session = createSessionForWorkspaceTests() as any;
|
||||||
const workspace = createPersistedWorkspaceRecord({
|
const workspace = createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-repo-archive",
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-repo-archive",
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
kind: "directory",
|
kind: "directory",
|
||||||
displayName: "repo",
|
displayName: "repo",
|
||||||
@@ -1540,7 +1560,7 @@ describe("workspace aggregation", () => {
|
|||||||
|
|
||||||
await session.handleMessage({
|
await session.handleMessage({
|
||||||
type: "archive_workspace_request",
|
type: "archive_workspace_request",
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-repo-archive",
|
||||||
requestId: "req-archive",
|
requestId: "req-archive",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1855,7 +1875,7 @@ describe("workspace aggregation", () => {
|
|||||||
test("listWorkspaceDescriptorsSnapshot keeps git workspaces on the baseline descriptor path", async () => {
|
test("listWorkspaceDescriptorsSnapshot keeps git workspaces on the baseline descriptor path", async () => {
|
||||||
const session = createSessionForWorkspaceTests() as any;
|
const session = createSessionForWorkspaceTests() as any;
|
||||||
const project = createPersistedProjectRecord({
|
const project = createPersistedProjectRecord({
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-baseline",
|
||||||
rootPath: "/tmp/repo",
|
rootPath: "/tmp/repo",
|
||||||
kind: "git",
|
kind: "git",
|
||||||
displayName: "repo",
|
displayName: "repo",
|
||||||
@@ -1863,7 +1883,7 @@ describe("workspace aggregation", () => {
|
|||||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
});
|
});
|
||||||
const workspace = createPersistedWorkspaceRecord({
|
const workspace = createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-baseline",
|
||||||
projectId: project.projectId,
|
projectId: project.projectId,
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
kind: "local_checkout",
|
kind: "local_checkout",
|
||||||
@@ -1942,7 +1962,7 @@ describe("workspace aggregation", () => {
|
|||||||
workspaceGitService,
|
workspaceGitService,
|
||||||
}) as any;
|
}) as any;
|
||||||
const project = createPersistedProjectRecord({
|
const project = createPersistedProjectRecord({
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-runtime-fetch",
|
||||||
rootPath: "/tmp/repo",
|
rootPath: "/tmp/repo",
|
||||||
kind: "git",
|
kind: "git",
|
||||||
displayName: "repo",
|
displayName: "repo",
|
||||||
@@ -1950,7 +1970,7 @@ describe("workspace aggregation", () => {
|
|||||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
});
|
});
|
||||||
const workspace = createPersistedWorkspaceRecord({
|
const workspace = createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-runtime-fetch",
|
||||||
projectId: project.projectId,
|
projectId: project.projectId,
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
kind: "local_checkout",
|
kind: "local_checkout",
|
||||||
@@ -1989,7 +2009,7 @@ describe("workspace aggregation", () => {
|
|||||||
expect(workspaceGitService.peekSnapshot).toHaveBeenCalledWith("/tmp/repo");
|
expect(workspaceGitService.peekSnapshot).toHaveBeenCalledWith("/tmp/repo");
|
||||||
expect(response?.payload.entries).toEqual([
|
expect(response?.payload.entries).toEqual([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: "/tmp/repo",
|
id: "ws-runtime-fetch",
|
||||||
gitRuntime: {
|
gitRuntime: {
|
||||||
currentBranch: "runtime-branch",
|
currentBranch: "runtime-branch",
|
||||||
remoteUrl: "https://github.com/acme/repo.git",
|
remoteUrl: "https://github.com/acme/repo.git",
|
||||||
@@ -2042,7 +2062,7 @@ describe("workspace aggregation", () => {
|
|||||||
workspaceGitService,
|
workspaceGitService,
|
||||||
}) as any;
|
}) as any;
|
||||||
const project = createPersistedProjectRecord({
|
const project = createPersistedProjectRecord({
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-runtime-update",
|
||||||
rootPath: "/tmp/repo",
|
rootPath: "/tmp/repo",
|
||||||
kind: "git",
|
kind: "git",
|
||||||
displayName: "repo",
|
displayName: "repo",
|
||||||
@@ -2050,7 +2070,7 @@ describe("workspace aggregation", () => {
|
|||||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
});
|
});
|
||||||
const workspace = createPersistedWorkspaceRecord({
|
const workspace = createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-runtime-update",
|
||||||
projectId: project.projectId,
|
projectId: project.projectId,
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
kind: "local_checkout",
|
kind: "local_checkout",
|
||||||
@@ -2095,7 +2115,7 @@ describe("workspace aggregation", () => {
|
|||||||
payload: {
|
payload: {
|
||||||
kind: "upsert",
|
kind: "upsert",
|
||||||
workspace: expect.objectContaining({
|
workspace: expect.objectContaining({
|
||||||
id: "/tmp/repo",
|
id: "ws-runtime-update",
|
||||||
gitRuntime: expect.objectContaining({
|
gitRuntime: expect.objectContaining({
|
||||||
currentBranch: "feature/runtime-payloads",
|
currentBranch: "feature/runtime-payloads",
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
@@ -2117,7 +2137,7 @@ describe("workspace aggregation", () => {
|
|||||||
const emitted: Array<{ type: string; payload: any }> = [];
|
const emitted: Array<{ type: string; payload: any }> = [];
|
||||||
const session = createSessionForWorkspaceTests() as any;
|
const session = createSessionForWorkspaceTests() as any;
|
||||||
const gitProject = createPersistedProjectRecord({
|
const gitProject = createPersistedProjectRecord({
|
||||||
projectId: "/tmp/repo",
|
projectId: "proj-git-subscribe",
|
||||||
rootPath: "/tmp/repo",
|
rootPath: "/tmp/repo",
|
||||||
kind: "git",
|
kind: "git",
|
||||||
displayName: "repo",
|
displayName: "repo",
|
||||||
@@ -2125,7 +2145,7 @@ describe("workspace aggregation", () => {
|
|||||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
});
|
});
|
||||||
const directoryProject = createPersistedProjectRecord({
|
const directoryProject = createPersistedProjectRecord({
|
||||||
projectId: "/tmp/docs",
|
projectId: "proj-docs-subscribe",
|
||||||
rootPath: "/tmp/docs",
|
rootPath: "/tmp/docs",
|
||||||
kind: "non_git",
|
kind: "non_git",
|
||||||
displayName: "docs",
|
displayName: "docs",
|
||||||
@@ -2133,7 +2153,7 @@ describe("workspace aggregation", () => {
|
|||||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
});
|
});
|
||||||
const gitWorkspace = createPersistedWorkspaceRecord({
|
const gitWorkspace = createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/repo",
|
workspaceId: "ws-git-subscribe",
|
||||||
projectId: gitProject.projectId,
|
projectId: gitProject.projectId,
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
kind: "local_checkout",
|
kind: "local_checkout",
|
||||||
@@ -2142,7 +2162,7 @@ describe("workspace aggregation", () => {
|
|||||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
});
|
});
|
||||||
const directoryWorkspace = createPersistedWorkspaceRecord({
|
const directoryWorkspace = createPersistedWorkspaceRecord({
|
||||||
workspaceId: "/tmp/docs",
|
workspaceId: "ws-docs-subscribe",
|
||||||
projectId: directoryProject.projectId,
|
projectId: directoryProject.projectId,
|
||||||
cwd: "/tmp/docs",
|
cwd: "/tmp/docs",
|
||||||
kind: "directory",
|
kind: "directory",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export async function bootstrapWorkspaceRegistries(options: {
|
|||||||
|
|
||||||
for (const [workspaceId, entry] of recordsByWorkspaceId.entries()) {
|
for (const [workspaceId, entry] of recordsByWorkspaceId.entries()) {
|
||||||
const { placement, records: workspaceRecords } = entry;
|
const { placement, records: workspaceRecords } = entry;
|
||||||
|
const workspaceCwd = placement.checkout.cwd;
|
||||||
let workspaceCreatedAt: string | null = null;
|
let workspaceCreatedAt: string | null = null;
|
||||||
let workspaceUpdatedAt: string | null = null;
|
let workspaceUpdatedAt: string | null = null;
|
||||||
for (const record of workspaceRecords) {
|
for (const record of workspaceRecords) {
|
||||||
@@ -106,10 +107,10 @@ export async function bootstrapWorkspaceRegistries(options: {
|
|||||||
createPersistedWorkspaceRecord({
|
createPersistedWorkspaceRecord({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
projectId: placement.projectKey,
|
projectId: placement.projectKey,
|
||||||
cwd: workspaceId,
|
cwd: workspaceCwd,
|
||||||
kind: deriveWorkspaceKind(placement.checkout),
|
kind: deriveWorkspaceKind(placement.checkout),
|
||||||
displayName: deriveWorkspaceDisplayName({
|
displayName: deriveWorkspaceDisplayName({
|
||||||
cwd: workspaceId,
|
cwd: workspaceCwd,
|
||||||
checkout: placement.checkout,
|
checkout: placement.checkout,
|
||||||
}),
|
}),
|
||||||
createdAt,
|
createdAt,
|
||||||
@@ -129,7 +130,7 @@ export async function bootstrapWorkspaceRegistries(options: {
|
|||||||
createPersistedProjectRecord({
|
createPersistedProjectRecord({
|
||||||
projectId: placement.projectKey,
|
projectId: placement.projectKey,
|
||||||
rootPath: deriveProjectRootPath({
|
rootPath: deriveProjectRootPath({
|
||||||
cwd: workspaceId,
|
cwd: workspaceCwd,
|
||||||
checkout: placement.checkout,
|
checkout: placement.checkout,
|
||||||
}),
|
}),
|
||||||
kind: deriveProjectKind(placement.checkout),
|
kind: deriveProjectKind(placement.checkout),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
|
|
||||||
function createEntry(overrides: Partial<ScriptRuntimeEntry> = {}): ScriptRuntimeEntry {
|
function createEntry(overrides: Partial<ScriptRuntimeEntry> = {}): ScriptRuntimeEntry {
|
||||||
return {
|
return {
|
||||||
workspaceId: 101,
|
workspaceId: "workspace-101",
|
||||||
scriptName: "web",
|
scriptName: "web",
|
||||||
type: "service",
|
type: "service",
|
||||||
lifecycle: "running",
|
lifecycle: "running",
|
||||||
@@ -23,8 +23,8 @@ describe("WorkspaceScriptRuntimeStore", () => {
|
|||||||
|
|
||||||
store.set(entry);
|
store.set(entry);
|
||||||
|
|
||||||
expect(store.get({ workspaceId: 101, scriptName: "web" })).toEqual(entry);
|
expect(store.get({ workspaceId: "workspace-101", scriptName: "web" })).toEqual(entry);
|
||||||
expect(store.listForWorkspace(101)).toEqual([entry]);
|
expect(store.listForWorkspace("workspace-101")).toEqual([entry]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves whether the runtime entry is a plain script or service", () => {
|
it("preserves whether the runtime entry is a plain script or service", () => {
|
||||||
@@ -36,7 +36,7 @@ describe("WorkspaceScriptRuntimeStore", () => {
|
|||||||
|
|
||||||
store.set(entry);
|
store.set(entry);
|
||||||
|
|
||||||
expect(store.get({ workspaceId: 101, scriptName: "typecheck" })).toEqual(entry);
|
expect(store.get({ workspaceId: "workspace-101", scriptName: "typecheck" })).toEqual(entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reports whether a script is currently running", () => {
|
it("reports whether a script is currently running", () => {
|
||||||
@@ -44,7 +44,7 @@ describe("WorkspaceScriptRuntimeStore", () => {
|
|||||||
store.set(createEntry());
|
store.set(createEntry());
|
||||||
store.set(
|
store.set(
|
||||||
createEntry({
|
createEntry({
|
||||||
workspaceId: 101,
|
workspaceId: "workspace-101",
|
||||||
scriptName: "typecheck",
|
scriptName: "typecheck",
|
||||||
lifecycle: "stopped",
|
lifecycle: "stopped",
|
||||||
terminalId: "terminal-2",
|
terminalId: "terminal-2",
|
||||||
@@ -52,19 +52,19 @@ describe("WorkspaceScriptRuntimeStore", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(store.isRunning({ workspaceId: 101, scriptName: "web" })).toBe(true);
|
expect(store.isRunning({ workspaceId: "workspace-101", scriptName: "web" })).toBe(true);
|
||||||
expect(store.isRunning({ workspaceId: 101, scriptName: "typecheck" })).toBe(false);
|
expect(store.isRunning({ workspaceId: "workspace-101", scriptName: "typecheck" })).toBe(false);
|
||||||
expect(store.isRunning({ workspaceId: 101, scriptName: "missing" })).toBe(false);
|
expect(store.isRunning({ workspaceId: "workspace-101", scriptName: "missing" })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes individual entries", () => {
|
it("removes individual entries", () => {
|
||||||
const store = new WorkspaceScriptRuntimeStore();
|
const store = new WorkspaceScriptRuntimeStore();
|
||||||
store.set(createEntry());
|
store.set(createEntry());
|
||||||
|
|
||||||
store.remove({ workspaceId: 101, scriptName: "web" });
|
store.remove({ workspaceId: "workspace-101", scriptName: "web" });
|
||||||
|
|
||||||
expect(store.get({ workspaceId: 101, scriptName: "web" })).toBeNull();
|
expect(store.get({ workspaceId: "workspace-101", scriptName: "web" })).toBeNull();
|
||||||
expect(store.listForWorkspace(101)).toEqual([]);
|
expect(store.listForWorkspace("workspace-101")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes all entries for a workspace without touching others", () => {
|
it("removes all entries for a workspace without touching others", () => {
|
||||||
@@ -72,25 +72,25 @@ describe("WorkspaceScriptRuntimeStore", () => {
|
|||||||
store.set(createEntry());
|
store.set(createEntry());
|
||||||
store.set(
|
store.set(
|
||||||
createEntry({
|
createEntry({
|
||||||
workspaceId: 101,
|
workspaceId: "workspace-101",
|
||||||
scriptName: "api",
|
scriptName: "api",
|
||||||
terminalId: "terminal-2",
|
terminalId: "terminal-2",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
store.set(
|
store.set(
|
||||||
createEntry({
|
createEntry({
|
||||||
workspaceId: 202,
|
workspaceId: "workspace-202",
|
||||||
scriptName: "docs",
|
scriptName: "docs",
|
||||||
terminalId: "terminal-3",
|
terminalId: "terminal-3",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
store.removeForWorkspace(101);
|
store.removeForWorkspace("workspace-101");
|
||||||
|
|
||||||
expect(store.listForWorkspace(101)).toEqual([]);
|
expect(store.listForWorkspace("workspace-101")).toEqual([]);
|
||||||
expect(store.listForWorkspace(202)).toEqual([
|
expect(store.listForWorkspace("workspace-202")).toEqual([
|
||||||
createEntry({
|
createEntry({
|
||||||
workspaceId: 202,
|
workspaceId: "workspace-202",
|
||||||
scriptName: "docs",
|
scriptName: "docs",
|
||||||
terminalId: "terminal-3",
|
terminalId: "terminal-3",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface ScriptRuntimeEntry {
|
export interface ScriptRuntimeEntry {
|
||||||
workspaceId: number | string;
|
workspaceId: string;
|
||||||
scriptName: string;
|
scriptName: string;
|
||||||
type: "script" | "service";
|
type: "script" | "service";
|
||||||
lifecycle: "running" | "stopped";
|
lifecycle: "running" | "stopped";
|
||||||
@@ -8,7 +8,7 @@ export interface ScriptRuntimeEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeEntryKey = {
|
type RuntimeEntryKey = {
|
||||||
workspaceId: number | string;
|
workspaceId: string;
|
||||||
scriptName: string;
|
scriptName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export class WorkspaceScriptRuntimeStore {
|
|||||||
this.removeScriptFromWorkspaceIndex(existing.workspaceId, existing.scriptName);
|
this.removeScriptFromWorkspaceIndex(existing.workspaceId, existing.scriptName);
|
||||||
}
|
}
|
||||||
|
|
||||||
listForWorkspace(workspaceId: number | string): ScriptRuntimeEntry[] {
|
listForWorkspace(workspaceId: string): ScriptRuntimeEntry[] {
|
||||||
const scriptNames = this.scriptsByWorkspace.get(this.toWorkspaceKey(workspaceId));
|
const scriptNames = this.scriptsByWorkspace.get(this.toWorkspaceKey(workspaceId));
|
||||||
if (!scriptNames) {
|
if (!scriptNames) {
|
||||||
return [];
|
return [];
|
||||||
@@ -65,7 +65,7 @@ export class WorkspaceScriptRuntimeStore {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeForWorkspace(workspaceId: number | string): void {
|
removeForWorkspace(workspaceId: string): void {
|
||||||
for (const entry of this.listForWorkspace(workspaceId)) {
|
for (const entry of this.listForWorkspace(workspaceId)) {
|
||||||
this.entries.delete(this.toEntryKey(entry));
|
this.entries.delete(this.toEntryKey(entry));
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ export class WorkspaceScriptRuntimeStore {
|
|||||||
this.scriptsByWorkspace.set(workspaceKey, scripts);
|
this.scriptsByWorkspace.set(workspaceKey, scripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeScriptFromWorkspaceIndex(workspaceId: number | string, scriptName: string): void {
|
private removeScriptFromWorkspaceIndex(workspaceId: string, scriptName: string): void {
|
||||||
const workspaceKey = this.toWorkspaceKey(workspaceId);
|
const workspaceKey = this.toWorkspaceKey(workspaceId);
|
||||||
const scripts = this.scriptsByWorkspace.get(workspaceKey);
|
const scripts = this.scriptsByWorkspace.get(workspaceKey);
|
||||||
if (!scripts) {
|
if (!scripts) {
|
||||||
@@ -99,7 +99,7 @@ export class WorkspaceScriptRuntimeStore {
|
|||||||
return `${this.toWorkspaceKey(key.workspaceId)}::${key.scriptName}`;
|
return `${this.toWorkspaceKey(key.workspaceId)}::${key.scriptName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private toWorkspaceKey(workspaceId: number | string): string {
|
private toWorkspaceKey(workspaceId: string): string {
|
||||||
return String(workspaceId);
|
return workspaceId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ describe("runWorktreeSetupInBackground", () => {
|
|||||||
const logger = createLogger();
|
const logger = createLogger();
|
||||||
const emitWorkspaceUpdateForCwd = vi.fn(async () => {});
|
const emitWorkspaceUpdateForCwd = vi.fn(async () => {});
|
||||||
const archiveWorkspaceRecord = vi.fn(async () => {});
|
const archiveWorkspaceRecord = vi.fn(async () => {});
|
||||||
const workspaceId = 101;
|
const workspaceId = "ws-broken-feature";
|
||||||
|
|
||||||
await runWorktreeSetupInBackground(
|
await runWorktreeSetupInBackground(
|
||||||
{
|
{
|
||||||
@@ -282,7 +282,7 @@ describe("runWorktreeSetupInBackground", () => {
|
|||||||
{
|
{
|
||||||
requestCwd: repoDir,
|
requestCwd: repoDir,
|
||||||
repoRoot: repoDir,
|
repoRoot: repoDir,
|
||||||
workspaceId: String(workspaceId),
|
workspaceId,
|
||||||
worktree: {
|
worktree: {
|
||||||
branchName: "broken-feature",
|
branchName: "broken-feature",
|
||||||
worktreePath,
|
worktreePath,
|
||||||
@@ -303,11 +303,11 @@ describe("runWorktreeSetupInBackground", () => {
|
|||||||
expect(progressMessages[1]?.payload.status).toBe("failed");
|
expect(progressMessages[1]?.payload.status).toBe("failed");
|
||||||
expect(progressMessages[1]?.payload.error).toContain("Failed to parse paseo.json");
|
expect(progressMessages[1]?.payload.error).toContain("Failed to parse paseo.json");
|
||||||
expect(progressMessages[1]?.payload.detail.commands).toEqual([]);
|
expect(progressMessages[1]?.payload.detail.commands).toEqual([]);
|
||||||
expect(snapshots.get("101")).toMatchObject({
|
expect(snapshots.get(workspaceId)).toMatchObject({
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error: expect.stringContaining("Failed to parse paseo.json"),
|
error: expect.stringContaining("Failed to parse paseo.json"),
|
||||||
});
|
});
|
||||||
expect(archiveWorkspaceRecord).toHaveBeenCalledWith(String(workspaceId));
|
expect(archiveWorkspaceRecord).toHaveBeenCalledWith(workspaceId);
|
||||||
expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(worktreePath);
|
expect(emitWorkspaceUpdateForCwd).toHaveBeenCalledWith(worktreePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -671,7 +671,7 @@ describe("runWorktreeSetupInBackground", () => {
|
|||||||
const emitted: SessionOutboundMessage[] = [];
|
const emitted: SessionOutboundMessage[] = [];
|
||||||
const snapshots = new Map([
|
const snapshots = new Map([
|
||||||
[
|
[
|
||||||
"/repo/.paseo/worktrees/feature-a",
|
"ws-feature-a",
|
||||||
{
|
{
|
||||||
status: "completed",
|
status: "completed",
|
||||||
detail: {
|
detail: {
|
||||||
@@ -694,7 +694,7 @@ describe("runWorktreeSetupInBackground", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "workspace_setup_status_request",
|
type: "workspace_setup_status_request",
|
||||||
workspaceId: "/repo/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
requestId: "req-status",
|
requestId: "req-status",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -703,7 +703,7 @@ describe("runWorktreeSetupInBackground", () => {
|
|||||||
type: "workspace_setup_status_response",
|
type: "workspace_setup_status_response",
|
||||||
payload: {
|
payload: {
|
||||||
requestId: "req-status",
|
requestId: "req-status",
|
||||||
workspaceId: "/repo/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
snapshot: {
|
snapshot: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
detail: {
|
detail: {
|
||||||
@@ -730,7 +730,7 @@ describe("runWorktreeSetupInBackground", () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "workspace_setup_status_request",
|
type: "workspace_setup_status_request",
|
||||||
workspaceId: "/repo/.paseo/worktrees/missing",
|
workspaceId: "ws-missing",
|
||||||
requestId: "req-missing",
|
requestId: "req-missing",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -739,7 +739,7 @@ describe("runWorktreeSetupInBackground", () => {
|
|||||||
type: "workspace_setup_status_response",
|
type: "workspace_setup_status_response",
|
||||||
payload: {
|
payload: {
|
||||||
requestId: "req-missing",
|
requestId: "req-missing",
|
||||||
workspaceId: "/repo/.paseo/worktrees/missing",
|
workspaceId: "ws-missing",
|
||||||
snapshot: null,
|
snapshot: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -784,8 +784,8 @@ describe("handleCreatePaseoWorktreeRequest", () => {
|
|||||||
emit: (message) => emitted.push(message),
|
emit: (message) => emitted.push(message),
|
||||||
registerPendingWorktreeWorkspace: async ({ worktreePath, branchName }) =>
|
registerPendingWorktreeWorkspace: async ({ worktreePath, branchName }) =>
|
||||||
({
|
({
|
||||||
id: 1,
|
id: "ws-pr-worktree",
|
||||||
projectId: 1,
|
projectId: "proj-pr-worktree",
|
||||||
directory: worktreePath,
|
directory: worktreePath,
|
||||||
displayName: branchName,
|
displayName: branchName,
|
||||||
kind: "worktree",
|
kind: "worktree",
|
||||||
@@ -904,8 +904,8 @@ describe("handleCreatePaseoWorktreeRequest", () => {
|
|||||||
sessionLogger: createLogger(),
|
sessionLogger: createLogger(),
|
||||||
emit: (message) => emitted.push(message),
|
emit: (message) => emitted.push(message),
|
||||||
registerPendingWorktreeWorkspace: vi.fn(async (options) => ({
|
registerPendingWorktreeWorkspace: vi.fn(async (options) => ({
|
||||||
workspaceId: options.worktreePath,
|
workspaceId: "ws-single-call",
|
||||||
projectId: options.repoRoot,
|
projectId: "proj-single-call",
|
||||||
})),
|
})),
|
||||||
describeWorkspaceRecord: vi.fn(async (workspace) => ({
|
describeWorkspaceRecord: vi.fn(async (workspace) => ({
|
||||||
id: workspace.workspaceId,
|
id: workspace.workspaceId,
|
||||||
@@ -914,7 +914,7 @@ describe("handleCreatePaseoWorktreeRequest", () => {
|
|||||||
projectRootPath: repoDir,
|
projectRootPath: repoDir,
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
workspaceKind: "worktree",
|
workspaceKind: "worktree",
|
||||||
name: path.basename(workspace.workspaceId),
|
name: "single-call",
|
||||||
status: "done",
|
status: "done",
|
||||||
activityAt: null,
|
activityAt: null,
|
||||||
})),
|
})),
|
||||||
@@ -947,6 +947,7 @@ describe("handleCreatePaseoWorktreeRequest", () => {
|
|||||||
const paseoHome = path.join(tempDir, ".paseo");
|
const paseoHome = path.join(tempDir, ".paseo");
|
||||||
const emitted: SessionOutboundMessage[] = [];
|
const emitted: SessionOutboundMessage[] = [];
|
||||||
const backgroundWork = vi.fn(async () => {});
|
const backgroundWork = vi.fn(async () => {});
|
||||||
|
let registeredWorktreePath: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleCreatePaseoWorktreeRequest(
|
await handleCreatePaseoWorktreeRequest(
|
||||||
@@ -956,9 +957,10 @@ describe("handleCreatePaseoWorktreeRequest", () => {
|
|||||||
emit: (message) => emitted.push(message),
|
emit: (message) => emitted.push(message),
|
||||||
registerPendingWorktreeWorkspace: vi.fn(async (options) => {
|
registerPendingWorktreeWorkspace: vi.fn(async (options) => {
|
||||||
expect(existsSync(options.worktreePath)).toBe(true);
|
expect(existsSync(options.worktreePath)).toBe(true);
|
||||||
|
registeredWorktreePath = options.worktreePath;
|
||||||
return {
|
return {
|
||||||
workspaceId: options.worktreePath,
|
workspaceId: "ws-response-after-create",
|
||||||
projectId: options.repoRoot,
|
projectId: "proj-response-after-create",
|
||||||
} as any;
|
} as any;
|
||||||
}),
|
}),
|
||||||
describeWorkspaceRecord: vi.fn(async (workspace) => ({
|
describeWorkspaceRecord: vi.fn(async (workspace) => ({
|
||||||
@@ -968,7 +970,7 @@ describe("handleCreatePaseoWorktreeRequest", () => {
|
|||||||
projectRootPath: repoDir,
|
projectRootPath: repoDir,
|
||||||
projectKind: "git",
|
projectKind: "git",
|
||||||
workspaceKind: "worktree",
|
workspaceKind: "worktree",
|
||||||
name: path.basename(workspace.workspaceId),
|
name: "response-after-create",
|
||||||
status: "done",
|
status: "done",
|
||||||
activityAt: null,
|
activityAt: null,
|
||||||
})),
|
})),
|
||||||
@@ -990,14 +992,15 @@ describe("handleCreatePaseoWorktreeRequest", () => {
|
|||||||
);
|
);
|
||||||
expect(response?.payload.error).toBeNull();
|
expect(response?.payload.error).toBeNull();
|
||||||
expect(response?.payload.workspace?.id).toBeTruthy();
|
expect(response?.payload.workspace?.id).toBeTruthy();
|
||||||
expect(existsSync(response!.payload.workspace!.id)).toBe(true);
|
expect(registeredWorktreePath).toBeTruthy();
|
||||||
|
expect(existsSync(registeredWorktreePath!)).toBe(true);
|
||||||
expect(backgroundWork).toHaveBeenCalledWith(
|
expect(backgroundWork).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
requestCwd: repoDir,
|
requestCwd: repoDir,
|
||||||
repoRoot: repoDir,
|
repoRoot: repoDir,
|
||||||
worktree: {
|
worktree: {
|
||||||
branchName: "response-after-create",
|
branchName: "response-after-create",
|
||||||
worktreePath: response!.payload.workspace!.id,
|
worktreePath: registeredWorktreePath,
|
||||||
},
|
},
|
||||||
shouldBootstrap: true,
|
shouldBootstrap: true,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ import type {
|
|||||||
WorkspaceRegistry,
|
WorkspaceRegistry,
|
||||||
} from "./workspace-registry.js";
|
} from "./workspace-registry.js";
|
||||||
import type { WorkspaceGitService } from "./workspace-git-service.js";
|
import type { WorkspaceGitService } from "./workspace-git-service.js";
|
||||||
import { normalizeWorkspaceId as normalizePersistedWorkspaceId } from "./workspace-registry-model.js";
|
import {
|
||||||
|
deriveWorkspaceId,
|
||||||
|
normalizeWorkspaceId as normalizePersistedWorkspaceId,
|
||||||
|
} from "./workspace-registry-model.js";
|
||||||
import {
|
import {
|
||||||
applyWorktreeSetupProgressEvent,
|
applyWorktreeSetupProgressEvent,
|
||||||
buildWorktreeSetupDetail,
|
buildWorktreeSetupDetail,
|
||||||
@@ -98,12 +101,14 @@ type ArchivePaseoWorktreeDependencies = {
|
|||||||
type RegisterPendingWorktreeWorkspaceDependencies = {
|
type RegisterPendingWorktreeWorkspaceDependencies = {
|
||||||
buildPersistedProjectRecord: (input: {
|
buildPersistedProjectRecord: (input: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
cwd: string;
|
||||||
placement: ProjectPlacementPayload;
|
placement: ProjectPlacementPayload;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}) => PersistedProjectRecord;
|
}) => PersistedProjectRecord;
|
||||||
buildPersistedWorkspaceRecord: (input: {
|
buildPersistedWorkspaceRecord: (input: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
cwd: string;
|
||||||
placement: ProjectPlacementPayload;
|
placement: ProjectPlacementPayload;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -128,13 +133,12 @@ type CreatePaseoWorktreeInBackgroundDependencies = {
|
|||||||
scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
|
scriptRuntimeStore: WorkspaceScriptRuntimeStore | null;
|
||||||
getDaemonTcpPort: (() => number | null) | null;
|
getDaemonTcpPort: (() => number | null) | null;
|
||||||
getDaemonTcpHost: (() => string | null) | null;
|
getDaemonTcpHost: (() => string | null) | null;
|
||||||
onScriptsChanged: ((workspaceDirectory: string) => void) | null;
|
onScriptsChanged: ((workspaceId: string, workspaceDirectory: string) => void) | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HandleWorkspaceSetupStatusRequestDependencies = {
|
type HandleWorkspaceSetupStatusRequestDependencies = {
|
||||||
emit: EmitSessionMessage;
|
emit: EmitSessionMessage;
|
||||||
workspaceSetupSnapshots: ReadonlyMap<string, WorkspaceSetupSnapshot>;
|
workspaceSetupSnapshots: ReadonlyMap<string, WorkspaceSetupSnapshot>;
|
||||||
workspaceRegistry: WorkspaceRegistry;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type HandleCreatePaseoWorktreeRequestDependencies = {
|
type HandleCreatePaseoWorktreeRequestDependencies = {
|
||||||
@@ -649,12 +653,12 @@ export async function registerPendingWorktreeWorkspace(
|
|||||||
branchName: string;
|
branchName: string;
|
||||||
},
|
},
|
||||||
): Promise<PersistedWorkspaceRecord> {
|
): Promise<PersistedWorkspaceRecord> {
|
||||||
const workspaceDirectory = normalizePersistedWorkspaceId(options.worktreePath);
|
const normalizedWorktreePath = normalizePersistedWorkspaceId(options.worktreePath);
|
||||||
const basePlacement = await dependencies.buildProjectPlacement(options.repoRoot);
|
const basePlacement = await dependencies.buildProjectPlacement(options.repoRoot);
|
||||||
const placement: ProjectPlacementPayload = {
|
const placement: ProjectPlacementPayload = {
|
||||||
...basePlacement,
|
...basePlacement,
|
||||||
checkout: {
|
checkout: {
|
||||||
cwd: workspaceDirectory,
|
cwd: normalizedWorktreePath,
|
||||||
isGit: true,
|
isGit: true,
|
||||||
currentBranch: options.branchName,
|
currentBranch: options.branchName,
|
||||||
remoteUrl: basePlacement.checkout.remoteUrl,
|
remoteUrl: basePlacement.checkout.remoteUrl,
|
||||||
@@ -663,17 +667,20 @@ export async function registerPendingWorktreeWorkspace(
|
|||||||
mainRepoRoot: options.repoRoot,
|
mainRepoRoot: options.repoRoot,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const workspaceId = deriveWorkspaceId(normalizedWorktreePath, placement.checkout);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const existingWorkspace = await dependencies.findWorkspaceByDirectory(workspaceDirectory);
|
const existingWorkspace = await dependencies.findWorkspaceByDirectory(normalizedWorktreePath);
|
||||||
const existingProject = await dependencies.projectRegistry.get(placement.projectKey);
|
const existingProject = await dependencies.projectRegistry.get(placement.projectKey);
|
||||||
const nextProjectRecord = dependencies.buildPersistedProjectRecord({
|
const nextProjectRecord = dependencies.buildPersistedProjectRecord({
|
||||||
workspaceId: workspaceDirectory,
|
workspaceId,
|
||||||
|
cwd: normalizedWorktreePath,
|
||||||
placement,
|
placement,
|
||||||
createdAt: existingProject?.createdAt ?? now,
|
createdAt: existingProject?.createdAt ?? now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
const nextWorkspaceRecord = dependencies.buildPersistedWorkspaceRecord({
|
const nextWorkspaceRecord = dependencies.buildPersistedWorkspaceRecord({
|
||||||
workspaceId: workspaceDirectory,
|
workspaceId,
|
||||||
|
cwd: normalizedWorktreePath,
|
||||||
placement,
|
placement,
|
||||||
createdAt: existingWorkspace?.createdAt ?? now,
|
createdAt: existingWorkspace?.createdAt ?? now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -681,7 +688,7 @@ export async function registerPendingWorktreeWorkspace(
|
|||||||
|
|
||||||
await dependencies.projectRegistry.upsert(nextProjectRecord);
|
await dependencies.projectRegistry.upsert(nextProjectRecord);
|
||||||
await dependencies.workspaceRegistry.upsert(nextWorkspaceRecord);
|
await dependencies.workspaceRegistry.upsert(nextWorkspaceRecord);
|
||||||
await dependencies.syncWorkspaceGitWatchTarget(workspaceDirectory, { isGit: true });
|
await dependencies.syncWorkspaceGitWatchTarget(normalizedWorktreePath, { isGit: true });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
existingWorkspace &&
|
existingWorkspace &&
|
||||||
@@ -802,16 +809,7 @@ export async function handleWorkspaceSetupStatusRequest(
|
|||||||
request: Extract<SessionInboundMessage, { type: "workspace_setup_status_request" }>,
|
request: Extract<SessionInboundMessage, { type: "workspace_setup_status_request" }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const workspaceId = request.workspaceId;
|
const workspaceId = request.workspaceId;
|
||||||
let snapshot = dependencies.workspaceSetupSnapshots.get(workspaceId) ?? null;
|
const snapshot = dependencies.workspaceSetupSnapshots.get(workspaceId) ?? null;
|
||||||
|
|
||||||
// Fallback: if workspaceId is a directory path, resolve to numeric ID and retry lookup
|
|
||||||
if (!snapshot && Number.isNaN(Number(workspaceId))) {
|
|
||||||
const workspaces = await dependencies.workspaceRegistry.list();
|
|
||||||
const match = workspaces.find((w) => w.cwd === workspaceId && !w.archivedAt);
|
|
||||||
if (match) {
|
|
||||||
snapshot = dependencies.workspaceSetupSnapshots.get(match.workspaceId) ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies.emit({
|
dependencies.emit({
|
||||||
type: "workspace_setup_status_response",
|
type: "workspace_setup_status_response",
|
||||||
@@ -909,7 +907,7 @@ export async function runWorktreeSetupInBackground(
|
|||||||
) {
|
) {
|
||||||
await spawnWorktreeScripts({
|
await spawnWorktreeScripts({
|
||||||
repoRoot: worktree.worktreePath,
|
repoRoot: worktree.worktreePath,
|
||||||
workspaceId: worktree.worktreePath,
|
workspaceId: options.workspaceId,
|
||||||
branchName: worktree.branchName,
|
branchName: worktree.branchName,
|
||||||
daemonPort: dependencies.getDaemonTcpPort?.() ?? null,
|
daemonPort: dependencies.getDaemonTcpPort?.() ?? null,
|
||||||
daemonListenHost: dependencies.getDaemonTcpHost?.() ?? null,
|
daemonListenHost: dependencies.getDaemonTcpHost?.() ?? null,
|
||||||
@@ -918,7 +916,7 @@ export async function runWorktreeSetupInBackground(
|
|||||||
terminalManager: dependencies.terminalManager,
|
terminalManager: dependencies.terminalManager,
|
||||||
logger: dependencies.sessionLogger,
|
logger: dependencies.sessionLogger,
|
||||||
onLifecycleChanged: () => {
|
onLifecycleChanged: () => {
|
||||||
dependencies.onScriptsChanged?.(worktree.worktreePath);
|
dependencies.onScriptsChanged?.(options.workspaceId, worktree.worktreePath);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -739,7 +739,7 @@ export const FetchWorkspacesRequestMessageSchema = z.object({
|
|||||||
filter: z
|
filter: z
|
||||||
.object({
|
.object({
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
projectId: z.union([z.string(), z.number()]).transform(String).optional(),
|
projectId: z.string().optional(),
|
||||||
idPrefix: z.string().optional(),
|
idPrefix: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -843,7 +843,7 @@ export type GitSetupOptions = z.infer<typeof GitSetupOptionsSchema>;
|
|||||||
export const CreateAgentRequestMessageSchema = z.object({
|
export const CreateAgentRequestMessageSchema = z.object({
|
||||||
type: z.literal("create_agent_request"),
|
type: z.literal("create_agent_request"),
|
||||||
config: AgentSessionConfigSchema,
|
config: AgentSessionConfigSchema,
|
||||||
workspaceId: z.union([z.string(), z.number()]).transform(String).optional(),
|
workspaceId: z.string().optional(),
|
||||||
worktreeName: z.string().optional(),
|
worktreeName: z.string().optional(),
|
||||||
initialPrompt: z.string().optional(),
|
initialPrompt: z.string().optional(),
|
||||||
clientMessageId: z.string().optional(),
|
clientMessageId: z.string().optional(),
|
||||||
@@ -1285,7 +1285,7 @@ export const OpenProjectRequestSchema = z.object({
|
|||||||
|
|
||||||
export const ArchiveWorkspaceRequestSchema = z.object({
|
export const ArchiveWorkspaceRequestSchema = z.object({
|
||||||
type: z.literal("archive_workspace_request"),
|
type: z.literal("archive_workspace_request"),
|
||||||
workspaceId: z.union([z.string(), z.number()]).transform(String),
|
workspaceId: z.string(),
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1993,8 +1993,8 @@ const WorkspaceGitHubRuntimePayloadSchema = z
|
|||||||
.nullable();
|
.nullable();
|
||||||
|
|
||||||
export const WorkspaceDescriptorPayloadSchema = z.object({
|
export const WorkspaceDescriptorPayloadSchema = z.object({
|
||||||
id: z.union([z.string(), z.number()]).transform(String),
|
id: z.string(),
|
||||||
projectId: z.union([z.string(), z.number()]).transform(String),
|
projectId: z.string(),
|
||||||
projectDisplayName: z.string(),
|
projectDisplayName: z.string(),
|
||||||
projectRootPath: z.string(),
|
projectRootPath: z.string(),
|
||||||
workspaceDirectory: z.string(),
|
workspaceDirectory: z.string(),
|
||||||
@@ -2101,7 +2101,7 @@ export const WorkspaceUpdateMessageSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
kind: z.literal("remove"),
|
kind: z.literal("remove"),
|
||||||
id: z.union([z.string(), z.number()]).transform(String),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
@@ -2180,7 +2180,7 @@ export const ArchiveWorkspaceResponseMessageSchema = z.object({
|
|||||||
type: z.literal("archive_workspace_response"),
|
type: z.literal("archive_workspace_response"),
|
||||||
payload: z.object({
|
payload: z.object({
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
workspaceId: z.union([z.string(), z.number()]).transform(String),
|
workspaceId: z.string(),
|
||||||
archivedAt: z.string().nullable(),
|
archivedAt: z.string().nullable(),
|
||||||
error: z.string().nullable(),
|
error: z.string().nullable(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe("workspace message schemas", () => {
|
|||||||
requestId: "req-1",
|
requestId: "req-1",
|
||||||
filter: {
|
filter: {
|
||||||
query: "repo",
|
query: "repo",
|
||||||
projectId: 12,
|
projectId: "proj-12",
|
||||||
idPrefix: "/Users/me",
|
idPrefix: "/Users/me",
|
||||||
},
|
},
|
||||||
sort: [{ key: "activity_at", direction: "desc" }],
|
sort: [{ key: "activity_at", direction: "desc" }],
|
||||||
@@ -102,8 +102,8 @@ describe("workspace message schemas", () => {
|
|||||||
payload: {
|
payload: {
|
||||||
kind: "upsert",
|
kind: "upsert",
|
||||||
workspace: {
|
workspace: {
|
||||||
id: 1,
|
id: "ws-invalid",
|
||||||
projectId: 1,
|
projectId: "proj-invalid",
|
||||||
projectDisplayName: "repo",
|
projectDisplayName: "repo",
|
||||||
projectRootPath: "/repo",
|
projectRootPath: "/repo",
|
||||||
projectKind: "directory",
|
projectKind: "directory",
|
||||||
@@ -125,8 +125,8 @@ describe("workspace message schemas", () => {
|
|||||||
payload: {
|
payload: {
|
||||||
kind: "upsert",
|
kind: "upsert",
|
||||||
workspace: {
|
workspace: {
|
||||||
id: 1,
|
id: "ws-1",
|
||||||
projectId: 1,
|
projectId: "proj-1",
|
||||||
projectDisplayName: "repo",
|
projectDisplayName: "repo",
|
||||||
projectRootPath: "/repo",
|
projectRootPath: "/repo",
|
||||||
workspaceDirectory: "/repo",
|
workspaceDirectory: "/repo",
|
||||||
@@ -200,7 +200,7 @@ describe("workspace message schemas", () => {
|
|||||||
const parsed = SessionOutboundMessageSchema.parse({
|
const parsed = SessionOutboundMessageSchema.parse({
|
||||||
type: "script_status_update",
|
type: "script_status_update",
|
||||||
payload: {
|
payload: {
|
||||||
workspaceId: "/repo",
|
workspaceId: "ws-repo",
|
||||||
scripts: [
|
scripts: [
|
||||||
{
|
{
|
||||||
scriptName: "web",
|
scriptName: "web",
|
||||||
@@ -215,7 +215,7 @@ describe("workspace message schemas", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(parsed.type).toBe("script_status_update");
|
expect(parsed.type).toBe("script_status_update");
|
||||||
expect(parsed.payload.workspaceId).toBe("/repo");
|
expect(parsed.payload.workspaceId).toBe("ws-repo");
|
||||||
expect(parsed.payload.scripts[0]).toMatchObject({
|
expect(parsed.payload.scripts[0]).toMatchObject({
|
||||||
type: "service",
|
type: "service",
|
||||||
exitCode: null,
|
exitCode: null,
|
||||||
@@ -226,7 +226,7 @@ describe("workspace message schemas", () => {
|
|||||||
const parsed = SessionOutboundMessageSchema.parse({
|
const parsed = SessionOutboundMessageSchema.parse({
|
||||||
type: "workspace_setup_progress",
|
type: "workspace_setup_progress",
|
||||||
payload: {
|
payload: {
|
||||||
workspaceId: "/repo/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
detail: {
|
detail: {
|
||||||
type: "worktree_setup",
|
type: "worktree_setup",
|
||||||
@@ -255,7 +255,7 @@ describe("workspace message schemas", () => {
|
|||||||
test("parses workspace_setup_status_request", () => {
|
test("parses workspace_setup_status_request", () => {
|
||||||
const parsed = SessionInboundMessageSchema.parse({
|
const parsed = SessionInboundMessageSchema.parse({
|
||||||
type: "workspace_setup_status_request",
|
type: "workspace_setup_status_request",
|
||||||
workspaceId: "/repo/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
requestId: "req-status",
|
requestId: "req-status",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ describe("workspace message schemas", () => {
|
|||||||
type: "workspace_setup_status_response",
|
type: "workspace_setup_status_response",
|
||||||
payload: {
|
payload: {
|
||||||
requestId: "req-status",
|
requestId: "req-status",
|
||||||
workspaceId: "/repo/.paseo/worktrees/feature-a",
|
workspaceId: "ws-feature-a",
|
||||||
snapshot: {
|
snapshot: {
|
||||||
status: "completed",
|
status: "completed",
|
||||||
detail: {
|
detail: {
|
||||||
@@ -292,7 +292,7 @@ describe("workspace message schemas", () => {
|
|||||||
requestId: "req-workspaces",
|
requestId: "req-workspaces",
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
id: "/tmp/repo",
|
id: "ws-main",
|
||||||
projectId: "remote:github.com/acme/repo",
|
projectId: "remote:github.com/acme/repo",
|
||||||
projectDisplayName: "acme/repo",
|
projectDisplayName: "acme/repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
@@ -357,7 +357,7 @@ describe("workspace message schemas", () => {
|
|||||||
requestId: "req-workspaces",
|
requestId: "req-workspaces",
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
id: "/tmp/repo",
|
id: "ws-main",
|
||||||
projectId: "remote:github.com/acme/repo",
|
projectId: "remote:github.com/acme/repo",
|
||||||
projectDisplayName: "acme/repo",
|
projectDisplayName: "acme/repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
@@ -429,7 +429,7 @@ describe("workspace message schemas", () => {
|
|||||||
const parsed = legacyMessageSchema.parse(message);
|
const parsed = legacyMessageSchema.parse(message);
|
||||||
|
|
||||||
expect(parsed.payload.entries[0]).toEqual({
|
expect(parsed.payload.entries[0]).toEqual({
|
||||||
id: "/tmp/repo",
|
id: "ws-main",
|
||||||
projectId: "remote:github.com/acme/repo",
|
projectId: "remote:github.com/acme/repo",
|
||||||
projectDisplayName: "acme/repo",
|
projectDisplayName: "acme/repo",
|
||||||
projectRootPath: "/tmp/repo",
|
projectRootPath: "/tmp/repo",
|
||||||
|
|||||||
Reference in New Issue
Block a user