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

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

# Environment
.env
.env.local
Expand Down
4 changes: 4 additions & 0 deletions apps/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"build-icons": "bash scripts/generate-icns.sh",
"typecheck": "tsc -p tsconfig.node.json --noEmit && tsc -p tsconfig.web.json --noEmit",
"generate-client": "tsx scripts/update-openapi-client.ts",
"generate-routes": "node scripts/generate-routes.mjs",
"test": "vitest run",
"test:e2e": "playwright test --config=tests/e2e/playwright.config.ts",
"test:e2e:headed": "playwright test --config=tests/e2e/playwright.config.ts --headed",
Expand Down Expand Up @@ -143,6 +144,9 @@
"@radix-ui/themes": "^3.2.1",
"@tailwindcss/vite": "^4.2.2",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-router": "^1.95.0",
"@tanstack/react-router-devtools": "^1.95.0",
"@tanstack/router-plugin": "^1.95.0",
"@tiptap/core": "^3.13.0",
"@tiptap/extension-mention": "^3.13.0",
"@tiptap/extension-placeholder": "^3.13.0",
Expand Down
20 changes: 20 additions & 0 deletions apps/code/scripts/generate-routes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Generator, getConfig } from "@tanstack/router-generator";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, "..");

const config = getConfig(
{
target: "react",
autoCodeSplitting: true,
routesDirectory: path.resolve(root, "src/renderer/routes"),
generatedRouteTree: path.resolve(root, "src/renderer/routeTree.gen.ts"),
},
root,
);

const generator = new Generator({ config, root });
await generator.run();
console.log("Generated routeTree.gen.ts");
5 changes: 3 additions & 2 deletions apps/code/src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ErrorBoundary } from "@components/ErrorBoundary";
import { LoginTransition } from "@components/LoginTransition";
import { MainLayout } from "@components/MainLayout";
import { ScopeReauthPrompt } from "@components/ScopeReauthPrompt";
import { AiApprovalScreen } from "@features/ai-approval/components/AiApprovalScreen";
import { AuthScreen } from "@features/auth/components/AuthScreen";
Expand All @@ -18,6 +17,7 @@ import { OnboardingFlow } from "@features/onboarding/components/OnboardingFlow";
import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
import { Flex, Spinner, Text } from "@radix-ui/themes";
import { initializeConnectivityToast } from "@renderer/features/connectivity/connectivityToast";
import { router } from "@renderer/router";
import { initializeConnectivityStore } from "@renderer/stores/connectivityStore";
import { useFocusStore } from "@renderer/stores/focusStore";
import { useThemeStore } from "@renderer/stores/themeStore";
Expand All @@ -26,6 +26,7 @@ import { trpcClient, useTRPC } from "@renderer/trpc/client";
import { isNotAuthenticatedError } from "@shared/errors";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { useQueryClient } from "@tanstack/react-query";
import { RouterProvider } from "@tanstack/react-router";
import { useSubscription } from "@trpc/tanstack-react-query";
import { initializePostHog, registerAppVersion, track } from "@utils/analytics";
import { logger } from "@utils/logger";
Expand Down Expand Up @@ -292,7 +293,7 @@ function App() {
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: showTransition ? 0.5 : 0 }}
>
<MainLayout />
<RouterProvider router={router} />
</motion.div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("@renderer/router", () => ({
router: {
navigate: vi.fn(),
state: { matches: [] },
},
}));

import { useSettingsDialogStore } from "./settingsDialogStore";

describe("settingsDialogStore", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { router } from "@renderer/router";
import { create } from "zustand";

export type SettingsCategory =
Expand Down Expand Up @@ -56,16 +57,22 @@ export const useSettingsDialogStore = create<SettingsDialogStore>()(
window.history.pushState({ settingsOpen: true }, "");
}
const isAction = typeof contextOrAction === "string";
const nextCategory = category ?? get().activeCategory;
set({
isOpen: true,
activeCategory: category ?? get().activeCategory,
activeCategory: nextCategory,
context: isAction ? {} : (contextOrAction ?? {}),
initialAction: isAction ? contextOrAction : null,
formMode: false,
});
void router.navigate({
to: "/settings/$category",
params: { category: nextCategory },
});
},
close: () => {
if (get().isOpen && window.history.state?.settingsOpen) {
const wasOpen = get().isOpen;
if (wasOpen && window.history.state?.settingsOpen) {
window.history.back();
}
set({
Comment on lines 73 to 78
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.

P1 Since the window.history.pushState({ settingsOpen: true }) call in open() is now dead (see companion comment), the guard here reading window.history.state?.settingsOpen is always false and window.history.back() is never called. This dead branch can be removed, leaving just the router.navigate({ to: "/code" }) path to handle the URL change on close.

Suggested change
close: () => {
if (get().isOpen && window.history.state?.settingsOpen) {
const wasOpen = get().isOpen;
if (wasOpen && window.history.state?.settingsOpen) {
window.history.back();
}
set({
close: () => {
const wasOpen = get().isOpen;
set({
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/settings/stores/settingsDialogStore.ts
Line: 73-78

Comment:
Since the `window.history.pushState({ settingsOpen: true })` call in `open()` is now dead (see companion comment), the guard here reading `window.history.state?.settingsOpen` is always false and `window.history.back()` is never called. This dead branch can be removed, leaving just the `router.navigate({ to: "/code" })` path to handle the URL change on close.

```suggestion
    close: () => {
      const wasOpen = get().isOpen;
      set({
```

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

Expand All @@ -74,9 +81,23 @@ export const useSettingsDialogStore = create<SettingsDialogStore>()(
initialAction: null,
formMode: false,
});
if (wasOpen) {
const matches = router.state.matches;
const onSettings = matches.some((m) =>
m.routeId.startsWith("/settings"),
);
if (onSettings) {
void router.navigate({ to: "/code" });
}
}
},
setCategory: (category) => {
set({ activeCategory: category, initialAction: null, formMode: false });
void router.navigate({
to: "/settings/$category",
params: { category },
});
},
setCategory: (category) =>
set({ activeCategory: category, initialAction: null, formMode: false }),
clearContext: () => set({ context: {} }),
consumeInitialAction: () => {
const action = get().initialAction;
Expand Down
18 changes: 18 additions & 0 deletions apps/code/src/renderer/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
createHashHistory,
createRouter as createTanStackRouter,
} from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";

export const router = createTanStackRouter({
routeTree,
history: createHashHistory(),
defaultPreload: "intent",
scrollRestoration: false,
});

declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,14 @@ import { HeaderRow } from "@components/HeaderRow";
import { HedgehogMode } from "@components/HedgehogMode";
import { KeyboardShortcutsSheet } from "@components/KeyboardShortcutsSheet";
import { SpaceSwitcher } from "@components/SpaceSwitcher";

import { ArchivedTasksView } from "@features/archive/components/ArchivedTasksView";
import { UsageLimitModal } from "@features/billing/components/UsageLimitModal";
import { CommandMenu } from "@features/command/components/CommandMenu";
import { CommandCenterView } from "@features/command-center/components/CommandCenterView";
import { InboxView } from "@features/inbox/components/InboxView";
import { useInboxDeepLink } from "@features/inbox/hooks/useInboxDeepLink";
import { McpServersView } from "@features/mcp-servers/components/McpServersView";
import { FolderSettingsView } from "@features/settings/components/FolderSettingsView";
import { SettingsDialog } from "@features/settings/components/SettingsDialog";
import { useSetupDiscovery } from "@features/setup/hooks/useSetupDiscovery";
import { MainSidebar } from "@features/sidebar/components/MainSidebar";
import { useSidebarData } from "@features/sidebar/hooks/useSidebarData";
import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder";
import { SkillsView } from "@features/skills/components/SkillsView";
import { TaskDetail } from "@features/task-detail/components/TaskDetail";
import { TaskInput } from "@features/task-detail/components/TaskInput";
import { TaskPendingView } from "@features/task-detail/components/TaskPendingView";
import { useTasks } from "@features/tasks/hooks/useTasks";
import { TourOverlay } from "@features/tour/components/TourOverlay";
import {
Expand All @@ -35,23 +25,23 @@ import { useCommandMenuStore } from "@stores/commandMenuStore";
import { useNavigationStore } from "@stores/navigationStore";
import { useShortcutsSheetStore } from "@stores/shortcutsSheetStore";
import { useQueryClient } from "@tanstack/react-query";
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { logger } from "@utils/logger";
import { useCallback, useEffect, useRef } from "react";
import { GlobalEventHandlers } from "../components/GlobalEventHandlers";
import { useNewTaskDeepLink } from "../hooks/useNewTaskDeepLink";
import { useTaskDeepLink } from "../hooks/useTaskDeepLink";
import { GlobalEventHandlers } from "./GlobalEventHandlers";

const log = logger.scope("main-layout");
const log = logger.scope("root-route");

export function MainLayout() {
const {
view,
hydrateTask,
navigateToTaskInput,
navigateToTask,
taskInputReportAssociation,
taskInputCloudRepository,
} = useNavigationStore();
export const Route = createRootRoute({
component: RootLayout,
});

function RootLayout() {
const { view, hydrateTask, navigateToTaskInput, navigateToTask } =
useNavigationStore();
const {
isOpen: commandMenuOpen,
setOpen: setCommandMenuOpen,
Expand All @@ -70,7 +60,6 @@ export function MainLayout() {
const billingEnabled = useFeatureFlag(BILLING_FLAG);
const syncCloudTasksEnabled = useFeatureFlag(SYNC_CLOUD_TASKS_FLAG);

// Space switcher data
const sidebarData = useSidebarData({ activeView: view });
const visualTaskOrder = useVisualTaskOrder(sidebarData);
const activeTaskId =
Expand Down Expand Up @@ -100,8 +89,6 @@ export function MainLayout() {
if (missing.length === 0) return;
const missingIds = missing.map((t) => t.id);
for (const id of missingIds) reconcilingTaskIds.current.add(id);
// Single batched IPC instead of one mutation per task — with many cloud
// tasks the per-task pattern saturates the main thread at boot.
workspaceApi
.reconcileCloudWorkspaces(missingIds)
.then((result) => {
Expand Down Expand Up @@ -140,42 +127,8 @@ export function MainLayout() {
<HeaderRow />
<Flex flexGrow="1" overflow="hidden">
<MainSidebar />

<Box flexGrow="1" overflow="hidden">
{view.type === "task-input" && (
<TaskInput
initialPrompt={view.initialPrompt}
initialPromptKey={view.taskInputRequestId}
initialCloudRepository={
view.initialCloudRepository ?? taskInputCloudRepository
}
initialModel={view.initialModel}
initialMode={view.initialMode}
reportAssociation={
view.reportAssociation ?? taskInputReportAssociation
}
/>
)}

{view.type === "task-detail" && view.data && (
<TaskDetail key={view.data.id} task={view.data} />
)}

{view.type === "task-pending" && view.pendingTaskKey && (
<TaskPendingView pendingTaskKey={view.pendingTaskKey} />
)}

{view.type === "folder-settings" && <FolderSettingsView />}

{view.type === "inbox" && <InboxView />}

{view.type === "archived" && <ArchivedTasksView />}

{view.type === "command-center" && <CommandCenterView />}

{view.type === "skills" && <SkillsView />}

{view.type === "mcp-servers" && <McpServersView />}
<Outlet />
</Box>
</Flex>

Expand All @@ -200,6 +153,9 @@ export function MainLayout() {
<TourOverlay />
{billingEnabled && <UsageLimitModal />}
<HedgehogMode />
{import.meta.env.DEV && (
<TanStackRouterDevtools position="bottom-right" />
Comment on lines 155 to +157
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 DevTools bundle not tree-shaken in prod — The top-level import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" is unconditional, so the entire devtools chunk is included in the production bundle regardless of the import.meta.env.DEV guard on the JSX. The fix is to use React.lazy with a conditional dynamic import so the module is only ever loaded in development builds.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/routes/__root.tsx
Line: 155-157

Comment:
**DevTools bundle not tree-shaken in prod** — The top-level `import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"` is unconditional, so the entire devtools chunk is included in the production bundle regardless of the `import.meta.env.DEV` guard on the JSX. The fix is to use `React.lazy` with a conditional dynamic import so the module is only ever loaded in development builds.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

)}
</Flex>
);
}
6 changes: 6 additions & 0 deletions apps/code/src/renderer/routes/code/archived.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ArchivedTasksView } from "@features/archive/components/ArchivedTasksView";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/code/archived")({
component: ArchivedTasksView,
});
6 changes: 6 additions & 0 deletions apps/code/src/renderer/routes/code/inbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { InboxView } from "@features/inbox/components/InboxView";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/code/inbox")({
component: InboxView,
});
44 changes: 44 additions & 0 deletions apps/code/src/renderer/routes/code/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { TaskInput } from "@features/task-detail/components/TaskInput";
import { useNavigationStore } from "@stores/navigationStore";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/code/")({
component: CodeIndexRoute,
});

function CodeIndexRoute() {
const view = useNavigationStore((s) => s.view);
const taskInputReportAssociation = useNavigationStore(
(s) => s.taskInputReportAssociation,
);
const taskInputCloudRepository = useNavigationStore(
(s) => s.taskInputCloudRepository,
);

const initialPrompt =
view.type === "task-input" ? view.initialPrompt : undefined;
const initialPromptKey =
view.type === "task-input" ? view.taskInputRequestId : undefined;
const initialCloudRepository =
view.type === "task-input"
? (view.initialCloudRepository ?? taskInputCloudRepository)
: taskInputCloudRepository;
const initialModel =
view.type === "task-input" ? view.initialModel : undefined;
const initialMode = view.type === "task-input" ? view.initialMode : undefined;
const reportAssociation =
view.type === "task-input"
? (view.reportAssociation ?? taskInputReportAssociation)
: taskInputReportAssociation;

return (
<TaskInput
initialPrompt={initialPrompt}
initialPromptKey={initialPromptKey}
initialCloudRepository={initialCloudRepository}
initialModel={initialModel}
initialMode={initialMode}
reportAssociation={reportAssociation}
/>
);
}
Loading
Loading