Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 787451d

Browse files
Merge main into shared cloud task API client
Generated-By: PostHog Code Task-Id: c1bbe3cf-742b-4b24-bf96-d11a18b4cf22
2 parents b69d17d + d153078 commit 787451d

25 files changed

Lines changed: 524 additions & 158 deletions

packages/core/src/context-menu/context-menu.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ describe("ContextMenuService.showTaskContextMenu", () => {
142142
);
143143
});
144144

145+
it("can hide Archive prior tasks for task lists without that action", async () => {
146+
const menu = new FakeContextMenu();
147+
makeService(menu).showTaskContextMenu({
148+
...baseTask,
149+
showArchivePrior: false,
150+
});
151+
await menu.shown;
152+
expect(labels(menu.lastItems)).not.toContain("Archive prior tasks");
153+
expect(labels(menu.lastItems)).toContain("Archive");
154+
});
155+
145156
it("resolves to null when the menu is dismissed", async () => {
146157
const menu = new FakeContextMenu();
147158
const result = makeService(menu).showTaskContextMenu(baseTask);

packages/core/src/context-menu/context-menu.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export class ContextMenuService {
117117
canStop,
118118
isInCommandCenter,
119119
hasEmptyCommandCenterCell,
120+
showArchivePrior = true,
120121
channels,
121122
} = input;
122123
const { apps, lastUsedAppId } = await this.getExternalAppsData();
@@ -162,7 +163,7 @@ export class ContextMenuService {
162163
...(!isInCommandCenter
163164
? [
164165
this.separator(),
165-
this.item(
166+
this.item<TaskAction>(
166167
"Add to Command Center",
167168
{ type: "add-to-command-center" as const },
168169
{ enabled: hasEmptyCommandCenterCell ?? true },
@@ -172,19 +173,23 @@ export class ContextMenuService {
172173
...fileToItems,
173174
this.separator(),
174175
this.item("Archive", { type: "archive" }),
175-
this.item(
176-
"Archive prior tasks",
177-
{ type: "archive-prior" },
178-
{
179-
confirm: {
180-
title: "Archive Prior Tasks",
181-
message: "Archive all tasks older than this one?",
182-
detail:
183-
"This will archive every task created before this one. You can unarchive them later.",
184-
confirmLabel: "Archive",
185-
},
186-
},
187-
),
176+
...(showArchivePrior
177+
? [
178+
this.item<TaskAction>(
179+
"Archive prior tasks",
180+
{ type: "archive-prior" },
181+
{
182+
confirm: {
183+
title: "Archive Prior Tasks",
184+
message: "Archive all tasks older than this one?",
185+
detail:
186+
"This will archive every task created before this one. You can unarchive them later.",
187+
confirmLabel: "Archive",
188+
},
189+
},
190+
),
191+
]
192+
: []),
188193
]);
189194
}
190195

packages/core/src/context-menu/schemas.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const taskContextMenuInput = z.object({
99
canStop: z.boolean().optional(),
1010
isInCommandCenter: z.boolean().optional(),
1111
hasEmptyCommandCenterCell: z.boolean().optional(),
12+
showArchivePrior: z.boolean().optional(),
1213
// Top-level desktop_file_system channels available as "File to…" targets.
1314
// Omit (or pass empty) to hide the submenu entirely.
1415
channels: z.array(z.object({ id: z.string(), name: z.string() })).optional(),

packages/ui/src/features/canvas/components/ChannelItemRow.test.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { ChannelItemModel } from "@posthog/core/canvas/channelItems";
22
import type { TaskRunStatus } from "@posthog/shared/domain-types";
33
import { Theme } from "@radix-ui/themes";
4-
import { render, screen } from "@testing-library/react";
5-
import { describe, expect, it } from "vitest";
4+
import { fireEvent, render, screen } from "@testing-library/react";
5+
import { describe, expect, it, vi } from "vitest";
66
import { ChannelItemRow } from "./ChannelItemRow";
77

88
const actions = {
@@ -74,4 +74,23 @@ describe("ChannelItemRow", () => {
7474
// The glyph is wrapped, not replaced — no spinner swapped in its place.
7575
expect(running.querySelector("svg")).not.toBeNull();
7676
});
77+
78+
it("opens the task context menu from the row", () => {
79+
const onContextMenu = vi.fn();
80+
81+
render(
82+
<Theme>
83+
<ChannelItemRow
84+
actions={actions}
85+
isActive={false}
86+
item={item()}
87+
onContextMenu={onContextMenu}
88+
/>
89+
</Theme>,
90+
);
91+
92+
fireEvent.contextMenu(screen.getByText("Investigate signup drop-off"));
93+
94+
expect(onContextMenu).toHaveBeenCalledOnce();
95+
});
7796
});

packages/ui/src/features/canvas/components/ChannelItemRow.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { formatRelativeTimeShort } from "@posthog/shared";
1111
import { UserAvatar } from "@posthog/ui/features/auth/UserAvatar";
1212
import { iconForTemplate } from "@posthog/ui/features/canvas/components/canvasTemplateIcon";
1313
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
14+
import { InlineEditInput } from "@posthog/ui/features/sidebar/components/items/TaskItem";
1415
import { SidebarItem } from "@posthog/ui/features/sidebar/components/SidebarItem";
1516
import { NestedButton } from "@posthog/ui/primitives/NestedButton";
1617
import { Tooltip } from "@posthog/ui/primitives/Tooltip";
@@ -70,10 +71,18 @@ export function ChannelItemRow({
7071
item,
7172
isActive,
7273
actions,
74+
isEditing = false,
75+
onContextMenu,
76+
onEditSubmit,
77+
onEditCancel,
7378
}: {
7479
item: ChannelItemModel;
7580
isActive: boolean;
7681
actions: ChannelItemActions;
82+
isEditing?: boolean;
83+
onContextMenu?: (event: React.MouseEvent) => void;
84+
onEditSubmit?: (newTitle: string) => void;
85+
onEditCancel?: () => void;
7786
}) {
7887
const icon = itemIcon(item);
7988
const statusLabel = runStatusLabel(item.rawStatus);
@@ -86,6 +95,19 @@ export function ChannelItemRow({
8695
icon
8796
);
8897

98+
if (isEditing) {
99+
return (
100+
<InlineEditInput
101+
depth={0}
102+
icon={rowIcon}
103+
label={item.title}
104+
isActive={isActive}
105+
onSubmit={(newTitle) => onEditSubmit?.(newTitle)}
106+
onCancel={() => onEditCancel?.()}
107+
/>
108+
);
109+
}
110+
89111
return (
90112
<PreviewCard.Root>
91113
<PreviewCard.Trigger
@@ -100,6 +122,7 @@ export function ChannelItemRow({
100122
label={<span>{item.title}</span>}
101123
isActive={isActive}
102124
onClick={() => actions.open(item)}
125+
onContextMenu={onContextMenu}
103126
endContent={
104127
<>
105128
<span className={TIMESTAMP_CLASS}>

packages/ui/src/features/canvas/components/ChannelSidebar.tsx

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ import { ChannelBackRow } from "@posthog/ui/features/canvas/components/ChannelBa
3333
import { ChannelItemRow } from "@posthog/ui/features/canvas/components/ChannelItemRow";
3434
import { ChannelsFab } from "@posthog/ui/features/canvas/components/ChannelsFab";
3535
import { useChannelItems } from "@posthog/ui/features/canvas/hooks/useChannelItems";
36+
import { useCommandCenterStore } from "@posthog/ui/features/command-center/commandCenterStore";
3637
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
3738
import { SidebarItem } from "@posthog/ui/features/sidebar/components/SidebarItem";
39+
import { useTaskContextMenu } from "@posthog/ui/features/tasks/useTaskContextMenu";
40+
import { useRenameTask } from "@posthog/ui/features/tasks/useTaskMutations";
41+
import { useTasks } from "@posthog/ui/features/tasks/useTasks";
42+
import { navigateToCommandCenter } from "@posthog/ui/router/navigationBridge";
43+
import { logger } from "@posthog/ui/shell/logger";
3844
import { useNavigate, useRouterState } from "@tanstack/react-router";
3945
import { type ReactNode, useMemo, useState } from "react";
4046

@@ -52,6 +58,7 @@ const cnHeaderButton = (active: boolean) =>
5258
cn(HEADER_ICON_BUTTON_CLASS, active && "bg-fill-selected text-foreground");
5359

5460
const RECENTS_CAP = 30;
61+
const log = logger.scope("channel-sidebar");
5562

5663
function RecentSectionHeader({
5764
searchOpen,
@@ -199,6 +206,18 @@ export function ChannelSidebar({ channelId }: { channelId: string }) {
199206

200207
const { items, actions, me, isLoading, channelMissing } =
201208
useChannelItems(channelId);
209+
const { showContextMenu, editingTaskId, setEditingTaskId } =
210+
useTaskContextMenu();
211+
const { renameTask } = useRenameTask();
212+
const commandCenterCells = useCommandCenterStore((state) => state.cells);
213+
const assignTaskToCommandCenter = useCommandCenterStore(
214+
(state) => state.assignTask,
215+
);
216+
const { data: allTasks = [] } = useTasks({ showAllUsers: true });
217+
const allTaskIds = useMemo(
218+
() => new Set(allTasks.map((task) => task.id)),
219+
[allTasks],
220+
);
202221

203222
const [searchOpen, setSearchOpen] = useState(false);
204223
const [query, setQuery] = useState("");
@@ -227,6 +246,56 @@ export function ChannelSidebar({ channelId }: { channelId: string }) {
227246
[items, query, createdByFilter, statusFilter, me],
228247
);
229248

249+
const taskRow = (item: (typeof items)[number]) => (
250+
<ChannelItemRow
251+
key={item.key}
252+
item={item}
253+
isActive={item.key === activeKey}
254+
actions={actions}
255+
isEditing={item.kind === "task" && editingTaskId === item.id}
256+
onContextMenu={
257+
item.kind === "task"
258+
? (event) =>
259+
void showContextMenu(item, event, {
260+
isPinned: item.pinned,
261+
isInCommandCenter: commandCenterCells.includes(item.id),
262+
hasEmptyCommandCenterCell: commandCenterCells.some(
263+
(taskId) => taskId == null || !allTaskIds.has(taskId),
264+
),
265+
showArchivePrior: false,
266+
onTogglePin: () => actions.togglePin(item),
267+
onArchive: () => actions.archive(item),
268+
onAddToCommandCenter: () => {
269+
const cellIndex = commandCenterCells.findIndex(
270+
(taskId) => taskId == null || !allTaskIds.has(taskId),
271+
);
272+
if (cellIndex === -1) return;
273+
assignTaskToCommandCenter(cellIndex, item.id);
274+
navigateToCommandCenter();
275+
},
276+
})
277+
: undefined
278+
}
279+
onEditSubmit={
280+
item.kind === "task"
281+
? async (newTitle) => {
282+
setEditingTaskId(null);
283+
try {
284+
await renameTask({
285+
taskId: item.id,
286+
currentTitle: item.title,
287+
newTitle,
288+
});
289+
} catch (error) {
290+
log.error("Failed to rename task", error);
291+
}
292+
}
293+
: undefined
294+
}
295+
onEditCancel={() => setEditingTaskId(null)}
296+
/>
297+
);
298+
230299
const sectionRow = (
231300
label: string,
232301
icon: ReactNode,
@@ -313,14 +382,7 @@ export function ChannelSidebar({ channelId }: { channelId: string }) {
313382
<>
314383
<MenuLabel>Pinned</MenuLabel>
315384
<div className="flex flex-col gap-px">
316-
{pinnedItems.map((item) => (
317-
<ChannelItemRow
318-
key={item.key}
319-
item={item}
320-
isActive={item.key === activeKey}
321-
actions={actions}
322-
/>
323-
))}
385+
{pinnedItems.map(taskRow)}
324386
</div>
325387
</>
326388
)}
@@ -343,14 +405,7 @@ export function ChannelSidebar({ channelId }: { channelId: string }) {
343405
/>
344406
{recentItems.length > 0 ? (
345407
<div className="flex flex-col gap-px">
346-
{recentItems.map((item) => (
347-
<ChannelItemRow
348-
key={item.key}
349-
item={item}
350-
isActive={item.key === activeKey}
351-
actions={actions}
352-
/>
353-
))}
408+
{recentItems.map(taskRow)}
354409
</div>
355410
) : (
356411
<Empty className="border-0 py-6">

packages/ui/src/features/canvas/components/TaskArtifactsList.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,26 @@ describe("TaskArtifactsList", () => {
8383
expect(state.reviewModes[task.id]).toBe("split");
8484
});
8585

86+
it("lists every PR produced by the same run", () => {
87+
mocks.runs = [
88+
{
89+
...run("run-1"),
90+
output: {
91+
pr_url: "https://github.com/acme/repo/pull/1",
92+
pr_urls: [
93+
"https://github.com/acme/repo/pull/1",
94+
"https://github.com/acme/other-repo/pull/2",
95+
],
96+
},
97+
} as TaskRun,
98+
];
99+
100+
render(<TaskArtifactsList task={task} timeline={[]} />);
101+
102+
expect(screen.getByText("Pull request #1")).toBeTruthy();
103+
expect(screen.getByText("Pull request #2")).toBeTruthy();
104+
});
105+
86106
it("lists the files the agent uploaded, with their size", () => {
87107
mocks.runs = [
88108
run("run-1", { artifacts: [outputFile({ id: "a", size: 16861 })] }),

packages/ui/src/features/canvas/components/TaskArtifactsList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
EmptyMedia,
1717
EmptyTitle,
1818
} from "@posthog/quill";
19+
import { readPrUrls } from "@posthog/shared";
1920
import type {
2021
Task,
2122
TaskRun,
@@ -92,8 +93,7 @@ function buildRows(
9293
// every copy would bury the current one under its own drafts.
9394
const newestByName = new Map<string, { file: RunArtifact; runId: string }>();
9495
for (const run of allRuns) {
95-
const outputPr = run.output?.pr_url;
96-
if (typeof outputPr === "string" && outputPr) {
96+
for (const outputPr of readPrUrls(run.output)) {
9797
addPr(outputPr, `output-pr:${outputPr}`);
9898
}
9999
for (const file of readRunOutputs(run)) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Space collections stay visible while they revalidate. Only mounted (active)
2+
// space queries poll; inactive spaces remain cached for fast switching.
3+
export const SPACE_QUERY_STALE_TIME_MS = 15_000;
4+
export const SPACE_QUERY_GC_TIME_MS = 30 * 60_000;
5+
export const SPACE_QUERY_REFETCH_INTERVAL_MS = 15_000;

packages/ui/src/features/canvas/hooks/useChannelFeed.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import type { Task } from "@posthog/shared/domain-types";
22
import { useAuthenticatedQuery } from "@posthog/ui/hooks/useAuthenticatedQuery";
33
import { useMemo } from "react";
4+
import {
5+
SPACE_QUERY_GC_TIME_MS,
6+
SPACE_QUERY_STALE_TIME_MS,
7+
} from "./spaceQueryPolicy";
48

59
// Feeds are multiplayer: poll fast enough that a teammate's new task card and
610
// run-status flips feel live without a dedicated push channel.
711
const CHANNEL_FEED_POLL_INTERVAL_MS = 5_000;
12+
export const channelFeedQueryRoot = ["channel-feed"] as const;
813

914
export function channelFeedQueryKey(channelId: string | undefined) {
10-
return ["channel-feed", channelId ?? "none"] as const;
15+
return [...channelFeedQueryRoot, channelId ?? "none"] as const;
1116
}
1217

1318
/**
@@ -24,7 +29,9 @@ export function useChannelFeed(channelId: string | undefined): {
2429
client.getTasks({ channel: channelId }) as unknown as Promise<Task[]>,
2530
{
2631
enabled: !!channelId,
32+
gcTime: SPACE_QUERY_GC_TIME_MS,
2733
refetchInterval: CHANNEL_FEED_POLL_INTERVAL_MS,
34+
staleTime: SPACE_QUERY_STALE_TIME_MS,
2835
},
2936
);
3037
const tasks = useMemo(

0 commit comments

Comments
 (0)