Skip to content
Draft
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
1 change: 1 addition & 0 deletions apps/code/src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const SELF_DRIVING_SETUP_TASK_FLAG =
export const SYNC_CLOUD_TASKS_FLAG = "posthog-code-sync-cloud-tasks";
export const HOME_TAB_FLAG = "posthog-code-home-tab";
export const DISCOVERY_RUN_FLAG = "posthog-code-discovery-run";
export const WORK_ITEM_SUGGESTIONS_FLAG = "posthog-code-work-item-suggestions";
export const BRANCH_PREFIX = "posthog-code/";
export const DATA_DIR = ".posthog-code";
export const WORKTREES_DIR = ".posthog-code/worktrees";
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/git/router-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,23 @@ export const createPrProgressPayload = z.object({
});

export type CreatePrProgressPayload = z.infer<typeof createPrProgressPayload>;

// PR work items: the current user's open PRs that need action. Each kind is a
// distinct reason a single PR is "waiting on you" (a PR can yield several).
export const prWorkItemKindSchema = z.enum(["review", "ci", "conflict"]);
export type PrWorkItemKind = z.infer<typeof prWorkItemKindSchema>;

export const prWorkItemSchema = z.object({
kind: prWorkItemKindSchema,
prNumber: z.number(),
title: z.string(),
url: z.string(),
headRefName: z.string(),
// Head commit SHA — lets dismissals be commit-scoped (a new push re-surfaces).
headSha: z.string(),
});

export type PrWorkItem = z.infer<typeof prWorkItemSchema>;

export const getPrWorkItemsInput = directoryPathInput;
export const getPrWorkItemsOutput = z.array(prWorkItemSchema);
11 changes: 11 additions & 0 deletions packages/host-router/src/routers/git.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import {
getPrTemplateOutput,
getPrUrlForBranchInput,
getPrUrlForBranchOutput,
getPrWorkItemsInput,
getPrWorkItemsOutput,
ghAuthTokenOutput,
ghStatusOutput,
gitStateSnapshotSchema,
Expand Down Expand Up @@ -449,6 +451,15 @@ export const gitRouter = router({
}),
),

getPrWorkItems: publicProcedure
.input(getPrWorkItemsInput)
.output(getPrWorkItemsOutput)
.query(({ ctx, input }) =>
getWorkspaceClient(ctx.container).git.getPrWorkItems.query({
directoryPath: input.directoryPath,
}),
),

createPr: publicProcedure
.input(createPrInput)
.output(createPrOutput)
Expand Down
20 changes: 20 additions & 0 deletions packages/shared/src/analytics-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,22 @@ export interface SetupTaskDismissedProperties {
total_discovered: number;
}

export type WorkItemKind = "review" | "ci" | "conflict";

export interface SetupWorkItemSelectedProperties {
kind: WorkItemKind;
pr_number: number;
position: number;
total: number;
}

export interface SetupWorkItemDismissedProperties {
kind: WorkItemKind;
pr_number: number;
position: number;
total: number;
}

// Inbox events
export type InboxReportOpenMethod =
| "click"
Expand Down Expand Up @@ -1010,6 +1026,8 @@ export const ANALYTICS_EVENTS = {
SETUP_DISCOVERY_FAILED: "Setup discovery failed",
SETUP_TASK_SELECTED: "Setup task selected",
SETUP_TASK_DISMISSED: "Setup task dismissed",
SETUP_WORK_ITEM_SELECTED: "Setup work item selected",
SETUP_WORK_ITEM_DISMISSED: "Setup work item dismissed",

// Deep link events
DEEP_LINK_NEW_TASK: "Deep link new task",
Expand Down Expand Up @@ -1153,6 +1171,8 @@ export type EventPropertyMap = {
[ANALYTICS_EVENTS.SETUP_DISCOVERY_FAILED]: SetupDiscoveryFailedProperties;
[ANALYTICS_EVENTS.SETUP_TASK_SELECTED]: SetupTaskSelectedProperties;
[ANALYTICS_EVENTS.SETUP_TASK_DISMISSED]: SetupTaskDismissedProperties;
[ANALYTICS_EVENTS.SETUP_WORK_ITEM_SELECTED]: SetupWorkItemSelectedProperties;
[ANALYTICS_EVENTS.SETUP_WORK_ITEM_DISMISSED]: SetupWorkItemDismissedProperties;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate type definition

WorkItemKind here ("review" | "ci" | "conflict") is identical to PrWorkItemKind already 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 reuse PrWorkItemKind directly, as SuggestedTasksPanel.tsx already imports from that path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/shared/types/analytics.ts
Line: 908

Comment:
**Duplicate type definition**

`WorkItemKind` here (`"review" | "ci" | "conflict"`) is identical to `PrWorkItemKind` already 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 reuse `PrWorkItemKind` directly, as `SuggestedTasksPanel.tsx` already imports from that path.

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!


// Deep link events
[ANALYTICS_EVENTS.DEEP_LINK_NEW_TASK]: DeepLinkNewTaskProperties;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
EXPERIMENT_SUGGESTIONS_FLAG,
HOME_TAB_FLAG,
SYNC_CLOUD_TASKS_FLAG,
WORK_ITEM_SUGGESTIONS_FLAG,
} from "./flags";

export const SELF_DRIVING_SETUP_TASK_FLAG =
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const EXPERIMENT_SUGGESTIONS_FLAG =
export const SYNC_CLOUD_TASKS_FLAG = "posthog-code-sync-cloud-tasks";
export const HOME_TAB_FLAG = "posthog-code-home-tab";
export const DISCOVERY_RUN_FLAG = "posthog-code-discovery-run";
export const WORK_ITEM_SUGGESTIONS_FLAG = "posthog-code-work-item-suggestions";
// Gates the entire canvas feature: the app rail's Channels space, the /website
// routes, channels and dashboards.
export const PROJECT_BLUEBIRD_FLAG = "project-bluebird";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function SuggestedTaskCard({
e.stopPropagation();
onDismiss(task);
}}
className="flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-(--gray-9) hover:bg-(--gray-a3) hover:text-(--gray-12)"
className="flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-md bg-(--gray-3) text-(--gray-11) shadow-sm transition-colors hover:bg-(--gray-4) hover:text-(--gray-12)"
>
<X size={12} weight="bold" />
</button>
Expand Down
121 changes: 109 additions & 12 deletions packages/ui/src/features/task-detail/components/SuggestedTasksPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Prompt To Fix With AI
This 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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/features/task-detail/components/TaskInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FileText, X } from "@phosphor-icons/react";
import { xmlToContent } from "@posthog/core/message-editor/content";
import { isValidConfigValue } from "@posthog/core/task-detail/configOptions";
import { useHostTRPC, useHostTRPCClient } from "@posthog/host-router/react";
import { ButtonGroup } from "@posthog/quill";
Expand Down Expand Up @@ -207,9 +208,11 @@ export function TaskInput({

useEffect(() => {
if (!initialPrompt || !prefillRequestKey) return;
useDraftStore.getState().actions.setPendingContent(sessionId, {
segments: [{ type: "text", text: initialPrompt }],
});
// Hydrate chip tags (e.g. <github_pr/>) into real pills; plain prompts
// round-trip unchanged to a single text segment.
useDraftStore
.getState()
.actions.setPendingContent(sessionId, xmlToContent(initialPrompt));
}, [initialPrompt, prefillRequestKey, sessionId]);

useEffect(() => {
Expand Down
Loading
Loading