Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/code/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
},
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
<Flex
align="center"
justify="between"
gap="2"
className="rounded-(--radius-2) border border-(--gray-a3) bg-(--gray-2) py-[6px] pr-2 pl-3"
>
<Flex align="center" gap="2" className="min-w-0">
<Text className="select-none font-[var(--code-font-family)] text-(--gray-9) text-sm">
$
</Text>
<Text className="truncate font-[var(--code-font-family)] text-(--gray-12) text-sm">
{command}
</Text>
</Flex>
<Tooltip content={copied ? "Copied!" : "Copy command"}>
<IconButton
size="1"
variant="ghost"
color="gray"
onClick={() => void handleCopy()}
aria-label="Copy command"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</IconButton>
</Tooltip>
</Flex>
);
}

interface CliInstallStepProps {
onNext: () => void;
onBack: () => void;
Expand Down Expand Up @@ -135,29 +176,13 @@ export function CliInstallStep({ onNext, onBack }: CliInstallStepProps) {
Install with Homebrew or Xcode Command Line Tools:
</Text>
<Flex direction="column" gap="2">
<Flex align="center" gap="2">
<Terminal
size={14}
className="shrink-0 text-(--gray-9)"
/>
<Code variant="soft" className="text-sm">
brew install git
</Code>
</Flex>
<Flex align="center" gap="2">
<Terminal
size={14}
className="shrink-0 text-(--gray-9)"
/>
<Code variant="soft" className="text-sm">
xcode-select --install
</Code>
</Flex>
<CommandLine command="brew install git" />
<CommandLine command="xcode-select --install" />
</Flex>
<Flex align="center" gap="3">
<Flex align="center" justify="between" gap="3">
<Button
size="1"
variant="soft"
variant="ghost"
color="gray"
onClick={() =>
trpcClient.os.openExternal.mutate({
Expand Down Expand Up @@ -245,19 +270,11 @@ export function CliInstallStep({ onNext, onBack }: CliInstallStepProps) {
<Text className="text-(--gray-11) text-sm">
Install with Homebrew:
</Text>
<Flex align="center" gap="2">
<Terminal
size={14}
className="shrink-0 text-(--gray-9)"
/>
<Code variant="soft" className="text-sm">
brew install gh
</Code>
</Flex>
<Flex align="center" gap="3">
<CommandLine command="brew install gh" />
<Flex align="center" justify="between" gap="3">
<Button
size="1"
variant="soft"
variant="ghost"
color="gray"
onClick={() =>
trpcClient.os.openExternal.mutate({
Expand Down Expand Up @@ -286,26 +303,19 @@ export function CliInstallStep({ onNext, onBack }: CliInstallStepProps) {
<Text className="text-(--gray-11) text-sm">
Run this in your terminal to log in:
</Text>
<Flex align="center" gap="2">
<Terminal
size={14}
className="shrink-0 text-(--gray-9)"
/>
<Code variant="soft" className="text-sm">
gh auth login
</Code>
<CommandLine command="gh auth login" />
<Flex justify="end">
<Button
size="1"
variant="soft"
color="gray"
onClick={() => void handleCheckGh()}
loading={isCheckingGh}
>
<ArrowsClockwise size={12} />
Check again
</Button>
</Flex>
<Button
size="1"
variant="soft"
color="gray"
onClick={() => void handleCheckGh()}
loading={isCheckingGh}
className="self-start"
>
<ArrowsClockwise size={12} />
Check again
</Button>
</Flex>
)}
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkspaceMode>(() => {
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,
Expand Down Expand Up @@ -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) => {
Expand Down
31 changes: 14 additions & 17 deletions apps/code/src/renderer/features/tour/components/TourOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,30 +105,27 @@ 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") {
settleTimer = setTimeout(tryAdvance, SETTLE_MS);
}
};

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();
Expand Down
8 changes: 7 additions & 1 deletion apps/code/src/renderer/stores/sagas/focusSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ async function toRelativePath(
async function checkout(repoPath: string, branch: string): Promise<void> {
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);
}
}

Expand Down
Loading