Skip to content

Commit 989f30d

Browse files
committed
🤖 refactor: centralize workspace project runtime recreation
1 parent 60a2570 commit 989f30d

File tree

4 files changed

+246
-135
lines changed

4 files changed

+246
-135
lines changed

src/node/services/aiService.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ import type { InitStateManager } from "./initStateManager";
2222
import type { SendMessageError } from "@/common/types/errors";
2323
import { getToolsForModel } from "@/common/utils/tools/tools";
2424
import { cloneToolPreservingDescriptors } from "@/common/utils/tools/cloneToolPreservingDescriptors";
25-
import { createRuntime } from "@/node/runtime/runtimeFactory";
2625
import {
2726
createRuntimeContextForWorkspace,
2827
resolveWorkspaceExecutionPath,
2928
} from "@/node/runtime/runtimeHelpers";
30-
import { getWorkspacePathHintForProject } from "@/node/services/workspaceProjectRepos";
29+
import { createRuntimeForWorkspaceProject } from "@/node/services/workspaceProjectRepos";
3130
import { MultiProjectRuntime } from "@/node/runtime/multiProjectRuntime";
3231
import { getMuxEnv, getRuntimeType } from "@/node/runtime/initHook";
3332
import { getSrcBaseDir, isSSHRuntime } from "@/common/types/runtime";
@@ -950,6 +949,14 @@ export class AIService extends EventEmitter {
950949
return multiProjectExecutionGate;
951950
}
952951

952+
const workspaceProjectRuntimeParams = {
953+
workspaceName: metadata.name,
954+
workspacePath: workspace.workspacePath,
955+
runtimeConfig: metadata.runtimeConfig,
956+
projectPath: metadata.projectPath,
957+
projectName: metadata.projectName,
958+
projects: metadata.projects,
959+
};
953960
const singleProjectContext = isMultiProject(metadata)
954961
? undefined
955962
: createRuntimeContextForWorkspace(metadataWithPath);
@@ -960,24 +967,10 @@ export class AIService extends EventEmitter {
960967
getProjects(metadata).map((project) => ({
961968
projectPath: project.projectPath,
962969
projectName: project.projectName,
963-
runtime: createRuntime(metadata.runtimeConfig, {
964-
projectPath: project.projectPath,
965-
workspaceName: metadata.name,
966-
workspacePath: isSSHRuntime(metadata.runtimeConfig)
967-
? getWorkspacePathHintForProject(
968-
{
969-
workspaceId,
970-
workspaceName: metadata.name,
971-
workspacePath: workspace.workspacePath,
972-
runtimeConfig: metadata.runtimeConfig,
973-
projectPath: metadata.projectPath,
974-
projectName: metadata.projectName,
975-
projects: metadata.projects,
976-
},
977-
project.projectPath
978-
)
979-
: undefined,
980-
}),
970+
runtime: createRuntimeForWorkspaceProject(
971+
workspaceProjectRuntimeParams,
972+
project.projectPath
973+
),
981974
})),
982975
metadata.name
983976
);

src/node/services/workspaceProjectRepos.test.ts

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { describe, expect, it } from "bun:test";
22

3+
import type { RuntimeConfig } from "@/common/types/runtime";
34
import {
45
buildLegacyRemoteProjectLayout,
56
buildRemoteProjectLayout,
67
getRemoteWorkspacePath,
78
} from "@/node/runtime/remoteProjectLayout";
89
import {
10+
createRuntimeForWorkspaceProject,
911
getWorkspacePathHintForProject,
1012
getWorkspaceProjectRepos,
13+
resolveWorkspacePathForProject,
1114
} from "@/node/services/workspaceProjectRepos";
1215

1316
describe("getWorkspaceProjectRepos", () => {
@@ -74,6 +77,93 @@ describe("getWorkspaceProjectRepos", () => {
7477
expect(repos[0]?.repoCwd).toBe(workspacePath);
7578
});
7679

80+
it("recreates single-project SSH runtimes from the persisted workspace path", () => {
81+
const runtimeConfig = {
82+
type: "ssh",
83+
host: "example.com",
84+
srcBaseDir: "/tmp/src",
85+
} as const;
86+
const projectPath = "/tmp/projects/main";
87+
const workspaceName = "main";
88+
const workspacePath = getRemoteWorkspacePath(
89+
buildLegacyRemoteProjectLayout(runtimeConfig.srcBaseDir, projectPath),
90+
workspaceName
91+
);
92+
const runtime = createRuntimeForWorkspaceProject(
93+
{
94+
workspaceName,
95+
workspacePath,
96+
runtimeConfig,
97+
projectPath,
98+
projectName: "main",
99+
},
100+
projectPath
101+
);
102+
103+
expect(runtime.getWorkspacePath(projectPath, workspaceName)).toBe(workspacePath);
104+
expect(
105+
resolveWorkspacePathForProject(
106+
{
107+
workspaceName,
108+
workspacePath,
109+
runtimeConfig,
110+
projectPath,
111+
projectName: "main",
112+
},
113+
projectPath,
114+
runtime
115+
)
116+
).toBe(workspacePath);
117+
});
118+
119+
it("resolves single-project paths without constructing a runtime", () => {
120+
expect(
121+
resolveWorkspacePathForProject(
122+
{
123+
workspaceName: "main",
124+
workspacePath: "/tmp/workspaces/main",
125+
runtimeConfig: { type: "made-up-runtime" } as unknown as RuntimeConfig,
126+
projectPath: "/tmp/projects/main",
127+
projectName: "main",
128+
},
129+
"/tmp/projects/main"
130+
)
131+
).toBe("/tmp/workspaces/main");
132+
});
133+
134+
it("resolves canonical sibling paths when a multi-project SSH workspace has no recognized layout hint", () => {
135+
const runtimeConfig = {
136+
type: "ssh",
137+
host: "example.com",
138+
srcBaseDir: "/tmp/src",
139+
} as const;
140+
const workspaceName = "main";
141+
const primaryProjectPath = "/tmp/projects/main";
142+
const secondaryProjectPath = "/tmp/projects/other";
143+
144+
expect(
145+
resolveWorkspacePathForProject(
146+
{
147+
workspaceName,
148+
workspacePath: "/tmp/src/containers/main",
149+
runtimeConfig,
150+
projectPath: primaryProjectPath,
151+
projectName: "main",
152+
projects: [
153+
{ projectPath: primaryProjectPath, projectName: "main" },
154+
{ projectPath: secondaryProjectPath, projectName: "other" },
155+
],
156+
},
157+
secondaryProjectPath
158+
)
159+
).toBe(
160+
getRemoteWorkspacePath(
161+
buildRemoteProjectLayout(runtimeConfig.srcBaseDir, secondaryProjectPath),
162+
workspaceName
163+
)
164+
);
165+
});
166+
77167
it("derives hashed SSH paths for secondary multi-project repos", () => {
78168
const runtimeConfig = {
79169
type: "ssh",
@@ -116,7 +206,6 @@ describe("getWorkspaceProjectRepos", () => {
116206

117207
const hint = getWorkspacePathHintForProject(
118208
{
119-
workspaceId: "workspace-1",
120209
workspaceName,
121210
workspacePath: getRemoteWorkspacePath(
122211
buildLegacyRemoteProjectLayout(runtimeConfig.srcBaseDir, primaryProjectPath),
@@ -151,7 +240,6 @@ describe("getWorkspaceProjectRepos", () => {
151240

152241
const hint = getWorkspacePathHintForProject(
153242
{
154-
workspaceId: "workspace-1",
155243
workspaceName: "main",
156244
workspacePath: "/tmp/src/containers/main",
157245
runtimeConfig,

src/node/services/workspaceProjectRepos.ts

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as path from "node:path";
44
import type { ProjectRef } from "@/common/types/workspace";
55
import { isSSHRuntime, type RuntimeConfig } from "@/common/types/runtime";
66
import { PlatformPaths } from "@/common/utils/paths";
7+
import type { Runtime } from "@/node/runtime/Runtime";
78
import {
89
buildLegacyRemoteProjectLayout,
910
buildRemoteProjectLayout,
@@ -18,8 +19,7 @@ export interface WorkspaceProjectRepo {
1819
repoCwd: string;
1920
}
2021

21-
interface WorkspaceProjectRepoParams {
22-
workspaceId: string;
22+
export interface WorkspaceProjectRuntimeParams {
2323
workspaceName: string;
2424
workspacePath: string;
2525
runtimeConfig: RuntimeConfig;
@@ -28,6 +28,10 @@ interface WorkspaceProjectRepoParams {
2828
projects?: ProjectRef[];
2929
}
3030

31+
interface WorkspaceProjectRepoParams extends WorkspaceProjectRuntimeParams {
32+
workspaceId: string;
33+
}
34+
3135
interface WorkspaceProjectStorageKeyParams {
3236
projectPath: string;
3337
projectName?: string;
@@ -138,8 +142,14 @@ export function getWorkspaceProjectStorageKeys(
138142
return storageKeys;
139143
}
140144

145+
function hasMultipleWorkspaceProjects(
146+
params: Pick<WorkspaceProjectRuntimeParams, "projects">
147+
): boolean {
148+
return (params.projects?.length ?? 0) > 1;
149+
}
150+
141151
export function getWorkspacePathHintForProject(
142-
params: WorkspaceProjectRepoParams,
152+
params: WorkspaceProjectRuntimeParams,
143153
targetProjectPath: string
144154
): string | undefined {
145155
if (!isSSHRuntime(params.runtimeConfig)) {
@@ -172,6 +182,50 @@ export function getWorkspacePathHintForProject(
172182
return undefined;
173183
}
174184

185+
/**
186+
* Recreate the runtime for one project inside an existing workspace.
187+
*
188+
* Why: multi-project SSH workspaces sometimes need a sibling checkout hint derived from the
189+
* persisted workspace root, while single-project workspaces should always keep using their exact
190+
* checkout path from config.
191+
*/
192+
export function createRuntimeForWorkspaceProject(
193+
params: WorkspaceProjectRuntimeParams,
194+
targetProjectPath: string
195+
): Runtime {
196+
const workspacePath = hasMultipleWorkspaceProjects(params)
197+
? getWorkspacePathHintForProject(params, targetProjectPath)
198+
: params.workspacePath;
199+
200+
return createRuntime(params.runtimeConfig, {
201+
projectPath: targetProjectPath,
202+
workspaceName: params.workspaceName,
203+
workspacePath,
204+
});
205+
}
206+
207+
export function resolveWorkspacePathForProject(
208+
params: WorkspaceProjectRuntimeParams,
209+
targetProjectPath: string,
210+
runtime?: Runtime
211+
): string {
212+
if (!hasMultipleWorkspaceProjects(params)) {
213+
assert(
214+
params.workspacePath.trim().length > 0,
215+
"resolveWorkspacePathForProject: workspacePath must be non-empty"
216+
);
217+
return params.workspacePath;
218+
}
219+
220+
const projectRuntime = runtime ?? createRuntimeForWorkspaceProject(params, targetProjectPath);
221+
const workspacePath = projectRuntime.getWorkspacePath(targetProjectPath, params.workspaceName);
222+
assert(
223+
workspacePath.trim().length > 0,
224+
`resolveWorkspacePathForProject: workspacePath missing for ${targetProjectPath}`
225+
);
226+
return workspacePath;
227+
}
228+
175229
export function getWorkspaceProjectRepos(
176230
params: WorkspaceProjectRepoParams
177231
): WorkspaceProjectRepo[] {
@@ -197,25 +251,9 @@ export function getWorkspaceProjectRepos(
197251
projectName: params.projectName,
198252
projects: params.projects,
199253
});
200-
const isMultiProject = projectStorageKeys.length > 1;
201254

202255
const repos = projectStorageKeys.map((project) => {
203-
const sshWorkspacePathHint = isMultiProject
204-
? getWorkspacePathHintForProject(params, project.projectPath)
205-
: undefined;
206-
207-
const repoCwd = !isMultiProject
208-
? params.workspacePath
209-
: (sshWorkspacePathHint ??
210-
createRuntime(params.runtimeConfig, {
211-
projectPath: project.projectPath,
212-
workspaceName: params.workspaceName,
213-
}).getWorkspacePath(project.projectPath, params.workspaceName));
214-
215-
assert(
216-
repoCwd.trim().length > 0,
217-
`getWorkspaceProjectRepos: repoCwd missing for ${project.projectName}`
218-
);
256+
const repoCwd = resolveWorkspacePathForProject(params, project.projectPath);
219257

220258
return {
221259
projectPath: project.projectPath,

0 commit comments

Comments
 (0)