Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
70d6c73
Product tours
charlesvien Apr 15, 2026
f1bd3a2
Latest
charlesvien Apr 15, 2026
b919eec
lint
charlesvien Apr 15, 2026
21340b3
Fix tour completion on task creation and step transitions
charlesvien Apr 15, 2026
2e875b3
Suppress biome lint for intentional step-change deps
charlesvien Apr 15, 2026
e74bfb2
Guard tour advance with expected tour and step IDs
charlesvien Apr 15, 2026
cc456de
Remove unused caretDirection from tour types
charlesvien Apr 15, 2026
1dd6335
Update tourStore.ts
charlesvien Apr 17, 2026
e2819da
Position tour tooltip relative to target element
charlesvien Apr 20, 2026
48d478e
Grandfather existing users out of first task tour
charlesvien Apr 22, 2026
f7b95e7
Default clean script to dev data only, add --all flag
charlesvien Apr 22, 2026
ab10fda
analytics
charlesvien Apr 22, 2026
19192ec
Update PromptInput.tsx
charlesvien Apr 22, 2026
0df7bf2
Update PromptInput.tsx
charlesvien Apr 22, 2026
1f5ef98
Update TourOverlay.tsx
charlesvien Apr 22, 2026
fe4a04b
Replace RAF polling with event-driven measurement in useElementRect
charlesvien Apr 22, 2026
9146401
Scope tour data attributes to task creation view only
charlesvien Apr 22, 2026
89c3ec8
Remove dead useTour hook and add analytics to completeTour
charlesvien Apr 22, 2026
ae1e4e0
Simplify Caret component with data-driven triangle styles
charlesvien Apr 22, 2026
0cb1a0a
Revert unrelated scaling change from tour branch
charlesvien Apr 22, 2026
f8e043c
Update vitest.config.ts
charlesvien Apr 22, 2026
686904b
Bundle @posthog/enricher in agent tsup build
charlesvien Apr 22, 2026
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
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,11 +9,15 @@ 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 { 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 @@ -39,6 +43,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 @@ -54,6 +63,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 @@ -99,6 +116,7 @@ export function MainLayout() {
onToggleShortcutsSheet={toggleShortcutsSheet}
/>
<SettingsDialog />
<TourOverlay />
<HedgehogMode />
</Flex>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface PromptInputProps {
// manual submit override (for flows like new-task that submit outside the editor hook)
onSubmitClick?: () => void;
submitTooltipOverride?: string;
tourTarget?: string;
}

export const PromptInput = forwardRef<EditorHandle, PromptInputProps>(
Expand Down Expand Up @@ -88,6 +89,7 @@ export const PromptInput = forwardRef<EditorHandle, PromptInputProps>(
onBlur,
onSubmitClick,
submitTooltipOverride,
tourTarget,
},
ref,
) => {
Expand Down Expand Up @@ -236,6 +238,7 @@ export const PromptInput = forwardRef<EditorHandle, PromptInputProps>(
onClick={handleSubmitClick}
disabled={submitBlocked}
aria-label="Send message"
{...(tourTarget && { "data-tour": `${tourTarget}-submit` })}
>
<ArrowUp size={14} weight="bold" />
</InputGroupButton>
Expand All @@ -248,6 +251,10 @@ export const PromptInput = forwardRef<EditorHandle, PromptInputProps>(
onClick={handleContainerClick}
className={`h-auto bg-card ${isBashMode ? "ring-1 ring-blue-9" : ""}`}
style={{ cursor: "text" }}
{...(tourTarget && {
"data-tour": `${tourTarget}-editor`,
"data-tour-ready": !isEmpty ? "true" : undefined,
})}
>
{attachments.length > 0 && (
<InputGroupAddon align="block-start">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 Down Expand Up @@ -31,6 +32,18 @@ export function AdvancedSettings() {
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>
</SettingRow>
<SettingRow
label="Clear application storage"
description="This will remove all locally stored application data"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,19 @@ export function TaskInput({
disabled={isCreatingTask}
/>
)}
<ButtonGroup ref={buttonGroupRef}>
<ButtonGroup
ref={buttonGroupRef}
data-tour="folder-picker"
data-tour-ready={
(
workspaceMode === "cloud"
? selectedRepository
: selectedDirectory
)
? "true"
: undefined
}
>
{workspaceMode === "cloud" ? (
<GitHubRepoPicker
value={selectedRepository}
Expand Down Expand Up @@ -512,6 +524,7 @@ export function TaskInput({
autoFocus
clearOnSubmit={false}
submitDisabledExternal={!canSubmit || isCreatingTask || !isOnline}
tourTarget="task-input"
repoPath={selectedDirectory}
modeOption={modeOption}
onModeChange={handleModeChange}
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 @@ -170,6 +172,7 @@ export function useTaskCreation({
} else {
navigateToTask(output.task);
}
useTourStore.getState().completeTour(createFirstTaskTour.id);
editor.clear();
});

Expand Down
149 changes: 149 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,149 @@
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;
advance(tourId, stepId);
}
};

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}
targetRect={targetRect}
/>
)}
</>
);
}
Loading
Loading