From e583edc909eb03b6b91a30bf6a8608eb2589fbd9 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 5 May 2026 22:06:58 -0700 Subject: [PATCH 1/9] onboarding nits --- apps/code/src/renderer/App.tsx | 7 +- .../components/BranchSelector.tsx | 7 ++ .../onboarding/components/CliInstallStep.tsx | 78 +++++++++---------- .../src/renderer/stores/sagas/focusSagas.ts | 8 +- 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/apps/code/src/renderer/App.tsx b/apps/code/src/renderer/App.tsx index 4c93f2c99..e4e5ea40a 100644 --- a/apps/code/src/renderer/App.tsx +++ b/apps/code/src/renderer/App.tsx @@ -158,7 +158,12 @@ function App() { log.warn( `Foreign branch checkout detected: ${focusedBranch} -> ${foreignBranch}. Auto-unfocusing.`, ); - await useFocusStore.getState().disableFocus(); + const result = await useFocusStore.getState().disableFocus(); + if (!result.success && result.error) { + toast.error("Could not unfocus workspace", { + description: result.error, + }); + } }, }), ); diff --git a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx index ab6615bb1..0ea81f965 100644 --- a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx @@ -127,6 +127,13 @@ export function BranchSelector({ onError: (error, { branchName }) => { const message = error instanceof Error ? error.message : "Unknown error occurred"; + if (/would be overwritten by checkout/i.test(message)) { + toast.error(`Can't switch to ${branchName}`, { + description: + "You have uncommitted changes that would be overwritten. Commit or stash them first.", + }); + return; + } toast.error(`Failed to checkout ${branchName}`, { description: message, }); diff --git a/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx b/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx index 85a72636a..209d7c874 100644 --- a/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx @@ -1,16 +1,19 @@ +import { Tooltip } from "@components/ui/Tooltip"; import { ArrowLeft, ArrowRight, ArrowSquareOut, ArrowsClockwise, + Check, CheckCircle, CircleNotch, + Copy, GitBranch, GithubLogo, Terminal, Warning, } from "@phosphor-icons/react"; -import { Box, Button, Code, Flex, Text } from "@radix-ui/themes"; +import { Box, Button, Code, Flex, IconButton, Text } from "@radix-ui/themes"; import builderHog from "@renderer/assets/images/hedgehogs/builder-hog-03.png"; import { trpcClient, useTRPC } from "@renderer/trpc/client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; @@ -20,6 +23,36 @@ import { useCallback, useState } from "react"; import { OnboardingHogTip } from "./OnboardingHogTip"; import { StepActions } from "./StepActions"; +function CommandLine({ command }: { command: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(async () => { + await navigator.clipboard.writeText(command); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }, [command]); + + return ( + + + + {command} + + + void handleCopy()} + aria-label="Copy command" + > + {copied ? : } + + + + ); +} + interface CliInstallStepProps { onNext: () => void; onBack: () => void; @@ -135,24 +168,8 @@ export function CliInstallStep({ onNext, onBack }: CliInstallStepProps) { Install with Homebrew or Xcode Command Line Tools: - - - - brew install git - - - - - - xcode-select --install - - + + + + + )} From 9291c2728eef7a40e6ce6a3170c4a703f66e0827 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 5 May 2026 22:25:18 -0700 Subject: [PATCH 3/9] fix tour stuck on step when target remounts --- .../features/tour/components/TourOverlay.tsx | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/code/src/renderer/features/tour/components/TourOverlay.tsx b/apps/code/src/renderer/features/tour/components/TourOverlay.tsx index e3b1e6778..3de387ed3 100644 --- a/apps/code/src/renderer/features/tour/components/TourOverlay.tsx +++ b/apps/code/src/renderer/features/tour/components/TourOverlay.tsx @@ -105,7 +105,13 @@ export function TourOverlay() { } }; - const resetTimer = () => { + const initialEl = document.querySelector(selector); + if (initialEl?.getAttribute("data-tour-ready") === "true") { + tryAdvance(); + return; + } + + const onMutation = () => { if (settleTimer) clearTimeout(settleTimer); const el = document.querySelector(selector); if (el?.getAttribute("data-tour-ready") === "true") { @@ -113,22 +119,13 @@ export function TourOverlay() { } }; - const observer = new MutationObserver(resetTimer); - - const el = document.querySelector(selector); - if (el) { - if (el.getAttribute("data-tour-ready") === "true") { - tryAdvance(); - return; - } - - observer.observe(el, { - subtree: true, - childList: true, - characterData: true, - attributes: true, - }); - } + const observer = new MutationObserver(onMutation); + observer.observe(document.body, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ["data-tour-ready"], + }); return () => { observer.disconnect(); From 0e73cd6ec1474cf8c8b2fb9c30249f3c527f3a67 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 5 May 2026 22:39:55 -0700 Subject: [PATCH 4/9] surface dirty-tree error on branch checkout --- apps/code/src/main/services/git/service.ts | 19 +++++++++++++++++-- .../components/BranchSelector.tsx | 7 ++++--- .../src/renderer/stores/sagas/focusSagas.ts | 8 +------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index e69bb3d9d..31270626f 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -22,6 +22,7 @@ import { getLatestCommit, getRemoteUrl, getStagedDiff, + getStatus, getSyncStatus, getUnstagedDiff, fetch as gitFetch, @@ -36,6 +37,7 @@ import { DiscardFileChangesSaga } from "@posthog/git/sagas/discard"; import { PullSaga } from "@posthog/git/sagas/pull"; import { PushSaga } from "@posthog/git/sagas/push"; import { parseGitHubUrl, parsePrUrl } from "@posthog/git/utils"; +import { TRPCError } from "@trpc/server"; import { inject, injectable } from "inversify"; import { MAIN_TOKENS } from "../../di/tokens"; import { logger } from "../../utils/logger"; @@ -300,8 +302,21 @@ export class GitService extends TypedEventEmitter { ): Promise<{ previousBranch: string; currentBranch: string }> { const saga = new SwitchBranchSaga(); const result = await saga.run({ baseDir: directoryPath, branchName }); - if (!result.success) throw new Error(result.error); - return result.data; + if (result.success) return result.data; + + const status = await getStatus(directoryPath).catch(() => null); + const hasLocalChanges = + !!status && + (status.staged.length > 0 || + status.modified.length > 0 || + status.deleted.length > 0); + if (hasLocalChanges) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: "Working tree has uncommitted changes", + }); + } + throw new Error(result.error); } public async getChangedFilesHead( diff --git a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx index 0ea81f965..094d36725 100644 --- a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx @@ -125,15 +125,16 @@ export function BranchSelector({ if (repoPath) invalidateGitBranchQueries(repoPath); }, onError: (error, { branchName }) => { - const message = - error instanceof Error ? error.message : "Unknown error occurred"; - if (/would be overwritten by checkout/i.test(message)) { + const code = (error as { data?: { code?: string } }).data?.code; + if (code === "PRECONDITION_FAILED") { toast.error(`Can't switch to ${branchName}`, { description: "You have uncommitted changes that would be overwritten. Commit or stash them first.", }); return; } + const message = + error instanceof Error ? error.message : "Unknown error occurred"; toast.error(`Failed to checkout ${branchName}`, { description: message, }); diff --git a/apps/code/src/renderer/stores/sagas/focusSagas.ts b/apps/code/src/renderer/stores/sagas/focusSagas.ts index 713f6f974..157c5bda2 100644 --- a/apps/code/src/renderer/stores/sagas/focusSagas.ts +++ b/apps/code/src/renderer/stores/sagas/focusSagas.ts @@ -69,13 +69,7 @@ async function toRelativePath( async function checkout(repoPath: string, branch: string): Promise { const result = await trpcClient.focus.checkout.mutate({ repoPath, branch }); if (!result.success) { - const error = result.error ?? `Failed to checkout ${branch}`; - if (/would be overwritten by checkout/i.test(error)) { - throw new Error( - `Can't switch to ${branch}: uncommitted changes would be overwritten. Commit or stash them first.`, - ); - } - throw new Error(error); + throw new Error(result.error ?? `Failed to checkout ${branch}`); } } From 2633c09647667f2d6b6a83e5ebc5d573965c8b28 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 5 May 2026 22:52:21 -0700 Subject: [PATCH 5/9] address greptile review comments --- apps/code/src/main/services/git/service.ts | 9 +-------- apps/code/src/renderer/App.tsx | 13 ++++++++++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index 31270626f..5ffeba8b5 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -22,7 +22,6 @@ import { getLatestCommit, getRemoteUrl, getStagedDiff, - getStatus, getSyncStatus, getUnstagedDiff, fetch as gitFetch, @@ -304,13 +303,7 @@ export class GitService extends TypedEventEmitter { const result = await saga.run({ baseDir: directoryPath, branchName }); if (result.success) return result.data; - const status = await getStatus(directoryPath).catch(() => null); - const hasLocalChanges = - !!status && - (status.staged.length > 0 || - status.modified.length > 0 || - status.deleted.length > 0); - if (hasLocalChanges) { + if (/would be overwritten by checkout/i.test(result.error)) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: "Working tree has uncommitted changes", diff --git a/apps/code/src/renderer/App.tsx b/apps/code/src/renderer/App.tsx index e4e5ea40a..783905829 100644 --- a/apps/code/src/renderer/App.tsx +++ b/apps/code/src/renderer/App.tsx @@ -158,10 +158,17 @@ function App() { log.warn( `Foreign branch checkout detected: ${focusedBranch} -> ${foreignBranch}. Auto-unfocusing.`, ); - const result = await useFocusStore.getState().disableFocus(); - if (!result.success && result.error) { + try { + const result = await useFocusStore.getState().disableFocus(); + if (!result.success && result.error) { + toast.error("Could not unfocus workspace", { + description: result.error, + }); + } + } catch (err) { + log.error("Failed to disable focus on foreign checkout", err); toast.error("Could not unfocus workspace", { - description: result.error, + description: err instanceof Error ? err.message : String(err), }); } }, From 38cfcab28ec8676edd8d6f120ed1da49ec71f20a Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 5 May 2026 23:06:06 -0700 Subject: [PATCH 6/9] pre-flight check for dirty tree on branch checkout --- apps/code/src/main/services/git/service.ts | 29 ++++++++++++++-------- apps/code/src/main/trpc/routers/git.ts | 26 ++++++++++++++++--- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index 5ffeba8b5..783d8a3c5 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -22,6 +22,7 @@ import { getLatestCommit, getRemoteUrl, getStagedDiff, + getStatus, getSyncStatus, getUnstagedDiff, fetch as gitFetch, @@ -36,7 +37,6 @@ import { DiscardFileChangesSaga } from "@posthog/git/sagas/discard"; import { PullSaga } from "@posthog/git/sagas/pull"; import { PushSaga } from "@posthog/git/sagas/push"; import { parseGitHubUrl, parsePrUrl } from "@posthog/git/utils"; -import { TRPCError } from "@trpc/server"; import { inject, injectable } from "inversify"; import { MAIN_TOKENS } from "../../di/tokens"; import { logger } from "../../utils/logger"; @@ -126,6 +126,13 @@ function toUnifiedDiffPatch( return `diff --git a/${oldPath} b/${filename}\n--- ${fromPath}\n+++ ${toPath}\n${rawPatch}`; } +export class WorkingTreeDirtyError extends Error { + constructor() { + super("Working tree has uncommitted changes"); + this.name = "WorkingTreeDirtyError"; + } +} + @injectable() export class GitService extends TypedEventEmitter { private lastFetchTime = new Map(); @@ -299,17 +306,19 @@ export class GitService extends TypedEventEmitter { directoryPath: string, branchName: string, ): Promise<{ previousBranch: string; currentBranch: string }> { + const status = await getStatus(directoryPath); + if ( + status.staged.length > 0 || + status.modified.length > 0 || + status.deleted.length > 0 + ) { + throw new WorkingTreeDirtyError(); + } + const saga = new SwitchBranchSaga(); const result = await saga.run({ baseDir: directoryPath, branchName }); - if (result.success) return result.data; - - if (/would be overwritten by checkout/i.test(result.error)) { - throw new TRPCError({ - code: "PRECONDITION_FAILED", - message: "Working tree has uncommitted changes", - }); - } - throw new Error(result.error); + if (!result.success) throw new Error(result.error); + return result.data; } public async getChangedFilesHead( diff --git a/apps/code/src/main/trpc/routers/git.ts b/apps/code/src/main/trpc/routers/git.ts index 25665cbd1..09f34568d 100644 --- a/apps/code/src/main/trpc/routers/git.ts +++ b/apps/code/src/main/trpc/routers/git.ts @@ -1,3 +1,4 @@ +import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { container } from "../../di/container"; import { MAIN_TOKENS } from "../../di/tokens"; @@ -82,7 +83,11 @@ import { validateRepoInput, validateRepoOutput, } from "../../services/git/schemas"; -import { type GitService, GitServiceEvent } from "../../services/git/service"; +import { + type GitService, + GitServiceEvent, + WorkingTreeDirtyError, +} from "../../services/git/service"; import { publicProcedure, router } from "../trpc"; const getService = () => container.get(MAIN_TOKENS.GitService); @@ -139,9 +144,22 @@ export const gitRouter = router({ checkoutBranch: publicProcedure .input(checkoutBranchInput) .output(checkoutBranchOutput) - .mutation(({ input }) => - getService().checkoutBranch(input.directoryPath, input.branchName), - ), + .mutation(async ({ input }) => { + try { + return await getService().checkoutBranch( + input.directoryPath, + input.branchName, + ); + } catch (error) { + if (error instanceof WorkingTreeDirtyError) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + cause: error, + }); + } + throw error; + } + }), // File change operations getChangedFilesHead: publicProcedure From 450870172e423f8ec5eca8c1fee36cfe494d134e Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 5 May 2026 23:08:31 -0700 Subject: [PATCH 7/9] stop persisting cloud mode on cloud-repo deep link --- .../task-detail/components/TaskInput.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx index ab2fa468a..1182cb4cf 100644 --- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx @@ -182,17 +182,27 @@ export function TaskInput({ // Stay optimistic while the integration list resolves to avoid flicker. const cloudAvailable = isLoadingRepos || hasGithubIntegration; - const workspaceMode: WorkspaceMode = - !cloudAvailable && lastUsedWorkspaceMode === "cloud" - ? lastUsedLocalWorkspaceMode - : lastUsedWorkspaceMode || "local"; + const [workspaceMode, setWorkspaceModeState] = useState(() => { + if (initialCloudRepository) return "cloud"; + if (!cloudAvailable && lastUsedWorkspaceMode === "cloud") { + return lastUsedLocalWorkspaceMode; + } + return lastUsedWorkspaceMode || "local"; + }); const setWorkspaceMode = (mode: WorkspaceMode) => { + setWorkspaceModeState(mode); setLastUsedWorkspaceMode(mode); if (mode !== "cloud") { setLastUsedLocalWorkspaceMode(mode); } }; + + useEffect(() => { + if (workspaceMode === "cloud" && !cloudAvailable) { + setWorkspaceModeState(lastUsedLocalWorkspaceMode); + } + }, [workspaceMode, cloudAvailable, lastUsedLocalWorkspaceMode]); const { repositories: visibleCloudRepositories, isPending: cloudRepositoriesLoading, @@ -290,9 +300,9 @@ export function TaskInput({ useEffect(() => { if (!initialCloudRepository) return; - setLastUsedWorkspaceMode("cloud"); + setWorkspaceModeState("cloud"); setSelectedRepository(initialCloudRepository.toLowerCase()); - }, [initialCloudRepository, setLastUsedWorkspaceMode]); + }, [initialCloudRepository]); const handleRefreshRepositories = useCallback(() => { void refreshRepositories().catch((error) => { From af0252418ee929ca5b871f8d0eda119f123b81a3 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 5 May 2026 23:17:22 -0700 Subject: [PATCH 8/9] guard non-string account fields in github settings --- .../components/sections/GitHubSettings.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx index 105e61f86..7c9836464 100644 --- a/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx +++ b/apps/code/src/renderer/features/settings/components/sections/GitHubSettings.tsx @@ -38,9 +38,14 @@ import { useState } from "react"; const REPO_PREVIEW_COUNT = 3; function githubInstallationSettingsUrl(integration: UserGitHubIntegration) { - const accountType = integration.account?.type?.toLowerCase(); + const accountType = integration.account?.type; const accountName = integration.account?.name; - if (accountType === "organization" && accountName) { + if ( + typeof accountType === "string" && + accountType.toLowerCase() === "organization" && + typeof accountName === "string" && + accountName + ) { return `https://github.com/organizations/${accountName}/settings/installations/${integration.installation_id}`; } return `https://github.com/settings/installations/${integration.installation_id}`; @@ -164,7 +169,10 @@ function GitHubIntegrationRow({ }, }); - const accountName = integration.account?.name?.trim() || "GitHub account"; + const rawAccountName = integration.account?.name; + const accountName = + (typeof rawAccountName === "string" && rawAccountName.trim()) || + "GitHub account"; const repoCount = repos.length; const canExpand = repoCount > 0; const settingsUrl = githubInstallationSettingsUrl(integration); From 23df09d9380a809a1f1673e7cb805d04b59ef582 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Tue, 5 May 2026 23:27:50 -0700 Subject: [PATCH 9/9] revert dirty-tree checkout error handling --- apps/code/src/main/services/git/service.ts | 17 ------------ apps/code/src/main/trpc/routers/git.ts | 26 +++---------------- apps/code/src/renderer/App.tsx | 13 +++------- .../components/BranchSelector.tsx | 7 +++-- .../src/renderer/stores/sagas/focusSagas.ts | 8 +++++- 5 files changed, 17 insertions(+), 54 deletions(-) diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index 783d8a3c5..e69bb3d9d 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -22,7 +22,6 @@ import { getLatestCommit, getRemoteUrl, getStagedDiff, - getStatus, getSyncStatus, getUnstagedDiff, fetch as gitFetch, @@ -126,13 +125,6 @@ function toUnifiedDiffPatch( return `diff --git a/${oldPath} b/${filename}\n--- ${fromPath}\n+++ ${toPath}\n${rawPatch}`; } -export class WorkingTreeDirtyError extends Error { - constructor() { - super("Working tree has uncommitted changes"); - this.name = "WorkingTreeDirtyError"; - } -} - @injectable() export class GitService extends TypedEventEmitter { private lastFetchTime = new Map(); @@ -306,15 +298,6 @@ export class GitService extends TypedEventEmitter { directoryPath: string, branchName: string, ): Promise<{ previousBranch: string; currentBranch: string }> { - const status = await getStatus(directoryPath); - if ( - status.staged.length > 0 || - status.modified.length > 0 || - status.deleted.length > 0 - ) { - throw new WorkingTreeDirtyError(); - } - const saga = new SwitchBranchSaga(); const result = await saga.run({ baseDir: directoryPath, branchName }); if (!result.success) throw new Error(result.error); diff --git a/apps/code/src/main/trpc/routers/git.ts b/apps/code/src/main/trpc/routers/git.ts index 09f34568d..25665cbd1 100644 --- a/apps/code/src/main/trpc/routers/git.ts +++ b/apps/code/src/main/trpc/routers/git.ts @@ -1,4 +1,3 @@ -import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { container } from "../../di/container"; import { MAIN_TOKENS } from "../../di/tokens"; @@ -83,11 +82,7 @@ import { validateRepoInput, validateRepoOutput, } from "../../services/git/schemas"; -import { - type GitService, - GitServiceEvent, - WorkingTreeDirtyError, -} from "../../services/git/service"; +import { type GitService, GitServiceEvent } from "../../services/git/service"; import { publicProcedure, router } from "../trpc"; const getService = () => container.get(MAIN_TOKENS.GitService); @@ -144,22 +139,9 @@ export const gitRouter = router({ checkoutBranch: publicProcedure .input(checkoutBranchInput) .output(checkoutBranchOutput) - .mutation(async ({ input }) => { - try { - return await getService().checkoutBranch( - input.directoryPath, - input.branchName, - ); - } catch (error) { - if (error instanceof WorkingTreeDirtyError) { - throw new TRPCError({ - code: "PRECONDITION_FAILED", - cause: error, - }); - } - throw error; - } - }), + .mutation(({ input }) => + getService().checkoutBranch(input.directoryPath, input.branchName), + ), // File change operations getChangedFilesHead: publicProcedure diff --git a/apps/code/src/renderer/App.tsx b/apps/code/src/renderer/App.tsx index 783905829..e4e5ea40a 100644 --- a/apps/code/src/renderer/App.tsx +++ b/apps/code/src/renderer/App.tsx @@ -158,17 +158,10 @@ function App() { log.warn( `Foreign branch checkout detected: ${focusedBranch} -> ${foreignBranch}. Auto-unfocusing.`, ); - try { - const result = await useFocusStore.getState().disableFocus(); - if (!result.success && result.error) { - toast.error("Could not unfocus workspace", { - description: result.error, - }); - } - } catch (err) { - log.error("Failed to disable focus on foreign checkout", err); + const result = await useFocusStore.getState().disableFocus(); + if (!result.success && result.error) { toast.error("Could not unfocus workspace", { - description: err instanceof Error ? err.message : String(err), + description: result.error, }); } }, diff --git a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx index 094d36725..0ea81f965 100644 --- a/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/BranchSelector.tsx @@ -125,16 +125,15 @@ export function BranchSelector({ if (repoPath) invalidateGitBranchQueries(repoPath); }, onError: (error, { branchName }) => { - const code = (error as { data?: { code?: string } }).data?.code; - if (code === "PRECONDITION_FAILED") { + const message = + error instanceof Error ? error.message : "Unknown error occurred"; + if (/would be overwritten by checkout/i.test(message)) { toast.error(`Can't switch to ${branchName}`, { description: "You have uncommitted changes that would be overwritten. Commit or stash them first.", }); return; } - const message = - error instanceof Error ? error.message : "Unknown error occurred"; toast.error(`Failed to checkout ${branchName}`, { description: message, }); diff --git a/apps/code/src/renderer/stores/sagas/focusSagas.ts b/apps/code/src/renderer/stores/sagas/focusSagas.ts index 157c5bda2..713f6f974 100644 --- a/apps/code/src/renderer/stores/sagas/focusSagas.ts +++ b/apps/code/src/renderer/stores/sagas/focusSagas.ts @@ -69,7 +69,13 @@ async function toRelativePath( async function checkout(repoPath: string, branch: string): Promise { const result = await trpcClient.focus.checkout.mutate({ repoPath, branch }); if (!result.success) { - throw new Error(result.error ?? `Failed to checkout ${branch}`); + const error = result.error ?? `Failed to checkout ${branch}`; + if (/would be overwritten by checkout/i.test(error)) { + throw new Error( + `Can't switch to ${branch}: uncommitted changes would be overwritten. Commit or stash them first.`, + ); + } + throw new Error(error); } }