From 53f0d97096cf65509bc6c5010a5a162e3ecf1bd0 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 15 Apr 2026 12:29:38 -0700 Subject: [PATCH 1/6] fix(desktop): persist v2 sidebar base branch across creation and display The v2 right sidebar always rendered the repo default branch because the BaseBranchSelector was hardcoded to display defaultBranchName, and ensureSidebarWorkspaceRecord never seeded sidebarState.baseBranch from the pending workspace's selection. Thread the persisted value into the selector and seed it at creation time from pending.baseBranch. --- apps/desktop/docs/V2_BASE_BRANCH.md | 24 +++++++++++++++++++ .../_dashboard/pending/$pendingId/page.tsx | 6 ++++- .../ChangesHeader/ChangesHeader.tsx | 4 +++- .../ChangesTabContent/ChangesTabContent.tsx | 3 +++ .../hooks/useChangesTab/useChangesTab.tsx | 1 + .../useDashboardSidebarState.ts | 15 ++++++++++-- 6 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/docs/V2_BASE_BRANCH.md diff --git a/apps/desktop/docs/V2_BASE_BRANCH.md b/apps/desktop/docs/V2_BASE_BRANCH.md new file mode 100644 index 00000000000..c7c7fa28a67 --- /dev/null +++ b/apps/desktop/docs/V2_BASE_BRANCH.md @@ -0,0 +1,24 @@ +# V2 base branch storage + +V2 workspaces track the "compare base branch" (the branch the changes panel diffs against) independently from V1. If you're touching base-branch behavior, read this before assuming V1's storage applies. + +## Source of truth + +- **V1**: DB-backed via `worktrees.baseBranch` + git config (`getBranchBaseConfig`). See `src/lib/trpc/routers/changes/branches.ts` (`getBranches` / `updateBaseBranch`) and `src/lib/trpc/routers/workspaces/utils/workspace-init.ts`. +- **V2**: Renderer-only, persisted on the `v2WorkspaceLocalState` collection at `sidebarState.baseBranch` (schema: `renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts`). + +V2 does not read or write `worktrees.baseBranch`. Don't bridge them — the collection is the single source of truth for V2's sidebar. + +## Seeding at creation + +`ensureSidebarWorkspaceRecord` (`renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts`) inserts a fresh `v2WorkspaceLocalState` row the first time a workspace is opened. It accepts an optional `baseBranch` so callers that know the creation-time base (the pending page, from `pendingWorkspaces.baseBranch`) can seed it. Callers without that context pass nothing and get `null`, which falls back to the repo's default branch at read time. + +When adding a new creation path, pass the chosen base branch into `ensureWorkspaceInSidebar(workspaceId, projectId, baseBranch)` so the sidebar reflects the user's selection immediately instead of jumping to the default. + +## Reading for display + +`useChangesTab` reads `sidebarState.baseBranch` and threads it to `ChangesHeader` → `BaseBranchSelector`. The selector displays `baseBranch ?? defaultBranchName` so a `null` value (never picked / legacy workspace) transparently falls back to the repo default without overwriting the stored value. The same `baseBranch` is passed as `baseBranch ?? undefined` to `git.listCommits`, letting the server fall back to its default when unset. + +## Why this split exists + +V2's sidebar state is a CRDT-ish local collection that survives offline edits and syncs via the same mechanism as other sidebar state (tab order, sections). Reusing `worktrees.baseBranch` would force V2 to round-trip through tRPC for something that is purely a UI preference, and would couple V2's sidebar to V1's branch-switching side effects (which clear `worktrees.baseBranch`). Keeping them separate keeps V2 responsive and lets its selection persist across branch switches inside the workspace. diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx index 6d6f6f07767..7c6d077141a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx @@ -240,7 +240,11 @@ function PendingWorkspacePage() { !navigatedRef.current ) { navigatedRef.current = true; - ensureWorkspaceInSidebar(pending.workspaceId, pending.projectId); + ensureWorkspaceInSidebar( + pending.workspaceId, + pending.projectId, + pending.baseBranch, + ); if (pending.terminals.length > 0) { const paneLayout = buildSetupPaneLayout(pending.terminals); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx index bbac83f7061..69addd6fe26 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx @@ -8,6 +8,7 @@ import { CommitFilterDropdown } from "../CommitFilterDropdown"; interface ChangesHeaderProps { currentBranch: { name: string; aheadCount: number; behindCount: number }; defaultBranchName: string; + baseBranch: string | null; commitCount: number; totalFiles: number; totalAdditions: number; @@ -25,6 +26,7 @@ interface ChangesHeaderProps { export function ChangesHeader({ currentBranch, defaultBranchName, + baseBranch, commitCount, totalFiles, totalAdditions, @@ -103,7 +105,7 @@ export function ChangesHeader({ {commitCount} {commitCount === 1 ? "commit" : "commits"} from{" "} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx index 8e48ffa4078..8cd63eba17f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx @@ -16,6 +16,7 @@ interface ChangesTabContentProps { commits: { data: RouterOutputs["git"]["listCommits"] | undefined }; branches: { data: RouterOutputs["git"]["listBranches"] | undefined }; filter: ChangesFilter; + baseBranch: string | null; files: ChangesetFile[]; isLoading: boolean; totalChanges: number; @@ -35,6 +36,7 @@ export const ChangesTabContent = memo(function ChangesTabContent({ commits, branches, filter, + baseBranch, files, isLoading, totalChanges, @@ -69,6 +71,7 @@ export const ChangesTabContent = memo(function ChangesTabContent({ , workspaceId: string, projectId: string, + baseBranch: string | null = null, ): void { if (collections.v2WorkspaceLocalState.get(workspaceId)) { return; @@ -62,6 +63,7 @@ function ensureSidebarWorkspaceRecord( projectId, tabOrder: getNextTabOrder(topLevelOrders), sectionId: null, + baseBranch, }, paneLayout: { version: 1, @@ -82,9 +84,18 @@ export function useDashboardSidebarState() { ); const ensureWorkspaceInSidebar = useCallback( - (workspaceId: string, projectId: string) => { + ( + workspaceId: string, + projectId: string, + baseBranch: string | null = null, + ) => { ensureSidebarProjectRecord(collections, projectId); - ensureSidebarWorkspaceRecord(collections, workspaceId, projectId); + ensureSidebarWorkspaceRecord( + collections, + workspaceId, + projectId, + baseBranch, + ); }, [collections], ); From b33a914bc61bf3e9135b06c9a7543f344b8f8394 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 15 Apr 2026 12:50:42 -0700 Subject: [PATCH 2/6] refactor(desktop): derive v2 base branch from git config instead of storing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace sidebarState.baseBranch in v2WorkspaceLocalState with a host-service git.getBaseBranch query backed by git config (branch..base, the same key v1 uses). Writes go through a new git.setBaseBranch mutation that invalidates the dependent queries. Removes the duplicate store (and the seed-at-creation workaround) so the v2 sidebar reflects whatever git has, which is already per-branch and persists across worktree recreates. React Query's cache is the only cache we need — base branch changes rarely enough that an extra roundtrip on focus is imperceptible. --- apps/desktop/docs/V2_BASE_BRANCH.md | 24 --------- .../host-service/useGitStatus/useGitStatus.ts | 12 ++--- .../_dashboard/pending/$pendingId/page.tsx | 6 +-- .../hooks/useChangesTab/useChangesTab.tsx | 25 ++++++--- .../useSidebarDiffRef/useSidebarDiffRef.ts | 8 ++- .../useDashboardSidebarState.ts | 15 +----- .../dashboardSidebarLocal/schema.ts | 1 - .../host-service/src/trpc/router/git/git.ts | 52 +++++++++++++++++++ 8 files changed, 86 insertions(+), 57 deletions(-) delete mode 100644 apps/desktop/docs/V2_BASE_BRANCH.md diff --git a/apps/desktop/docs/V2_BASE_BRANCH.md b/apps/desktop/docs/V2_BASE_BRANCH.md deleted file mode 100644 index c7c7fa28a67..00000000000 --- a/apps/desktop/docs/V2_BASE_BRANCH.md +++ /dev/null @@ -1,24 +0,0 @@ -# V2 base branch storage - -V2 workspaces track the "compare base branch" (the branch the changes panel diffs against) independently from V1. If you're touching base-branch behavior, read this before assuming V1's storage applies. - -## Source of truth - -- **V1**: DB-backed via `worktrees.baseBranch` + git config (`getBranchBaseConfig`). See `src/lib/trpc/routers/changes/branches.ts` (`getBranches` / `updateBaseBranch`) and `src/lib/trpc/routers/workspaces/utils/workspace-init.ts`. -- **V2**: Renderer-only, persisted on the `v2WorkspaceLocalState` collection at `sidebarState.baseBranch` (schema: `renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts`). - -V2 does not read or write `worktrees.baseBranch`. Don't bridge them — the collection is the single source of truth for V2's sidebar. - -## Seeding at creation - -`ensureSidebarWorkspaceRecord` (`renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts`) inserts a fresh `v2WorkspaceLocalState` row the first time a workspace is opened. It accepts an optional `baseBranch` so callers that know the creation-time base (the pending page, from `pendingWorkspaces.baseBranch`) can seed it. Callers without that context pass nothing and get `null`, which falls back to the repo's default branch at read time. - -When adding a new creation path, pass the chosen base branch into `ensureWorkspaceInSidebar(workspaceId, projectId, baseBranch)` so the sidebar reflects the user's selection immediately instead of jumping to the default. - -## Reading for display - -`useChangesTab` reads `sidebarState.baseBranch` and threads it to `ChangesHeader` → `BaseBranchSelector`. The selector displays `baseBranch ?? defaultBranchName` so a `null` value (never picked / legacy workspace) transparently falls back to the repo default without overwriting the stored value. The same `baseBranch` is passed as `baseBranch ?? undefined` to `git.listCommits`, letting the server fall back to its default when unset. - -## Why this split exists - -V2's sidebar state is a CRDT-ish local collection that survives offline edits and syncs via the same mechanism as other sidebar state (tab order, sections). Reusing `worktrees.baseBranch` would force V2 to round-trip through tRPC for something that is purely a UI preference, and would couple V2's sidebar to V1's branch-switching side effects (which clear `worktrees.baseBranch`). Keeping them separate keeps V2 responsive and lets its selection persist across branch switches inside the workspace. diff --git a/apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts b/apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts index 6d23f8f470a..2d84d303e07 100644 --- a/apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts @@ -1,6 +1,5 @@ import { workspaceTrpc } from "@superset/workspace-client"; import { useCallback } from "react"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useWorkspaceEvent } from "../useWorkspaceEvent"; /** @@ -15,13 +14,14 @@ import { useWorkspaceEvent } from "../useWorkspaceEvent"; * debounce needed. */ export function useGitStatus(workspaceId: string) { - const collections = useCollections(); - const baseBranch: string | null = - collections.v2WorkspaceLocalState.get(workspaceId)?.sidebarState - ?.baseBranch ?? null; - const utils = workspaceTrpc.useUtils(); + const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery( + { workspaceId }, + { staleTime: Number.POSITIVE_INFINITY, enabled: Boolean(workspaceId) }, + ); + const baseBranch = baseBranchQuery.data?.baseBranch ?? null; + const query = workspaceTrpc.git.getStatus.useQuery( { workspaceId, baseBranch: baseBranch ?? undefined }, { refetchOnWindowFocus: true, enabled: Boolean(workspaceId) }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx index 7c6d077141a..6d6f6f07767 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx @@ -240,11 +240,7 @@ function PendingWorkspacePage() { !navigatedRef.current ) { navigatedRef.current = true; - ensureWorkspaceInSidebar( - pending.workspaceId, - pending.projectId, - pending.baseBranch, - ); + ensureWorkspaceInSidebar(pending.workspaceId, pending.projectId); if (pending.terminals.length > 0) { const paneLayout = buildSetupPaneLayout(pending.terminals); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index 8c77d5271a7..6e7670441ea 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -24,12 +24,17 @@ export function useChangesTab({ onSelectFile, }: UseChangesTabParams): SidebarTabDefinition { const collections = useCollections(); + const utils = workspaceTrpc.useUtils(); const localState = collections.v2WorkspaceLocalState.get(workspaceId); const filter: ChangesFilter = localState?.sidebarState?.changesFilter ?? { kind: "all", }; - const baseBranch: string | null = - localState?.sidebarState?.baseBranch ?? null; + + const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery( + { workspaceId }, + { staleTime: Number.POSITIVE_INFINITY }, + ); + const baseBranch = baseBranchQuery.data?.baseBranch ?? null; const { viewedSet, setViewed } = useViewedFiles(workspaceId); @@ -46,14 +51,20 @@ export function useChangesTab({ [collections, workspaceId], ); + const setBaseBranchMutation = workspaceTrpc.git.setBaseBranch.useMutation({ + onSuccess: () => { + void utils.git.getBaseBranch.invalidate({ workspaceId }); + void utils.git.getStatus.invalidate({ workspaceId }); + void utils.git.listCommits.invalidate({ workspaceId }); + void utils.git.getDiff.invalidate({ workspaceId }); + }, + }); + const setBaseBranch = useCallback( (branchName: string) => { - if (!collections.v2WorkspaceLocalState.get(workspaceId)) return; - collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { - draft.sidebarState.baseBranch = branchName; - }); + setBaseBranchMutation.mutate({ workspaceId, baseBranch: branchName }); }, - [collections, workspaceId], + [setBaseBranchMutation, workspaceId], ); const commits = workspaceTrpc.git.listCommits.useQuery( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useSidebarDiffRef/useSidebarDiffRef.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useSidebarDiffRef/useSidebarDiffRef.ts index 6c8e935939f..cbf779c25b7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useSidebarDiffRef/useSidebarDiffRef.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useSidebarDiffRef/useSidebarDiffRef.ts @@ -1,3 +1,4 @@ +import { workspaceTrpc } from "@superset/workspace-client"; import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { useMemo } from "react"; @@ -15,7 +16,12 @@ export function useSidebarDiffRef(workspaceId: string): DiffRef { ); const sidebarState = rows[0]?.sidebarState; const filter = sidebarState?.changesFilter ?? { kind: "all" }; - const baseBranch = sidebarState?.baseBranch ?? null; + + const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery( + { workspaceId }, + { staleTime: Number.POSITIVE_INFINITY }, + ); + const baseBranch = baseBranchQuery.data?.baseBranch ?? null; const filterKind = filter.kind; const commitHash = diff --git a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts index 7454880082b..83aee1ce847 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts @@ -37,7 +37,6 @@ function ensureSidebarWorkspaceRecord( >, workspaceId: string, projectId: string, - baseBranch: string | null = null, ): void { if (collections.v2WorkspaceLocalState.get(workspaceId)) { return; @@ -63,7 +62,6 @@ function ensureSidebarWorkspaceRecord( projectId, tabOrder: getNextTabOrder(topLevelOrders), sectionId: null, - baseBranch, }, paneLayout: { version: 1, @@ -84,18 +82,9 @@ export function useDashboardSidebarState() { ); const ensureWorkspaceInSidebar = useCallback( - ( - workspaceId: string, - projectId: string, - baseBranch: string | null = null, - ) => { + (workspaceId: string, projectId: string) => { ensureSidebarProjectRecord(collections, projectId); - ensureSidebarWorkspaceRecord( - collections, - workspaceId, - projectId, - baseBranch, - ); + ensureSidebarWorkspaceRecord(collections, workspaceId, projectId); }, [collections], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts index d715b8b2cc0..920b6f61edf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts @@ -36,7 +36,6 @@ export const workspaceLocalStateSchema = z.object({ tabOrder: z.number().int().default(0), sectionId: z.string().uuid().nullable().default(null), changesFilter: changesFilterSchema.default({ kind: "all" }), - baseBranch: z.string().nullable().default(null), }), paneLayout: paneWorkspaceStateSchema, rightSidebarOpen: z.boolean().default(false), diff --git a/packages/host-service/src/trpc/router/git/git.ts b/packages/host-service/src/trpc/router/git/git.ts index 262756e1fbf..5e89983293f 100644 --- a/packages/host-service/src/trpc/router/git/git.ts +++ b/packages/host-service/src/trpc/router/git/git.ts @@ -231,6 +231,58 @@ export const gitRouter = router({ return { files }; }), + getBaseBranch: protectedProcedure + .input(z.object({ workspaceId: z.string() })) + .query(async ({ ctx, input }) => { + const worktreePath = resolveWorktreePath(ctx, input.workspaceId); + const git = await ctx.git(worktreePath); + const currentBranch = ( + await git.revparse(["--abbrev-ref", "HEAD"]).catch(() => "") + ).trim(); + if (!currentBranch || currentBranch === "HEAD") { + return { baseBranch: null as string | null }; + } + const configured = ( + await git + .raw(["config", `branch.${currentBranch}.base`]) + .catch(() => "") + ).trim(); + return { baseBranch: (configured || null) as string | null }; + }), + + setBaseBranch: protectedProcedure + .input( + z.object({ + workspaceId: z.string(), + baseBranch: z.string().nullable(), + }), + ) + .mutation(async ({ ctx, input }) => { + const worktreePath = resolveWorktreePath(ctx, input.workspaceId); + const git = await ctx.git(worktreePath); + const currentBranch = ( + await git.revparse(["--abbrev-ref", "HEAD"]).catch(() => "") + ).trim(); + if (!currentBranch || currentBranch === "HEAD") { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "Cannot set base branch on detached HEAD", + }); + } + if (input.baseBranch) { + await git.raw([ + "config", + `branch.${currentBranch}.base`, + input.baseBranch, + ]); + } else { + await git + .raw(["config", "--unset", `branch.${currentBranch}.base`]) + .catch(() => {}); + } + return { baseBranch: input.baseBranch }; + }), + renameBranch: protectedProcedure .input( z.object({ From 64d342ff2ec4f1c86af7f0dab60b624aa0f8fb51 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 15 Apr 2026 14:38:55 -0700 Subject: [PATCH 3/6] fix(host-service): persist v2 fork base branch to git config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v2 `create` mutation used the selected base branch only as the `git worktree add` start point and never wrote it to `branch..base`. Combined with the v2 sidebar deriving the base branch from that config, a freshly forked workspace's Changes tab always compared against the repo default — the user's fork-time selection was effectively dropped the moment creation finished. Record `startPoint.shortName` into the config right after the worktree is created. Skipped for "head" start points where no meaningful base exists. --- .../workspace-creation/workspace-creation.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts b/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts index 207b14b388b..645e43542db 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts @@ -655,6 +655,25 @@ export const workspaceCreationRouter = router({ : startPointArg, ]); + // Record the base branch in git config so the Changes tab knows what + // to compare against on first open. startPoint.shortName is the ref + // we actually forked from (user selection, resolved against local / + // remote). Skipped for "head" start point — no meaningful base. + if (startPoint.kind !== "head") { + await git + .raw([ + "config", + `branch.${branchName}.base`, + startPoint.shortName, + ]) + .catch((err) => { + console.warn( + `[workspaceCreation.create] failed to record base branch ${startPoint.shortName}:`, + err, + ); + }); + } + setProgress(input.pendingId, "registering"); // 4. Register cloud workspace row From 92db6aac04328cf3c3f99c080002d6d5e10f9991 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 15 Apr 2026 15:01:19 -0700 Subject: [PATCH 4/6] Lint --- bun.lock | 2 +- .../trpc/router/workspace-creation/workspace-creation.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/bun.lock b/bun.lock index cde4c6c4a76..4aedae65728 100644 --- a/bun.lock +++ b/bun.lock @@ -110,7 +110,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "1.5.3", + "version": "1.5.5", "dependencies": { "@ai-sdk/anthropic": "^3.0.43", "@ai-sdk/openai": "3.0.36", diff --git a/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts b/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts index 645e43542db..f11dfd4f1f6 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts @@ -661,11 +661,7 @@ export const workspaceCreationRouter = router({ // remote). Skipped for "head" start point — no meaningful base. if (startPoint.kind !== "head") { await git - .raw([ - "config", - `branch.${branchName}.base`, - startPoint.shortName, - ]) + .raw(["config", `branch.${branchName}.base`, startPoint.shortName]) .catch((err) => { console.warn( `[workspaceCreation.create] failed to record base branch ${startPoint.shortName}:`, From bfbe83d5237780789ebff9e09a699d39068baf69 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 15 Apr 2026 15:31:16 -0700 Subject: [PATCH 5/6] fix(host-service): enable push.autoSetupRemote on v2 worktree creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V2's create and checkout mutations produced worktrees without push.autoSetupRemote set, so the first terminal `git push` failed with "no upstream branch" unless the user had the config globally. V1's equivalent path already writes this per-worktree; match that. Safe against wrong upstream targeting: --no-track (create) guarantees no upstream at first push, so auto-create always wins and always sets /, never the base branch. Checkout from a remote branch already has upstream via --track, so the config is a no-op there. Also rewrite the --no-track comment at the create site — it had incorrectly tied --no-track to push targeting, when --no-track is actually about suppressing `git pull` / ahead-behind counts against the start point; push targeting is governed independently by push.autoSetupRemote. --- .../workspace-creation/workspace-creation.ts | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts b/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts index f11dfd4f1f6..4d3fdfe264b 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts @@ -639,8 +639,9 @@ export const workspaceCreationRouter = router({ // Always create a new branch — never check out an existing one. // Checking out existing branches is a separate intent (createFromPr, // or the picker's Check out action via the `checkout` procedure). - // --no-track prevents the new branch from tracking the remote ref - // (e.g. origin/main); push.autoSetupRemote handles first-push tracking. + // --no-track keeps `git pull` / ahead-behind counts from treating + // the start point as the branch's home. Push targeting is handled + // separately by push.autoSetupRemote (set below). const startPointArg = startPoint.kind === "head" ? "HEAD" : startPoint.shortName; await git.raw([ @@ -655,6 +656,27 @@ export const workspaceCreationRouter = router({ : startPointArg, ]); + // Enable autoSetupRemote per-worktree so the first terminal + // `git push` creates origin/ and sets it as upstream + // without requiring `-u`. Safe because --no-track above guarantees + // no upstream exists yet, so auto-create always wins and always + // uses the branch's own name (never the base branch). + await git + .raw([ + "-C", + worktreePath, + "config", + "--local", + "push.autoSetupRemote", + "true", + ]) + .catch((err) => { + console.warn( + "[workspaceCreation.create] failed to set push.autoSetupRemote:", + err, + ); + }); + // Record the base branch in git config so the Changes tab knows what // to compare against on first open. startPoint.shortName is the ref // we actually forked from (user selection, resolved against local / @@ -924,6 +946,26 @@ export const workspaceCreationRouter = router({ throw new TRPCError({ code: "CONFLICT", message }); } + // Enable autoSetupRemote so the first terminal `git push` on a + // local-only branch creates origin/ without requiring -u. + // Branches checked out from a remote already have upstream set + // via --track above, so this config is a no-op for them. + await git + .raw([ + "-C", + worktreePath, + "config", + "--local", + "push.autoSetupRemote", + "true", + ]) + .catch((err) => { + console.warn( + "[workspaceCreation.checkout] failed to set push.autoSetupRemote:", + err, + ); + }); + setProgress(input.pendingId, "registering"); const rollbackWorktree = async () => { From 269dc9d1451294d5b7513bcd2b2abe7c936da369 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 15 Apr 2026 16:20:21 -0700 Subject: [PATCH 6/6] fix(desktop): invalidate getBaseBranch on git:changed and clarify config scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review: 1. P1 (CodeRabbit / cubic): getBaseBranch was cached with staleTime: Infinity but never invalidated on git:changed. An external branch switch (terminal `git checkout`) would leave the sidebar comparing against the previous branch's base. Add invalidation alongside getStatus in useGitStatus — since useGitStatus is always mounted while the Changes UI is visible, this refreshes the shared cache for all consumers (useChangesTab, useSidebarDiffRef). 2. P2 (cubic): my earlier comment claimed push.autoSetupRemote was set "per-worktree" but `git config --local` in a linked worktree writes to the shared repo config, not a worktree-specific one. Rewrite the comments to note the repo-wide scope is intentional — every superset-managed worktree wants the same ergonomics. --- .../host-service/useGitStatus/useGitStatus.ts | 4 ++++ .../workspace-creation/workspace-creation.ts | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts b/apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts index 2d84d303e07..0596ef443b4 100644 --- a/apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts @@ -29,6 +29,10 @@ export function useGitStatus(workspaceId: string) { const invalidate = useCallback(() => { void utils.git.getStatus.invalidate({ workspaceId }); + // Current branch may have changed (external checkout), and + // branch..base is per-branch — drop the cache so the next read + // picks up the new branch's base. + void utils.git.getBaseBranch.invalidate({ workspaceId }); }, [utils, workspaceId]); useWorkspaceEvent("git:changed", workspaceId, invalidate); diff --git a/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts b/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts index 4d3fdfe264b..af5c2685d25 100644 --- a/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts +++ b/packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts @@ -656,11 +656,14 @@ export const workspaceCreationRouter = router({ : startPointArg, ]); - // Enable autoSetupRemote per-worktree so the first terminal - // `git push` creates origin/ and sets it as upstream - // without requiring `-u`. Safe because --no-track above guarantees - // no upstream exists yet, so auto-create always wins and always - // uses the branch's own name (never the base branch). + // Enable autoSetupRemote so the first terminal `git push` creates + // origin/ and sets it as upstream without requiring + // `-u`. Note: `--local` in a linked worktree writes to the shared + // repo config, so this applies repo-wide — intentional, every + // workspace worktree wants the same ergonomics. Safe against + // wrong-upstream targeting because --no-track above guarantees no + // upstream exists at first push, so auto-create always wins and + // always uses the branch's own name (never the base branch). await git .raw([ "-C", @@ -950,6 +953,8 @@ export const workspaceCreationRouter = router({ // local-only branch creates origin/ without requiring -u. // Branches checked out from a remote already have upstream set // via --track above, so this config is a no-op for them. + // `--local` in a linked worktree writes to the shared repo config, + // so this applies repo-wide — intentional. await git .raw([ "-C",