diff --git a/apps/code/src/main/services/workspace/service.ts b/apps/code/src/main/services/workspace/service.ts index 2510b65c1..92161dbe1 100644 --- a/apps/code/src/main/services/workspace/service.ts +++ b/apps/code/src/main/services/workspace/service.ts @@ -14,6 +14,7 @@ import { import { CreateOrSwitchBranchSaga } from "@posthog/git/sagas/branch"; import { DetachHeadSaga } from "@posthog/git/sagas/head"; import { WorktreeManager } from "@posthog/git/worktree"; +import { ANALYTICS_EVENTS } from "@shared/types/analytics"; import { inject, injectable } from "inversify"; import type { RepositoryRepository } from "../../db/repositories/repository-repository"; import type { WorkspaceRepository } from "../../db/repositories/workspace-repository"; @@ -340,9 +341,9 @@ export class WorkspaceService extends TypedEventEmitter branchName, error, }); - trackAppEvent("branch_link_default_branch_unknown", { - taskId, - branchName, + trackAppEvent(ANALYTICS_EVENTS.BRANCH_LINK_DEFAULT_BRANCH_UNKNOWN, { + task_id: taskId, + branch_name: branchName, }); return; } @@ -368,7 +369,7 @@ export class WorkspaceService extends TypedEventEmitter taskId, branchName, }); - trackAppEvent("branch_linked", { + trackAppEvent(ANALYTICS_EVENTS.BRANCH_LINKED, { task_id: taskId, branch_name: branchName, source: source ?? "unknown", @@ -382,7 +383,7 @@ export class WorkspaceService extends TypedEventEmitter taskId, branchName: null, }); - trackAppEvent("branch_unlinked", { + trackAppEvent(ANALYTICS_EVENTS.BRANCH_UNLINKED, { task_id: taskId, source: source ?? "unknown", }); diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index af6b83c5a..d5cb04302 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -13,6 +13,7 @@ import { useInboxDeepLink } from "@features/inbox/hooks/useInboxDeepLink"; 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 { useSidebarData } from "@features/sidebar/hooks/useSidebarData"; import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder"; @@ -129,6 +130,8 @@ export function MainLayout() { {view.type === "command-center" && } {view.type === "skills" && } + + {view.type === "setup" && } 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 new file mode 100644 index 000000000..5d90b2adc --- /dev/null +++ b/apps/code/src/renderer/features/onboarding/components/context-collection/SuggestedTasks.tsx @@ -0,0 +1,151 @@ +import type { DiscoveredTask } from "@features/setup/types"; +import type { Icon } from "@phosphor-icons/react"; +import { + ArrowRight, + Bug, + ChartLine, + Copy, + Flag, + Funnel, + Lightning, + Lock, + Trash, + Warning, + Wrench, +} from "@phosphor-icons/react"; +import { Flex, Text } from "@radix-ui/themes"; +import { motion } from "framer-motion"; + +const CATEGORY_CONFIG: Record< + DiscoveredTask["category"], + { icon: Icon; color: string } +> = { + bug: { icon: Bug, color: "red" }, + security: { icon: Lock, color: "red" }, + dead_code: { icon: Trash, color: "gray" }, + duplication: { icon: Copy, color: "orange" }, + performance: { icon: Lightning, color: "green" }, + stale_feature_flag: { icon: Flag, color: "amber" }, + error_tracking: { icon: Warning, color: "orange" }, + event_tracking: { icon: ChartLine, color: "blue" }, + funnel: { icon: Funnel, color: "violet" }, +}; + +interface SuggestedTasksProps { + tasks: DiscoveredTask[]; + onSelectTask: (task: DiscoveredTask) => void; +} + +export function SuggestedTasks({ tasks, onSelectTask }: SuggestedTasksProps) { + if (tasks.length === 0) { + return ( + + No issues found. Your codebase looks clean! + + ); + } + + return ( + + {tasks.map((task, index) => { + const config = CATEGORY_CONFIG[task.category] ?? { + icon: Wrench, + color: "gray", + }; + const TaskIcon = config.icon; + return ( + onSelectTask(task)} + type="button" + style={{ + display: "flex", + alignItems: "flex-start", + gap: 14, + padding: "16px 18px", + backgroundColor: "var(--color-panel-solid)", + border: "1px solid var(--gray-a3)", + borderRadius: 12, + boxShadow: + "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)", + cursor: "pointer", + textAlign: "left", + width: "100%", + transition: "border-color 0.15s ease, box-shadow 0.15s ease", + }} + whileHover={{ + borderColor: `var(--${config.color}-6)`, + boxShadow: + "0 2px 8px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.04)", + }} + > + + + + + + + {task.title} + + + + + {task.description} + + {task.file && ( + + {task.file} + {task.lineHint ? `:${task.lineHint}` : ""} + + )} + + + ); + })} + + ); +} diff --git a/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts b/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts index c2563bc63..bc75d76ca 100644 --- a/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts +++ b/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts @@ -8,6 +8,7 @@ const log = logger.scope("onboarding-store"); interface OnboardingStoreState { currentStep: OnboardingStep; hasCompletedOnboarding: boolean; + hasCompletedSetup: boolean; isConnectingGithub: boolean; selectedProjectId: number | null; selectedDirectory: string; @@ -16,6 +17,7 @@ interface OnboardingStoreState { interface OnboardingStoreActions { setCurrentStep: (step: OnboardingStep) => void; completeOnboarding: () => void; + completeSetup: () => void; resetOnboarding: () => void; resetSelections: () => void; setConnectingGithub: (isConnecting: boolean) => void; @@ -28,6 +30,7 @@ type OnboardingStore = OnboardingStoreState & OnboardingStoreActions; const initialState: OnboardingStoreState = { currentStep: "welcome", hasCompletedOnboarding: false, + hasCompletedSetup: false, isConnectingGithub: false, selectedProjectId: null, selectedDirectory: "", @@ -43,6 +46,7 @@ export const useOnboardingStore = create()( log.info("completeOnboarding"); set({ hasCompletedOnboarding: true }); }, + completeSetup: () => set({ hasCompletedSetup: true }), resetOnboarding: () => set({ ...initialState }), resetSelections: () => set({ @@ -59,6 +63,7 @@ export const useOnboardingStore = create()( partialize: (state) => ({ currentStep: state.currentStep, hasCompletedOnboarding: state.hasCompletedOnboarding, + hasCompletedSetup: state.hasCompletedSetup, selectedProjectId: state.selectedProjectId, selectedDirectory: state.selectedDirectory, }), diff --git a/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx b/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx new file mode 100644 index 000000000..7636cf890 --- /dev/null +++ b/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx @@ -0,0 +1,317 @@ +import { DotsCircleSpinner } from "@components/DotsCircleSpinner"; +import type { Icon } from "@phosphor-icons/react"; +import { + ArrowsClockwise, + ArrowsLeftRight, + Brain, + CheckCircle, + FileText, + Globe, + MagnifyingGlass, + PencilSimple, + Terminal, + Trash, + Wrench, +} from "@phosphor-icons/react"; +import { Flex, Text } from "@radix-ui/themes"; +import { AnimatePresence, motion } from "framer-motion"; + +interface ActivityEntry { + id: number; + toolCallId: string; + tool: string; + filePath: string | null; + title: string; +} + +interface SetupScanFeedProps { + label: string; + icon: Icon; + color: string; + currentTool: string | null; + recentEntries: ActivityEntry[]; + isDone: boolean; + doneLabel?: string; +} + +const TOOL_VERBS: Record = { + Read: "Reading a file...", + Glob: "Searching files...", + Grep: "Searching code...", + Bash: "Running a command...", + Edit: "Making changes...", + Write: "Writing a file...", + Agent: "Thinking...", + ListDirectory: "Browsing files...", + ToolSearch: "Looking up tools...", + WebSearch: "Searching the web...", + WebFetch: "Fetching a page...", + NotebookEdit: "Editing notebook...", + Monitor: "Monitoring...", + SearchReplace: "Making changes...", + MultiEdit: "Making changes...", + StructuredOutput: "Preparing results...", + create_output: "Preparing results...", + TodoRead: "Reviewing tasks...", + TodoWrite: "Updating tasks...", + TaskCreate: "Creating a task...", + TaskUpdate: "Updating a task...", + TaskGet: "Checking task status...", + TaskList: "Listing tasks...", + AskFollowupQuestion: "Thinking...", +}; + +const TOOL_KIND: Record = { + Read: "read", + Edit: "edit", + Write: "edit", + Grep: "search", + Glob: "search", + Bash: "execute", + Agent: "think", + ToolSearch: "search", + WebSearch: "search", + WebFetch: "fetch", + StructuredOutput: "other", + create_output: "other", +}; + +const KIND_ICONS: Record = { + read: FileText, + edit: PencilSimple, + delete: Trash, + move: ArrowsLeftRight, + search: MagnifyingGlass, + execute: Terminal, + think: Brain, + fetch: Globe, + switch_mode: ArrowsClockwise, + other: Wrench, +}; + +function shortenPath(path: string): string { + const parts = path.split("/"); + if (parts.length <= 3) return path; + return `.../${parts.slice(-3).join("/")}`; +} + +const GENERIC_TITLES = new Set([ + "Read File", + "Execute command", + "Edit", + "Write", + "Find", + "Fetch", + "Working", + "Task", + "Terminal", +]); + +function entryDisplayText(entry: ActivityEntry): string { + if (entry.filePath) return shortenPath(entry.filePath); + if (entry.title && !GENERIC_TITLES.has(entry.title)) return entry.title; + return TOOL_VERBS[entry.tool] ?? "Working..."; +} + +function toolLabel(tool: string): string { + return TOOL_VERBS[tool] ?? "Working..."; +} + +export function SetupScanFeed({ + label, + icon: LabelIcon, + color, + currentTool, + recentEntries, + isDone, + doneLabel = "Complete", +}: SetupScanFeedProps) { + const activeLabel = currentTool ? toolLabel(currentTool) : "Starting..."; + + return ( + + + + + {isDone ? ( + + + + ) : ( + + )} + + + {label} + + + +
+ + {!isDone && activeLabel && ( + + + + {activeLabel} + + + )} + {isDone && ( + + + {doneLabel} + + + )} + +
+
+ + {!isDone && recentEntries.length > 0 && ( + + + + {recentEntries.slice(-4).map((entry, index, arr) => { + const isLatest = index === arr.length - 1; + const kind = TOOL_KIND[entry.tool] ?? "other"; + const EntryIcon = KIND_ICONS[kind] ?? Wrench; + const entryText = entryDisplayText(entry); + return ( + + + + + {entryText} + + + + ); + })} + + + + )} +
+ ); +} diff --git a/apps/code/src/renderer/features/setup/components/SetupView.tsx b/apps/code/src/renderer/features/setup/components/SetupView.tsx new file mode 100644 index 000000000..a488c5bb2 --- /dev/null +++ b/apps/code/src/renderer/features/setup/components/SetupView.tsx @@ -0,0 +1,242 @@ +import { DotPatternBackground } from "@components/DotPatternBackground"; +import { SuggestedTasks } from "@features/onboarding/components/context-collection/SuggestedTasks"; +import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; +import { SetupScanFeed } from "@features/setup/components/SetupScanFeed"; +import { useSetupRun } from "@features/setup/hooks/useSetupRun"; +import { useSetupStore } from "@features/setup/stores/setupStore"; +import type { DiscoveredTask } from "@features/setup/types"; +import { useSetHeaderContent } from "@hooks/useSetHeaderContent"; +import { MagicWand, 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 { ANALYTICS_EVENTS } from "@shared/types/analytics"; +import { useNavigationStore } from "@stores/navigationStore"; +import { track } from "@utils/analytics"; +import { motion } from "framer-motion"; +import { useRef } from "react"; + +export function SetupView() { + const { + discoveryFeed, + wizardFeed, + isDiscoveryDone, + isWizardStarted, + wizardSkipped, + discoveredTasks, + error, + } = useSetupRun(); + const completeSetup = useOnboardingStore((state) => state.completeSetup); + const navigateToTaskInput = useNavigationStore( + (state) => state.navigateToTaskInput, + ); + const viewTrackedRef = useRef(false); + + useSetHeaderContent( + + + + Finish setup + + , + ); + + if (!viewTrackedRef.current) { + viewTrackedRef.current = true; + track(ANALYTICS_EVENTS.SETUP_VIEWED, { + discovery_status: useSetupStore.getState().discoveryStatus, + }); + } + + const handleSelectTask = (task: DiscoveredTask) => { + const position = discoveredTasks.findIndex((t) => t.id === task.id); + track(ANALYTICS_EVENTS.SETUP_TASK_SELECTED, { + discovered_task_id: task.id, + category: task.category, + position: position >= 0 ? position : 0, + total_discovered: discoveredTasks.length, + }); + completeSetup(); + navigateToTaskInput(); + }; + + const handleSkip = () => { + track(ANALYTICS_EVENTS.SETUP_SKIPPED, { + discovery_status: useSetupStore.getState().discoveryStatus, + had_discovered_tasks: discoveredTasks.length > 0, + }); + completeSetup(); + navigateToTaskInput(); + }; + + return ( + + + + + + + + Setting up PostHog + + + We're configuring your integration and scanning for quick wins. + + + + + + {isWizardStarted && !wizardSkipped && ( + + + + )} + + + + + + + + + + + {isDiscoveryDone + ? "Pick a task to get started, or skip for now." + : "Hang tight while we get everything ready..."} + + + + + {error && ( + + {error} + + )} + + {isDiscoveryDone && ( + + + {discoveredTasks.length > 0 && ( + + + Recommended first tasks + + + + )} + + + + + + + )} + + + + ); +} diff --git a/apps/code/src/renderer/features/setup/hooks/useSetupRun.ts b/apps/code/src/renderer/features/setup/hooks/useSetupRun.ts new file mode 100644 index 000000000..b75ab2afa --- /dev/null +++ b/apps/code/src/renderer/features/setup/hooks/useSetupRun.ts @@ -0,0 +1,548 @@ +import { getAuthenticatedClient } from "@features/auth/hooks/authClient"; +import { fetchAuthState } from "@features/auth/hooks/authQueries"; +import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; +import { DISCOVERY_PROMPT, WIZARD_PROMPT } from "@features/setup/prompts"; +import { useSetupStore } from "@features/setup/stores/setupStore"; +import type { DiscoveredTask } from "@features/setup/types"; +import { TASK_DISCOVERY_JSON_SCHEMA } from "@features/setup/types"; +import type { PostHogAPIClient } from "@renderer/api/posthogClient"; +import { + type TaskCreationInput, + TaskCreationSaga, +} from "@renderer/sagas/task/task-creation"; +import { trpcClient } from "@renderer/trpc/client"; +import { isTerminalStatus, type Task } from "@shared/types"; +import { ANALYTICS_EVENTS } from "@shared/types/analytics"; +import { getCloudUrlFromRegion } from "@shared/utils/urls"; +import { captureException, track } from "@utils/analytics"; +import { logger } from "@utils/logger"; +import { queryClient } from "@utils/queryClient"; +import { useCallback, useEffect, useRef, useState } from "react"; + +const log = logger.scope("setup-run"); + +interface ActivityEntry { + id: number; + toolCallId: string; + tool: string; + filePath: string | null; + title: string; +} + +function handleSessionUpdate( + payload: unknown, + pushActivity: (entry: ActivityEntry) => void, +) { + const acpMsg = payload as { message?: Record }; + const inner = acpMsg.message; + if (!inner) return; + + if ("method" in inner && inner.method === "session/update") { + const params = inner.params as Record | undefined; + if (!params) return; + + const update = (params.update as Record) ?? params; + + const entry = extractToolCall(update); + if (entry) { + pushActivity(entry); + } + } +} + +let activityIdCounter = 0; + +function extractPathFromRawInput( + tool: string, + rawInput: Record | undefined, +): string | null { + if (!rawInput) return null; + + switch (tool) { + case "Read": + case "Edit": + case "Write": + return (rawInput.file_path as string) ?? null; + case "Grep": + return (rawInput.pattern as string) + ? `"${rawInput.pattern}"${rawInput.path ? ` in ${rawInput.path}` : ""}` + : ((rawInput.path as string) ?? null); + case "Glob": + return (rawInput.pattern as string) ?? null; + case "Bash": { + const cmd = rawInput.command as string | undefined; + if (!cmd) return null; + return cmd.length > 80 ? `${cmd.slice(0, 77)}...` : cmd; + } + default: { + const filePath = + rawInput.file_path ?? rawInput.path ?? rawInput.notebook_path; + if (typeof filePath === "string") return filePath; + const pattern = rawInput.pattern; + if (typeof pattern === "string") return `"${pattern}"`; + const command = rawInput.command; + if (typeof command === "string") + return command.length > 80 ? `${command.slice(0, 77)}...` : command; + const url = rawInput.url; + if (typeof url === "string") return url; + const query = rawInput.query; + if (typeof query === "string") return query; + return null; + } + } +} + +function extractToolCall( + update: Record, +): ActivityEntry | null { + const sessionUpdate = update.sessionUpdate as string | undefined; + if (sessionUpdate !== "tool_call" && sessionUpdate !== "tool_call_update") + return null; + + const meta = update._meta as + | { claudeCode?: { toolName?: string } } + | undefined; + const tool = meta?.claudeCode?.toolName ?? "Working"; + const locations = update.locations as + | { path?: string; line?: number }[] + | undefined; + const rawInput = (update.rawInput ?? update.input) as + | Record + | undefined; + const filePath = + locations?.[0]?.path ?? extractPathFromRawInput(tool, rawInput); + const title = (update.title as string) ?? ""; + const toolCallId = (update.toolCallId as string) ?? ""; + + activityIdCounter += 1; + return { id: activityIdCounter, toolCallId, tool, filePath, title }; +} + +const POSTHOG_PACKAGES = [ + "posthog-js", + "posthog-node", + "posthog-react-native", + "@posthog/react-native-session-replay", + "posthog-android", + "posthog-ios", + "posthog-flutter", +]; + +async function isPosthogInstalled(repoPath: string): Promise { + try { + const content = await trpcClient.fs.readRepoFile.query({ + repoPath, + filePath: "package.json", + }); + if (!content) return false; + const pkg = JSON.parse(content); + const allDeps = { + ...pkg.dependencies, + ...pkg.devDependencies, + }; + return POSTHOG_PACKAGES.some((name) => name in allDeps); + } catch { + return false; + } +} + +async function resolveWizardWorkspaceMode( + client: PostHogAPIClient, +): Promise<"cloud" | "worktree"> { + try { + const integrations = await client.getIntegrations(); + const hasGithub = (integrations as { kind: string }[]).some( + (i) => i.kind === "github", + ); + if (hasGithub) return "cloud"; + } catch (err) { + log.warn("Failed to check GitHub integration, falling back to worktree", { + error: err, + }); + } + return "worktree"; +} + +export function useSetupRun() { + const selectedDirectory = useOnboardingStore((s) => s.selectedDirectory); + const discoveryStatus = useSetupStore((s) => s.discoveryStatus); + const storedTasks = useSetupStore((s) => s.discoveredTasks); + const storedWizardTaskId = useSetupStore((s) => s.wizardTaskId); + const wizardSkipped = useSetupStore((s) => s.wizardSkipped); + const discoveryFeed = useSetupStore((s) => s.discoveryFeed); + const wizardFeed = useSetupStore((s) => s.wizardFeed); + + const [isDiscoveryDone, setIsDiscoveryDone] = useState( + discoveryStatus === "done", + ); + const [discoveredTasks, setDiscoveredTasks] = + useState(storedTasks); + const [isWizardStarted, setIsWizardStarted] = useState(!!storedWizardTaskId); + const [error, setError] = useState(null); + + const startedRef = useRef(false); + const discoveryStartedAtRef = useRef(null); + + const subscribeToWizardEvents = useCallback((taskId: string) => { + const checkForRun = async () => { + const client = await getAuthenticatedClient(); + if (!client) return; + + for (let i = 0; i < 30; i++) { + await new Promise((r) => setTimeout(r, 2000)); + try { + const taskData = (await client.getTask(taskId)) as unknown as Task; + const runId = taskData.latest_run?.id; + if (runId) { + log.debug("Wizard run found, subscribing", { taskId, runId }); + trpcClient.agent.onSessionEvent.subscribe( + { taskRunId: runId }, + { + onData: (payload: unknown) => { + handleSessionUpdate(payload, (entry) => { + useSetupStore.getState().pushWizardActivity(entry); + }); + }, + onError: (err) => { + log.error("Wizard subscription error", { error: err }); + }, + }, + ); + return; + } + } catch { + // keep polling + } + } + }; + checkForRun().catch((err) => + log.error("Wizard event subscribe failed", { error: err }), + ); + }, []); + + const startWizardTask = useCallback(async () => { + const existingId = useSetupStore.getState().wizardTaskId; + if (existingId) { + log.debug("Wizard task already exists, skipping", { + wizardTaskId: existingId, + }); + setIsWizardStarted(true); + return; + } + + log.debug("Starting wizard task"); + try { + const client = await getAuthenticatedClient(); + if (!client) { + log.error("getAuthenticatedClient returned null for wizard"); + track(ANALYTICS_EVENTS.SETUP_WIZARD_FAILED, { + reason: "unauthenticated_client", + }); + return; + } + + const repoPath = selectedDirectory; + if (!repoPath) { + log.warn("No selectedDirectory for wizard task"); + track(ANALYTICS_EVENTS.SETUP_WIZARD_FAILED, { + reason: "missing_directory", + }); + return; + } + + if (await isPosthogInstalled(repoPath)) { + log.info("PostHog already installed, skipping wizard"); + useSetupStore.getState().skipWizard(); + track(ANALYTICS_EVENTS.SETUP_WIZARD_FAILED, { + reason: "already_installed", + }); + return; + } + + const workspaceMode = await resolveWizardWorkspaceMode(client); + log.info("Wizard workspace mode resolved", { workspaceMode }); + + const sagaInput: TaskCreationInput = { + taskDescription: WIZARD_PROMPT, + content: WIZARD_PROMPT, + repoPath, + workspaceMode, + executionMode: "auto", + }; + + const saga = new TaskCreationSaga({ + posthogClient: client, + onTaskReady: ({ task }) => { + useSetupStore.getState().setWizardTaskId(task.id); + setIsWizardStarted(true); + track(ANALYTICS_EVENTS.SETUP_WIZARD_STARTED, { + wizard_task_id: task.id, + workspace_mode: workspaceMode, + }); + queryClient.invalidateQueries({ queryKey: ["tasks", "list"] }); + subscribeToWizardEvents(task.id); + }, + }); + + const result = await saga.run(sagaInput); + + if (!result.success) { + throw new Error( + `Wizard saga failed at step: ${result.failedStep ?? "unknown"}`, + ); + } + + const task = result.data.task; + useSetupStore.getState().setWizardTaskId(task.id); + setIsWizardStarted(true); + queryClient.invalidateQueries({ queryKey: ["tasks", "list"] }); + subscribeToWizardEvents(task.id); + } catch (err) { + log.error("Failed to start wizard task", { error: err }); + const message = + err instanceof Error ? err.message : "Failed to start wizard task."; + track(ANALYTICS_EVENTS.SETUP_WIZARD_FAILED, { + reason: "startup_error", + error_message: message, + }); + if (err instanceof Error) { + captureException(err, { scope: "setup.start_wizard_task" }); + } + } + }, [selectedDirectory, subscribeToWizardEvents]); + + const startDiscovery = useCallback(async () => { + const state = useSetupStore.getState(); + if ( + state.discoveryStatus === "done" || + state.discoveryStatus === "running" + ) { + return; + } + + try { + const authState = await fetchAuthState(); + const apiHost = authState.cloudRegion + ? getCloudUrlFromRegion(authState.cloudRegion) + : null; + const projectId = authState.projectId; + + if (!apiHost || !projectId) { + log.error("Missing auth for discovery", { apiHost, projectId }); + setError("Authentication required."); + useSetupStore.getState().failDiscovery(); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED, { + reason: "startup_error", + error_message: "missing_auth", + }); + return; + } + + const client = await getAuthenticatedClient(); + if (!client) { + setError("Authentication required."); + useSetupStore.getState().failDiscovery(); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED, { + reason: "startup_error", + error_message: "unauthenticated_client", + }); + return; + } + + const repoPath = selectedDirectory; + if (!repoPath) { + setError("No directory selected."); + useSetupStore.getState().failDiscovery(); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED, { + reason: "startup_error", + error_message: "missing_directory", + }); + return; + } + + const task = (await client.createTask({ + title: "Discover first tasks", + description: DISCOVERY_PROMPT, + json_schema: TASK_DISCOVERY_JSON_SCHEMA as Record, + })) as unknown as Task; + + const taskRun = await client.createTaskRun(task.id); + if (!taskRun?.id) { + throw new Error("Failed to create discovery task run"); + } + + useSetupStore.getState().startDiscovery(task.id, taskRun.id); + discoveryStartedAtRef.current = Date.now(); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_STARTED, { + discovery_task_id: task.id, + discovery_task_run_id: taskRun.id, + }); + + await trpcClient.agent.start.mutate({ + taskId: task.id, + taskRunId: taskRun.id, + repoPath, + apiHost, + projectId, + permissionMode: "bypassPermissions", + jsonSchema: TASK_DISCOVERY_JSON_SCHEMA as Record, + }); + + trpcClient.agent.prompt + .mutate({ + sessionId: taskRun.id, + prompt: [{ type: "text", text: DISCOVERY_PROMPT }], + }) + .catch((err) => { + log.error("Failed to send discovery prompt", { error: err }); + }); + + const subscription = trpcClient.agent.onSessionEvent.subscribe( + { taskRunId: taskRun.id }, + { + onData: (payload: unknown) => { + handleSessionUpdate(payload, (entry) => { + useSetupStore.getState().pushDiscoveryActivity(entry); + }); + }, + onError: (err) => { + log.error("Discovery subscription error", { error: err }); + }, + }, + ); + + const pollForCompletion = async () => { + const maxAttempts = 120; + const intervalMs = 5000; + + for (let i = 0; i < maxAttempts; i++) { + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + + try { + const run = await client.getTaskRun(task.id, taskRun.id); + + if (isTerminalStatus(run.status)) { + subscription.unsubscribe(); + + const startedAt = discoveryStartedAtRef.current; + const durationSeconds = startedAt + ? Math.round((Date.now() - startedAt) / 1000) + : 0; + + if (run.status === "completed" && run.output) { + const output = run.output as { tasks?: DiscoveredTask[] }; + const tasks = output.tasks ?? []; + log.info("Discovery completed", { taskCount: tasks.length }); + useSetupStore.getState().completeDiscovery(tasks); + setDiscoveredTasks(tasks); + setIsDiscoveryDone(true); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_COMPLETED, { + discovery_task_id: task.id, + discovery_task_run_id: taskRun.id, + task_count: tasks.length, + duration_seconds: durationSeconds, + }); + } else if ( + run.status === "failed" || + run.status === "cancelled" + ) { + log.error("Discovery failed", { status: run.status }); + useSetupStore.getState().failDiscovery(); + setError("Discovery failed. You can skip or retry."); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED, { + discovery_task_id: task.id, + discovery_task_run_id: taskRun.id, + reason: run.status, + }); + } else { + useSetupStore.getState().completeDiscovery([]); + setDiscoveredTasks([]); + setIsDiscoveryDone(true); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_COMPLETED, { + discovery_task_id: task.id, + discovery_task_run_id: taskRun.id, + task_count: 0, + duration_seconds: durationSeconds, + }); + } + return; + } + } catch (err) { + log.warn("Failed to poll discovery", { + attempt: i + 1, + error: err, + }); + } + } + + subscription.unsubscribe(); + useSetupStore.getState().failDiscovery(); + setError("Discovery timed out. You can skip or retry."); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED, { + discovery_task_id: task.id, + discovery_task_run_id: taskRun.id, + reason: "timeout", + }); + }; + + pollForCompletion().catch((err) => { + log.error("Discovery poll failed", { error: err }); + useSetupStore.getState().failDiscovery(); + setError("Discovery failed unexpectedly."); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED, { + discovery_task_id: task.id, + discovery_task_run_id: taskRun.id, + reason: "failed", + error_message: + err instanceof Error ? err.message : "discovery_poll_error", + }); + if (err instanceof Error) { + captureException(err, { scope: "setup.discovery_poll" }); + } + }); + } catch (err) { + log.error("Failed to start discovery", { error: err }); + useSetupStore.getState().failDiscovery(); + const message = + err instanceof Error ? err.message : "Failed to start discovery."; + setError(message); + track(ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED, { + reason: "startup_error", + error_message: message, + }); + if (err instanceof Error) { + captureException(err, { scope: "setup.start_discovery" }); + } + } + }, [selectedDirectory]); + + useEffect(() => { + if (startedRef.current) return; + startedRef.current = true; + + if (discoveryStatus === "done") { + setDiscoveredTasks(storedTasks); + setIsDiscoveryDone(true); + return; + } + + startWizardTask().catch((err) => { + log.error("Wizard task startup failed", { error: err }); + }); + + startDiscovery().catch((err) => { + log.error("Discovery startup failed", { error: err }); + }); + }, [discoveryStatus, storedTasks, startWizardTask, startDiscovery]); + + return { + discoveryFeed, + wizardFeed, + isDiscoveryDone, + isWizardStarted, + wizardSkipped, + discoveredTasks, + wizardTaskId: storedWizardTaskId, + error, + }; +} diff --git a/apps/code/src/renderer/features/setup/prompts.ts b/apps/code/src/renderer/features/setup/prompts.ts new file mode 100644 index 000000000..246e6186b --- /dev/null +++ b/apps/code/src/renderer/features/setup/prompts.ts @@ -0,0 +1,53 @@ +export const WIZARD_PROMPT = `You are a PostHog integration wizard. Your job is to set up PostHog in this repository by detecting the framework, installing the SDK and instrumenting the app. + +Follow these steps IN ORDER: + +STEP 1 — Detect the framework. +Examine the project to determine which framework is in use (Next.js, React, Vue, Svelte, Django, Flask, FastAPI, Rails, React Native, Angular, Astro, Laravel, Swift, Android, etc.). Check package.json, requirements.txt, build configs and directory structure. If you cannot detect a framework, pick the best language-level integration (JavaScript/Node, Python, Ruby). + +STEP 2 — Find and use the integration skill. +You have bundled PostHog skills available. Use the "instrument-integration" skill to integrate PostHog into this project. Read its SKILL.md and follow its workflow files in numbered order (e.g. 1.0-*, 1.1-*, 1.2-*). Each workflow file tells you what to do and which file comes next. + +STEP 3 — Set up environment variables. +Never hardcode PostHog API keys or tokens directly in source code. Create or update the appropriate .env file (.env.local, .env, etc.) with the PostHog public token and host using the environment variable naming convention for this framework. Reference these variables in code. + +STEP 4 — Install packages. +Use the project's package manager (npm, pnpm, yarn, bun, pip, poetry, etc.) to install the PostHog SDK. Start the install as a background task and continue with other work — do not block waiting for it. + +STEP 5 — Create a pull request. +Commit all changes with clear atomic commit messages. Then create a pull request with a descriptive title (e.g. "Add PostHog analytics integration") and a body summarizing what was instrumented. + +Rules: +- Before writing any file you MUST read it immediately beforehand, even if you read it earlier. +- Do not ask the user questions. Run autonomously with sensible defaults. +- Prefer minimal targeted edits. Do not refactor unrelated code. +- Focus on: product analytics, error tracking and session replay. +- Do not spawn subagents.`; + +export const DISCOVERY_PROMPT = `You are analyzing this codebase to find the highest-value first tasks for the developer. + +Scan the codebase for issues in two tiers. Tier 1 applies to every repo. Tier 2 only applies when PostHog is already installed (look for posthog-js, posthog-node, posthog-react-native or similar PostHog SDK imports). + +## Tier 1 -- Code health (always) + +- **Dead code**: Unused exports, unreachable branches, orphaned files, stale imports. Category: dead_code +- **Duplication / KISS violations**: Copy-pasted logic that should be a shared function, over-abstracted code that could be simpler. Category: duplication +- **Security vulnerabilities**: XSS, SQL injection, command injection, hardcoded secrets, open redirects, missing auth checks, insecure deserialization. Category: security +- **Bugs**: Null dereferences, race conditions, unchecked array access, off-by-one errors, unhandled promise rejections around I/O. Category: bug +- **Performance anti-patterns**: N+1 queries, unbounded loops, synchronous blocking on hot paths, missing pagination. Category: performance + +## Tier 2 -- PostHog-specific (only when PostHog SDK is detected) + +- **Stale feature flags**: Flags that are always evaluated the same way, flags referenced in code but never toggled, flags guarding code that shipped long ago. Category: stale_feature_flag +- **Error tracking gaps**: Catch blocks that swallow errors without reporting, missing error boundaries, untracked 5xx responses. Category: error_tracking +- **Event tracking improvements**: Key user actions (signup, purchase, invite, upgrade) with no analytics event, events missing useful properties (plan, user role, page context). Category: event_tracking +- **Funnel weak spots**: Multi-step flows (onboarding, checkout, activation) where intermediate steps have no tracking, making drop-off invisible. Category: funnel + +## Rules + +- Be concrete: reference exact file paths, function names and line numbers. +- Prioritize by impact. Lead with the findings that would save the most time or prevent the most damage. +- Do NOT suggest documentation, comment or style/formatting changes. +- Maximum 4 tasks. Quality over quantity. + +When you are done analyzing, call create_output with your findings.`; diff --git a/apps/code/src/renderer/features/setup/stores/setupStore.ts b/apps/code/src/renderer/features/setup/stores/setupStore.ts new file mode 100644 index 000000000..994dbc9a2 --- /dev/null +++ b/apps/code/src/renderer/features/setup/stores/setupStore.ts @@ -0,0 +1,149 @@ +import type { DiscoveredTask } from "@features/setup/types"; +import { logger } from "@utils/logger"; +import { create } from "zustand"; + +const log = logger.scope("setup-store"); + +type DiscoveryStatus = "idle" | "running" | "done" | "error"; + +interface ActivityEntry { + id: number; + toolCallId: string; + tool: string; + filePath: string | null; + title: string; +} + +export interface AgentFeedState { + currentTool: string | null; + currentFilePath: string | null; + recentEntries: ActivityEntry[]; +} + +const EMPTY_FEED: AgentFeedState = { + currentTool: null, + currentFilePath: null, + recentEntries: [], +}; + +interface SetupStoreState { + discoveredTasks: DiscoveredTask[]; + discoveryStatus: DiscoveryStatus; + discoveryTaskId: string | null; + discoveryTaskRunId: string | null; + wizardTaskId: string | null; + wizardSkipped: boolean; + discoveryFeed: AgentFeedState; + wizardFeed: AgentFeedState; +} + +interface SetupStoreActions { + startDiscovery: (taskId: string, taskRunId: string) => void; + completeDiscovery: (tasks: DiscoveredTask[]) => void; + failDiscovery: () => void; + resetDiscovery: () => void; + setWizardTaskId: (taskId: string) => void; + skipWizard: () => void; + pushDiscoveryActivity: (entry: ActivityEntry) => void; + pushWizardActivity: (entry: ActivityEntry) => void; +} + +type SetupStore = SetupStoreState & SetupStoreActions; + +const initialState: SetupStoreState = { + discoveredTasks: [], + discoveryStatus: "idle", + discoveryTaskId: null, + discoveryTaskRunId: null, + wizardTaskId: null, + wizardSkipped: false, + discoveryFeed: EMPTY_FEED, + wizardFeed: EMPTY_FEED, +}; + +function pushEntry(prev: AgentFeedState, entry: ActivityEntry): AgentFeedState { + const existingIdx = entry.toolCallId + ? prev.recentEntries.findIndex((e) => e.toolCallId === entry.toolCallId) + : -1; + + let newEntries: ActivityEntry[]; + if (existingIdx >= 0) { + newEntries = [...prev.recentEntries]; + const old = newEntries[existingIdx]; + newEntries[existingIdx] = { + ...old, + tool: entry.tool || old.tool, + filePath: entry.filePath || old.filePath, + title: entry.title || old.title, + }; + } else { + newEntries = [...prev.recentEntries.slice(-4), entry]; + } + + return { + currentTool: entry.tool, + currentFilePath: entry.filePath ?? prev.currentFilePath, + recentEntries: newEntries, + }; +} + +export const useSetupStore = create()((set) => ({ + ...initialState, + + startDiscovery: (taskId, taskRunId) => { + log.info("Discovery started", { taskId, taskRunId }); + set({ + discoveryStatus: "running", + discoveryTaskId: taskId, + discoveryTaskRunId: taskRunId, + discoveredTasks: [], + discoveryFeed: EMPTY_FEED, + }); + }, + + completeDiscovery: (tasks) => { + log.info("Discovery completed", { taskCount: tasks.length }); + set({ + discoveryStatus: "done", + discoveredTasks: tasks, + }); + }, + + failDiscovery: () => { + log.warn("Discovery failed"); + set({ discoveryStatus: "error" }); + }, + + resetDiscovery: () => { + log.info("Discovery reset"); + set({ + discoveryStatus: "idle", + discoveryTaskId: null, + discoveryTaskRunId: null, + discoveredTasks: [], + discoveryFeed: EMPTY_FEED, + }); + }, + + setWizardTaskId: (taskId) => { + log.info("Wizard task created", { taskId }); + set({ wizardTaskId: taskId }); + }, + + skipWizard: () => { + log.info("Wizard skipped (PostHog already installed)"); + set({ wizardSkipped: true }); + }, + + pushDiscoveryActivity: (entry) => { + set((state) => ({ + discoveryFeed: pushEntry(state.discoveryFeed, entry), + })); + }, + + pushWizardActivity: (entry) => { + set((state) => ({ + wizardFeed: pushEntry(state.wizardFeed, entry), + })); + }, +})); diff --git a/apps/code/src/renderer/features/setup/types.ts b/apps/code/src/renderer/features/setup/types.ts new file mode 100644 index 000000000..deab6d1fb --- /dev/null +++ b/apps/code/src/renderer/features/setup/types.ts @@ -0,0 +1,66 @@ +export interface DiscoveredTask { + id: string; + title: string; + description: string; + category: + | "bug" + | "security" + | "dead_code" + | "duplication" + | "performance" + | "stale_feature_flag" + | "error_tracking" + | "event_tracking" + | "funnel"; + file?: string; + lineHint?: number; +} + +export const TASK_DISCOVERY_JSON_SCHEMA = { + type: "object", + properties: { + tasks: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string", description: "A short kebab-case identifier" }, + title: { + type: "string", + description: "One-line summary of the task", + }, + description: { + type: "string", + description: + "2-3 sentences explaining the problem and what to fix, including file path and line if known", + }, + category: { + type: "string", + enum: [ + "bug", + "security", + "dead_code", + "duplication", + "performance", + "stale_feature_flag", + "error_tracking", + "event_tracking", + "funnel", + ], + }, + file: { + type: "string", + description: "Relative file path where the issue lives", + }, + lineHint: { + type: "integer", + description: "Approximate line number", + }, + }, + required: ["id", "title", "description", "category"], + }, + maxItems: 4, + }, + }, + required: ["tasks"], +} as const; diff --git a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx index 47222ff98..f926d54c4 100644 --- a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx +++ b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx @@ -6,6 +6,7 @@ import { INBOX_PIPELINE_STATUS_FILTER, INBOX_REFETCH_INTERVAL_MS, } from "@features/inbox/utils/inboxConstants"; +import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore"; import { getSessionService } from "@features/sessions/service/service"; import { archiveTaskImperative, @@ -28,6 +29,7 @@ import { useSidebarData } from "../hooks/useSidebarData"; import { useTaskViewed } from "../hooks/useTaskViewed"; import { CommandCenterItem } from "./items/CommandCenterItem"; import { InboxItem, NewTaskItem } from "./items/HomeItem"; +import { SetupItem } from "./items/SetupItem"; import { SkillsItem } from "./items/SkillsItem"; import { SidebarItem } from "./SidebarItem"; import { TaskListView } from "./TaskListView"; @@ -40,6 +42,7 @@ function SidebarMenuComponent() { navigateToInbox, navigateToCommandCenter, navigateToSkills, + navigateToSetup, } = useNavigationStore(); const { data: allTasks = [] } = useTasks(); @@ -52,6 +55,10 @@ function SidebarMenuComponent() { const { archiveTask } = useArchiveTask(); const { togglePin } = usePinnedTasks(); + const hasCompletedSetup = useOnboardingStore( + (state) => state.hasCompletedSetup, + ); + const sidebarData = useSidebarData({ activeView: view, }); @@ -114,6 +121,10 @@ function SidebarMenuComponent() { navigateToSkills(); }; + const handleSetupClick = () => { + navigateToSetup(); + }; + const handleTaskClick = (taskId: string) => { const task = taskMap.get(taskId); if (task) { @@ -277,6 +288,15 @@ function SidebarMenuComponent() { /> + {!hasCompletedSetup && ( + + + + )} + void; +} + +export function SetupItem({ isActive, onClick }: SetupItemProps) { + return ( + + ); +} diff --git a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts index 3fca36207..16149a63f 100644 --- a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts +++ b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts @@ -40,6 +40,7 @@ export interface SidebarData { isInboxActive: boolean; isCommandCenterActive: boolean; isSkillsActive: boolean; + isSetupActive: boolean; isLoading: boolean; activeTaskId: string | null; pinnedTasks: TaskData[]; @@ -58,7 +59,8 @@ interface ViewState { | "inbox" | "archived" | "command-center" - | "skills"; + | "skills" + | "setup"; data?: Task; } @@ -113,6 +115,7 @@ export function useSidebarData({ const isInboxActive = activeView.type === "inbox"; const isCommandCenterActive = activeView.type === "command-center"; const isSkillsActive = activeView.type === "skills"; + const isSetupActive = activeView.type === "setup"; const activeTaskId = activeView.type === "task-detail" && activeView.data @@ -222,6 +225,7 @@ export function useSidebarData({ isInboxActive, isCommandCenterActive, isSkillsActive, + isSetupActive, isLoading, activeTaskId, pinnedTasks, diff --git a/apps/code/src/renderer/stores/navigationStore.ts b/apps/code/src/renderer/stores/navigationStore.ts index 32232fd4f..b935b3d5c 100644 --- a/apps/code/src/renderer/stores/navigationStore.ts +++ b/apps/code/src/renderer/stores/navigationStore.ts @@ -19,7 +19,8 @@ type ViewType = | "inbox" | "archived" | "command-center" - | "skills"; + | "skills" + | "setup"; export interface TaskInputReportAssociation { reportId: string; @@ -60,6 +61,7 @@ interface NavigationStore { navigateToArchived: () => void; navigateToCommandCenter: () => void; navigateToSkills: () => void; + navigateToSetup: () => void; goBack: () => void; goForward: () => void; canGoBack: () => boolean; @@ -93,6 +95,9 @@ const isSameView = (view1: ViewState, view2: ViewState): boolean => { if (view1.type === "skills" && view2.type === "skills") { return true; } + if (view1.type === "setup" && view2.type === "setup") { + return true; + } return false; }; @@ -271,6 +276,10 @@ export const useNavigationStore = create()( navigate({ type: "skills" }); }, + navigateToSetup: () => { + navigate({ type: "setup" }); + }, + goBack: () => { const { history, historyIndex } = get(); if (historyIndex > 0) { diff --git a/apps/code/src/shared/types/analytics.ts b/apps/code/src/shared/types/analytics.ts index 03740d8f8..7b80a7d55 100644 --- a/apps/code/src/shared/types/analytics.ts +++ b/apps/code/src/shared/types/analytics.ts @@ -124,6 +124,25 @@ export interface AgentFileActivityProperties { branch_name: string | null; } +// Branch link events +type BranchLinkSource = "agent" | "user" | "unknown"; + +export interface BranchLinkedProperties { + task_id: string; + branch_name: string; + source: BranchLinkSource; +} + +export interface BranchUnlinkedProperties { + task_id: string; + source: BranchLinkSource; +} + +export interface BranchLinkDefaultBranchUnknownProperties { + task_id: string; + branch_name: string; +} + // File interactions export interface FileOpenedProperties { file_extension: string; @@ -250,6 +269,67 @@ export interface TaskFeedbackProperties { feedback_comment?: string; } +// Setup / onboarding events +type SetupDiscoveredTaskCategory = + | "bug" + | "security" + | "dead_code" + | "duplication" + | "performance" + | "stale_feature_flag" + | "error_tracking" + | "event_tracking" + | "funnel"; + +export interface SetupViewedProperties { + discovery_status: "idle" | "running" | "done" | "error"; +} + +export interface SetupDiscoveryStartedProperties { + discovery_task_id: string; + discovery_task_run_id: string; +} + +export interface SetupDiscoveryCompletedProperties { + discovery_task_id: string; + discovery_task_run_id: string; + task_count: number; + duration_seconds: number; +} + +export interface SetupDiscoveryFailedProperties { + discovery_task_id?: string; + discovery_task_run_id?: string; + reason: "failed" | "cancelled" | "timeout" | "startup_error"; + error_message?: string; +} + +export interface SetupTaskSelectedProperties { + discovered_task_id: string; + category: SetupDiscoveredTaskCategory; + position: number; + total_discovered: number; +} + +export interface SetupSkippedProperties { + discovery_status: "idle" | "running" | "done" | "error"; + had_discovered_tasks: boolean; +} + +export interface SetupWizardStartedProperties { + wizard_task_id: string; + workspace_mode?: string; +} + +export interface SetupWizardFailedProperties { + reason: + | "unauthenticated_client" + | "missing_directory" + | "startup_error" + | "already_installed"; + error_message?: string; +} + // Event names as constants export const ANALYTICS_EVENTS = { // App lifecycle @@ -277,6 +357,9 @@ export const ANALYTICS_EVENTS = { GIT_ACTION_EXECUTED: "Git action executed", PR_CREATED: "PR created", AGENT_FILE_ACTIVITY: "Agent file activity", + BRANCH_LINKED: "Branch linked", + BRANCH_UNLINKED: "Branch unlinked", + BRANCH_LINK_DEFAULT_BRANCH_UNKNOWN: "Branch link default branch unknown", // File interactions FILE_OPENED: "File opened", @@ -316,6 +399,16 @@ export const ANALYTICS_EVENTS = { // Tour events TOUR_EVENT: "Tour event", + // Setup / onboarding events + SETUP_VIEWED: "Setup viewed", + SETUP_DISCOVERY_STARTED: "Setup discovery started", + SETUP_DISCOVERY_COMPLETED: "Setup discovery completed", + SETUP_DISCOVERY_FAILED: "Setup discovery failed", + SETUP_TASK_SELECTED: "Setup task selected", + SETUP_SKIPPED: "Setup skipped", + SETUP_WIZARD_STARTED: "Setup wizard started", + SETUP_WIZARD_FAILED: "Setup wizard failed", + // Error events TASK_CREATION_FAILED: "Task creation failed", AGENT_SESSION_ERROR: "Agent session error", @@ -341,6 +434,9 @@ export type EventPropertyMap = { [ANALYTICS_EVENTS.GIT_ACTION_EXECUTED]: GitActionExecutedProperties; [ANALYTICS_EVENTS.PR_CREATED]: PrCreatedProperties; [ANALYTICS_EVENTS.AGENT_FILE_ACTIVITY]: AgentFileActivityProperties; + [ANALYTICS_EVENTS.BRANCH_LINKED]: BranchLinkedProperties; + [ANALYTICS_EVENTS.BRANCH_UNLINKED]: BranchUnlinkedProperties; + [ANALYTICS_EVENTS.BRANCH_LINK_DEFAULT_BRANCH_UNKNOWN]: BranchLinkDefaultBranchUnknownProperties; // File interactions [ANALYTICS_EVENTS.FILE_OPENED]: FileOpenedProperties; @@ -380,6 +476,16 @@ export type EventPropertyMap = { // Tour events [ANALYTICS_EVENTS.TOUR_EVENT]: TourEventProperties; + // Setup / onboarding events + [ANALYTICS_EVENTS.SETUP_VIEWED]: SetupViewedProperties; + [ANALYTICS_EVENTS.SETUP_DISCOVERY_STARTED]: SetupDiscoveryStartedProperties; + [ANALYTICS_EVENTS.SETUP_DISCOVERY_COMPLETED]: SetupDiscoveryCompletedProperties; + [ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED]: SetupDiscoveryFailedProperties; + [ANALYTICS_EVENTS.SETUP_TASK_SELECTED]: SetupTaskSelectedProperties; + [ANALYTICS_EVENTS.SETUP_SKIPPED]: SetupSkippedProperties; + [ANALYTICS_EVENTS.SETUP_WIZARD_STARTED]: SetupWizardStartedProperties; + [ANALYTICS_EVENTS.SETUP_WIZARD_FAILED]: SetupWizardFailedProperties; + // Error events [ANALYTICS_EVENTS.TASK_CREATION_FAILED]: TaskCreationFailedProperties; [ANALYTICS_EVENTS.AGENT_SESSION_ERROR]: AgentSessionErrorProperties; diff --git a/packages/agent/src/adapters/claude/session/options.ts b/packages/agent/src/adapters/claude/session/options.ts index c8686edf2..7aadf0107 100644 --- a/packages/agent/src/adapters/claude/session/options.ts +++ b/packages/agent/src/adapters/claude/session/options.ts @@ -154,7 +154,7 @@ function buildHooks( const PH_EXPLORE_AGENT: NonNullable[string] = { description: 'Fast agent for exploring and understanding codebases. Use this when you need to find files by pattern (eg. "src/components/**/*.tsx"), search for code or keywords (eg. "where is the auth middleware?"), or answer questions about how the codebase works (eg. "how does the session service handle reconnects?"). When calling this agent, specify a thoroughness level: "quick" for targeted lookups, "medium" for broader exploration, or "very thorough" for comprehensive analysis across multiple locations.', - model: "haiku", + model: "sonnet", prompt: `You are a fast, read-only codebase exploration agent. Your job is to find files, search code, read the most relevant sources, and report findings clearly.