fix(spawn): stop sending branch on spawn, render API errors, wire worker name#171
Conversation
…ker name Three spawn-modal bugs, re-landed from the closed redesign branch (#156): - createTask no longer sends `branch`: the API field names the session's NEW worktree branch, so submitting the modal's default ("main") made the daemon 409 with BRANCH_CHECKED_OUT_ELSEWHERE on every spawn. The "Based on" pane is informational — workers branch off the project's default branch in a fresh worktree. - API errors render their envelope message instead of "[object Object]": openapi-fetch resolves non-2xx responses to a plain {code,error,message} object, not an Error; new apiErrorMessage unwraps it (message + code). - The "Worker name" field is actually used: after spawn, a best-effort PATCH rename sets the displayName (a failed rename must not look like a failed spawn — the worker is already running). Also revives the renderer test suite, which made these tests (and 7 of 9 suite files) impossible to run on main: - vitest.config.ts re-exports vite.renderer.config so `vitest run` actually loads the jsdom environment + setup file. Forge's per-target vite.*.config.ts names are invisible to vitest, so the existing `test` block was dead config and every DOM-touching test died on "window is not defined". - vite.renderer.config.ts imports defineConfig from vitest/config so its `test` key typechecks. - routeTree.gen.ts + the session route are regenerated by the pinned @tanstack/router-plugin (it runs as part of loading the renderer config; the checked-in tree predated the installed plugin version and its route-ID drift caused 3 of main's 7 typecheck errors). - App.test.tsx wraps App in TooltipProvider, mirroring routes/__root.tsx. Frontend: 9/9 test files, 99/99 tests pass (was 2/9 files). Typecheck is down from 7 errors to 3 — the survivors (forge.config notarize/maker types, update-electron-app call signature) predate this branch and are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThis PR fixes three spawn-modal bugs (409 on branch field,
Confidence Score: 5/5Safe to merge — all three spawn-modal bugs are cleanly fixed, the backend fallback is guarded by both a unit test and a new integration test, and the test revival brings 99/99 tests passing. Every changed path has direct test coverage: the no-branch POST body, rename round-trip, and error-envelope rendering are each pinned by a new test. The backend candidate-list change is covered by an updated unit test and a new end-to-end integration test. The vitest config fix is structural (re-export) with no logic of its own. No previously-flagged issues are reintroduced. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant UI as SpawnWorkerModal
participant App as App.createTask
participant API as Daemon API
UI->>App: "onCreateTask({ projectId, prompt, name?, harness })"
Note over App: No branch field sent
App->>API: POST /api/v1/sessions
alt 2xx response
API-->>App: "{ session: { id, ... } }"
opt name provided
App->>API: "PATCH /api/v1/sessions/{sessionId}"
alt rename succeeds
API-->>App: "{ displayName }"
Note over App: title = displayName
else rename fails
Note over App: title = prompt (silent fallback)
end
end
App-->>UI: resolved, modal closes
else non-2xx
API-->>App: "{ error: { code, message } }"
Note over App: apiErrorMessage unwraps envelope
App-->>UI: throw Error(message + code)
UI->>UI: setError shown in modal
end
Reviews (2): Last reviewed commit: "fix(gitworktree): base new session branc..." | Re-trigger Greptile |
| // Best-effort: the session is already running, so a failed rename should | ||
| // not surface as a failed spawn (retrying would create a duplicate). | ||
| let displayName: string | undefined; | ||
| if (input.name) { | ||
| const renamed = await apiClient.PATCH("/api/v1/sessions/{sessionId}", { | ||
| params: { path: { sessionId: session.id } }, | ||
| body: { displayName: input.name }, | ||
| }); | ||
| displayName = renamed.data?.displayName; |
There was a problem hiding this comment.
Rename errors are silently swallowed with no user feedback
When the PATCH rename fails (network error, 4xx/5xx from the daemon), renamed.data?.displayName is undefined, displayName stays undefined, and the session title silently falls back to the prompt text. The user sees the modal close and the session appear, but with a different name than they typed — there's no indication the rename failed. The comment says this is intentional, but at minimum a console.warn would help diagnose the situation. If the UX contract is "rename failure = silently use the prompt as title", a brief inline note in the UI (or at least a console.warn) would make the silent fallback discoverable.
| test("renames the spawned worker when a name is given", async () => { | ||
| const user = userEvent.setup(); | ||
| mockData.projects = [{ id: "proj-1", name: "my-app", path: "/home/me/my-app", sessionPrefix: "" }]; | ||
| postMock.mockResolvedValueOnce({ | ||
| data: { | ||
| session: { | ||
| id: "new-task", | ||
| projectId: "proj-1", | ||
| harness: "claude-code", | ||
| isTerminated: false, | ||
| }, | ||
| }, | ||
| }); | ||
| patchMock.mockResolvedValueOnce({ | ||
| data: { ok: true, sessionId: "new-task", displayName: "fix-login" }, | ||
| }); | ||
|
|
||
| renderApp(); | ||
|
|
||
| await screen.findByRole("button", { name: "Select my-app" }); | ||
|
|
||
| await user.click(screen.getByRole("button", { name: "New worker" })); | ||
| await user.type(await screen.findByLabelText("Worker name"), "fix-login"); | ||
| await user.type(screen.getByLabelText("Prompt"), "Fix the login bug"); | ||
| await user.click(screen.getByRole("button", { name: /Spawn worker/ })); | ||
|
|
||
| expect(patchMock).toHaveBeenCalledWith("/api/v1/sessions/{sessionId}", { | ||
| params: { path: { sessionId: "new-task" } }, | ||
| body: { displayName: "fix-login" }, | ||
| }); | ||
| expect(await screen.findByRole("button", { name: "fix-login" })).toBeInTheDocument(); | ||
| }); |
There was a problem hiding this comment.
Rename-failure path is untested
The new "renames the spawned worker when a name is given" test covers the happy path (PATCH succeeds, displayName returned). There is no test for the case where patchMock resolves with { error: ... } or { data: undefined } — i.e., the best-effort silent fallback described in the App.tsx comment. Without a test, it's easy for a future refactor to accidentally surface the rename error to the user (breaking the spawn) or show the wrong title without anyone noticing.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…ch when no remote exists Re-lands the remoteless fallback from the closed redesign branch (archived in 641b712). Creating a session worktree resolved the base for a NEW branch only via the remote-tracking ref (origin/<defaultBranch>), so a registered repo with no remote failed every spawn with BRANCH_NOT_FETCHED — an error that misleadingly names the new session branch and suggests `git fetch`, which is impossible without a remote. refs/heads/<defaultBranch> now follows origin/<defaultBranch> in the candidate list: remote-tracking still wins whenever it exists, and a remoteless repo bases session branches on its local default branch. Verified live: a plain `git init` repo (no remote) that previously failed now spawns, and the integration suite covers it (TestWorkspaceIntegrationCreateInRemotelessRepo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: run the frontend vitest suite on pull requests The renderer suite was dead for months with nothing noticing — no workflow executed it (and until #171 it could not even run: vitest auto-loads only vite.config.ts / vitest.config.ts, which the repo lacked). This job keeps the revived suite alive. Typecheck is intentionally left out until the pre-existing forge.config / update-electron-app type errors are fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(frontend): regenerate package-lock.json in sync with package.json The committed lock was missing dozens of entries (ansi-regex, error-ex, minimatch, ...), so `npm ci` under CI's npm 10 hard-fails with "Missing: <pkg> from lock file" — the new Frontend workflow caught it on its first run. Newer local npm versions tolerated the drift, which is why it went unnoticed. Regenerated with npm install (npm 10.9.8, lockfileVersion 3) and validated with a clean `npm ci` + full vitest run (9/9 files, 99/99 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What
Re-lands the spawn-modal fixes from the closed redesign branch (#156) onto main, revives the renderer test suite that would have caught them, and fixes the remoteless-repo spawn failure found in post-merge QA.
Spawn modal (3 bugs)
createTasksentbranch: "main", but the API'sbranchfield names the session's new worktree branch — sending an already-checked-out branch makes the daemon replyBRANCH_CHECKED_OUT_ELSEWHERE. The field is gone from the request; the "Based on" pane is now informational (workers branch off the project's default branch in a fresh worktree).[object Object]. openapi-fetch resolves non-2xx responses to a plain{code, error, message}envelope, not anError. NewapiErrorMessageunwraps it, so the user sees e.g.main is checked out at /home/me/my-app (BRANCH_CHECKED_OUT_ELSEWHERE).PATCHrename (a failed rename must not surface as a failed spawn — the worker is already running; retrying would spawn a duplicate).Remoteless-repo spawn fix (backend)
Found in post-merge QA: a registered repo with no git remote failed every spawn with
BRANCH_NOT_FETCHED— an error that names the new session branch and suggestsgit fetch, impossible without a remote. The workspace layer resolved the base for a new session branch only viaorigin/<defaultBranch>;refs/heads/<defaultBranch>now follows it in the candidate list (remote-tracking still wins when present). Re-landed from the redesign archive with its integration test, and verified live against the repo that failed QA.Test-suite revival
vitest runonly auto-loadsvite.config.ts/vitest.config.ts— Forge's per-targetvite.*.config.tsfiles are invisible to it, so thetestblock invite.renderer.config.ts(jsdom + setup file) was dead config and 7 of 9 suite files failed withwindow is not defined. A one-linevitest.config.tsre-exports the renderer config.Knock-on fixes from actually loading that config:
vite.renderer.config.tsimportsdefineConfigfromvitest/configso itstestkey typechecks.routeTree.gen.ts+ the session route file are regenerated by the pinned@tanstack/router-plugin(the checked-in tree predated the installed plugin version; its route-ID drift caused 3 of main's 7 typecheck errors, and anynpm startwould regenerate these files anyway).App.test.tsxwrapsAppinTooltipProvider, mirroringroutes/__root.tsx.Verification
[object Object]).go test ./...green incl.TestWorkspaceIntegrationCreateInRemotelessRepo; golangci-lint 0 issues on touched packages.git initrepo (no remote) that failed spawn on main spawns successfully with this branch's daemon.npm run typecheck: 7 errors → 3. The 3 survivors (forge.config.tsnotarize/maker types,update-electron-appcall signature) predate this branch and are untouched.🤖 Generated with Claude Code