Skip to content
Closed
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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ out/
storybook-static
bin/

# TanStack Router generated route tree
apps/code/src/renderer/routeTree.gen.ts

# Environment
.env
Expand Down
1 change: 1 addition & 0 deletions apps/code/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/renderer/routeTree.gen.ts linguist-generated=true
323 changes: 26 additions & 297 deletions apps/code/src/renderer/features/settings/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,318 +1,47 @@
import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useLogoutMutation } from "@features/auth/hooks/authMutations";
import {
useAuthStateValue,
useCurrentUser,
} from "@features/auth/hooks/authQueries";
import { getUserInitials } from "@features/auth/utils/userInitials";
import {
type SettingsCategory,
useSettingsDialogStore,
} from "@features/settings/stores/settingsDialogStore";
import { useFeatureFlag } from "@hooks/useFeatureFlag";
import { useSeat } from "@hooks/useSeat";
import {
ArrowLeft,
ArrowsClockwise,
CaretRight,
Code,
CreditCard,
Cube,
Folder,
GearSix,
GithubLogo,
Keyboard,
Palette,
SignOut,
SlackLogo,
Terminal,
TrafficSignal,
TreeStructure,
Wrench,
} from "@phosphor-icons/react";
import { Avatar, Box, Flex, ScrollArea, Text } from "@radix-ui/themes";
import { BILLING_FLAG } from "@shared/constants";
import { type ReactNode, useEffect, useMemo } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { AdvancedSettings } from "./sections/AdvancedSettings";
import { ClaudeCodeSettings } from "./sections/ClaudeCodeSettings";
import { EnvironmentsSettings } from "./sections/environments/EnvironmentsSettings";
import { GeneralSettings } from "./sections/GeneralSettings";
import { GitHubSettings } from "./sections/GitHubSettings";
import { PersonalizationSettings } from "./sections/PersonalizationSettings";
import { PlanUsageSettings } from "./sections/PlanUsageSettings";
import { ShortcutsSettings } from "./sections/ShortcutsSettings";
import { SignalSourcesSettings } from "./sections/SignalSourcesSettings";
import { SlackSettings } from "./sections/SlackSettings";
import { TerminalSettings } from "./sections/TerminalSettings";
import { UpdatesSettings } from "./sections/UpdatesSettings";
import { WorkspacesSettings } from "./sections/WorkspacesSettings";
import { WorktreesSettings } from "./sections/worktrees/WorktreesSettings";

interface SidebarItem {
id: SettingsCategory;
label: string;
icon: ReactNode;
hasChevron?: boolean;
}

const SIDEBAR_ITEMS: SidebarItem[] = [
{ id: "general", label: "General", icon: <GearSix size={16} /> },
{ id: "plan-usage", label: "Plan & usage", icon: <CreditCard size={16} /> },
{ id: "workspaces", label: "Workspaces", icon: <Folder size={16} /> },
{ id: "worktrees", label: "Worktrees", icon: <TreeStructure size={16} /> },
{
id: "environments",
label: "Environments",
icon: <Cube size={16} />,
},
{
id: "personalization",
label: "Personalization",
icon: <Palette size={16} />,
},
{ id: "terminal", label: "Terminal", icon: <Terminal size={16} /> },
{ id: "claude-code", label: "Claude Code", icon: <Code size={16} /> },
{ id: "shortcuts", label: "Shortcuts", icon: <Keyboard size={16} /> },
{ id: "github", label: "GitHub", icon: <GithubLogo size={16} /> },
{ id: "slack", label: "Slack", icon: <SlackLogo size={16} /> },

{
id: "signals",
label: "Signals",
icon: <TrafficSignal size={16} />,
},
{ id: "updates", label: "Updates", icon: <ArrowsClockwise size={16} /> },
{ id: "advanced", label: "Advanced", icon: <Wrench size={16} /> },
];

const CATEGORY_TITLES: Record<SettingsCategory, string> = {
general: "General",
"plan-usage": "Plan & usage",
workspaces: "Workspaces",
worktrees: "Worktrees",
environments: "Environments",
"cloud-environments": "Environments",
personalization: "Personalization",
terminal: "Terminal",
"claude-code": "Claude Code",
shortcuts: "Shortcuts",
github: "GitHub",
slack: "Slack integration",

signals: "Signals",
updates: "Updates",
advanced: "Advanced",
};

const CATEGORY_COMPONENTS: Record<SettingsCategory, React.ComponentType> = {
general: GeneralSettings,
"plan-usage": PlanUsageSettings,
workspaces: WorkspacesSettings,
worktrees: WorktreesSettings,
environments: EnvironmentsSettings,
"cloud-environments": EnvironmentsSettings,
personalization: PersonalizationSettings,
terminal: TerminalSettings,
"claude-code": ClaudeCodeSettings,
shortcuts: ShortcutsSettings,
github: GitHubSettings,
slack: SlackSettings,

signals: SignalSourcesSettings,
updates: UpdatesSettings,
advanced: AdvancedSettings,
};

import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import { useEffect } from "react";
import { SettingsPanel } from "./SettingsPanel";

// Modal/overlay form of the settings UI. Used in pre-router shells (e.g.
// `AiApprovalScreen`) where the routed `/settings/$category` page isn't
// available because RouterProvider hasn't mounted yet. Inside the main app,
// settings is a real route — `routes/settings/$category.tsx` renders
// `<SettingsPanel/>` directly.
export function SettingsDialog() {
const { isOpen, activeCategory, close, setCategory, formMode } =
useSettingsDialogStore();
const isAuthenticated = useAuthStateValue(
(state) => state.status === "authenticated",
);
const client = useOptionalAuthenticatedClient();
const { data: user } = useCurrentUser({ client });
const { seat, planLabel } = useSeat();
const billingEnabled = useFeatureFlag(BILLING_FLAG);
const logoutMutation = useLogoutMutation();

const sidebarItems = useMemo(
() =>
billingEnabled
? SIDEBAR_ITEMS
: SIDEBAR_ITEMS.filter((item) => item.id !== "plan-usage"),
[billingEnabled],
);

useHotkeys("escape", close, {
enabled: isOpen,
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
});
const isOpen = useSettingsDialogStore((s) => s.isOpen);
const close = useSettingsDialogStore((s) => s.close);

useEffect(() => {
if (!isOpen) return;
const handlePopState = () => {
if (isOpen && !window.history.state?.settingsOpen) {
if (!window.history.state?.settingsOpen) {
useSettingsDialogStore.setState({ isOpen: false });
}
};

window.addEventListener("popstate", handlePopState);
return () => window.removeEventListener("popstate", handlePopState);
}, [isOpen]);
Comment on lines 14 to 23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Popstate condition is always-false guard — dead code

open() no longer calls window.history.pushState({ settingsOpen: true }), so window.history.state?.settingsOpen is always undefined / falsy. The condition !window.history.state?.settingsOpen is therefore always true, meaning any popstate event while the overlay is open (e.g. the user pressing Electron's back button in AiApprovalScreen) will unconditionally close the dialog — which was never the original intent.

Either remove this handler entirely (the overlay is now the only remaining caller and there's no pushed state to guard against), or replace the condition with one that reflects the current behaviour.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/settings/components/SettingsDialog.tsx
Line: 14-23

Comment:
**Popstate condition is always-false guard — dead code**

`open()` no longer calls `window.history.pushState({ settingsOpen: true })`, so `window.history.state?.settingsOpen` is always `undefined` / falsy. The condition `!window.history.state?.settingsOpen` is therefore always `true`, meaning any `popstate` event while the overlay is open (e.g. the user pressing Electron's back button in `AiApprovalScreen`) will unconditionally close the dialog — which was never the original intent.

Either remove this handler entirely (the overlay is now the only remaining caller and there's no pushed state to guard against), or replace the condition with one that reflects the current behaviour.

How can I resolve this? If you propose a fix, please make it concise.


if (!isOpen) {
return null;
}

const ActiveComponent = CATEGORY_COMPONENTS[activeCategory];
useEffect(() => {
if (!isOpen) return;
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
close();
}
};
window.addEventListener("keydown", handleEscape);
return () => window.removeEventListener("keydown", handleEscape);
}, [isOpen, close]);
Comment on lines +25 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant Escape handler — SettingsPanel already owns it

SettingsPanel now registers useHotkeys("escape", close, { enabled: true, ... }) unconditionally while it is mounted. When the overlay is open, SettingsPanel is rendered inside SettingsDialog, so Escape already fires close() via the hotkey. This second listener on window fires a duplicate close() call immediately after. The guard if (!wasOpen) return in close() makes it harmless, but it violates the OnceAndOnlyOnce rule. This keydown effect can be dropped from SettingsDialog.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/settings/components/SettingsDialog.tsx
Line: 25-35

Comment:
**Redundant Escape handler — `SettingsPanel` already owns it**

`SettingsPanel` now registers `useHotkeys("escape", close, { enabled: true, ... })` unconditionally while it is mounted. When the overlay is open, `SettingsPanel` is rendered inside `SettingsDialog`, so Escape already fires `close()` via the hotkey. This second listener on `window` fires a duplicate `close()` call immediately after. The guard `if (!wasOpen) return` in `close()` makes it harmless, but it violates the OnceAndOnlyOnce rule. This `keydown` effect can be dropped from `SettingsDialog`.

How can I resolve this? If you propose a fix, please make it concise.


const initials = getUserInitials(user);
if (!isOpen) return null;

return (
<div
className="fixed inset-0 z-[100] flex bg-(--color-background)"
data-overlay="settings"
>
<div className="flex h-full w-[256px] shrink-0 flex-col border-gray-6 border-r">
<div className="drag h-[36px] shrink-0 border-b border-b-(--gray-6)" />

{isAuthenticated && user && (
<Flex
align="center"
gap="3"
px="3"
py="3"
className="border-b border-b-(--gray-5)"
>
<Avatar size="2" fallback={initials} radius="full" color="amber" />
<Flex direction="column" className="min-w-0">
<Text truncate className="font-medium text-sm">
{user.email}
</Text>
{seat && (
<Text className="text-(--gray-9) text-[13px]">
{planLabel} Plan
</Text>
)}
</Flex>
</Flex>
)}

<button
type="button"
className="mt-2 flex cursor-pointer items-center gap-2 border-0 bg-transparent px-3 py-2 text-left text-[13px] text-gray-11 transition-colors hover:bg-gray-3"
onClick={close}
>
<ArrowLeft size={14} />
<span>Back to app</span>
</button>

<ScrollArea className="flex-1">
<div className="flex flex-col pt-2">
{sidebarItems.map((item) => {
const isActive =
activeCategory === item.id ||
(item.id === "environments" &&
activeCategory === "cloud-environments");
return (
<SidebarNavItem
key={item.id}
item={item}
isActive={isActive}
onClick={() => setCategory(item.id)}
/>
);
})}
</div>
</ScrollArea>

{isAuthenticated && (
<button
type="button"
disabled={logoutMutation.isPending}
className="flex cursor-pointer items-center gap-2 border-0 border-gray-5 border-t bg-transparent px-3 py-2.5 text-left font-mono text-[12px] text-gray-9 transition-colors hover:bg-gray-3 hover:text-gray-11 disabled:pointer-events-none disabled:opacity-50"
onClick={() => {
close();
logoutMutation.mutate();
}}
>
<SignOut size={14} />
<span>Sign out</span>
</button>
)}
</div>

<div className="relative flex flex-1 flex-col overflow-hidden">
<div className="drag h-[36px] shrink-0 border-b border-b-(--gray-6)" />
<div className="relative flex flex-1 justify-center overflow-hidden">
<svg
aria-hidden="true"
style={{
maskImage: "linear-gradient(to top, black 0%, transparent 100%)",
WebkitMaskImage:
"linear-gradient(to top, black 0%, transparent 100%)",
}}
className="pointer-events-none absolute bottom-0 left-0 h-full w-full opacity-40"
>
<defs>
<pattern
id="settings-dot-pattern"
patternUnits="userSpaceOnUse"
width="8"
height="8"
>
<circle cx="0" cy="0" r="1" fill="var(--gray-6)" />
<circle cx="0" cy="8" r="1" fill="var(--gray-6)" />
<circle cx="8" cy="8" r="1" fill="var(--gray-6)" />
<circle cx="8" cy="0" r="1" fill="var(--gray-6)" />
<circle cx="4" cy="4" r="1" fill="var(--gray-6)" />
</pattern>
</defs>
<rect
width="100%"
height="100%"
fill="url(#settings-dot-pattern)"
/>
</svg>
<ScrollArea className="h-full w-full">
<Box p="6" mx="auto" className="relative z-[1] max-w-[800px]">
<Flex direction="column" gap="4">
{!formMode && (
<Text className="font-medium text-lg leading-6.5">
{CATEGORY_TITLES[activeCategory]}
</Text>
)}
<ActiveComponent />
</Flex>
</Box>
</ScrollArea>
</div>
</div>
<SettingsPanel />
</div>
);
}

interface SidebarNavItemProps {
item: SidebarItem;
isActive: boolean;
onClick: () => void;
}

function SidebarNavItem({ item, isActive, onClick }: SidebarNavItemProps) {
return (
<button
type="button"
className="flex w-full cursor-pointer items-center justify-between gap-2 border-0 bg-transparent px-3 py-1.5 text-left text-[13px] text-gray-11 transition-colors hover:bg-gray-3 data-[active]:bg-accent-4 data-[active]:text-gray-12"
data-active={isActive || undefined}
onClick={onClick}
>
<span className="flex items-center gap-2">
<span className="text-gray-10">{item.icon}</span>
<span>{item.label}</span>
</span>
{item.hasChevron && <CaretRight size={12} className="text-gray-9" />}
</button>
);
}
Loading
Loading