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..7c3cd4a17 100644 --- a/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx +++ b/apps/code/src/renderer/features/onboarding/components/CliInstallStep.tsx @@ -1,16 +1,18 @@ +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, 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 +22,45 @@ 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,29 +176,13 @@ export function CliInstallStep({ onNext, onBack }: CliInstallStepProps) { Install with Homebrew or Xcode Command Line Tools: - - - - brew install git - - - - - - xcode-select --install - - + + - + - )} 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); 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) => { 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(); 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); } }