Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion apps/code/src/renderer/features/setup/hooks/useSetupDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { RENDERER_TOKENS } from "@renderer/di/tokens";
import { useActiveRepoStore } from "@stores/activeRepoStore";
import { useEffect } from "react";

const SIMULATE_SUGGESTIONS_STORAGE_KEY = "posthog-code:simulate-suggestions";

function shouldSimulateSuggestions(): boolean {
if (!import.meta.env.DEV) return false;
if (import.meta.env.VITE_SIMULATE_SUGGESTIONS === "1") return true;
return window.localStorage.getItem(SIMULATE_SUGGESTIONS_STORAGE_KEY) === "1";
}

export function useSetupDiscovery() {
const selectedDirectory = useActiveRepoStore((s) => s.path);

Expand All @@ -14,8 +22,17 @@ export function useSetupDiscovery() {
// Enricher runs per repo on every selection (gated on per-repo status
// inside the service).
useEffect(() => {
if (!selectedDirectory) return;
if (!selectedDirectory) return undefined;
const service = get<SetupRunService>(RENDERER_TOKENS.SetupRunService);
if (shouldSimulateSuggestions()) {
service.startSuggestionSimulation(
selectedDirectory,
shouldSimulateSuggestions,
);
return () => service.stopSuggestionSimulation(selectedDirectory);
}
Comment thread
szymeo marked this conversation as resolved.

service.stopSuggestionSimulation();
const discoveryEverStarted = Object.values(
useSetupStore.getState().discoveryByRepo,
).some((d) => d.status !== "idle");
Expand All @@ -24,5 +41,6 @@ export function useSetupDiscovery() {
} else {
service.startSetup(selectedDirectory);
}
return undefined;
}, [selectedDirectory]);
}
88 changes: 88 additions & 0 deletions apps/code/src/renderer/features/setup/services/setupRunService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,43 @@ const log = logger.scope("setup-run-service");

let activityIdCounter = 0;

const SIMULATED_SUGGESTION_CATEGORIES: DiscoveredTask["category"][] = [
"bug",
"performance",
"dead_code",
"duplication",
"event_tracking",
"experiment",
];

const SIMULATED_SUGGESTION_INTERVAL_MS = 2500;
const SIMULATED_SUGGESTION_LIMIT = 12;

function buildSimulatedSuggestion(
repoPath: string,
index: number,
): DiscoveredTask {
const category =
SIMULATED_SUGGESTION_CATEGORIES[
index % SIMULATED_SUGGESTION_CATEGORIES.length
];

return {
id: `simulated-suggestion-${index}`,
source: "agent",
repoPath,
category,
title: `Simulated suggestion ${index + 1}`,
description:
"A generated setup suggestion for exercising empty-state layout stability while new content keeps arriving.",
impact:
"This is dev-only test data for checking whether the composer and suggestion rows stay anchored during incremental updates.",
recommendation:
"Use this simulation to hover and click suggestions while the list keeps changing underneath the same UI surface.",
prompt: `Investigate simulated suggestion ${index + 1}`,
};
}

function extractPathFromRawInput(
tool: string,
rawInput: Record<string, unknown> | undefined,
Expand Down Expand Up @@ -236,6 +273,9 @@ export class SetupRunService {
private anyDiscoveryEverLaunched = false;
private discoveryStartingByRepo = new Set<string>();
private enricherSuggestionsRunningByRepo = new Set<string>();
private simulatedSuggestionsRepo: string | null = null;
private simulatedSuggestionsTimer: number | null = null;
private simulatedSuggestionsCount = 0;

startSetup(directory: string): void {
// Defense in depth: never auto-run from a non-idle persisted state.
Expand All @@ -256,6 +296,54 @@ export class SetupRunService {
this.injectEnricherSuggestions(directory);
}

startSuggestionSimulation(
directory: string,
shouldContinue: () => boolean = () => true,
): void {
if (!directory) return;
if (this.simulatedSuggestionsRepo === directory) return;

if (this.simulatedSuggestionsTimer !== null) {
window.clearInterval(this.simulatedSuggestionsTimer);
}

this.simulatedSuggestionsRepo = directory;
this.simulatedSuggestionsCount = 0;

const addNextSuggestion = () => {
if (
!shouldContinue() ||
this.simulatedSuggestionsCount >= SIMULATED_SUGGESTION_LIMIT
) {
this.stopSuggestionSimulation(directory);
return;
}

useSetupStore
.getState()
.addDiscoveredTaskIfMissing(
buildSimulatedSuggestion(directory, this.simulatedSuggestionsCount),
);
this.simulatedSuggestionsCount += 1;
};

addNextSuggestion();
this.simulatedSuggestionsTimer = window.setInterval(
addNextSuggestion,
SIMULATED_SUGGESTION_INTERVAL_MS,
);
Comment thread
szymeo marked this conversation as resolved.
}

stopSuggestionSimulation(directory?: string): void {
if (directory && this.simulatedSuggestionsRepo !== directory) return;
if (this.simulatedSuggestionsTimer !== null) {
window.clearInterval(this.simulatedSuggestionsTimer);
}
this.simulatedSuggestionsTimer = null;
this.simulatedSuggestionsRepo = null;
this.simulatedSuggestionsCount = 0;
}

startDiscovery(directory: string): void {
if (!directory) return;
if (this.anyDiscoveryEverLaunched) return;
Expand Down
16 changes: 16 additions & 0 deletions apps/code/src/renderer/features/setup/stores/setupStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ interface SetupStoreActions {
completeEnrichment: (repoPath: string) => void;
failEnrichment: (repoPath: string) => void;
removeDiscoveredTask: (taskId: string, repoPath: string | null) => void;
addDiscoveredTaskIfMissing: (task: DiscoveredTask) => void;
addEnricherSuggestionIfMissing: (task: DiscoveredTask) => void;
pushDiscoveryActivity: (repoPath: string, entry: ActivityEntry) => void;
resetSetup: () => void;
Expand Down Expand Up @@ -263,6 +264,21 @@ export const useSetupStore = create<SetupStore>()(
}));
},

addDiscoveredTaskIfMissing: (task) => {
set((state) => {
if (
state.discoveredTasks.some(
(t) => t.id === task.id && t.repoPath === task.repoPath,
)
) {
return state;
}
return {
discoveredTasks: [...state.discoveredTasks, task],
};
});
},

// Adds an enricher-source suggestion if there isn't already one with
// the same id+repoPath. Idempotent — safe to call repeatedly on every
// detection run. Dismissed suggestions stay dismissed until `resetSetup`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ import { motion } from "framer-motion";

export interface SuggestedTaskCardProps {
task: DiscoveredTask;
index: number;
onSelect: (task: DiscoveredTask) => void;
onDismiss: (task: DiscoveredTask) => void;
}

export function SuggestedTaskCard({
task,
index,
onSelect,
onDismiss,
}: SuggestedTaskCardProps) {
Expand All @@ -26,19 +24,15 @@ export function SuggestedTaskCard({
return (
<motion.div
layout
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{
opacity: 0,
y: -4,
transition: { duration: 0.12, delay: 0 },
}}
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{
duration: 0.18,
delay: index * 0.04,
layout: { type: "spring", damping: 25, stiffness: 300 },
opacity: { duration: 0.15, ease: "easeOut" },
scale: { duration: 0.15, ease: "easeOut" },
layout: { duration: 0.22, ease: [0.22, 1, 0.36, 1] },
}}
className="group relative"
className="group relative origin-center"
>
<button
onClick={() => onSelect(task)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,6 @@ const BOTTOM_PADDING = 56;
const LOG_LINE_HEIGHT = 24;
const LOG_FEED_PADDING = 16;

const pageVariants = {
enter: (dir: number) => ({ x: dir * 32, opacity: 0 }),
center: { x: 0, opacity: 1 },
exit: (dir: number) => ({ x: -dir * 32, opacity: 0 }),
};

const fadeMotion = {
initial: { opacity: 0 },
animate: { opacity: 1 },
Expand Down Expand Up @@ -227,31 +221,42 @@ export function SuggestedTasksPanel() {
</Flex>
)}
{hasTasks && (
<div className="relative overflow-hidden">
<AnimatePresence
mode="popLayout"
initial={false}
custom={pageDirection}
>
<div className="relative">
<AnimatePresence mode="wait" initial={false} custom={pageDirection}>
<motion.div
key={effectivePageStart}
custom={pageDirection}
variants={pageVariants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.18, ease: "easeOut" }}
variants={{
enter: (dir: number) => ({ x: dir * 32, opacity: 0 }),
center: {
x: 0,
opacity: 1,
transition: {
x: { duration: 0.22, ease: [0.22, 1, 0.36, 1] },
opacity: { duration: 0.18, ease: "easeOut" },
},
},
exit: (dir: number) => ({
x: -dir * 32,
opacity: 0,
transition: { duration: 0.13, ease: "easeIn" },
}),
}}
className="flex flex-col gap-2"
>
{visibleTasks.map((task, index) => (
<SuggestedTaskCard
key={task.id}
task={task}
index={index}
onSelect={handleSelectTask}
onDismiss={handleDismiss}
/>
))}
<AnimatePresence mode="sync" initial={false}>
{visibleTasks.map((task) => (
<SuggestedTaskCard
key={task.id}
task={task}
onSelect={handleSelectTask}
onDismiss={handleDismiss}
/>
))}
</AnimatePresence>
</motion.div>
</AnimatePresence>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,20 +621,20 @@ export function TaskInput({
className="relative h-full w-full"
>
<DropZoneOverlay isVisible={isDraggingFile} />
<Flex
align="center"
justify="center"
height="100%"
className="relative px-4 pt-[10vh]"
>
<Flex height="100%" className="relative px-4">
<DotPatternBackground className="h-[100.333%]" />
<div
style={{
zIndex: 1,
top: "50%",
transform: "translate(-50%, -50%)",
}}
className="relative flex w-full max-w-[600px] flex-col gap-2"
className="absolute left-1/2 z-[1] flex w-[calc(100%-2rem)] max-w-[600px] flex-col gap-2"
>
<Flex gap="2" align="center" className="min-w-0">
<Flex
gap="2"
align="center"
className="absolute bottom-full left-0 mb-2 min-w-0"
>
<WorkspaceModeSelect
value={workspaceMode}
onChange={setWorkspaceMode}
Expand Down Expand Up @@ -829,8 +829,10 @@ export function TaskInput({
<CloudGithubMissingNotice />
</div>
)}
<SuggestedTasksPanel />
</Flex>
<div className="absolute top-full right-0 left-0 z-10">
<SuggestedTasksPanel />
</div>
</div>
</Flex>

Expand Down
1 change: 1 addition & 0 deletions apps/code/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface ImportMetaEnv {
readonly VITE_POSTHOG_API_KEY?: string;
readonly VITE_POSTHOG_API_HOST?: string;
readonly VITE_POSTHOG_UI_HOST?: string;
readonly VITE_SIMULATE_SUGGESTIONS?: string;
}

interface ImportMeta {
Expand Down
Loading