feat: empty-container spawn (scratch workspace, no repo volume); fix bridge session isolation — explicit SessionManager per spawn stops SDK resuming the repo's old session (which also overrode LVMH_MODEL)

This commit is contained in:
Raphael Westphal
2026-08-20 15:12:45 +02:00
parent bdfa61a8b2
commit f5ee3a0f2d
7 changed files with 134 additions and 46 deletions
+25
View File
@@ -715,3 +715,28 @@ describe("spawn model selection", () => {
expect(bodies[0]).toContain('"model":"zai-renaud/glm-5.3"');
});
});
describe("empty container spawn", () => {
it("checkbox sends empty:true and resets after", async () => {
const bodies: string[] = [];
mockFetchJson((url, init) => {
if (init?.method === "POST" && url.endsWith("/api/spawn")) {
bodies.push(String(init.body));
return { sessionId: "e1", imageUsed: "lvmh-worker:latest" };
}
if (url.endsWith("/api/gitlab/status"))
return { connected: true, baseUrl: "https://gl", username: "a" };
if (url.endsWith("/api/gitlab/repos")) return [repo("g/p")];
return [];
});
render(tree(makeStore()));
await flush();
fireEvent.click(screen.getByText("g/p"));
const cb = screen.getByRole("checkbox");
fireEvent.click(cb);
expect(cb).toBeChecked();
fireEvent.click(screen.getByLabelText("Spawn container"));
await flush();
expect(bodies[0]).toContain('"empty":true');
});
});
+14
View File
@@ -61,6 +61,7 @@ export default function SpawnView({ store, pushToast }: Props) {
const [query, setQuery] = useState<string>("");
const [selected, setSelected] = useState<Repo | null>(null);
const [branch, setBranch] = useState<string>("");
const [empty, setEmpty] = useState<boolean>(false);
const [model, setModel] = useState<string>("");
const [catalog, setCatalog] = useState<ModelCatalogEntry[] | null>(null);
const [busy, setBusy] = useState<boolean>(false);
@@ -176,6 +177,7 @@ export default function SpawnView({ store, pushToast }: Props) {
repo: selected!.path,
...(branch.trim().length > 0 ? { branch: branch.trim() } : {}),
...(model.length > 0 ? { model } : {}),
...(empty ? { empty: true } : {}),
};
const res = await fetchJson<SpawnResponse>(Route.Spawn, {
method: "POST",
@@ -429,6 +431,18 @@ export default function SpawnView({ store, pushToast }: Props) {
))}
</select>
</div>
<label
className="repo-meta"
style={{ marginTop: 8, display: "flex", gap: 6, alignItems: "center" }}
>
<input
type="checkbox"
checked={empty}
onChange={(e) => setEmpty(e.target.checked)}
disabled={busy}
/>
Empty container (no repo blank workspace)
</label>
{error.length > 0 && <p className="error-text">{error}</p>}
</div>
</>