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
1 change: 1 addition & 0 deletions apps/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-router": "^1.95.0",
"@tanstack/react-router-devtools": "^1.95.0",
"@tanstack/react-virtual": "^3.13.26",
"@tanstack/router-plugin": "^1.95.0",
"@tiptap/core": "^3.13.0",
"@tiptap/extension-mention": "^3.13.0",
Expand Down
76 changes: 33 additions & 43 deletions apps/code/src/renderer/components/GlobalEventHandlers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@ import { useReviewNavigationStore } from "@features/code-review/stores/reviewNav
import { useFolders } from "@features/folders/hooks/useFolders";
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
import { getSessionService } from "@features/sessions/service/service";
import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import { openSettings } from "@features/settings/hooks/useOpenSettings";
import { useSidebarData } from "@features/sidebar/hooks/useSidebarData";
import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder";
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
import { useTasks } from "@features/tasks/hooks/useTasks";
import { useFocusWorkspace } from "@features/workspace/hooks/useFocusWorkspace";
import { useWorkspaces } from "@features/workspace/hooks/useWorkspace";
import { useAppView } from "@hooks/useAppView";
import { openTask, openTaskInput } from "@hooks/useOpenTask";
import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts";
import {
goBackInHistory,
goForwardInHistory,
navigateToFolderSettings,
navigateToInbox,
} from "@renderer/navigationBridge";
import { useTRPC } from "@renderer/trpc";
import type { Task } from "@shared/types";
import { useCommandMenuStore } from "@stores/commandMenuStore";
import { useNavigationStore } from "@stores/navigationStore";
import { useSubscription } from "@trpc/tanstack-react-query";
import { clearApplicationStorage } from "@utils/clearStorage";
import { shipIt } from "@utils/confetti";
Expand All @@ -32,18 +39,10 @@ export function GlobalEventHandlers({
}: GlobalEventHandlersProps) {
const trpcReact = useTRPC();
const commandMenuOpen = useCommandMenuStore((s) => s.isOpen);
const openSettingsDialog = useSettingsDialogStore((state) => state.open);
const navigateToTaskInput = useNavigationStore(
(state) => state.navigateToTaskInput,
);
const navigateToTask = useNavigationStore((state) => state.navigateToTask);
const navigateToInbox = useNavigationStore((state) => state.navigateToInbox);
const navigateToFolderSettings = useNavigationStore(
(state) => state.navigateToFolderSettings,
);
const view = useNavigationStore((state) => state.view);
const goBack = useNavigationStore((state) => state.goBack);
const goForward = useNavigationStore((state) => state.goForward);
const openSettingsDialog = openSettings;
const view = useAppView();
const goBack = goBackInHistory;
const goForward = goForwardInHistory;
const { folders, loadFolders } = useFolders();
const { data: workspaces = {} } = useWorkspaces();
const clearAllLayouts = usePanelLayoutStore((state) => state.clearAllLayouts);
Expand All @@ -55,7 +54,7 @@ export function GlobalEventHandlers({
(state) => state.getReviewMode,
);

const currentTaskId = view.type === "task-detail" ? view.data?.id : undefined;
const currentTaskId = view.type === "task-detail" ? view.taskId : undefined;
const { workspace: currentWorkspace, handleToggleFocus } = useFocusWorkspace(
currentTaskId ?? "",
);
Expand All @@ -77,60 +76,51 @@ export function GlobalEventHandlers({
(index: number) => {
const taskData = visualTaskOrder[index - 1];
const task = taskData ? taskById.get(taskData.id) : undefined;
if (task) {
navigateToTask(task);
}
if (task) void openTask(task);
},
[visualTaskOrder, taskById, navigateToTask],
[visualTaskOrder, taskById],
);

const handlePrevTask = useCallback(() => {
if (visualTaskOrder.length === 0) return;
if (view.type !== "task-detail" || !view.data) {
if (view.type !== "task-detail" || !view.taskId) {
const lastTaskData = visualTaskOrder[visualTaskOrder.length - 1];
const task = lastTaskData ? taskById.get(lastTaskData.id) : undefined;
if (task) navigateToTask(task);
if (task) void openTask(task);
return;
}
const currentIndex = visualTaskOrder.findIndex(
(t) => t.id === view.data?.id,
);
const currentIndex = visualTaskOrder.findIndex((t) => t.id === view.taskId);
const prevIndex =
currentIndex <= 0 ? visualTaskOrder.length - 1 : currentIndex - 1;
const prevTaskData = visualTaskOrder[prevIndex];
const task = prevTaskData ? taskById.get(prevTaskData.id) : undefined;
if (task) navigateToTask(task);
}, [visualTaskOrder, taskById, navigateToTask, view]);
if (task) void openTask(task);
}, [visualTaskOrder, taskById, view]);

const handleNextTask = useCallback(() => {
if (visualTaskOrder.length === 0) return;
if (view.type !== "task-detail" || !view.data) {
if (view.type !== "task-detail" || !view.taskId) {
const firstTaskData = visualTaskOrder[0];
const task = firstTaskData ? taskById.get(firstTaskData.id) : undefined;
if (task) navigateToTask(task);
if (task) void openTask(task);
return;
}
const currentIndex = visualTaskOrder.findIndex(
(t) => t.id === view.data?.id,
);
const currentIndex = visualTaskOrder.findIndex((t) => t.id === view.taskId);
const nextIndex =
currentIndex >= visualTaskOrder.length - 1 ? 0 : currentIndex + 1;
const nextTaskData = visualTaskOrder[nextIndex];
const task = nextTaskData ? taskById.get(nextTaskData.id) : undefined;
if (task) navigateToTask(task);
}, [visualTaskOrder, taskById, navigateToTask, view]);
if (task) void openTask(task);
}, [visualTaskOrder, taskById, view]);

const handleOpenSettings = useCallback(() => {
openSettingsDialog();
}, [openSettingsDialog]);

const handleFocusTaskMode = useCallback(
(data?: unknown) => {
if (!data) return;
navigateToTaskInput();
},
[navigateToTaskInput],
);
const handleFocusTaskMode = useCallback((data?: unknown) => {
if (!data) return;
openTaskInput();
}, []);

const handleResetLayout = useCallback(
(data?: unknown) => {
Expand Down Expand Up @@ -271,16 +261,16 @@ export function GlobalEventHandlers({

// Check if current task's folder became invalid (e.g., moved while app was open)
useEffect(() => {
if (view.type !== "task-detail" || !view.data) return;
if (view.type !== "task-detail" || !view.taskId) return;

const workspace = workspaces[view.data.id];
const workspace = workspaces[view.taskId];
if (!workspace?.folderId) return;

const folder = folders.find((f) => f.id === workspace.folderId);
if (folder && folder.exists === false) {
navigateToFolderSettings(folder.id);
}
}, [view, folders, workspaces, navigateToFolderSettings]);
}, [view, folders, workspaces]);

useSubscription(
trpcReact.ui.onOpenSettings.subscriptionOptions(undefined, {
Expand Down
4 changes: 2 additions & 2 deletions apps/code/src/renderer/components/HeaderRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import { SidebarTrigger } from "@features/sidebar/components/SidebarTrigger";
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
import { SkillButtonsMenu } from "@features/skill-buttons/components/SkillButtonsMenu";
import { useWorkspace } from "@features/workspace/hooks/useWorkspace";
import { useAppView } from "@hooks/useAppView";
import { useFeatureFlag } from "@hooks/useFeatureFlag";
import { Cloud, Spinner } from "@phosphor-icons/react";
import { Button as QuillButton } from "@posthog/quill";
import { Box, Flex } from "@radix-ui/themes";
import type { Task } from "@shared/types";
import { useHeaderStore } from "@stores/headerStore";
import { useNavigationStore } from "@stores/navigationStore";
import { isWindows } from "@utils/platform";
import { useState } from "react";

Expand Down Expand Up @@ -108,7 +108,7 @@ const WINDOWS_TITLEBAR_INSET = 140;

export function HeaderRow() {
const content = useHeaderStore((state) => state.content);
const view = useNavigationStore((state) => state.view);
const view = useAppView();

const sidebarOpen = useSidebarStore((state) => state.open);
const sidebarWidth = useSidebarStore((state) => state.width);
Expand Down
18 changes: 18 additions & 0 deletions apps/code/src/renderer/components/RoutePending.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Flex, Spinner } from "@radix-ui/themes";

// Default per-route pending UI. TanStack Router renders a route's
// `pendingComponent` (falling back to this) the moment its loader is pending,
// so navigation commits instantly and each route shows a loading state while
// its data resolves. Routes can override `pendingComponent` with a tailored
// skeleton later — this centered spinner is the baseline.
//
// It fills its slot in normal flow (height: 100%) rather than `absolute
// inset-0`: the Outlet's container isn't positioned, so an absolute overlay
// would escape to the viewport and flash over the sidebar/header.
export function RoutePending() {
return (
<Flex align="center" justify="center" height="100%" width="100%">
<Spinner size="3" />
</Flex>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { FullScreenLayout } from "@components/FullScreenLayout";
import { useLogoutMutation } from "@features/auth/hooks/authMutations";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { SettingsDialog } from "@features/settings/components/SettingsDialog";
import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import {
openSettingsDialog,
SettingsDialog,
} from "@features/settings/components/SettingsDialog";
import {
ArrowSquareOut,
GearSix,
Expand All @@ -27,15 +29,14 @@ interface AiApprovalScreenProps {

export function AiApprovalScreen({ orgName, isAdmin }: AiApprovalScreenProps) {
const logoutMutation = useLogoutMutation();
const openSettings = useSettingsDialogStore((s) => s.open);
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);

// biome-ignore lint/correctness/useExhaustiveDependencies: fire once on mount; later isAdmin changes from query resolution should not re-fire
useEffect(() => {
track(ANALYTICS_EVENTS.AI_CONSENT_GATE_SHOWN, { is_org_admin: isAdmin });
}, []);

useHotkeys(SHORTCUTS.SETTINGS, () => openSettings(), {
useHotkeys(SHORTCUTS.SETTINGS, () => openSettingsDialog(), {
preventDefault: true,
enableOnFormTags: true,
});
Expand All @@ -54,7 +55,7 @@ export function AiApprovalScreen({ orgName, isAdmin }: AiApprovalScreenProps) {
size="1"
variant="ghost"
color="gray"
onClick={() => openSettings()}
onClick={() => openSettingsDialog()}
className="opacity-70"
>
<GearSix size={14} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DotsCircleSpinner } from "@components/DotsCircleSpinner";
import { Tooltip } from "@components/ui/Tooltip";
import { useTasks } from "@features/tasks/hooks/useTasks";
import { openTask } from "@hooks/useOpenTask";
import { useSetHeaderContent } from "@hooks/useSetHeaderContent";
import type { WorkspaceMode } from "@main/services/workspace/schemas";
import {
Expand All @@ -26,7 +27,6 @@ import {
import { trpcClient, useTRPC } from "@renderer/trpc";
import type { Task } from "@shared/types";
import type { ArchivedTask } from "@shared/types/archive";
import { useNavigationStore } from "@stores/navigationStore";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { formatRelativeTimeLong } from "@utils/time";
import { toast } from "@utils/toast";
Expand Down Expand Up @@ -524,7 +524,7 @@ export function ArchivedTasksView() {
action: task
? {
label: "View task",
onClick: () => useNavigationStore.getState().navigateToTask(task),
onClick: () => void openTask(task),
}
: undefined,
});
Expand Down Expand Up @@ -600,7 +600,7 @@ export function ArchivedTasksView() {
action: task
? {
label: "View task",
onClick: () => useNavigationStore.getState().navigateToTask(task),
onClick: () => void openTask(task),
}
: undefined,
});
Expand Down
6 changes: 3 additions & 3 deletions apps/code/src/renderer/features/auth/hooks/authMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import {
import { useAuthUiStateStore } from "@features/auth/stores/authUiStateStore";
import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
import { resetSessionService } from "@features/sessions/service/service";
import { openTaskInput } from "@hooks/useOpenTask";
import { trpcClient } from "@renderer/trpc/client";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import type { CloudRegion } from "@shared/types/regions";
import { useNavigationStore } from "@stores/navigationStore";
import { useMutation } from "@tanstack/react-query";
import { track } from "@utils/analytics";

Expand Down Expand Up @@ -54,7 +54,7 @@ export function useSelectProjectMutation() {
onSuccess: async () => {
clearAuthScopedQueries();
await refreshAuthStateQuery();
useNavigationStore.getState().navigateToTaskInput();
openTaskInput();
},
});
}
Expand Down Expand Up @@ -82,7 +82,7 @@ export function useLogoutMutation() {
onSuccess: async ({ previousState }) => {
clearAuthScopedQueries();
useAuthUiStateStore.getState().setStaleRegion(previousState.cloudRegion);
useNavigationStore.getState().navigateToTaskInput();
openTaskInput();
useOnboardingStore.getState().resetSelections();

await trpcClient.auth.logout.mutate();
Expand Down
6 changes: 2 additions & 4 deletions apps/code/src/renderer/features/auth/stores/authStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,8 @@
},
}));

vi.mock("@stores/navigationStore", () => ({
useNavigationStore: {
getState: () => ({ navigateToTaskInput: vi.fn() }),
},
vi.mock("@hooks/useOpenTask", () => ({
openTaskInput: vi.fn(),
}));

import { resetUser, setUserGroups } from "@utils/analytics";
Expand Down Expand Up @@ -209,7 +207,7 @@
const logoutPromise = useAuthStore.getState().logout();
await Promise.resolve();

expect(useAuthStore.getState().isAuthenticated).toBe(false);

Check failure on line 210 in apps/code/src/renderer/features/auth/stores/authStore.test.ts

View workflow job for this annotation

GitHub Actions / unit-test

src/renderer/features/auth/stores/authStore.test.ts > authStore > clears auth state immediately on logout before the auth service responds

AssertionError: expected true to be false // Object.is equality - Expected + Received - false + true ❯ src/renderer/features/auth/stores/authStore.test.ts:210:53
expect(useAuthStore.getState().client).toBeNull();
expect(useAuthStore.getState().projectId).toBeNull();
expect(useAuthStore.getState().needsScopeReauth).toBe(false);
Expand Down
10 changes: 5 additions & 5 deletions apps/code/src/renderer/features/auth/stores/authStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useSeatStore } from "@features/billing/stores/seatStore";
import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import { closeSettings } from "@features/settings/hooks/useOpenSettings";
import { openTaskInput } from "@hooks/useOpenTask";
import { PostHogAPIClient } from "@renderer/api/posthogClient";
import { trpcClient } from "@renderer/trpc/client";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import type { CloudRegion } from "@shared/types/regions";
import { getCloudUrlFromRegion } from "@shared/utils/urls";
import { useNavigationStore } from "@stores/navigationStore";
import {
identifyUser,
resetUser,
Expand Down Expand Up @@ -240,14 +240,14 @@ export const useAuthStore = create<AuthStoreState>((set) => ({
sessionResetCallback?.();
await trpcClient.auth.selectProject.mutate({ projectId });
await syncAuthState();
useNavigationStore.getState().navigateToTaskInput();
openTaskInput();
},

logout: async () => {
track(ANALYTICS_EVENTS.USER_LOGGED_OUT);
sessionResetCallback?.();
useSeatStore.getState().reset();
useSettingsDialogStore.getState().close();
closeSettings();

set((state) => ({
...state,
Expand All @@ -267,7 +267,7 @@ export const useAuthStore = create<AuthStoreState>((set) => ({
lastCompletedAuthSyncKey = null;

clearAuthenticatedRendererState({ clearAllQueries: true });
useNavigationStore.getState().navigateToTaskInput();
openTaskInput();
await trpcClient.auth.logout.mutate();
},
}));
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useFreeUsage } from "@features/billing/hooks/useFreeUsage";
import { formatResetTime, isUsageExceeded } from "@features/billing/utils";
import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import { openSettings } from "@features/settings/hooks/useOpenSettings";
import { useFeatureFlag } from "@hooks/useFeatureFlag";
import { Circle } from "@phosphor-icons/react";
import { BILLING_FLAG } from "@shared/constants";
Expand All @@ -15,7 +15,7 @@ export function SidebarUsageBar() {

const handleUpgrade = () => {
track(ANALYTICS_EVENTS.UPGRADE_PROMPT_CLICKED, { surface: "sidebar" });
useSettingsDialogStore.getState().open("plan-usage");
openSettings("plan-usage");
};

if (!usage) {
Expand Down
Loading
Loading