diff --git a/apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts b/apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts index 0d8cf966873..da476c8a504 100644 --- a/apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts +++ b/apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts @@ -6,6 +6,7 @@ describe("git-operations error handling", () => { const upstreamDeletedMessages = [ "Your configuration specifies to merge with the ref 'refs/heads/feature-branch' from the remote, but no such ref was fetched.", "fatal: couldn't find remote ref refs/heads/deleted-branch", + "fatal: kitenite/dont-hide-changes-tab cannot be resolved to branch", "There is no tracking information for the current branch", ]; diff --git a/apps/desktop/src/lib/trpc/routers/changes/git-operations.ts b/apps/desktop/src/lib/trpc/routers/changes/git-operations.ts index dbec334b5f0..9ce8aa825d3 100644 --- a/apps/desktop/src/lib/trpc/routers/changes/git-operations.ts +++ b/apps/desktop/src/lib/trpc/routers/changes/git-operations.ts @@ -1,3 +1,4 @@ +import { TRPCError } from "@trpc/server"; import { shell } from "electron"; import simpleGit from "simple-git"; import { z } from "zod"; @@ -22,7 +23,68 @@ async function fetchCurrentBranch( git: ReturnType, ): Promise { const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim(); - await git.fetch(["origin", branch]); + try { + await git.fetch(["origin", branch]); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (isUpstreamMissingError(message)) { + try { + await git.fetch(["origin"]); + } catch (fallbackError) { + const fallbackMessage = + fallbackError instanceof Error + ? fallbackError.message + : String(fallbackError); + if (!isUpstreamMissingError(fallbackMessage)) { + console.error( + `[git/fetch] failed fallback fetch for branch ${branch}:`, + fallbackError, + ); + throw fallbackError; + } + } + return; + } + throw error; + } +} + +async function pushWithSetUpstream({ + git, + branch, +}: { + git: ReturnType; + branch: string; +}): Promise { + const trimmedBranch = branch.trim(); + if (!trimmedBranch || trimmedBranch === "HEAD") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + "Cannot push from detached HEAD. Please checkout a branch and try again.", + }); + } + + // Use HEAD refspec to avoid resolving the branch name as a local ref. + // This is more reliable for worktrees where upstream tracking isn't set yet. + await git.push([ + "--set-upstream", + "origin", + `HEAD:refs/heads/${trimmedBranch}`, + ]); +} + +function shouldRetryPushWithUpstream(message: string): boolean { + const lowerMessage = message.toLowerCase(); + return ( + lowerMessage.includes("no upstream branch") || + lowerMessage.includes("no tracking information") || + lowerMessage.includes( + "upstream branch of your current branch does not match", + ) || + lowerMessage.includes("cannot be resolved to branch") || + lowerMessage.includes("couldn't find remote ref") + ); } export const createGitOperationsRouter = () => { @@ -62,9 +124,20 @@ export const createGitOperationsRouter = () => { if (input.setUpstream && !hasUpstream) { const branch = await git.revparse(["--abbrev-ref", "HEAD"]); - await git.push(["--set-upstream", "origin", branch.trim()]); + await pushWithSetUpstream({ git, branch }); } else { - await git.push(); + try { + await git.push(); + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + if (shouldRetryPushWithUpstream(message)) { + const branch = await git.revparse(["--abbrev-ref", "HEAD"]); + await pushWithSetUpstream({ git, branch }); + } else { + throw error; + } + } } await fetchCurrentBranch(git); return { success: true }; @@ -112,7 +185,7 @@ export const createGitOperationsRouter = () => { error instanceof Error ? error.message : String(error); if (isUpstreamMissingError(message)) { const branch = await git.revparse(["--abbrev-ref", "HEAD"]); - await git.push(["--set-upstream", "origin", branch.trim()]); + await pushWithSetUpstream({ git, branch }); await fetchCurrentBranch(git); return { success: true }; } @@ -148,10 +221,20 @@ export const createGitOperationsRouter = () => { // Ensure branch is pushed first if (!hasUpstream) { - await git.push(["--set-upstream", "origin", branch]); + await pushWithSetUpstream({ git, branch }); } else { // Push any unpushed commits - await git.push(); + try { + await git.push(); + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + if (shouldRetryPushWithUpstream(message)) { + await pushWithSetUpstream({ git, branch }); + } else { + throw error; + } + } } // Get the remote URL to construct the GitHub compare URL diff --git a/apps/desktop/src/lib/trpc/routers/changes/git-utils.ts b/apps/desktop/src/lib/trpc/routers/changes/git-utils.ts index 701c2e62f4c..663d00897b3 100644 --- a/apps/desktop/src/lib/trpc/routers/changes/git-utils.ts +++ b/apps/desktop/src/lib/trpc/routers/changes/git-utils.ts @@ -5,6 +5,7 @@ export function isUpstreamMissingError(message: string): boolean { return ( message.includes("no such ref was fetched") || message.includes("no tracking information") || - message.includes("couldn't find remote ref") + message.includes("couldn't find remote ref") || + message.includes("cannot be resolved to branch") ); } diff --git a/apps/desktop/src/renderer/index.tsx b/apps/desktop/src/renderer/index.tsx index 5eaaf0a7fcd..a767f1bc511 100644 --- a/apps/desktop/src/renderer/index.tsx +++ b/apps/desktop/src/renderer/index.tsx @@ -26,21 +26,18 @@ const router = createRouter({ }, }); -// Track pageviews on navigation const unsubscribe = router.subscribe("onResolved", (event) => { posthog.capture("$pageview", { $current_url: event.toLocation.pathname, }); }); -// Handle deep link navigation from main process const handleDeepLink = (path: string) => { console.log("[deep-link] Navigating to:", path); router.navigate({ to: path }); }; window.ipcRenderer.on("deep-link-navigate", handleDeepLink); -// Clean up subscription on HMR if (import.meta.hot) { import.meta.hot.dispose(() => { unsubscribe(); diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateFromPr.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateFromPr.ts index 6d1ebb8f87b..6df746884c7 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateFromPr.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateFromPr.ts @@ -19,7 +19,6 @@ export function useCreateFromPr(options?: MutationOptions) { return electronTrpc.workspaces.createFromPr.useMutation({ ...options, onSuccess: async (data, ...rest) => { - // Set optimistic progress before navigation for new workspaces if (!data.wasExisting && data.initialCommands) { const optimisticProgress: WorkspaceInitProgress = { workspaceId: data.workspace.id, @@ -30,7 +29,6 @@ export function useCreateFromPr(options?: MutationOptions) { updateProgress(optimisticProgress); } - // Setup terminal if there are initial commands if (data.initialCommands) { addPendingTerminalSetup({ workspaceId: data.workspace.id, @@ -41,7 +39,6 @@ export function useCreateFromPr(options?: MutationOptions) { await utils.workspaces.invalidate(); - // Navigate to the workspace navigateToWorkspace(data.workspace.id, navigate); await options?.onSuccess?.(data, ...rest); diff --git a/apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts index ef03a33a223..fe1dba6dac2 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts @@ -33,11 +33,9 @@ export function useDeleteWorkspace( return electronTrpc.workspaces.delete.useMutation({ ...options, onMutate: async ({ id }) => { - // Check if we're viewing the workspace being deleted const wasViewingDeleted = params.workspaceId === id; let navigatedTo: string | null = null; - // If viewing deleted workspace, get navigation target BEFORE optimistic update if (wasViewingDeleted) { const prevWorkspaceId = await utils.workspaces.getPreviousWorkspace.fetch({ id }); @@ -55,7 +53,6 @@ export function useDeleteWorkspace( } } - // Cancel outgoing queries and get snapshots await Promise.all([ utils.workspaces.getAll.cancel(), utils.workspaces.getAllGrouped.cancel(), @@ -64,7 +61,6 @@ export function useDeleteWorkspace( const previousGrouped = utils.workspaces.getAllGrouped.getData(); const previousAll = utils.workspaces.getAll.getData(); - // Optimistic update: remove workspace from cache if (previousGrouped) { utils.workspaces.getAllGrouped.setData( undefined, @@ -96,11 +92,9 @@ export function useDeleteWorkspace( await options?.onSettled?.(...args); }, onSuccess: async (data, variables, ...rest) => { - // Navigation already handled in onMutate (optimistic) await options?.onSuccess?.(data, variables, ...rest); }, onError: async (_err, variables, context, ...rest) => { - // Rollback optimistic cache updates if (context?.previousGrouped !== undefined) { utils.workspaces.getAllGrouped.setData( undefined, @@ -111,7 +105,6 @@ export function useDeleteWorkspace( utils.workspaces.getAll.setData(undefined, context.previousAll); } - // If we optimistically navigated away, navigate back to the deleted workspace if (context?.wasViewingDeleted) { navigateToWorkspace(variables.id, navigate); } diff --git a/apps/desktop/src/renderer/react-query/workspaces/useOpenWorktree.ts b/apps/desktop/src/renderer/react-query/workspaces/useOpenWorktree.ts index f040663a6f4..02221724eb9 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useOpenWorktree.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useOpenWorktree.ts @@ -28,9 +28,7 @@ export function useOpenWorktree( return electronTrpc.workspaces.openWorktree.useMutation({ ...options, onSuccess: async (data, ...rest) => { - // Auto-invalidate all workspace queries await utils.workspaces.invalidate(); - // Invalidate project queries since openWorktree updates project metadata await utils.projects.getRecents.invalidate(); const initialCommands = @@ -38,13 +36,10 @@ export function useOpenWorktree( ? data.initialCommands : undefined; - // Always create a terminal tab when opening a worktree const { tabId, paneId } = addTab(data.workspace.id); if (initialCommands) { setTabAutoTitle(tabId, "Workspace Setup"); } - // Pre-create terminal session (with initial commands if present) - // Terminal component will attach to this session when it mounts createOrAttach.mutate({ paneId, tabId, @@ -53,7 +48,6 @@ export function useOpenWorktree( }); if (!initialCommands) { - // Show config toast if no setup commands toast.info("No setup script configured", { description: "Automate workspace setup with a config.json file", action: { @@ -66,10 +60,8 @@ export function useOpenWorktree( }); } - // Navigate to the opened workspace navigateToWorkspace(data.workspace.id, navigate); - // Call user's onSuccess if provided await options?.onSuccess?.(data, ...rest); }, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts index a03224bf544..ec33397b0d5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts @@ -30,7 +30,7 @@ export function useHybridSearch(tasks: T[]) { }), [tasks], ); - // TODO(satya): Replace this with embeddings search + const fuzzyFuse = useMemo( () => new Fuse(tasks, { diff --git a/apps/web/src/app/auth/desktop/success/page.tsx b/apps/web/src/app/auth/desktop/success/page.tsx index b265f9b536b..19c13d94a25 100644 --- a/apps/web/src/app/auth/desktop/success/page.tsx +++ b/apps/web/src/app/auth/desktop/success/page.tsx @@ -23,7 +23,6 @@ export default async function DesktopSuccessPage({ ); } - // Get session from Better Auth let session: Awaited> | null = null; try { session = await auth.api.getSession({ headers: await headers() }); @@ -50,8 +49,7 @@ export default async function DesktopSuccessPage({ ); } - // Create a separate session for the desktop app instead of reusing the browser session - // This ensures desktop and web have independent sessions with separate activeOrganizationId + // Desktop and web need independent sessions with separate activeOrganizationId const headersObj = await headers(); const userAgent = headersObj.get("user-agent") || "Superset Desktop App"; const ipAddress = @@ -59,15 +57,11 @@ export default async function DesktopSuccessPage({ headersObj.get("x-real-ip") || undefined; - // Generate a unique session token for the desktop app const crypto = await import("node:crypto"); const token = crypto.randomBytes(32).toString("base64url"); const now = new Date(); - const expiresAt = new Date( - Date.now() + 60 * 60 * 24 * 30 * 1000, // 30 days (matching auth config) - ); + const expiresAt = new Date(Date.now() + 60 * 60 * 24 * 30 * 1000); - // Create a new session record in the database await db.insert(sessions).values({ token, userId: session.user.id, diff --git a/apps/web/src/app/oauth/consent/page.tsx b/apps/web/src/app/oauth/consent/page.tsx index eda97baccdc..fa750fcac9e 100644 --- a/apps/web/src/app/oauth/consent/page.tsx +++ b/apps/web/src/app/oauth/consent/page.tsx @@ -20,7 +20,6 @@ export default async function ConsentPage({ searchParams }: ConsentPageProps) { headers: await headers(), }); - // Redirect to sign-in if not authenticated if (!session) { const params = await searchParams; const returnUrl = `/oauth/consent?${new URLSearchParams(params as Record).toString()}`; @@ -29,7 +28,6 @@ export default async function ConsentPage({ searchParams }: ConsentPageProps) { const { consent_code, client_id, scope } = await searchParams; - // Validate required parameters if (!consent_code || !client_id) { return (
@@ -62,11 +60,9 @@ export default async function ConsentPage({ searchParams }: ConsentPageProps) { const scopes = scope?.split(" ").filter(Boolean) ?? ["openid"]; - // Fetch user's organization memberships via tRPC const trpc = await api(); const userOrganizations = await trpc.user.myOrganizations.query(); - // Get active organization from session or default to first const extendedSession = session.session as typeof session.session & { activeOrganizationId?: string | null; };