Skip to content

Commit d104885

Browse files
committed
🤖 feat: copy plan file when forking workspace
When a workspace is forked via /fork, the plan file is now copied along with the chat history, partial state, and usage tracking. This allows the forked workspace to continue working with the same plan context as the original. Changes: - Add copyPlanFile helper in src/node/utils/runtime/helpers.ts - Call copyPlanFile in workspaceService.fork() after session files Change-Id: Iaee04cb44c1b5a98e13eb693fa729a078b8d0013 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent cc478e5 commit d104885

File tree

2 files changed

+44
-2
lines changed

2 files changed

+44
-2
lines changed

src/node/services/workspaceService.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import { createBashTool } from "@/node/services/tools/bash";
5656
import type { AskUserQuestionToolSuccessResult, BashToolResult } from "@/common/types/tools";
5757
import { secretsToRecord } from "@/common/types/secrets";
5858

59-
import { movePlanFile } from "@/node/utils/runtime/helpers";
59+
import { movePlanFile, copyPlanFile } from "@/node/utils/runtime/helpers";
6060

6161
/** Maximum number of retry attempts when workspace name collides */
6262
const MAX_WORKSPACE_NAME_COLLISION_RETRIES = 3;
@@ -960,6 +960,9 @@ export class WorkspaceService extends EventEmitter {
960960
return Err(`Failed to copy chat history: ${message}`);
961961
}
962962

963+
// Copy plan file if it exists (checks both new and legacy paths)
964+
await copyPlanFile(runtime, sourceMetadata.name, sourceWorkspaceId, newName, projectName);
965+
963966
// Compute namedWorkspacePath for frontend metadata
964967
const namedWorkspacePath = runtime.getWorkspacePath(foundProjectPath, newName);
965968

src/node/utils/runtime/helpers.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,47 @@ export async function movePlanFile(
190190

191191
try {
192192
await runtime.stat(oldPath);
193-
await execBuffered(runtime, `mv "${oldPath}" "${newPath}"`, { cwd: "/tmp", timeout: 5 });
193+
// Resolve tildes to absolute paths - bash doesn't expand ~ inside quotes
194+
const resolvedOldPath = await runtime.resolvePath(oldPath);
195+
const resolvedNewPath = await runtime.resolvePath(newPath);
196+
await execBuffered(runtime, `mv "${resolvedOldPath}" "${resolvedNewPath}"`, {
197+
cwd: "/tmp",
198+
timeout: 5,
199+
});
194200
} catch {
195201
// No plan file to move, that's fine
196202
}
197203
}
204+
205+
/**
206+
* Copy a plan file from one workspace to another (e.g., during fork).
207+
* Checks both new path format and legacy path format for the source.
208+
* Silently succeeds if source file doesn't exist at either location.
209+
*/
210+
export async function copyPlanFile(
211+
runtime: Runtime,
212+
sourceWorkspaceName: string,
213+
sourceWorkspaceId: string,
214+
targetWorkspaceName: string,
215+
projectName: string
216+
): Promise<void> {
217+
const sourcePath = getPlanFilePath(sourceWorkspaceName, projectName);
218+
const legacySourcePath = getLegacyPlanFilePath(sourceWorkspaceId);
219+
const targetPath = getPlanFilePath(targetWorkspaceName, projectName);
220+
221+
// Prefer the new layout, but fall back to the legacy layout.
222+
//
223+
// Note: we intentionally use runtime file I/O instead of `cp` because:
224+
// 1) bash doesn't expand ~ inside quotes
225+
// 2) the target per-project plan directory may not exist yet
226+
// 3) runtime.writeFile() already handles directory creation + tilde expansion
227+
for (const candidatePath of [sourcePath, legacySourcePath]) {
228+
try {
229+
const content = await readFileString(runtime, candidatePath);
230+
await writeFileString(runtime, targetPath, content);
231+
return;
232+
} catch {
233+
// Try next candidate
234+
}
235+
}
236+
}

0 commit comments

Comments
 (0)