diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index c5a34892cd..783b2be1c8 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -56,7 +56,7 @@ import { createBashTool } from "@/node/services/tools/bash"; import type { AskUserQuestionToolSuccessResult, BashToolResult } from "@/common/types/tools"; import { secretsToRecord } from "@/common/types/secrets"; -import { movePlanFile } from "@/node/utils/runtime/helpers"; +import { movePlanFile, copyPlanFile } from "@/node/utils/runtime/helpers"; /** Maximum number of retry attempts when workspace name collides */ const MAX_WORKSPACE_NAME_COLLISION_RETRIES = 3; @@ -960,6 +960,9 @@ export class WorkspaceService extends EventEmitter { return Err(`Failed to copy chat history: ${message}`); } + // Copy plan file if it exists (checks both new and legacy paths) + await copyPlanFile(runtime, sourceMetadata.name, sourceWorkspaceId, newName, projectName); + // Compute namedWorkspacePath for frontend metadata const namedWorkspacePath = runtime.getWorkspacePath(foundProjectPath, newName); diff --git a/src/node/utils/runtime/helpers.ts b/src/node/utils/runtime/helpers.ts index 5df38083cb..5b09412f68 100644 --- a/src/node/utils/runtime/helpers.ts +++ b/src/node/utils/runtime/helpers.ts @@ -190,8 +190,47 @@ export async function movePlanFile( try { await runtime.stat(oldPath); - await execBuffered(runtime, `mv "${oldPath}" "${newPath}"`, { cwd: "/tmp", timeout: 5 }); + // Resolve tildes to absolute paths - bash doesn't expand ~ inside quotes + const resolvedOldPath = await runtime.resolvePath(oldPath); + const resolvedNewPath = await runtime.resolvePath(newPath); + await execBuffered(runtime, `mv "${resolvedOldPath}" "${resolvedNewPath}"`, { + cwd: "/tmp", + timeout: 5, + }); } catch { // No plan file to move, that's fine } } + +/** + * Copy a plan file from one workspace to another (e.g., during fork). + * Checks both new path format and legacy path format for the source. + * Silently succeeds if source file doesn't exist at either location. + */ +export async function copyPlanFile( + runtime: Runtime, + sourceWorkspaceName: string, + sourceWorkspaceId: string, + targetWorkspaceName: string, + projectName: string +): Promise { + const sourcePath = getPlanFilePath(sourceWorkspaceName, projectName); + const legacySourcePath = getLegacyPlanFilePath(sourceWorkspaceId); + const targetPath = getPlanFilePath(targetWorkspaceName, projectName); + + // Prefer the new layout, but fall back to the legacy layout. + // + // Note: we intentionally use runtime file I/O instead of `cp` because: + // 1) bash doesn't expand ~ inside quotes + // 2) the target per-project plan directory may not exist yet + // 3) runtime.writeFile() already handles directory creation + tilde expansion + for (const candidatePath of [sourcePath, legacySourcePath]) { + try { + const content = await readFileString(runtime, candidatePath); + await writeFileString(runtime, targetPath, content); + return; + } catch { + // Try next candidate + } + } +}