diff --git a/apps/code/src/renderer/features/onboarding/components/context-collection/SuggestedTasks.tsx b/apps/code/src/renderer/features/onboarding/components/context-collection/SuggestedTasks.tsx index 7e97c0dc3..11a82ff6c 100644 --- a/apps/code/src/renderer/features/onboarding/components/context-collection/SuggestedTasks.tsx +++ b/apps/code/src/renderer/features/onboarding/components/context-collection/SuggestedTasks.tsx @@ -69,9 +69,9 @@ function SuggestedTaskCard({ return ( onSelect(task)} type="button" className={`flex w-full cursor-pointer items-start rounded-xl border border-(--gray-a3) bg-(--color-panel-solid) text-left transition-[border-color,box-shadow] ${ diff --git a/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx b/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx index eacbf89b0..32a7b62da 100644 --- a/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx +++ b/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx @@ -29,6 +29,7 @@ interface SetupScanFeedProps { icon: Icon; color: string; currentTool: string | null; + activeLabelOverride?: string; recentEntries: ActivityEntry[]; isDone: boolean; doneLabel?: string; @@ -124,11 +125,14 @@ export function SetupScanFeed({ icon: LabelIcon, color, currentTool, + activeLabelOverride, recentEntries, isDone, doneLabel = "Complete", }: SetupScanFeedProps) { - const activeLabel = currentTool ? toolLabel(currentTool) : "Starting..."; + const activeLabel = + activeLabelOverride ?? + (currentTool ? toolLabel(currentTool) : "Starting..."); return ( diff --git a/apps/code/src/renderer/features/setup/components/SetupView.tsx b/apps/code/src/renderer/features/setup/components/SetupView.tsx index 767e83825..78a5134bb 100644 --- a/apps/code/src/renderer/features/setup/components/SetupView.tsx +++ b/apps/code/src/renderer/features/setup/components/SetupView.tsx @@ -7,23 +7,44 @@ import { useSetupStore } from "@features/setup/stores/setupStore"; import type { DiscoveredTask } from "@features/setup/types"; import { buildDiscoveredTaskPrompt } from "@features/setup/utils/buildDiscoveredTaskPrompt"; import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; -import { Robot, Rocket } from "@phosphor-icons/react"; -import { Box, Button, Flex, ScrollArea, Text } from "@radix-ui/themes"; -import explorerHog from "@renderer/assets/images/hedgehogs/explorer-hog.png"; +import { + ArrowRight, + Lightning, + MagnifyingGlass, + Rocket, +} from "@phosphor-icons/react"; +import { Button, Flex, ScrollArea, Text } from "@radix-ui/themes"; import { ANALYTICS_EVENTS } from "@shared/types/analytics"; import { useNavigationStore } from "@stores/navigationStore"; import { track } from "@utils/analytics"; -import { motion } from "framer-motion"; -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; export function SetupView() { - const { discoveryFeed, isDiscoveryDone, discoveredTasks, error } = - useSetupRun(); + const { + discoveryFeed, + isDiscoveryDone, + isEnricherRunning, + discoveredTasks, + error, + } = useSetupRun(); const completeSetup = useOnboardingStore((state) => state.completeSetup); const navigateToTaskInput = useNavigationStore( (state) => state.navigateToTaskInput, ); + const { enricherTasks, agentTasks } = useMemo(() => { + const enricher: DiscoveredTask[] = []; + const agent: DiscoveredTask[] = []; + for (const task of discoveredTasks) { + if (task.source === "enricher") enricher.push(task); + else agent.push(task); + } + return { enricherTasks: enricher, agentTasks: agent }; + }, [discoveredTasks]); + + const showQuickWins = enricherTasks.length > 0 || isEnricherRunning; + const isEnricherDone = !isEnricherRunning; + useSetHeaderContent( @@ -54,24 +75,15 @@ export function SetupView() { navigateToTaskInput({ initialPrompt }); }; - // Mid-scan: leave discovery running so the sidebar surfaces tasks when ready. - const handleSkipDuringScan = () => { + const handleStartFromScratch = () => { track(ANALYTICS_EVENTS.SETUP_SKIPPED, { discovery_status: useSetupStore.getState().discoveryStatus, had_discovered_tasks: discoveredTasks.length > 0, - entry_point: "during_scan", + entry_point: isDiscoveryDone ? "after_done" : "during_scan", }); - completeSetup(); - navigateToTaskInput(); - }; - - const handleSkipAfterDone = () => { - track(ANALYTICS_EVENTS.SETUP_SKIPPED, { - discovery_status: useSetupStore.getState().discoveryStatus, - had_discovered_tasks: discoveredTasks.length > 0, - entry_point: "after_done", - }); - useSetupStore.getState().resetDiscovery(); + if (isDiscoveryDone) { + useSetupStore.getState().resetDiscovery(); + } completeSetup(); navigateToTaskInput(); }; @@ -87,142 +99,183 @@ export function SetupView() { - - - - {isDiscoveryDone - ? "Your starter tasks are ready" - : discoveredTasks.length > 0 - ? "Some starter tasks are ready" - : "Finding your first tasks"} - - - {isDiscoveryDone - ? "Pick one to get going, or start from scratch — your suggestions stay in the sidebar." - : discoveredTasks.length > 0 - ? "Pick one to get going, or wait — we're still skimming your codebase for more." - : "This takes about a minute. We're scanning your code for a handful of starter tasks you can run in one click — bug fixes, cleanup, and PostHog enhancements where they apply."} - - - - - - + - - + Set up your first task + + + Pick something to work on, or describe your own. + - {discoveredTasks.length > 0 && ( - - - - Recommended first tasks +
+ {showQuickWins && ( + + )} + + +
+ + + - - Suggested tasks will appear in the sidebar when ready. - - -
-
- )} + + {!isDiscoveryDone && ( + + Suggested tasks will appear in the sidebar as they're ready. + + )} +
+
+
+ + ); +} - {error && ( - - {error} - - )} +interface QuickWinsColumnProps { + tasks: DiscoveredTask[]; + isDone: boolean; + onSelectTask: (task: DiscoveredTask) => void; +} - {isDiscoveryDone && ( - - - - - - )} +function QuickWinsColumn({ + tasks, + isDone, + onSelectTask, +}: QuickWinsColumnProps) { + return ( + + + + Quick wins + + + Spotted in your PostHog setup + + + + {tasks.length > 0 && ( + + )} + + ); +} + +interface DeeperScanColumnProps { + hasSibling: boolean; + isDone: boolean; + tasks: DiscoveredTask[]; + feed: ReturnType["discoveryFeed"]; + error: string | null; + onSelectTask: (task: DiscoveredTask) => void; +} + +function DeeperScanColumn({ + hasSibling, + isDone, + tasks, + feed, + error, + onSelectTask, +}: DeeperScanColumnProps) { + const isEmpty = isDone && tasks.length === 0; + + return ( +
+ + + + Deeper scan + + + {isDone + ? "We checked your code for bugs and improvements." + : "Bugs, dead code, and improvements (~1 min)."} + + + + {isDone && tasks.length > 0 && ( + + )} + + {isEmpty && !error && ( + + No issues found — your code looks clean ✨ + + )} + + {error && ( + + {error} + + )} - +
); } diff --git a/apps/code/src/renderer/features/setup/hooks/useSetupRun.ts b/apps/code/src/renderer/features/setup/hooks/useSetupRun.ts index 0fd36f273..fcc2b692f 100644 --- a/apps/code/src/renderer/features/setup/hooks/useSetupRun.ts +++ b/apps/code/src/renderer/features/setup/hooks/useSetupRun.ts @@ -8,6 +8,7 @@ import { useEffect, useRef } from "react"; export function useSetupRun() { const selectedDirectory = useOnboardingStore((s) => s.selectedDirectory); const discoveryStatus = useSetupStore((s) => s.discoveryStatus); + const enricherStatus = useSetupStore((s) => s.enricherStatus); const discoveredTasks = useSetupStore((s) => s.discoveredTasks); const discoveryFeed = useSetupStore((s) => s.discoveryFeed); const error = useSetupStore((s) => s.error); @@ -28,6 +29,7 @@ export function useSetupRun() { return { discoveryFeed, isDiscoveryDone: discoveryStatus === "done", + isEnricherRunning: enricherStatus === "running", discoveredTasks, error, }; diff --git a/apps/code/src/renderer/features/setup/services/setupRunService.ts b/apps/code/src/renderer/features/setup/services/setupRunService.ts index 8acca4733..d15469ab4 100644 --- a/apps/code/src/renderer/features/setup/services/setupRunService.ts +++ b/apps/code/src/renderer/features/setup/services/setupRunService.ts @@ -257,6 +257,7 @@ export class SetupRunService { if (!directory) return; if (this.enricherSuggestionsRunning) return; this.enricherSuggestionsRunning = true; + useSetupStore.getState().startEnrichment(); void (async () => { try { @@ -274,8 +275,10 @@ export class SetupRunService { const suggestion = buildPosthogSetupSuggestion(installState); useSetupStore.getState().addEnricherSuggestionIfMissing(suggestion); } + useSetupStore.getState().completeEnrichment(); } catch (err) { log.warn("Enricher run failed", { error: err }); + useSetupStore.getState().failEnrichment(); } finally { this.enricherSuggestionsRunning = false; } diff --git a/apps/code/src/renderer/features/setup/stores/setupStore.ts b/apps/code/src/renderer/features/setup/stores/setupStore.ts index 9a64e8833..5e42e2b46 100644 --- a/apps/code/src/renderer/features/setup/stores/setupStore.ts +++ b/apps/code/src/renderer/features/setup/stores/setupStore.ts @@ -6,6 +6,7 @@ import { persist } from "zustand/middleware"; const log = logger.scope("setup-store"); type DiscoveryStatus = "idle" | "running" | "done" | "error"; +type EnricherStatus = "idle" | "running" | "done" | "error"; interface ActivityEntry { id: number; @@ -33,6 +34,7 @@ interface SetupStoreState { discoveryTaskId: string | null; discoveryTaskRunId: string | null; discoveryFeed: AgentFeedState; + enricherStatus: EnricherStatus; error: string | null; selectedDiscoveredTaskId: string | null; } @@ -42,6 +44,9 @@ interface SetupStoreActions { completeDiscovery: (tasks: DiscoveredTask[]) => void; failDiscovery: (message?: string) => void; resetDiscovery: () => void; + startEnrichment: () => void; + completeEnrichment: () => void; + failEnrichment: () => void; removeDiscoveredTask: (taskId: string) => void; selectDiscoveredTask: (taskId: string | null) => void; addEnricherSuggestionIfMissing: (task: DiscoveredTask) => void; @@ -57,6 +62,7 @@ const initialState: SetupStoreState = { discoveryTaskId: null, discoveryTaskRunId: null, discoveryFeed: EMPTY_FEED, + enricherStatus: "idle", error: null, selectedDiscoveredTaskId: null, }; @@ -144,6 +150,18 @@ export const useSetupStore = create()( })); }, + startEnrichment: () => { + set({ enricherStatus: "running" }); + }, + + completeEnrichment: () => { + set({ enricherStatus: "done" }); + }, + + failEnrichment: () => { + set({ enricherStatus: "error" }); + }, + removeDiscoveredTask: (taskId) => { set((state) => ({ discoveredTasks: state.discoveredTasks.filter((t) => t.id !== taskId),