diff --git a/src/App.stories.tsx b/src/App.stories.tsx index 0a44d052f..04d4fde97 100644 --- a/src/App.stories.tsx +++ b/src/App.stories.tsx @@ -31,7 +31,8 @@ function setupMockAPI(options: { Promise.resolve({ success: true, metadata: { - id: `${projectPath.split("/").pop() ?? "project"}-${branchName}`, + // Mock stable ID (production uses crypto.randomBytes(5).toString('hex')) + id: Math.random().toString(36).substring(2, 12), name: branchName, projectPath, projectName: projectPath.split("/").pop() ?? "project", @@ -135,15 +136,15 @@ export const SingleProject: Story = { "/home/user/projects/my-app", { workspaces: [ - { path: "/home/user/.cmux/src/my-app/main", id: "my-app-main", name: "main" }, + { path: "/home/user/.cmux/src/my-app/main", id: "a1b2c3d4e5", name: "main" }, { path: "/home/user/.cmux/src/my-app/feature-auth", - id: "my-app-feature-auth", + id: "f6g7h8i9j0", name: "feature/auth", }, { path: "/home/user/.cmux/src/my-app/bugfix", - id: "my-app-bugfix", + id: "k1l2m3n4o5", name: "bugfix/memory-leak", }, ], @@ -153,14 +154,14 @@ export const SingleProject: Story = { const workspaces: FrontendWorkspaceMetadata[] = [ { - id: "my-app-main", + id: "a1b2c3d4e5", name: "main", projectPath: "/home/user/projects/my-app", projectName: "my-app", namedWorkspacePath: "/home/user/.cmux/src/my-app/main", }, { - id: "my-app-feature-auth", + id: "f6g7h8i9j0", name: "feature/auth", projectPath: "/home/user/projects/my-app", projectName: "my-app", @@ -351,7 +352,8 @@ export const ActiveWorkspaceWithChat: Story = { Promise.resolve({ success: true, metadata: { - id: `${projectPath.split("/").pop() ?? "project"}-${branchName}`, + // Mock stable ID (production uses crypto.randomBytes(5).toString('hex')) + id: Math.random().toString(36).substring(2, 12), name: branchName, projectPath, projectName: projectPath.split("/").pop() ?? "project", diff --git a/src/config.test.ts b/src/config.test.ts index 23bac50ab..ae6a62246 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -79,8 +79,9 @@ describe("Config", () => { // Create workspace directory fs.mkdirSync(workspacePath, { recursive: true }); - // Create metadata file using legacy ID format (project-workspace) - const legacyId = config.generateWorkspaceId(projectPath, workspacePath); + // Test backward compatibility: Create metadata file using legacy ID format. + // This simulates workspaces created before stable IDs were introduced. + const legacyId = config.generateLegacyId(projectPath, workspacePath); const sessionDir = config.getSessionDir(legacyId); fs.mkdirSync(sessionDir, { recursive: true }); const metadataPath = path.join(sessionDir, "metadata.json"); diff --git a/src/config.ts b/src/config.ts index 790f861b1..8562996ef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -111,13 +111,14 @@ export class Config { } /** - * DEPRECATED: Generate workspace ID from project and workspace paths. - * This method is used only for legacy workspace migration. - * New workspaces should use generateStableId() instead. + * DEPRECATED: Generate legacy workspace ID from project and workspace paths. + * This method is used only for legacy workspace migration to look up old workspaces. + * New workspaces use generateStableId() which returns a random stable ID. * - * Format: ${projectBasename}-${workspaceBasename} + * DO NOT use this method or its format to construct workspace IDs anywhere in the codebase. + * Workspace IDs are backend implementation details and must only come from backend operations. */ - generateWorkspaceId(projectPath: string, workspacePath: string): string { + generateLegacyId(projectPath: string, workspacePath: string): string { const projectBasename = this.getProjectName(projectPath); const workspaceBasename = workspacePath.split("/").pop() ?? workspacePath.split("\\").pop() ?? "unknown"; @@ -197,7 +198,7 @@ export class Config { } // Try legacy ID format as last resort - const legacyId = this.generateWorkspaceId(projectPath, workspace.path); + const legacyId = this.generateLegacyId(projectPath, workspace.path); if (legacyId === workspaceId) { return { workspacePath: workspace.path, projectPath }; } @@ -272,7 +273,7 @@ export class Config { // LEGACY FORMAT: Fall back to reading metadata.json // Try legacy ID format first (project-workspace) - used by E2E tests and old workspaces - const legacyId = this.generateWorkspaceId(projectPath, workspace.path); + const legacyId = this.generateLegacyId(projectPath, workspace.path); const metadataPath = path.join(this.getSessionDir(legacyId), "metadata.json"); let metadataFound = false; @@ -302,7 +303,7 @@ export class Config { // No metadata found anywhere - create basic metadata if (!metadataFound) { - const legacyId = this.generateWorkspaceId(projectPath, workspace.path); + const legacyId = this.generateLegacyId(projectPath, workspace.path); const metadata: WorkspaceMetadata = { id: legacyId, name: workspaceBasename, @@ -320,7 +321,7 @@ export class Config { } catch (error) { console.error(`Failed to load/migrate workspace metadata:`, error); // Fallback to basic metadata if migration fails - const legacyId = this.generateWorkspaceId(projectPath, workspace.path); + const legacyId = this.generateLegacyId(projectPath, workspace.path); const metadata: WorkspaceMetadata = { id: legacyId, name: workspaceBasename, diff --git a/src/debug/agentSessionCli.ts b/src/debug/agentSessionCli.ts index 6254d0027..5d0db5089 100644 --- a/src/debug/agentSessionCli.ts +++ b/src/debug/agentSessionCli.ts @@ -160,19 +160,12 @@ async function main(): Promise { const config = new Config(configRoot); const workspaceIdRaw = values["workspace-id"]; - const projectPathRaw = values["project-path"]; - - const workspaceId = - workspaceIdRaw && workspaceIdRaw.trim().length > 0 - ? workspaceIdRaw.trim() - : (() => { - if (typeof projectPathRaw !== "string" || projectPathRaw.trim().length === 0) { - throw new Error("Provide --workspace-id or --project-path to derive workspace ID"); - } - const projectPath = path.resolve(projectPathRaw.trim()); - return config.generateWorkspaceId(projectPath, workspacePath); - })(); + if (typeof workspaceIdRaw !== "string" || workspaceIdRaw.trim().length === 0) { + throw new Error("--workspace-id is required"); + } + const workspaceId = workspaceIdRaw.trim(); + const projectPathRaw = values["project-path"]; const projectName = typeof projectPathRaw === "string" && projectPathRaw.trim().length > 0 ? path.basename(path.resolve(projectPathRaw.trim())) diff --git a/src/debug/git-status.ts b/src/debug/git-status.ts index e5a7d4f62..8b90d1bc3 100644 --- a/src/debug/git-status.ts +++ b/src/debug/git-status.ts @@ -31,7 +31,10 @@ function findWorkspaces(): Array<{ id: string; path: string }> { const workspacePath = join(projectPath, branch); if (statSync(workspacePath).isDirectory()) { workspaces.push({ - id: `${project}-${branch}`, + // NOTE: Using directory name as display ID for debug purposes only. + // This is NOT how workspace IDs are determined in production code. + // Production workspace IDs come from metadata.json in the session dir. + id: branch, path: workspacePath, }); } diff --git a/src/debug/list-workspaces.ts b/src/debug/list-workspaces.ts index 2bfcc3b39..9b7c4de8f 100644 --- a/src/debug/list-workspaces.ts +++ b/src/debug/list-workspaces.ts @@ -15,8 +15,14 @@ export function listWorkspacesCommand() { console.log(` Workspaces: ${project.workspaces.length}`); for (const workspace of project.workspaces) { - const workspaceId = defaultConfig.generateWorkspaceId(projectPath, workspace.path); - console.log(` - ID: ${workspaceId} (generated)`); + const dirName = path.basename(workspace.path); + console.log(` - Directory: ${dirName}`); + if (workspace.id) { + console.log(` ID: ${workspace.id}`); + } + if (workspace.name) { + console.log(` Name: ${workspace.name}`); + } console.log(` Path: ${workspace.path}`); console.log(` Exists: ${fs.existsSync(workspace.path)}`); } diff --git a/tests/e2e/utils/demoProject.ts b/tests/e2e/utils/demoProject.ts index 36cd183f8..b1fc18198 100644 --- a/tests/e2e/utils/demoProject.ts +++ b/tests/e2e/utils/demoProject.ts @@ -49,9 +49,10 @@ export function prepareDemoProject( fs.mkdirSync(workspacePath, { recursive: true }); fs.mkdirSync(sessionsDir, { recursive: true }); - // Workspace metadata mirrors Config.generateWorkspaceId + // E2E tests use legacy workspace ID format to test backward compatibility. + // Production code now uses generateStableId() for new workspaces. const config = new Config(rootDir); - const workspaceId = config.generateWorkspaceId(projectPath, workspacePath); + const workspaceId = config.generateLegacyId(projectPath, workspacePath); const metadata = { id: workspaceId, name: workspaceBranch,