fix(spawn): stop sending branch on spawn, render API errors, wire worker name (#171)
* fix(spawn): stop sending branch on spawn, render API errors, wire worker 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> * chore: format with prettier [skip ci] * fix(gitworktree): base new session branches on the local default branch 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> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
40bd2dfb2b
commit
d60c49f5fb
|
|
@ -45,9 +45,14 @@ func worktreeListPorcelainArgs(repo string) []string {
|
|||
func baseRefCandidates(branch, defaultBranch string) []string {
|
||||
candidates := []string{"origin/" + branch}
|
||||
if strings.Contains(defaultBranch, "/") {
|
||||
// A qualified default ("upstream/main") is used verbatim; git's refname
|
||||
// disambiguation already falls back to refs/heads/<defaultBranch>.
|
||||
candidates = append(candidates, defaultBranch)
|
||||
} else {
|
||||
candidates = append(candidates, "origin/"+defaultBranch)
|
||||
// The local head comes after origin/<defaultBranch> so remote-tracking
|
||||
// still wins when present, but a remoteless repo can base new branches
|
||||
// on its local default branch instead of failing BRANCH_NOT_FETCHED.
|
||||
candidates = append(candidates, "origin/"+defaultBranch, "refs/heads/"+defaultBranch)
|
||||
}
|
||||
return append(candidates, branch)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,42 @@ func TestWorkspaceIntegrationDestroyDirtyWorktree(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestWorkspaceIntegrationCreateInRemotelessRepo guards the BRANCH_NOT_FETCHED
|
||||
// regression: a repo with no remote configured must still spawn worktrees for
|
||||
// new branches by basing them on the local default-branch head
|
||||
// (refs/heads/main) once no origin/* candidate resolves.
|
||||
func TestWorkspaceIntegrationCreateInRemotelessRepo(t *testing.T) {
|
||||
git := requireGit(t)
|
||||
tmp := t.TempDir()
|
||||
repo := filepath.Join(tmp, "repo")
|
||||
run(t, git, "init", repo)
|
||||
runGit(t, git, repo, "config", "user.email", "ao@example.com")
|
||||
runGit(t, git, repo, "config", "user.name", "Ao Agents")
|
||||
if err := os.WriteFile(filepath.Join(repo, "README.md"), []byte("seed\n"), 0o644); err != nil {
|
||||
t.Fatalf("write seed: %v", err)
|
||||
}
|
||||
runGit(t, git, repo, "add", "README.md")
|
||||
runGit(t, git, repo, "commit", "-m", "seed")
|
||||
runGit(t, git, repo, "branch", "-M", "main")
|
||||
|
||||
root := filepath.Join(tmp, "managed")
|
||||
ws, err := New(Options{Binary: git, ManagedRoot: root, RepoResolver: StaticRepoResolver{"proj": repo}})
|
||||
if err != nil {
|
||||
t.Fatalf("new: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
info, err := ws.Create(ctx, ports.WorkspaceConfig{ProjectID: "proj", SessionID: "sess", Branch: "feature/remoteless"})
|
||||
if err != nil {
|
||||
t.Fatalf("create in remoteless repo: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(info.Path, "README.md")); err != nil {
|
||||
t.Fatalf("created worktree missing seed file: %v", err)
|
||||
}
|
||||
if err := ws.Destroy(ctx, info); err != nil {
|
||||
t.Fatalf("destroy: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func requireGit(t *testing.T) string {
|
||||
t.Helper()
|
||||
git, err := exec.LookPath("git")
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func TestCommandArgs(t *testing.T) {
|
|||
|
||||
func TestBaseRefCandidates(t *testing.T) {
|
||||
got := baseRefCandidates("feature/test", "main")
|
||||
want := []string{"origin/feature/test", "origin/main", "feature/test"}
|
||||
want := []string{"origin/feature/test", "origin/main", "refs/heads/main", "feature/test"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("candidates = %#v, want %#v", got, want)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
import { App } from "./App";
|
||||
import { TooltipProvider } from "./components/ui/tooltip";
|
||||
import { useUiStore } from "./stores/ui-store";
|
||||
|
||||
const { postMock, mockData } = vi.hoisted(() => ({
|
||||
const { postMock, patchMock, mockData } = vi.hoisted(() => ({
|
||||
postMock: vi.fn(),
|
||||
patchMock: vi.fn(),
|
||||
mockData: {
|
||||
projectsError: undefined as Error | undefined,
|
||||
projects: [] as { id: string; name: string; path: string; sessionPrefix: string }[],
|
||||
|
|
@ -38,6 +40,7 @@ vi.mock("./lib/api-client", () => ({
|
|||
return { data: undefined, error: new Error(`unexpected GET ${url}`) };
|
||||
}),
|
||||
POST: postMock,
|
||||
PATCH: patchMock,
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
@ -47,15 +50,19 @@ vi.mock("./components/TerminalPane", () => ({
|
|||
|
||||
function renderApp() {
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
// TooltipProvider mirrors routes/__root.tsx, which wraps App in production.
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
postMock.mockReset();
|
||||
patchMock.mockReset();
|
||||
mockData.projectsError = undefined;
|
||||
mockData.projects = [];
|
||||
mockData.sessions = [];
|
||||
|
|
@ -158,15 +165,51 @@ test("spawns a worker from the New worker modal", async () => {
|
|||
await user.type(await screen.findByLabelText("Prompt"), "Make task creation work");
|
||||
await user.click(screen.getByRole("button", { name: /Spawn worker/ }));
|
||||
|
||||
// No `branch` field: it names the worktree branch, and sending the default
|
||||
// base ("main") makes the daemon 409 with BRANCH_CHECKED_OUT_ELSEWHERE.
|
||||
expect(postMock).toHaveBeenCalledWith("/api/v1/sessions", {
|
||||
body: {
|
||||
projectId: "proj-1",
|
||||
kind: "worker",
|
||||
harness: "claude-code",
|
||||
prompt: "Make task creation work",
|
||||
branch: "main",
|
||||
},
|
||||
});
|
||||
// No worker name given, so no rename round-trip.
|
||||
expect(patchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test("surfaces an error when spawning fails", async () => {
|
||||
|
|
@ -188,8 +231,33 @@ test("surfaces an error when spawning fails", async () => {
|
|||
kind: "worker",
|
||||
harness: "claude-code",
|
||||
prompt: "Failing task",
|
||||
branch: "main",
|
||||
},
|
||||
});
|
||||
expect(await screen.findByText("Failed to fetch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("surfaces the daemon error envelope message, not [object Object]", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockData.projects = [{ id: "proj-1", name: "my-app", path: "/home/me/my-app", sessionPrefix: "" }];
|
||||
// openapi-fetch resolves non-2xx bodies as a plain APIError envelope.
|
||||
postMock.mockResolvedValueOnce({
|
||||
error: {
|
||||
code: "BRANCH_CHECKED_OUT_ELSEWHERE",
|
||||
error: "Conflict",
|
||||
message: "main is checked out at /home/me/my-app",
|
||||
},
|
||||
});
|
||||
|
||||
renderApp();
|
||||
|
||||
await screen.findByRole("button", { name: "Select my-app" });
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "New worker" }));
|
||||
await user.type(await screen.findByLabelText("Prompt"), "Failing task");
|
||||
await user.click(screen.getByRole("button", { name: /Spawn worker/ }));
|
||||
|
||||
expect(
|
||||
await screen.findByText("main is checked out at /home/me/my-app (BRANCH_CHECKED_OUT_ELSEWHERE)"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("[object Object]")).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,6 +24,25 @@ function errorMessage(error: unknown) {
|
|||
return error instanceof Error ? error.message : "Could not load projects";
|
||||
}
|
||||
|
||||
/**
|
||||
* openapi-fetch resolves non-2xx responses to a plain APIError envelope
|
||||
* ({ code, error, message, ... }), not an Error — String() on it renders
|
||||
* "[object Object]".
|
||||
*/
|
||||
function apiErrorMessage(error: unknown, fallback: string): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof error === "string" && error) return error;
|
||||
if (error && typeof error === "object") {
|
||||
const envelope = error as { message?: unknown; code?: unknown };
|
||||
if (typeof envelope.message === "string" && envelope.message) {
|
||||
return typeof envelope.code === "string" && envelope.code
|
||||
? `${envelope.message} (${envelope.code})`
|
||||
: envelope.message;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function App({ routeSessionId, routeWorkspaceId }: AppProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
|
|
@ -106,7 +125,7 @@ export function App({ routeSessionId, routeWorkspaceId }: AppProps) {
|
|||
const createProject = async (input: { path: string }) => {
|
||||
const { data, error } = await apiClient.POST("/api/v1/projects", { body: { path: input.path } });
|
||||
|
||||
if (error) throw error;
|
||||
if (error) throw new Error(apiErrorMessage(error, "Could not add project"));
|
||||
if (!data?.project) throw new Error("Project creation returned no project");
|
||||
|
||||
const workspace: WorkspaceSummary = {
|
||||
|
|
@ -121,23 +140,36 @@ export function App({ routeSessionId, routeWorkspaceId }: AppProps) {
|
|||
selectWorkspace(workspace.id);
|
||||
};
|
||||
|
||||
const createTask = async (input: { projectId: string; prompt: string; branch?: string; harness?: AgentProvider }) => {
|
||||
const createTask = async (input: { projectId: string; prompt: string; name?: string; harness?: AgentProvider }) => {
|
||||
// No `branch` here: the API's branch field names the session's worktree
|
||||
// branch (sending a checked-out branch like "main" 409s); the base branch
|
||||
// comes from the project config.
|
||||
const { data, error } = await apiClient.POST("/api/v1/sessions", {
|
||||
body: {
|
||||
projectId: input.projectId,
|
||||
kind: "worker",
|
||||
harness: input.harness,
|
||||
prompt: input.prompt,
|
||||
branch: input.branch || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (error || !data?.session) {
|
||||
throw new Error(error instanceof Error ? error.message : error ? String(error) : "No session returned");
|
||||
throw new Error(apiErrorMessage(error, "No session returned"));
|
||||
}
|
||||
|
||||
const session = data.session;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
updateWorkspaces((current) =>
|
||||
current.map((item) =>
|
||||
item.id === input.projectId
|
||||
|
|
@ -149,9 +181,9 @@ export function App({ routeSessionId, routeWorkspaceId }: AppProps) {
|
|||
terminalHandleId: session.terminalHandleId,
|
||||
workspaceId: item.id,
|
||||
workspaceName: item.name,
|
||||
title: input.prompt,
|
||||
title: displayName ?? input.prompt,
|
||||
provider: toAgentProvider(session.harness),
|
||||
branch: input.branch ?? "",
|
||||
branch: "",
|
||||
status: toSessionStatus(session.status, session.isTerminated),
|
||||
updatedAt: "now",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,12 +27,7 @@ type SpawnWorkerModalProps = {
|
|||
onOpenChange: (open: boolean) => void;
|
||||
workspaces: WorkspaceSummary[];
|
||||
defaultProjectId?: string;
|
||||
onCreateTask: (input: {
|
||||
projectId: string;
|
||||
prompt: string;
|
||||
branch?: string;
|
||||
harness?: AgentProvider;
|
||||
}) => Promise<void>;
|
||||
onCreateTask: (input: { projectId: string; prompt: string; name?: string; harness?: AgentProvider }) => Promise<void>;
|
||||
};
|
||||
|
||||
export function SpawnWorkerModal({
|
||||
|
|
@ -47,7 +42,6 @@ export function SpawnWorkerModal({
|
|||
const [projectId, setProjectId] = useState(fallbackProjectId);
|
||||
const [agent, setAgent] = useState<AgentProvider>("claude-code");
|
||||
const [basedOn, setBasedOn] = useState<BasedOn>("Branch");
|
||||
const [branch, setBranch] = useState("main");
|
||||
const [tab, setTab] = useState<"Prompt" | "Workspace">("Prompt");
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -62,9 +56,6 @@ export function SpawnWorkerModal({
|
|||
}, [open, fallbackProjectId]);
|
||||
|
||||
const selectedWorkspace = workspaces.find((workspace) => workspace.id === projectId) ?? workspaces[0];
|
||||
const branchOptions = Array.from(
|
||||
new Set(["main", ...(selectedWorkspace?.sessions.map((session) => session.branch).filter(Boolean) ?? [])]),
|
||||
);
|
||||
const nameValid = name === "" || NAME_RULE.test(name);
|
||||
const canSubmit = prompt.trim().length > 0 && projectId !== "" && nameValid && !isSubmitting;
|
||||
|
||||
|
|
@ -77,12 +68,11 @@ export function SpawnWorkerModal({
|
|||
await onCreateTask({
|
||||
projectId,
|
||||
prompt: prompt.trim(),
|
||||
branch: basedOn === "Branch" ? branch.trim() || undefined : undefined,
|
||||
name: name || undefined,
|
||||
harness: agent,
|
||||
});
|
||||
setName("");
|
||||
setPrompt("");
|
||||
setBranch("main");
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Could not spawn worker");
|
||||
|
|
@ -169,19 +159,15 @@ export function SpawnWorkerModal({
|
|||
</div>
|
||||
</div>
|
||||
<div className="p-2.5">
|
||||
{basedOn === "Branch" ? (
|
||||
<SelectControl
|
||||
aria-label="Based on branch"
|
||||
className="flex w-full"
|
||||
onChange={setBranch}
|
||||
value={branch}
|
||||
options={branchOptions.map((option) => ({ value: option, label: option }))}
|
||||
/>
|
||||
) : (
|
||||
<p className="px-1 py-1.5 text-[12.5px] text-passive">
|
||||
{basedOn === "Issue" ? "Pick an issue to start from." : "Pick a pull request to start from."}
|
||||
{/* The API has no per-spawn base branch — the worker branches off the
|
||||
project's configured default branch in a fresh worktree. */}
|
||||
{basedOn === "Branch"
|
||||
? "Branches off the project's default branch in a fresh worktree."
|
||||
: basedOn === "Issue"
|
||||
? "Pick an issue to start from."
|
||||
: "Pick a pull request to start from."}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,57 +1,50 @@
|
|||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// This file is auto-generated by @tanstack/router-plugin
|
||||
// Do not edit this file manually — it is regenerated on every dev/build.
|
||||
|
||||
// Import Routes
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRoute } from "./routes/__root";
|
||||
import { Route as IndexRoute } from "./routes/index";
|
||||
import { Route as WorkspacesWorkspaceIdRoute } from "./routes/workspaces.$workspaceId";
|
||||
import { Route as WorkspacesWorkspaceIdSessionsSessionIdRoute } from "./routes/workspaces.$workspaceId_.sessions.$sessionId";
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as WorkspacesWorkspaceIdRouteImport } from "./routes/workspaces.$workspaceId";
|
||||
import { Route as WorkspacesWorkspaceIdSessionsSessionIdRouteImport } from "./routes/workspaces.$workspaceId_.sessions.$sessionId";
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const IndexRouteWithChildren = IndexRoute.update({
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => rootRoute,
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
|
||||
const WorkspacesWorkspaceIdRouteWithChildren = WorkspacesWorkspaceIdRoute.update({
|
||||
const WorkspacesWorkspaceIdRoute = WorkspacesWorkspaceIdRouteImport.update({
|
||||
id: "/workspaces/$workspaceId",
|
||||
path: "/workspaces/$workspaceId",
|
||||
getParentRoute: () => rootRoute,
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
|
||||
const WorkspacesWorkspaceIdSessionsSessionIdRouteWithChildren = WorkspacesWorkspaceIdSessionsSessionIdRoute.update({
|
||||
const WorkspacesWorkspaceIdSessionsSessionIdRoute = WorkspacesWorkspaceIdSessionsSessionIdRouteImport.update({
|
||||
id: "/workspaces/$workspaceId_/sessions/$sessionId",
|
||||
path: "/workspaces/$workspaceId/sessions/$sessionId",
|
||||
getParentRoute: () => rootRoute,
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
|
||||
// Populate FileRoutes
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
"/workspaces/$workspaceId": typeof WorkspacesWorkspaceIdRoute;
|
||||
"/workspaces/$workspaceId/sessions/$sessionId": typeof WorkspacesWorkspaceIdSessionsSessionIdRoute;
|
||||
}
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
"/workspaces/$workspaceId": typeof WorkspacesWorkspaceIdRoute;
|
||||
"/workspaces/$workspaceId/sessions/$sessionId": typeof WorkspacesWorkspaceIdSessionsSessionIdRoute;
|
||||
}
|
||||
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute;
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
"/workspaces/$workspaceId": typeof WorkspacesWorkspaceIdRoute;
|
||||
"/workspaces/$workspaceId_/sessions/$sessionId": typeof WorkspacesWorkspaceIdSessionsSessionIdRoute;
|
||||
}
|
||||
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths: "/" | "/workspaces/$workspaceId" | "/workspaces/$workspaceId/sessions/$sessionId";
|
||||
|
|
@ -60,17 +53,41 @@ export interface FileRouteTypes {
|
|||
id: "__root__" | "/" | "/workspaces/$workspaceId" | "/workspaces/$workspaceId_/sessions/$sessionId";
|
||||
fileRoutesById: FileRoutesById;
|
||||
}
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
WorkspacesWorkspaceIdRoute: typeof WorkspacesWorkspaceIdRoute;
|
||||
WorkspacesWorkspaceIdSessionsSessionIdRoute: typeof WorkspacesWorkspaceIdSessionsSessionIdRoute;
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRouteWithChildren,
|
||||
WorkspacesWorkspaceIdRoute: WorkspacesWorkspaceIdRouteWithChildren,
|
||||
WorkspacesWorkspaceIdSessionsSessionIdRoute: WorkspacesWorkspaceIdSessionsSessionIdRouteWithChildren,
|
||||
};
|
||||
declare module "@tanstack/react-router" {
|
||||
interface FileRoutesByPath {
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/workspaces/$workspaceId": {
|
||||
id: "/workspaces/$workspaceId";
|
||||
path: "/workspaces/$workspaceId";
|
||||
fullPath: "/workspaces/$workspaceId";
|
||||
preLoaderRoute: typeof WorkspacesWorkspaceIdRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/workspaces/$workspaceId_/sessions/$sessionId": {
|
||||
id: "/workspaces/$workspaceId_/sessions/$sessionId";
|
||||
path: "/workspaces/$workspaceId/sessions/$sessionId";
|
||||
fullPath: "/workspaces/$workspaceId/sessions/$sessionId";
|
||||
preLoaderRoute: typeof WorkspacesWorkspaceIdSessionsSessionIdRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const routeTree = rootRoute._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
WorkspacesWorkspaceIdRoute: WorkspacesWorkspaceIdRoute,
|
||||
WorkspacesWorkspaceIdSessionsSessionIdRoute: WorkspacesWorkspaceIdSessionsSessionIdRoute,
|
||||
};
|
||||
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { App } from "../App";
|
||||
|
||||
export const Route = createFileRoute("/workspaces/$workspaceId/sessions/$sessionId")({
|
||||
export const Route = createFileRoute("/workspaces/$workspaceId_/sessions/$sessionId")({
|
||||
component: SessionRoute,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { defineConfig } from "vite";
|
||||
// defineConfig comes from vitest/config (a superset of vite's) so the `test`
|
||||
// block below typechecks; the produced config is a plain vite config object.
|
||||
import { defineConfig } from "vitest/config";
|
||||
import type { Plugin } from "vite";
|
||||
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
// `vitest run` only auto-loads vite.config.ts / vitest.config.ts — it never
|
||||
// sees Forge's per-target vite.*.config.ts files, so the renderer config's
|
||||
// `test` block (jsdom environment, setup file) was dead config and every
|
||||
// DOM-touching test failed with "window is not defined". Re-export the
|
||||
// renderer config so tests run under the same plugins and test settings.
|
||||
export { default } from "./vite.renderer.config";
|
||||
Loading…
Reference in New Issue