Skip to content

fix(spawn): stop sending branch on spawn, render API errors, wire worker name#171

Merged
ashish921998 merged 3 commits into
mainfrom
fix/spawn-modal-no-branch
Jun 11, 2026
Merged

fix(spawn): stop sending branch on spawn, render API errors, wire worker name#171
ashish921998 merged 3 commits into
mainfrom
fix/spawn-modal-no-branch

Conversation

@ashish921998

@ashish921998 ashish921998 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

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)

  1. Every spawn 409'd when "Based on: Branch" was left at its default. createTask sent branch: "main", but the API's branch field names the session's new worktree branch — sending an already-checked-out branch makes the daemon reply BRANCH_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).
  2. Spawn failures rendered as [object Object]. openapi-fetch resolves non-2xx responses to a plain {code, error, message} envelope, not an Error. New apiErrorMessage unwraps it, so the user sees e.g. main is checked out at /home/me/my-app (BRANCH_CHECKED_OUT_ELSEWHERE).
  3. The "Worker name" field did nothing. After a successful spawn, the name is now sent as a best-effort PATCH rename (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 suggests git fetch, impossible without a remote. The workspace layer resolved the base for a new session branch only via origin/<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 run only auto-loads vite.config.ts/vitest.config.ts — Forge's per-target vite.*.config.ts files are invisible to it, so the test block in vite.renderer.config.ts (jsdom + setup file) was dead config and 7 of 9 suite files failed with window is not defined. A one-line vitest.config.ts re-exports the renderer config.

Knock-on fixes from actually loading that config:

  • vite.renderer.config.ts imports defineConfig from vitest/config so its test key 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 any npm start would regenerate these files anyway).
  • App.test.tsx wraps App in TooltipProvider, mirroring routes/__root.tsx.

Verification

  • Frontend: 9/9 test files, 99/99 tests pass (main: 2/9 files runnable). New tests pin all three fixes: no-branch spawn body, rename round-trip, envelope message (and no [object Object]).
  • Backend: full go test ./... green incl. TestWorkspaceIntegrationCreateInRemotelessRepo; golangci-lint 0 issues on touched packages.
  • Live: a plain git init repo (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.ts notarize/maker types, update-electron-app call signature) predate this branch and are untouched.

🤖 Generated with Claude Code

ashish921998 and others added 2 commits June 11, 2026 11:12
…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-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three spawn-modal bugs (409 on branch field, [object Object] error rendering, and the no-op worker name field), revives the renderer test suite under vitest, and extends the git worktree adapter to fall back to refs/heads/<defaultBranch> for remoteless repos.

  • Spawn fixes: branch is removed from the POST body; errors are now unwrapped through apiErrorMessage; a best-effort PATCH rename runs after a successful spawn and silently falls back to the prompt as title if the rename fails.
  • Test revival: vitest.config.ts re-exports the renderer config so its jsdom environment and setup file apply during test runs; three new integration tests cover no-branch spawn, rename round-trip, and error-envelope rendering.
  • Backend: baseRefCandidates appends refs/heads/<defaultBranch> after origin/<defaultBranch> for unqualified default branches, allowing worktree creation in repos without a configured remote.

Confidence Score: 5/5

Safe 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

Filename Overview
frontend/src/renderer/App.tsx Adds apiErrorMessage helper, removes branch from POST body, adds best-effort PATCH rename; logic is correct and well-commented.
frontend/src/renderer/components/SpawnWorkerModal.tsx Removes branch state and SelectControl in favour of informational text; wires name field into onCreateTask; clean removal.
frontend/src/renderer/App.test.tsx Adds patchMock, TooltipProvider wrapper, and three new targeted tests (no-branch POST, rename round-trip, error envelope); coverage is solid.
backend/internal/adapters/workspace/gitworktree/commands.go Appends refs/heads/ fallback for remoteless repos; guarded by unit and integration tests.
backend/internal/adapters/workspace/gitworktree/workspace_integration_test.go New TestWorkspaceIntegrationCreateInRemotelessRepo exercises the regression fix end-to-end against real git.
frontend/vitest.config.ts One-liner re-export of vite.renderer.config.ts; correctly makes the jsdom test environment visible to vitest.
frontend/vite.renderer.config.ts defineConfig import changed from vite to vitest/config for proper test block typechecking; functionally equivalent for production builds.
frontend/src/renderer/routeTree.gen.ts Auto-regenerated by @tanstack/router-plugin; module augmentation added, WithChildren suffix removed; machine-generated, no manual review needed.
frontend/src/renderer/routes/workspaces.$workspaceId_.sessions.$sessionId.tsx Route ID corrected to match the generated tree.
backend/internal/adapters/workspace/gitworktree/workspace_test.go TestBaseRefCandidates updated to expect the new refs/heads/ entry.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (2): Last reviewed commit: "fix(gitworktree): base new session branc..." | Re-trigger Greptile

Comment on lines +162 to +170
// 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +182 to 213
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();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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>
@ashish921998 ashish921998 merged commit d60c49f into main Jun 11, 2026
9 checks passed
ashish921998 added a commit that referenced this pull request Jun 11, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant