diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx
index b0867db06..8110437c1 100644
--- a/apps/code/src/renderer/components/MainLayout.tsx
+++ b/apps/code/src/renderer/components/MainLayout.tsx
@@ -14,7 +14,7 @@ import { McpServersView } from "@features/mcp-servers/components/McpServersView"
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 { useSetupDiscovery } from "@features/setup/hooks/useSetupDiscovery";
import { MainSidebar } from "@features/sidebar/components/MainSidebar";
import { useSidebarData } from "@features/sidebar/hooks/useSidebarData";
import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder";
@@ -87,6 +87,7 @@ export function MainLayout() {
useIntegrations();
useTaskDeepLink();
useInboxDeepLink();
+ useSetupDiscovery();
useEffect(() => {
if (tasks) {
@@ -194,7 +195,6 @@ export function MainLayout() {
{view.type === "skills" && }
{view.type === "mcp-servers" && }
- {view.type === "setup" && }
diff --git a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx
index 8c7fb2ca9..5664fb2bb 100644
--- a/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx
+++ b/apps/code/src/renderer/features/onboarding/components/OnboardingFlow.tsx
@@ -40,12 +40,7 @@ export function OnboardingFlow() {
const completeOnboarding = useOnboardingStore(
(state) => state.completeOnboarding,
);
- const completeSetup = useOnboardingStore((state) => state.completeSetup);
- const hasCompletedSetup = useOnboardingStore(
- (state) => state.hasCompletedSetup,
- );
const resetOnboarding = useOnboardingStore((state) => state.resetOnboarding);
- const navigateToSetup = useNavigationStore((state) => state.navigateToSetup);
const navigateToTaskInput = useNavigationStore(
(state) => state.navigateToTaskInput,
);
@@ -60,14 +55,11 @@ export function OnboardingFlow() {
const handleComplete = () => {
completeOnboarding();
- if (!hasCompletedSetup) {
- navigateToSetup();
- }
+ navigateToTaskInput();
};
const handleSkip = () => {
completeOnboarding();
- completeSetup();
navigateToTaskInput();
};
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
deleted file mode 100644
index 11a82ff6c..000000000
--- a/apps/code/src/renderer/features/onboarding/components/context-collection/SuggestedTasks.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import type { DiscoveredTask } from "@features/setup/types";
-import {
- CATEGORY_CONFIG,
- FALLBACK_CATEGORY_CONFIG,
-} from "@features/setup/utils/categoryConfig";
-import { ArrowRight } from "@phosphor-icons/react";
-import { Flex, Text } from "@radix-ui/themes";
-import { motion } from "framer-motion";
-
-type Variant = "default" | "compact";
-
-interface SuggestedTasksProps {
- tasks: DiscoveredTask[];
- onSelectTask: (task: DiscoveredTask) => void;
- variant?: Variant;
- /** When set, uses CSS grid with the given column class instead of a vertical stack. */
- layoutClassName?: string;
-}
-
-export function SuggestedTasks({
- tasks,
- onSelectTask,
- variant = "default",
- layoutClassName,
-}: SuggestedTasksProps) {
- if (tasks.length === 0) {
- return (
-
- No issues found. Your codebase looks clean!
-
- );
- }
-
- const containerClass = layoutClassName ?? "flex w-full flex-col gap-3";
-
- return (
-
- {tasks.map((task, index) => (
-
- ))}
-
- );
-}
-
-interface SuggestedTaskCardProps {
- task: DiscoveredTask;
- index: number;
- variant: Variant;
- onSelect: (task: DiscoveredTask) => void;
-}
-
-function SuggestedTaskCard({
- task,
- index,
- variant,
- onSelect,
-}: SuggestedTaskCardProps) {
- const config = CATEGORY_CONFIG[task.category] ?? FALLBACK_CATEGORY_CONFIG;
- const TaskIcon = config.icon;
- const isCompact = variant === "compact";
- const iconSize = isCompact ? 14 : 18;
- const titleSize = isCompact ? "1" : "2";
-
- 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] ${
- isCompact ? "gap-2.5 px-2.5 py-2" : "gap-3.5 px-[18px] py-4"
- }`}
- style={{
- boxShadow: "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)",
- }}
- 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 34af17e24..1835733ac 100644
--- a/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts
+++ b/apps/code/src/renderer/features/onboarding/stores/onboardingStore.ts
@@ -8,7 +8,6 @@ const log = logger.scope("onboarding-store");
interface OnboardingStoreState {
currentStep: OnboardingStep;
hasCompletedOnboarding: boolean;
- hasCompletedSetup: boolean;
selectedProjectId: number | null;
selectedDirectory: string;
}
@@ -16,7 +15,6 @@ interface OnboardingStoreState {
interface OnboardingStoreActions {
setCurrentStep: (step: OnboardingStep) => void;
completeOnboarding: () => void;
- completeSetup: () => void;
resetOnboarding: () => void;
resetSelections: () => void;
selectProjectId: (projectId: number | null) => void;
@@ -28,7 +26,6 @@ type OnboardingStore = OnboardingStoreState & OnboardingStoreActions;
const initialState: OnboardingStoreState = {
currentStep: "welcome",
hasCompletedOnboarding: false,
- hasCompletedSetup: false,
selectedProjectId: null,
selectedDirectory: "",
};
@@ -43,7 +40,6 @@ export const useOnboardingStore = create()(
log.info("completeOnboarding");
set({ hasCompletedOnboarding: true });
},
- completeSetup: () => set({ hasCompletedSetup: true }),
resetOnboarding: () => set({ ...initialState }),
resetSelections: () =>
set({
@@ -58,7 +54,6 @@ 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/DiscoveredTaskDetailPane.tsx b/apps/code/src/renderer/features/setup/components/DiscoveredTaskDetailPane.tsx
index a4efd6abd..5c80b0c8e 100644
--- a/apps/code/src/renderer/features/setup/components/DiscoveredTaskDetailPane.tsx
+++ b/apps/code/src/renderer/features/setup/components/DiscoveredTaskDetailPane.tsx
@@ -30,7 +30,6 @@ export function DiscoveredTaskDetailPane({
const tasks = useSetupStore((s) => s.discoveredTasks);
const selectedDirectory = useOnboardingStore((s) => s.selectedDirectory);
- const completeSetup = useOnboardingStore((s) => s.completeSetup);
const navigateToTaskInput = useNavigationStore((s) => s.navigateToTaskInput);
const { folders } = useFolders();
const detectedCloudRepository = useDetectedCloudRepository(selectedDirectory);
@@ -46,7 +45,6 @@ export function DiscoveredTaskDetailPane({
const initialPrompt = buildDiscoveredTaskPrompt(task);
const folderId = folders.find((f) => f.path === selectedDirectory)?.id;
- completeSetup();
useSetupStore.getState().removeDiscoveredTask(task.id);
navigateToTaskInput({
initialPrompt,
diff --git a/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx b/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx
index 32a7b62da..1310ed491 100644
--- a/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx
+++ b/apps/code/src/renderer/features/setup/components/SetupScanFeed.tsx
@@ -33,6 +33,7 @@ interface SetupScanFeedProps {
recentEntries: ActivityEntry[];
isDone: boolean;
doneLabel?: string;
+ maxEntries?: number;
}
const TOOL_VERBS: Record = {
@@ -129,6 +130,7 @@ export function SetupScanFeed({
recentEntries,
isDone,
doneLabel = "Complete",
+ maxEntries = 4,
}: SetupScanFeedProps) {
const activeLabel =
activeLabelOverride ??
@@ -210,63 +212,67 @@ export function SetupScanFeed({
- {!isDone && recentEntries.length > 0 && (
-
-
+ {!isDone && recentEntries.length > 0 && maxEntries > 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}
-
-
-
- );
- })}
-
-
-
- )}
+
+
+ {recentEntries.slice(-maxEntries).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
deleted file mode 100644
index 78a5134bb..000000000
--- a/apps/code/src/renderer/features/setup/components/SetupView.tsx
+++ /dev/null
@@ -1,281 +0,0 @@
-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 { buildDiscoveredTaskPrompt } from "@features/setup/utils/buildDiscoveredTaskPrompt";
-import { useSetHeaderContent } from "@hooks/useSetHeaderContent";
-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 { useEffect, useMemo } from "react";
-
-export function SetupView() {
- 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(
-
-
-
- {isDiscoveryDone ? "Tasks ready" : "Finish setup"}
-
- ,
- );
-
- useEffect(() => {
- 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,
- });
-
- const initialPrompt = buildDiscoveredTaskPrompt(task);
- completeSetup();
- useSetupStore.getState().removeDiscoveredTask(task.id);
- navigateToTaskInput({ initialPrompt });
- };
-
- const handleStartFromScratch = () => {
- track(ANALYTICS_EVENTS.SETUP_SKIPPED, {
- discovery_status: useSetupStore.getState().discoveryStatus,
- had_discovered_tasks: discoveredTasks.length > 0,
- entry_point: isDiscoveryDone ? "after_done" : "during_scan",
- });
- if (isDiscoveryDone) {
- useSetupStore.getState().resetDiscovery();
- }
- completeSetup();
- navigateToTaskInput();
- };
-
- return (
-
-
-
-
-
-
- Set up your first task
-
-
- Pick something to work on, or describe your own.
-
-
-
-
- {showQuickWins && (
-
- )}
-
-
-
-
-
-
- {!isDiscoveryDone && (
-
- Suggested tasks will appear in the sidebar as they're ready.
-
- )}
-
-
-
-
- );
-}
-
-interface QuickWinsColumnProps {
- tasks: DiscoveredTask[];
- isDone: boolean;
- onSelectTask: (task: DiscoveredTask) => void;
-}
-
-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/useSetupDiscovery.ts
similarity index 57%
rename from apps/code/src/renderer/features/setup/hooks/useSetupRun.ts
rename to apps/code/src/renderer/features/setup/hooks/useSetupDiscovery.ts
index fcc2b692f..a69530e1c 100644
--- a/apps/code/src/renderer/features/setup/hooks/useSetupRun.ts
+++ b/apps/code/src/renderer/features/setup/hooks/useSetupDiscovery.ts
@@ -5,32 +5,20 @@ import { get } from "@renderer/di/container";
import { RENDERER_TOKENS } from "@renderer/di/tokens";
import { useEffect, useRef } from "react";
-export function useSetupRun() {
+export function useSetupDiscovery() {
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);
const startedRef = useRef(false);
useEffect(() => {
if (startedRef.current) return;
- startedRef.current = true;
-
if (discoveryStatus === "done") return;
if (!selectedDirectory) return;
- const service = get(RENDERER_TOKENS.SetupRunService);
- service.startSetup(selectedDirectory);
+ startedRef.current = true;
+ get(RENDERER_TOKENS.SetupRunService).startSetup(
+ selectedDirectory,
+ );
}, [discoveryStatus, selectedDirectory]);
-
- return {
- discoveryFeed,
- isDiscoveryDone: discoveryStatus === "done",
- isEnricherRunning: enricherStatus === "running",
- discoveredTasks,
- error,
- };
}
diff --git a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx
index eb11dbdf4..7d9b26d49 100644
--- a/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx
+++ b/apps/code/src/renderer/features/sidebar/components/SidebarMenu.tsx
@@ -6,9 +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 { useSetupStore } from "@features/setup/stores/setupStore";
import {
archiveTaskImperative,
useArchiveTask,
@@ -35,7 +33,6 @@ import { CommandCenterItem } from "./items/CommandCenterItem";
import { InboxItem, NewTaskItem } from "./items/HomeItem";
import { McpServersItem } from "./items/McpServersItem";
import { SearchItem } from "./items/SearchItem";
-import { SetupItem } from "./items/SetupItem";
import { SkillsItem } from "./items/SkillsItem";
import { SidebarItem } from "./SidebarItem";
import { TaskListView } from "./TaskListView";
@@ -49,7 +46,6 @@ function SidebarMenuComponent() {
navigateToCommandCenter,
navigateToSkills,
navigateToMcpServers,
- navigateToSetup,
} = useNavigationStore();
// Must mirror useSidebarData's filters so taskMap covers every rendered
@@ -66,17 +62,6 @@ function SidebarMenuComponent() {
const { archiveTask } = useArchiveTask();
const { togglePin } = usePinnedTasks();
- const hasCompletedSetup = useOnboardingStore(
- (state) => state.hasCompletedSetup,
- );
- const showSetupItem = useSetupStore((s) => {
- if (!hasCompletedSetup) return true;
- if (s.discoveryStatus === "running") return true;
- if (s.discoveryStatus === "done" && s.discoveredTasks.length > 0)
- return true;
- return false;
- });
-
const sidebarData = useSidebarData({
activeView: view,
});
@@ -143,10 +128,6 @@ function SidebarMenuComponent() {
navigateToMcpServers();
};
- const handleSetupClick = () => {
- navigateToSetup();
- };
-
const openCommandMenu = useCommandMenuStore((s) => s.open);
const handleSearchClick = () => {
openCommandMenu();
@@ -323,15 +304,6 @@ function SidebarMenuComponent() {
/>
- {showSetupItem && (
-
-
-
- )}
-
diff --git a/apps/code/src/renderer/features/sidebar/components/items/SetupItem.tsx b/apps/code/src/renderer/features/sidebar/components/items/SetupItem.tsx
deleted file mode 100644
index 841ec0aa2..000000000
--- a/apps/code/src/renderer/features/sidebar/components/items/SetupItem.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { useSetupStore } from "@features/setup/stores/setupStore";
-import { Rocket } from "@phosphor-icons/react";
-
-interface SetupItemProps {
- isActive: boolean;
- onClick: () => void;
-}
-
-type ItemState = "running" | "ready" | "finish";
-
-function selectItemState(
- status: "idle" | "running" | "done" | "error",
- taskCount: number,
-): ItemState {
- if (status === "running") return "running";
- if (status === "done" && taskCount > 0) return "ready";
- return "finish";
-}
-
-const LABELS: Record = {
- running: "Scanning your code",
- ready: "Tasks ready",
- finish: "Finish setup",
-};
-
-export function SetupItem({ isActive, onClick }: SetupItemProps) {
- const state = useSetupStore((s) =>
- selectItemState(s.discoveryStatus, s.discoveredTasks.length),
- );
-
- return (
-
- );
-}
diff --git a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts
index c0f166b02..06e8d4892 100644
--- a/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts
+++ b/apps/code/src/renderer/features/sidebar/hooks/useSidebarData.ts
@@ -47,7 +47,6 @@ export interface SidebarData {
isCommandCenterActive: boolean;
isSkillsActive: boolean;
isMcpServersActive: boolean;
- isSetupActive: boolean;
isLoading: boolean;
activeTaskId: string | null;
pinnedTasks: TaskData[];
@@ -188,7 +187,6 @@ export function useSidebarData({
const isCommandCenterActive = activeView.type === "command-center";
const isSkillsActive = activeView.type === "skills";
const isMcpServersActive = activeView.type === "mcp-servers";
- const isSetupActive = activeView.type === "setup";
const activeTaskId =
activeView.type === "task-detail" && activeView.data
@@ -309,7 +307,6 @@ export function useSidebarData({
isCommandCenterActive,
isSkillsActive,
isMcpServersActive,
- isSetupActive,
isLoading,
activeTaskId,
pinnedTasks,
diff --git a/apps/code/src/renderer/features/task-detail/components/SuggestedTaskCard.tsx b/apps/code/src/renderer/features/task-detail/components/SuggestedTaskCard.tsx
new file mode 100644
index 000000000..81cf2947e
--- /dev/null
+++ b/apps/code/src/renderer/features/task-detail/components/SuggestedTaskCard.tsx
@@ -0,0 +1,119 @@
+import type { DiscoveredTask } from "@features/setup/types";
+import {
+ CATEGORY_CONFIG,
+ FALLBACK_CATEGORY_CONFIG,
+} from "@features/setup/utils/categoryConfig";
+import { ArrowSquareOut, X } from "@phosphor-icons/react";
+import { Flex, Text, Tooltip } from "@radix-ui/themes";
+import { motion } from "framer-motion";
+
+export interface SuggestedTaskCardProps {
+ task: DiscoveredTask;
+ index: number;
+ onSelect: (task: DiscoveredTask) => void;
+ onDismiss: (task: DiscoveredTask) => void;
+ onViewDetails: (task: DiscoveredTask) => void;
+}
+
+export function SuggestedTaskCard({
+ task,
+ index,
+ onSelect,
+ onDismiss,
+ onViewDetails,
+}: SuggestedTaskCardProps) {
+ const config = CATEGORY_CONFIG[task.category] ?? FALLBACK_CATEGORY_CONFIG;
+ const TaskIcon = config.icon;
+
+ return (
+
+ onSelect(task)}
+ type="button"
+ className="flex w-full cursor-pointer items-start gap-2.5 rounded-xl border border-(--gray-a3) bg-(--color-panel-solid) px-2.5 py-2 text-left transition-[border-color,box-shadow]"
+ style={{
+ boxShadow: "0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)",
+ }}
+ 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}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/code/src/renderer/features/task-detail/components/SuggestedTasksPanel.tsx b/apps/code/src/renderer/features/task-detail/components/SuggestedTasksPanel.tsx
new file mode 100644
index 000000000..ba0c90b02
--- /dev/null
+++ b/apps/code/src/renderer/features/task-detail/components/SuggestedTasksPanel.tsx
@@ -0,0 +1,185 @@
+import { SetupScanFeed } from "@features/setup/components/SetupScanFeed";
+import { useSetupStore } from "@features/setup/stores/setupStore";
+import type { DiscoveredTask } from "@features/setup/types";
+import { ArrowRight, Lightning, MagnifyingGlass } from "@phosphor-icons/react";
+import { Flex, Text } from "@radix-ui/themes";
+import { useNavigationStore } from "@stores/navigationStore";
+import { AnimatePresence, motion } from "framer-motion";
+import { useLayoutEffect, useRef, useState } from "react";
+import { SuggestedTaskCard } from "./SuggestedTaskCard";
+
+const VISIBLE_LIMIT = 3;
+const DEFAULT_LOG_LINES = 4;
+
+// Rough heights (px) used to drop cards / log lines that wouldn't fit below
+// the editor.
+const TOP_MARGIN = 12;
+const GAP = 8;
+const SCAN_PILL_HEIGHT = 48;
+const CARD_HEIGHT = 56;
+const SEE_MORE_HEIGHT = 24;
+const BOTTOM_PADDING = 56;
+const LOG_LINE_HEIGHT = 24;
+const LOG_FEED_PADDING = 16;
+
+interface SuggestedTasksPanelProps {
+ onSelect: (task: DiscoveredTask) => void;
+}
+
+export function SuggestedTasksPanel({ onSelect }: SuggestedTasksPanelProps) {
+ const discoveredTasks = useSetupStore((s) => s.discoveredTasks);
+ const discoveryStatus = useSetupStore((s) => s.discoveryStatus);
+ const enricherStatus = useSetupStore((s) => s.enricherStatus);
+ const discoveryFeed = useSetupStore((s) => s.discoveryFeed);
+ const removeDiscoveredTask = useSetupStore((s) => s.removeDiscoveredTask);
+ const selectDiscoveredTask = useSetupStore((s) => s.selectDiscoveredTask);
+ const navigateToInbox = useNavigationStore((s) => s.navigateToInbox);
+
+ const containerRef = useRef(null);
+ const [availableHeight, setAvailableHeight] = useState(Infinity);
+
+ useLayoutEffect(() => {
+ const el = containerRef.current;
+ if (!el) return;
+
+ const measure = () => {
+ const rect = el.getBoundingClientRect();
+ setAvailableHeight(window.innerHeight - rect.top);
+ };
+
+ measure();
+ const observer = new ResizeObserver(measure);
+ observer.observe(document.documentElement);
+ window.addEventListener("resize", measure);
+ return () => {
+ observer.disconnect();
+ window.removeEventListener("resize", measure);
+ };
+ }, []);
+
+ const handleDismiss = (task: DiscoveredTask) => {
+ removeDiscoveredTask(task.id);
+ };
+
+ const handleViewDetails = (task: DiscoveredTask) => {
+ selectDiscoveredTask(task.id);
+ navigateToInbox();
+ };
+
+ const isEnricherRunning = enricherStatus === "running";
+ const isDiscoveryRunning = discoveryStatus === "running";
+
+ const hasTasks = discoveredTasks.length > 0;
+
+ if (!hasTasks && !isEnricherRunning && !isDiscoveryRunning) return null;
+
+ const totalTasks = discoveredTasks.length;
+ const desiredVisible = Math.min(totalTasks, VISIBLE_LIMIT);
+ const discoveryFeedHasEntries = discoveryFeed.recentEntries.length > 0;
+
+ // Sum the sections that will be rendered, including inter-section gaps.
+ const measureTotalHeight = (cardCount: number, logLines: number): number => {
+ const sections: number[] = [];
+ if (cardCount > 0) {
+ sections.push(cardCount * CARD_HEIGHT + Math.max(0, cardCount - 1) * GAP);
+ }
+ if (totalTasks - cardCount > 0) {
+ sections.push(SEE_MORE_HEIGHT);
+ }
+ if (isEnricherRunning) {
+ sections.push(SCAN_PILL_HEIGHT);
+ }
+ if (isDiscoveryRunning) {
+ let h = SCAN_PILL_HEIGHT;
+ if (logLines > 0 && discoveryFeedHasEntries) {
+ h += LOG_FEED_PADDING + logLines * LOG_LINE_HEIGHT;
+ }
+ sections.push(h);
+ }
+ const sectionsTotal = sections.reduce((a, b) => a + b, 0);
+ const gapsTotal = Math.max(0, sections.length - 1) * GAP;
+ return TOP_MARGIN + sectionsTotal + gapsTotal + BOTTOM_PADDING;
+ };
+
+ // Drop log lines before cards: log lines are decorative, cards are the
+ // actionable items.
+ let visibleCount = desiredVisible;
+ let logLines = isDiscoveryRunning ? DEFAULT_LOG_LINES : 0;
+ if (Number.isFinite(availableHeight)) {
+ while (measureTotalHeight(visibleCount, logLines) > availableHeight) {
+ if (logLines > 0) logLines -= 1;
+ else if (visibleCount > 0) visibleCount -= 1;
+ else break;
+ }
+ }
+
+ const visibleTasks = discoveredTasks.slice(0, visibleCount);
+ const hiddenCount = totalTasks - visibleTasks.length;
+
+ const fadeMotion = {
+ initial: { opacity: 0 },
+ animate: { opacity: 1 },
+ exit: { opacity: 0 },
+ transition: { duration: 0.15 },
+ };
+
+ return (
+
+
+ {visibleTasks.map((task, index) => (
+
+ ))}
+ {hiddenCount > 0 && (
+
+
+
+ See {hiddenCount} more in Inbox
+
+
+
+
+ )}
+ {isEnricherRunning && (
+
+
+
+ )}
+ {isDiscoveryRunning && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx
index 0f7e3b571..95880a800 100644
--- a/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx
+++ b/apps/code/src/renderer/features/task-detail/components/TaskInput.tsx
@@ -23,6 +23,8 @@ import { UnifiedModelSelector } from "@features/sessions/components/UnifiedModel
import { getCurrentModeFromConfigOptions } from "@features/sessions/stores/sessionStore";
import type { AgentAdapter } from "@features/settings/stores/settingsStore";
import { useSettingsStore } from "@features/settings/stores/settingsStore";
+import { useSetupStore } from "@features/setup/stores/setupStore";
+import type { DiscoveredTask } from "@features/setup/types";
import { useAutoFocusOnTyping } from "@hooks/useAutoFocusOnTyping";
import { useConnectivity } from "@hooks/useConnectivity";
import {
@@ -47,6 +49,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { usePreviewConfig } from "../hooks/usePreviewConfig";
import { useTaskCreation } from "../hooks/useTaskCreation";
import { CloudGithubMissingNotice } from "./CloudGithubMissingNotice";
+import { SuggestedTasksPanel } from "./SuggestedTasksPanel";
import { type WorkspaceMode, WorkspaceModeSelect } from "./WorkspaceModeSelect";
interface TaskInputProps {
@@ -531,6 +534,15 @@ export function TaskInput({
editorRef.current?.setContent(text);
editorRef.current?.focus();
}, []);
+ const handleSelectSuggestion = useCallback(
+ async (task: DiscoveredTask) => {
+ const ok = await handleSubmit({
+ segments: [{ type: "text", text: task.prompt ?? task.title }],
+ });
+ if (ok) useSetupStore.getState().removeDiscoveredTask(task.id);
+ },
+ [handleSubmit],
+ );
const hasPendingDraft = useCallback(
() => !(editorRef.current?.isEmpty() ?? true),
[],
@@ -610,7 +622,7 @@ export function TaskInput({
align="center"
justify="center"
height="100%"
- className="relative px-4"
+ className="relative px-4 pb-[10vh]"
>
)}
+
+
+
diff --git a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts
index 6169c42ce..52cf459a8 100644
--- a/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts
+++ b/apps/code/src/renderer/features/task-detail/hooks/useTaskCreation.ts
@@ -4,6 +4,7 @@ import { useTaskInputHistoryStore } from "@features/message-editor/stores/taskIn
import type { EditorHandle } from "@features/message-editor/types";
import {
contentToXml,
+ type EditorContent,
extractFilePaths,
} from "@features/message-editor/utils/content";
import { useSettingsStore } from "@features/settings/stores/settingsStore";
@@ -48,7 +49,7 @@ interface UseTaskCreationOptions {
interface UseTaskCreationReturn {
isCreatingTask: boolean;
canSubmit: boolean;
- handleSubmit: () => void;
+ handleSubmit: (contentOverride?: EditorContent) => Promise;
}
function prepareTaskInput(
@@ -204,96 +205,114 @@ export function useTaskCreation({
!isCreatingTask &&
!editorIsEmpty;
- const handleSubmit = useCallback(async () => {
- const editor = editorRef.current;
- if (!canSubmit || !editor) return;
+ const handleSubmit = useCallback(
+ async (contentOverride?: EditorContent): Promise => {
+ const editor = editorRef.current;
+ const allowSubmit = contentOverride
+ ? isAuthenticated &&
+ isOnline &&
+ hasRequiredPath &&
+ !isCreatingTask &&
+ !!editor
+ : canSubmit && !!editor;
+ if (!allowSubmit || !editor) return false;
- setIsCreatingTask(true);
+ setIsCreatingTask(true);
- try {
- const content = editor.getContent();
-
- const plainText = editor.getText()?.trim();
- if (plainText) {
- useTaskInputHistoryStore.getState().addPrompt(plainText);
- }
+ try {
+ const content = contentOverride ?? editor.getContent();
- const input = prepareTaskInput(content, {
- selectedDirectory,
- selectedRepository,
- githubIntegrationId,
- githubUserIntegrationId,
- workspaceMode,
- branch,
- executionMode,
- adapter,
- model,
- reasoningLevel,
- environmentId,
- sandboxEnvironmentId,
- signalReportId,
- });
+ if (!contentOverride) {
+ const plainText = editor.getText()?.trim();
+ if (plainText) {
+ useTaskInputHistoryStore.getState().addPrompt(plainText);
+ }
+ }
- if (executionMode) {
- useSettingsStore.getState().setLastUsedInitialTaskMode(executionMode);
- }
+ const input = prepareTaskInput(content, {
+ selectedDirectory,
+ selectedRepository,
+ githubIntegrationId,
+ githubUserIntegrationId,
+ workspaceMode,
+ branch,
+ executionMode,
+ adapter,
+ model,
+ reasoningLevel,
+ environmentId,
+ sandboxEnvironmentId,
+ signalReportId,
+ });
- const taskService = get(RENDERER_TOKENS.TaskService);
- const result = await taskService.createTask(input, (output) => {
- invalidateTasks(output.task);
- if (signalReportId) {
- clearTaskInputReportAssociation();
- }
- if (onTaskCreated) {
- onTaskCreated(output.task);
- } else {
- navigateToTask(output.task);
+ if (executionMode) {
+ useSettingsStore.getState().setLastUsedInitialTaskMode(executionMode);
}
- useTourStore.getState().completeTour(createFirstTaskTour.id);
- editor.clear();
- });
-
- if (result.success) {
- void trackTaskCreated(input, selectedDirectory);
- }
- if (!result.success) {
- const title = getErrorTitle(result.failedStep);
- toast.error(title, { description: result.error });
- log.error("Task creation failed", {
- failedStep: result.failedStep,
- error: result.error,
+ const taskService = get(RENDERER_TOKENS.TaskService);
+ const result = await taskService.createTask(input, (output) => {
+ invalidateTasks(output.task);
+ if (signalReportId) {
+ clearTaskInputReportAssociation();
+ }
+ if (onTaskCreated) {
+ onTaskCreated(output.task);
+ } else {
+ navigateToTask(output.task);
+ }
+ useTourStore.getState().completeTour(createFirstTaskTour.id);
+ editor.clear();
});
+
+ if (result.success) {
+ void trackTaskCreated(input, selectedDirectory);
+ }
+
+ if (!result.success) {
+ const title = getErrorTitle(result.failedStep);
+ toast.error(title, { description: result.error });
+ log.error("Task creation failed", {
+ failedStep: result.failedStep,
+ error: result.error,
+ });
+ }
+ return result.success;
+ } catch (error) {
+ const description =
+ error instanceof Error ? error.message : "Unknown error";
+ toast.error("Failed to create task", { description });
+ log.error("Unexpected error during task creation", { error });
+ return false;
+ } finally {
+ setIsCreatingTask(false);
}
- } catch (error) {
- const description =
- error instanceof Error ? error.message : "Unknown error";
- toast.error("Failed to create task", { description });
- log.error("Unexpected error during task creation", { error });
- } finally {
- setIsCreatingTask(false);
- }
- }, [
- canSubmit,
- editorRef,
- selectedDirectory,
- selectedRepository,
- githubIntegrationId,
- githubUserIntegrationId,
- workspaceMode,
- branch,
- executionMode,
- adapter,
- model,
- reasoningLevel,
- environmentId,
- sandboxEnvironmentId,
- signalReportId,
- clearTaskInputReportAssociation,
- invalidateTasks,
- navigateToTask,
- onTaskCreated,
- ]);
+ },
+ [
+ canSubmit,
+ editorRef,
+ isAuthenticated,
+ isOnline,
+ hasRequiredPath,
+ isCreatingTask,
+ selectedDirectory,
+ selectedRepository,
+ githubIntegrationId,
+ githubUserIntegrationId,
+ workspaceMode,
+ branch,
+ executionMode,
+ adapter,
+ model,
+ reasoningLevel,
+ environmentId,
+ sandboxEnvironmentId,
+ signalReportId,
+ clearTaskInputReportAssociation,
+ invalidateTasks,
+ navigateToTask,
+ onTaskCreated,
+ ],
+ );
return {
isCreatingTask,
diff --git a/apps/code/src/renderer/stores/navigationStore.ts b/apps/code/src/renderer/stores/navigationStore.ts
index 93a9338c2..3edf3bcba 100644
--- a/apps/code/src/renderer/stores/navigationStore.ts
+++ b/apps/code/src/renderer/stores/navigationStore.ts
@@ -20,8 +20,7 @@ type ViewType =
| "archived"
| "command-center"
| "skills"
- | "mcp-servers"
- | "setup";
+ | "mcp-servers";
export interface TaskInputReportAssociation {
reportId: string;
@@ -63,7 +62,6 @@ interface NavigationStore {
navigateToCommandCenter: () => void;
navigateToSkills: () => void;
navigateToMcpServers: () => void;
- navigateToSetup: () => void;
goBack: () => void;
goForward: () => void;
canGoBack: () => boolean;
@@ -100,9 +98,6 @@ const isSameView = (view1: ViewState, view2: ViewState): boolean => {
if (view1.type === "mcp-servers" && view2.type === "mcp-servers") {
return true;
}
- if (view1.type === "setup" && view2.type === "setup") {
- return true;
- }
return false;
};
@@ -285,10 +280,6 @@ export const useNavigationStore = create()(
navigate({ type: "mcp-servers" });
},
- 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 7250605aa..9e13a0873 100644
--- a/apps/code/src/shared/types/analytics.ts
+++ b/apps/code/src/shared/types/analytics.ts
@@ -301,10 +301,6 @@ type SetupDiscoveredTaskCategory =
| "funnel"
| "posthog_setup";
-export interface SetupViewedProperties {
- discovery_status: "idle" | "running" | "done" | "error";
-}
-
export interface SetupDiscoveryStartedProperties {
discovery_task_id: string;
discovery_task_run_id: string;
@@ -339,12 +335,6 @@ export interface SetupTaskDismissedProperties {
total_discovered: number;
}
-export interface SetupSkippedProperties {
- discovery_status: "idle" | "running" | "done" | "error";
- had_discovered_tasks: boolean;
- entry_point: "during_scan" | "after_done";
-}
-
// Subscription / billing events
export interface SubscriptionStartedProperties {
plan_key: string;
@@ -425,13 +415,11 @@ export const ANALYTICS_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_TASK_DISMISSED: "Setup task dismissed",
- SETUP_SKIPPED: "Setup skipped",
// Error events
TASK_CREATION_FAILED: "Task creation failed",
@@ -512,13 +500,11 @@ export type EventPropertyMap = {
[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_TASK_DISMISSED]: SetupTaskDismissedProperties;
- [ANALYTICS_EVENTS.SETUP_SKIPPED]: SetupSkippedProperties;
// Error events
[ANALYTICS_EVENTS.TASK_CREATION_FAILED]: TaskCreationFailedProperties;