Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions src/App.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
},
],
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
19 changes: 10 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
17 changes: 5 additions & 12 deletions src/debug/agentSessionCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,19 +160,12 @@ async function main(): Promise<void> {
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()))
Expand Down
5 changes: 4 additions & 1 deletion src/debug/git-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Expand Down
10 changes: 8 additions & 2 deletions src/debug/list-workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`);
}
Expand Down
5 changes: 3 additions & 2 deletions tests/e2e/utils/demoProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down