Skip to content
Draft
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: 1 addition & 1 deletion apps/code/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const preview: Preview = {
grayColor="slate"
panelBackground="solid"
radius="none"
scaling="105%"
scaling="100%"
>
<Story />
</Theme>
Expand Down
18 changes: 18 additions & 0 deletions apps/code/src/renderer/components/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import { CommandCenterView } from "@features/command-center/components/CommandCe
import { InboxView } from "@features/inbox/components/InboxView";
import { FolderSettingsView } from "@features/settings/components/FolderSettingsView";
import { SettingsDialog } from "@features/settings/components/SettingsDialog";
import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import { SetupView } from "@features/setup/components/SetupView";
import { MainSidebar } from "@features/sidebar/components/MainSidebar";
import { SkillsView } from "@features/skills/components/SkillsView";
import { TaskDetail } from "@features/task-detail/components/TaskDetail";
import { TaskInput } from "@features/task-detail/components/TaskInput";
import { useTasks } from "@features/tasks/hooks/useTasks";
import { TourOverlay } from "@features/tour/components/TourOverlay";
import { useTourStore } from "@features/tour/stores/tourStore";
import { createFirstTaskTour } from "@features/tour/tours/createFirstTaskTour";
import { useConnectivity } from "@hooks/useConnectivity";
import { useIntegrations } from "@hooks/useIntegrations";
import { Box, Flex } from "@radix-ui/themes";
Expand All @@ -40,6 +44,11 @@ export function MainLayout() {
const { data: tasks } = useTasks();
const { showPrompt, isChecking, check, dismiss } = useConnectivity();

const startTour = useTourStore((s) => s.startTour);
const isFirstTaskTourDone = useTourStore((s) =>
s.completedTourIds.includes(createFirstTaskTour.id),
);

useIntegrations();
useTaskDeepLink();

Expand All @@ -55,6 +64,14 @@ export function MainLayout() {
}
}, [view, navigateToTaskInput]);

const settingsOpen = useSettingsDialogStore((s) => s.isOpen);

useEffect(() => {
if (isFirstTaskTourDone || settingsOpen) return;
const timer = setTimeout(() => startTour(createFirstTaskTour.id), 600);
return () => clearTimeout(timer);
}, [isFirstTaskTourDone, settingsOpen, startTour]);

const handleToggleCommandMenu = useCallback(() => {
toggleCommandMenu();
}, [toggleCommandMenu]);
Expand Down Expand Up @@ -102,6 +119,7 @@ export function MainLayout() {
onToggleShortcutsSheet={toggleShortcutsSheet}
/>
<SettingsDialog />
<TourOverlay />
<HedgehogMode />
</Flex>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/code/src/renderer/components/ThemeWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function ThemeWrapper({ children }: { children: React.ReactNode }) {
grayColor="slate"
panelBackground="solid"
radius="medium"
scaling="105%"
scaling="100%"
>
{children}
<div ref={portalRef} id="portal-container" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
import { SettingRow } from "@features/settings/components/SettingRow";
import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { useTourStore } from "@features/tour/stores/tourStore";
import { useFeatureFlag } from "@hooks/useFeatureFlag";
import { Button, Flex, Switch } from "@radix-ui/themes";
import { clearApplicationStorage } from "@utils/clearStorage";
Expand All @@ -22,7 +24,22 @@ export function AdvancedSettings() {
<Button
variant="soft"
size="1"
onClick={() => useOnboardingStore.getState().resetOnboarding()}
onClick={() => {
useOnboardingStore.getState().resetOnboarding();
useSettingsDialogStore.getState().close();
}}
>
Reset
</Button>
</SettingRow>
<SettingRow
label="Reset product tours"
description="Re-run product tours on next app restart"
>
<Button
variant="soft"
size="1"
onClick={() => useTourStore.getState().resetTours()}
>
Reset
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,24 +394,37 @@ export function TaskInput({
align="center"
style={{ minWidth: 0, overflow: "hidden" }}
>
{workspaceMode === "cloud" ? (
<GitHubRepoPicker
value={selectedRepository}
onChange={handleRepositorySelect}
repositories={repositories}
isLoading={isLoadingRepos}
placeholder="Select repository..."
size="1"
disabled={isCreatingTask}
/>
) : (
<FolderPicker
value={selectedDirectory}
onChange={setSelectedDirectory}
placeholder="Select repository..."
size="1"
/>
)}
<span
data-tour="folder-picker"
data-tour-ready={
(
workspaceMode === "cloud"
? selectedRepository
: selectedDirectory
)
? "true"
: undefined
}
>
{workspaceMode === "cloud" ? (
<GitHubRepoPicker
value={selectedRepository}
onChange={handleRepositorySelect}
repositories={repositories}
isLoading={isLoadingRepos}
placeholder="Select repository..."
size="1"
disabled={isCreatingTask}
/>
) : (
<FolderPicker
value={selectedDirectory}
onChange={setSelectedDirectory}
placeholder="Select repository..."
size="1"
/>
)}
</span>
<WorkspaceModeSelect
value={workspaceMode}
onChange={setWorkspaceMode}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export const TaskInputEditor = forwardRef<
return (
<Flex
direction="column"
data-tour="task-input-editor"
data-tour-ready={isEmpty ? undefined : "true"}
style={{
backgroundColor: "var(--gray-2)",
borderRadius: "var(--radius-2)",
Expand Down Expand Up @@ -274,6 +276,7 @@ export const TaskInputEditor = forwardRef<

<Flex align="center" gap="4">
<IconButton
data-tour="submit-button"
size="1"
variant="solid"
title={getSubmitTooltip()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
} from "@features/message-editor/utils/content";
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { useCreateTask } from "@features/tasks/hooks/useTasks";
import { useTourStore } from "@features/tour/stores/tourStore";
import { createFirstTaskTour } from "@features/tour/tours/createFirstTaskTour";
import { useConnectivity } from "@hooks/useConnectivity";
import type { WorkspaceMode } from "@main/services/workspace/schemas";
import { get } from "@renderer/di/container";
Expand Down Expand Up @@ -172,6 +174,7 @@ export function useTaskCreation({
} else {
navigateToTask(output.task);
}
useTourStore.getState().completeTour(createFirstTaskTour.id);
editor.clear();
log.info("Task ready, navigated early", { taskId: output.task.id });
});
Expand Down
148 changes: 148 additions & 0 deletions apps/code/src/renderer/features/tour/components/TourOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore";
import { useCommandMenuStore } from "@stores/commandMenuStore";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { useElementRect } from "../hooks/useElementRect";
import { useTourStore } from "../stores/tourStore";
import { TOUR_REGISTRY } from "../tours/tourRegistry";
import { TourTooltip } from "./TourTooltip";

const SPOTLIGHT_PADDING = 6;
const SPOTLIGHT_RADIUS = 8;

function SpotlightOverlay({ targetRect }: { targetRect: DOMRect | null }) {
return createPortal(
<AnimatePresence>
{targetRect && (
<motion.div
key="spotlight"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
top: targetRect.top - SPOTLIGHT_PADDING,
left: targetRect.left - SPOTLIGHT_PADDING,
width: targetRect.width + SPOTLIGHT_PADDING * 2,
height: targetRect.height + SPOTLIGHT_PADDING * 2,
}}
exit={{ opacity: 0 }}
transition={{ type: "spring", stiffness: 200, damping: 25 }}
style={{
position: "fixed",
borderRadius: SPOTLIGHT_RADIUS,
boxShadow: "0 0 0 9999px rgba(0, 0, 0, 0.5)",
zIndex: 199,
pointerEvents: "none",
}}
/>
)}
</AnimatePresence>,
document.body,
);
}

export function TourOverlay() {
const activeTourId = useTourStore((s) => s.activeTourId);
const activeStepIndex = useTourStore((s) => s.activeStepIndex);
const advance = useTourStore((s) => s.advance);
const dismiss = useTourStore((s) => s.dismiss);

const tour = activeTourId ? TOUR_REGISTRY[activeTourId] : null;
const step = tour?.steps[activeStepIndex] ?? null;

const selector = step ? `[data-tour="${step.target}"]` : null;
const targetRect = useElementRect(selector);

const advancedRef = useRef(false);

// biome-ignore lint/correctness/useExhaustiveDependencies: reset on step change
useEffect(() => {
advancedRef.current = false;
}, [activeStepIndex]);

useEffect(() => {
if (!step || !activeTourId || step.advanceOn.type !== "click" || !selector)
return;

const el = document.querySelector(selector);
if (!el) return;

const tourId = activeTourId;
const stepId = step.id;
const handler = () => {
if (!advancedRef.current) {
advancedRef.current = true;
setTimeout(() => advance(tourId, stepId), 0);
}
};

el.addEventListener("click", handler, { capture: true });
return () => el.removeEventListener("click", handler, { capture: true });
}, [step, selector, advance, activeTourId]);

useEffect(() => {
if (!step || !activeTourId || step.advanceOn.type !== "action" || !selector)
return;

const tourId = activeTourId;
const stepId = step.id;
const SETTLE_MS = 2000;
let settleTimer: ReturnType<typeof setTimeout> | null = null;

const tryAdvance = () => {
const el = document.querySelector(selector);
if (
el?.getAttribute("data-tour-ready") === "true" &&
!advancedRef.current
) {
advancedRef.current = true;
advance(tourId, stepId);
}
};

const resetTimer = () => {
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) {
observer.observe(el, {
subtree: true,
childList: true,
characterData: true,
attributes: true,
});
resetTimer();
}

return () => {
observer.disconnect();
if (settleTimer) clearTimeout(settleTimer);
};
}, [step, selector, advance, activeTourId]);

const settingsOpen = useSettingsDialogStore((s) => s.isOpen);
const commandMenuOpen = useCommandMenuStore((s) => s.isOpen);
const overlayBlocked = settingsOpen || commandMenuOpen;
const isActive = !!(tour && step && targetRect && !overlayBlocked);

return (
<>
<SpotlightOverlay targetRect={isActive ? targetRect : null} />
{isActive && (
<TourTooltip
step={step}
stepNumber={activeStepIndex + 1}
totalSteps={tour.steps.length}
onDismiss={dismiss}
/>
)}
</>
);
}
Loading
Loading