-
Notifications
You must be signed in to change notification settings - Fork 46
feat(code): suggest PR work items on new task page #2553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,19 @@ import { | |
| Lightning, | ||
| MagnifyingGlass, | ||
| } from "@phosphor-icons/react"; | ||
| import type { PrWorkItem } from "@posthog/core/git/router-schemas"; | ||
| import type { DiscoveredTask } from "@posthog/core/setup/types"; | ||
| import { ANALYTICS_EVENTS } from "@posthog/shared/analytics-events"; | ||
| import { useFolders } from "@posthog/ui/features/folders/useFolders"; | ||
| import { useDetectedCloudRepository } from "@posthog/ui/features/repo-files/useDetectedCloudRepository"; | ||
| import { buildWorkItemPrompt } from "@posthog/ui/features/work-items/buildWorkItemPrompt"; | ||
| import { | ||
| dismissedWorkItemKey, | ||
| useDismissedWorkItemsStore, | ||
| } from "@posthog/ui/features/work-items/dismissedWorkItemsStore"; | ||
| import { useWorkItemSuggestions } from "@posthog/ui/features/work-items/useWorkItemSuggestions"; | ||
| import { WorkItemCard } from "@posthog/ui/features/work-items/WorkItemCard"; | ||
| import { openTaskInput } from "@posthog/ui/router/useOpenTask"; | ||
| import { Flex, Text } from "@radix-ui/themes"; | ||
| import { AnimatePresence, motion } from "framer-motion"; | ||
| import { | ||
|
|
@@ -16,6 +28,7 @@ import { | |
| useState, | ||
| } from "react"; | ||
| import { useActiveRepoStore } from "../../../shell/activeRepoStore"; | ||
| import { track } from "../../../shell/analytics"; | ||
| import { DiscoveredTaskDetailDialog } from "../../setup/DiscoveredTaskDetailDialog"; | ||
| import { SetupScanFeed } from "../../setup/SetupScanFeed"; | ||
| import { | ||
|
|
@@ -26,6 +39,12 @@ import { | |
| } from "../../setup/setupStore"; | ||
| import { SuggestedTaskCard } from "./SuggestedTaskCard"; | ||
|
|
||
| const workItemKey = (item: PrWorkItem) => `${item.prNumber}:${item.kind}`; | ||
|
|
||
| type PanelEntry = | ||
| | { kind: "work"; id: string; item: PrWorkItem } | ||
| | { kind: "discovered"; id: string; task: DiscoveredTask }; | ||
|
|
||
| const VISIBLE_LIMIT = 3; | ||
| const DEFAULT_LOG_LINES = 4; | ||
|
|
||
|
|
@@ -66,6 +85,14 @@ export function SuggestedTasksPanel({ leading }: { leading?: ReactNode }) { | |
| ); | ||
| const removeDiscoveredTask = useSetupStore((s) => s.removeDiscoveredTask); | ||
|
|
||
| const workItemsRaw = useWorkItemSuggestions(selectedDirectory); | ||
| const dismissedWorkItemKeys = useDismissedWorkItemsStore( | ||
| (s) => s.dismissedKeys, | ||
| ); | ||
| const dismissWorkItem = useDismissedWorkItemsStore((s) => s.dismiss); | ||
| const { folders } = useFolders(); | ||
| const detectedCloudRepository = useDetectedCloudRepository(selectedDirectory); | ||
|
|
||
| const [detailTask, setDetailTask] = useState<DiscoveredTask | null>(null); | ||
| const [pageStart, setPageStart] = useState(0); | ||
| const [pageDirection, setPageDirection] = useState<1 | -1>(1); | ||
|
|
@@ -120,14 +147,63 @@ export function SuggestedTasksPanel({ leading }: { leading?: ReactNode }) { | |
| setDetailTask(null); | ||
| }, []); | ||
|
|
||
| const hasTasks = discoveredTasks.length > 0; | ||
| const handleSelectWorkItem = useCallback( | ||
| (item: PrWorkItem, position: number, total: number) => { | ||
| track(ANALYTICS_EVENTS.SETUP_WORK_ITEM_SELECTED, { | ||
| kind: item.kind, | ||
| pr_number: item.prNumber, | ||
| position, | ||
| total, | ||
| }); | ||
| const folderId = folders.find((f) => f.path === selectedDirectory)?.id; | ||
| openTaskInput({ | ||
| initialPrompt: buildWorkItemPrompt(item), | ||
| folderId, | ||
| initialCloudRepository: detectedCloudRepository ?? undefined, | ||
| }); | ||
| }, | ||
| [folders, selectedDirectory, detectedCloudRepository], | ||
| ); | ||
|
|
||
| const handleDismissWorkItem = useCallback( | ||
| (item: PrWorkItem, position: number, total: number) => { | ||
| track(ANALYTICS_EVENTS.SETUP_WORK_ITEM_DISMISSED, { | ||
| kind: item.kind, | ||
| pr_number: item.prNumber, | ||
| position, | ||
| total, | ||
| }); | ||
| dismissWorkItem(dismissedWorkItemKey(selectedDirectory, item)); | ||
| }, | ||
| [dismissWorkItem, selectedDirectory], | ||
| ); | ||
|
|
||
| const dismissed = new Set(dismissedWorkItemKeys); | ||
| const workItems = workItemsRaw.filter( | ||
| (item) => !dismissed.has(dismissedWorkItemKey(selectedDirectory, item)), | ||
| ); | ||
|
Comment on lines
+181
to
+184
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/code/src/renderer/features/task-detail/components/SuggestedTasksPanel.tsx
Line: 181-184
Comment:
**Unguarded Set construction in render body**
`dismissed` and `workItems` are recomputed (allocating a new `Set` and array) on every render, even when `dismissedWorkItemKeys` and `workItemsRaw` haven't changed. Wrapping both in a `useMemo` (dependencies: `dismissedWorkItemKeys`, `workItemsRaw`, `selectedDirectory`) would avoid churning downstream derived state on unrelated re-renders.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| const entries: PanelEntry[] = [ | ||
| ...discoveredTasks.map((task) => ({ | ||
| kind: "discovered" as const, | ||
| id: task.id, | ||
| task, | ||
| })), | ||
| ...workItems.map((item) => ({ | ||
| kind: "work" as const, | ||
| id: `work:${workItemKey(item)}`, | ||
| item, | ||
| })), | ||
| ]; | ||
|
|
||
| const hasTasks = entries.length > 0; | ||
| const showEnricherFeed = !hasTasks && enricherStatus === "running"; | ||
| const showDiscoveryFeed = discoveryStatus === "running"; | ||
|
|
||
| if (!hasTasks && !showEnricherFeed && !showDiscoveryFeed && !leading) | ||
| return null; | ||
|
|
||
| const totalTasks = discoveredTasks.length; | ||
| const totalTasks = entries.length; | ||
| const desiredVisible = Math.min(totalTasks, VISIBLE_LIMIT); | ||
| const discoveryFeedHasEntries = discoveryFeed.recentEntries.length > 0; | ||
|
|
||
|
|
@@ -163,13 +239,13 @@ export function SuggestedTasksPanel({ leading }: { leading?: ReactNode }) { | |
| const effectivePageStart = | ||
| visibleCount > 0 && pageStart < totalTasks ? pageStart : 0; | ||
|
|
||
| const visibleTasks = discoveredTasks.slice( | ||
| const visibleEntries = entries.slice( | ||
| effectivePageStart, | ||
| effectivePageStart + visibleCount, | ||
| ); | ||
|
|
||
| const canGoPrev = effectivePageStart > 0; | ||
| const canGoNext = effectivePageStart + visibleTasks.length < totalTasks; | ||
| const canGoNext = effectivePageStart + visibleEntries.length < totalTasks; | ||
| const showPager = visibleCount > 0 && totalTasks > visibleCount; | ||
| const currentPage = | ||
| visibleCount > 0 ? Math.floor(effectivePageStart / visibleCount) + 1 : 1; | ||
|
|
@@ -251,14 +327,35 @@ export function SuggestedTasksPanel({ leading }: { leading?: ReactNode }) { | |
| className="flex flex-col gap-2" | ||
| > | ||
| <AnimatePresence mode="sync" initial={false}> | ||
| {visibleTasks.map((task) => ( | ||
| <SuggestedTaskCard | ||
| key={task.id} | ||
| task={task} | ||
| onSelect={handleSelectTask} | ||
| onDismiss={handleDismiss} | ||
| /> | ||
| ))} | ||
| {visibleEntries.map((entry) => | ||
| entry.kind === "work" ? ( | ||
| <WorkItemCard | ||
| key={entry.id} | ||
| item={entry.item} | ||
| onSelect={(item) => | ||
| handleSelectWorkItem( | ||
| item, | ||
| workItems.indexOf(item), | ||
| workItems.length, | ||
| ) | ||
| } | ||
| onDismiss={(item) => | ||
| handleDismissWorkItem( | ||
| item, | ||
| workItems.indexOf(item), | ||
| workItems.length, | ||
| ) | ||
| } | ||
| /> | ||
| ) : ( | ||
| <SuggestedTaskCard | ||
| key={entry.id} | ||
| task={entry.task} | ||
| onSelect={handleSelectTask} | ||
| onDismiss={handleDismiss} | ||
| /> | ||
| ), | ||
| )} | ||
| </AnimatePresence> | ||
| </motion.div> | ||
| </AnimatePresence> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WorkItemKindhere ("review" | "ci" | "conflict") is identical toPrWorkItemKindalready exported from@main/services/git/schemas. Defining the same union twice means any future change to the work-item kinds must be applied in two places. The analytics properties could instead import and reusePrWorkItemKinddirectly, asSuggestedTasksPanel.tsxalready imports from that path.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!