Skip to content
Merged
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
32 changes: 16 additions & 16 deletions apps/code/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -317,33 +317,33 @@ snapshots:
features-messageeditor-promptinput--with-multiple-chips--light:
hash: v1.k4693efd2.f844653d745a308116b94546ec6605028e90246b6aa9fa640a1b8f9c579193e1.PkuP-7A0MOZk7V3mpsJDx4pwJA0VoIzMnp4BIgDQ45U
features-sessions-conversationview--all-tool-calls--dark:
hash: v1.k4693efd2.602ee87d6d7a745d4b09d9e49bd12a70e52299b78ca97a48b5ca73a01532d801.Imd_IYgKXb6kcfjhRfBhmc4-AoPoKcfg1QRNXB4NL_I
hash: v1.k4693efd2.f4949811298614ff01aef988b12418ce3b99f1c9ce2428b81cb9d1ca201afa83.2sjUZgIeLsL94--VLigefyWa7Dt-CMgn6e3kZBwIbbs
features-sessions-conversationview--all-tool-calls--light:
hash: v1.k4693efd2.1636dfc3ee0dd9d28065166f2fff0193f2d499692c74749d3afa7585aae898b5.7Ez0SM2DUXepp6Q8SgUyX3zVbdbf2kvglPqJsEc-eB4
hash: v1.k4693efd2.1b1de752527fdac9d07e0e4fadd22bebb6cedb9afe70b2f69cdc1f8efc1426cf.a9nv4k0LOdXfShX-O3KeQ8_fZZimICJhBfbFYSLEHvA
features-sessions-conversationview--empty--dark:
hash: v1.k4693efd2.f7b3396b9c83b952221044d2058df7ddf985fab959dba2c691c321ca528ca2af.0rJM8GT1SXRzo0MFo3OPgV6oL4d8sLPm9vz18-na5iw
features-sessions-conversationview--empty--light:
hash: v1.k4693efd2.8fe9c532387523d9642ec7e9265d88435137aa739cb3763a0024335997697b12.ZtBmVlV92Opv5y4NHYDTPt-xvgcel_Z8JMfgnZJ9ZOE
features-sessions-conversationview--long-conversation--dark:
hash: v1.k4693efd2.34cfdd21ad9f643d7cf82b618f0e8e5c25856bc9675f816159bb0c4e4f67fa23.wrleGZzGujLjrbU_CMCGg30OLjDf63_XJlJ-7QJZS3A
hash: v1.k4693efd2.07392ebacdd12e07e700f11cccf04c3be2c362cb7e2f41976e26923e59b27160.jOla4JdWmbahmhpjrr0w8tCip212FVig7-XeVlSNjjg
features-sessions-conversationview--long-conversation--light:
hash: v1.k4693efd2.46629a82ba4239edf9d576f46a9696b33e7f63115aa773ada55af910d1cfec0c.c_Dh3-59F1Y3ehdwhro7_ImEILRdDK_42mmHui-shSY
hash: v1.k4693efd2.e012849e1a5de1f56373b5e9160d2619bd337c982144284eb96a126f7a304416.YYx7K2lNsepUTPHfH3ySLJFCKB-n_4i33mz1rC5L4Q8
features-sessions-conversationview--markdown-debug--dark:
hash: v1.k4693efd2.a1c2f118c135f553d72f1ed783782a8d9d37de47280c94a01f0aeed764f64d33.kZrv6ZRnKbRkFhgm_QYWvlqjC7VQbBlWCOnfi5pxZyY
hash: v1.k4693efd2.3d1d3f2223d8610891f2e9a7f24ad99e9a3b368f64148df4b7d8beb4bf816a0b.ujd-hdWnmS3f85tkT4ip4mdbYWaWj0zhJSjrBdwrMrc
features-sessions-conversationview--markdown-debug--light:
hash: v1.k4693efd2.75d098556db1af478095a77c26d7672a17a1651b8630341bd987607254d5cba1.wqcro6zc8l95cDgebzeZt-h5HIxnlwQKCEHiNrlP1v8
hash: v1.k4693efd2.bfef475de0f4bbf6e8d75249764169782b55cd945b619a9c93e961ca8e65ce33.wjN3li8QUanEMSlbJO1y0k3VlGmLa_7z12rRqojY7wM
features-sessions-conversationview--markdown-showcase--dark:
hash: v1.k4693efd2.b9901edc583c07111e8f515c557eeb547d767b2f29d61fa9a50523fa5275f6ec.KXz4KANV2VPPLFzt7_A0UTivT__wivJLBJSgJQfy2Es
hash: v1.k4693efd2.1f48560e8958d45cb99858a5d46055a8b53ed341ab19ef3afd0812686c6459cd.8QjEBONE-KR7xm0-dYJsf4I_ftvUzEz5DW5KHSEALIk
features-sessions-conversationview--markdown-showcase--light:
hash: v1.k4693efd2.9ad5d2969e031ce98aeff5e9ff527118611ac6aaa73a83424d60acf777a32505.eNCuFlo2qByk8LARAV4cRqKE4UsIiknv920p6AyyfpQ
hash: v1.k4693efd2.999acf075dc1f41dee05f93a86f514a41e27e92f22ddbf441cfe204488d6aa71.163W210QTjmx3S8ZUO76-r8Qud5M15FD-78ZQFTd94I
features-sessions-conversationview--single-turn--dark:
hash: v1.k4693efd2.26b602bfb096c1b19ed2e9ed8d363d3c8c77a21828df32f71a83ff053b759a96.tpvBFAGHOcxqIfCbU49YjzSe0GwuDreM8fFCWhlDs58
features-sessions-conversationview--single-turn--light:
hash: v1.k4693efd2.20569531952a9f448601f8d0a0ccf5ae79341b904b4f79987142a2574c95e404.8HpLPHXhZpEX9kA5Hn78WskRYFAUev8X7SLPeWGSEbA
features-sessions-conversationview--with-pending-prompt--dark:
hash: v1.k4693efd2.602ee87d6d7a745d4b09d9e49bd12a70e52299b78ca97a48b5ca73a01532d801.F_1XKDzCxzMm_opEt9FvzczBb_2n-vyvOyGFXHGJmTk
hash: v1.k4693efd2.f4949811298614ff01aef988b12418ce3b99f1c9ce2428b81cb9d1ca201afa83.tERhjA4UEzpFY061lsvkohwSyDIzQphlOOLb3sv73_g
features-sessions-conversationview--with-pending-prompt--light:
hash: v1.k4693efd2.1636dfc3ee0dd9d28065166f2fff0193f2d499692c74749d3afa7585aae898b5.YxTPa4CsqI2YuGLUdck8340ITZQrpna_C6z-SgII4Ws
hash: v1.k4693efd2.1b1de752527fdac9d07e0e4fadd22bebb6cedb9afe70b2f69cdc1f8efc1426cf.CBx-n_SpJHiKhihAucVVc0AraAFqSQECVTEGsHE4jL4
features-sessions-toolcallblock--create-new-file--dark:
hash: v1.k4693efd2.8847f96037682460aba47f0e0f5113037918235fc98596933f2d1d06fd6f8f92.SHetexoVOtozsZGZQMcVbmpBlbk7M5D5Q0R_TJvPnyc
features-sessions-toolcallblock--create-new-file--light:
Expand Down Expand Up @@ -561,17 +561,17 @@ snapshots:
git-dialogs--sync-success--light:
hash: v1.k4693efd2.8d79d77c9338aeaa73734836f0e17fde987f6d3239914f4013889d3110e5088d.Rq-w2o1uhes_a5hUWqMXeiDkfnoPCZPt932LkEQXCco
loops-loopslistview--comprehensive--dark:
hash: v1.k4693efd2.b9223e477701b5ef4587310a1de71500dd5214222531648ef76d66766b049a9f.2lckIm16WmmwZv1xVZnUL42InIHDzemQwU3EJzzw9_U
hash: v1.k4693efd2.c128012b7f910839e65e8eaf14d5456c93f90d77747d0625ca35fddd8d5adf7a.fuugB5Ov6boP7fg6fWP7A7IG2yYnPUAAWLKZLuV8rZ0
loops-loopslistview--comprehensive--light:
hash: v1.k4693efd2.1f172c831aefdfd4b3bae97af84f80aae3c09c4919f6b1b68315240cc482af50.hnrwbv8il_Ay6DcYxmQXNtxKU3rqOIWAkjnGB8n4AT4
hash: v1.k4693efd2.4d25a48e0bd7e60d3bbb433e246486e4c9ac95979db86e20eb8bf5cb119d1eb2.EzrENjVHwVMyV00pEA-71TeVomJtchiym5DqWFkujlA
loops-loopslistview--long-mixed-list--dark:
hash: v1.k4693efd2.654250bc74fff46c496747eb13031e0e7a7ef264be5bb7be41451d82a497a515.vvrxXbbABkVY3hcHS3oThQ339JFgwIzB8E4n1usVWf0
hash: v1.k4693efd2.fdbf0c24cb386c4345279db3e648efe72aa8a2a984d491361522f62074a771c9.2QmRxRMWYFFfBFrbMm0Dm9rWcvd1lfRbc0egfzoESnY
loops-loopslistview--long-mixed-list--light:
hash: v1.k4693efd2.8dee426bab2a63843aefe9cf0765799ae9e3c5ece4adc522dc63faf3b362716b.K90qmg7hD8qD522iR-Y5lbr6ETEzRuYgQdYl0Z520do
hash: v1.k4693efd2.aa17d9b6f40fbd3914bd742951db9f90add0bd233c54170803c8483aeb45b43b.yXpqqGczOq1uW5k4Ie7ddNTnne-cFTglZ9cL800pXB4
loops-loopslistview--with-builder-sessions--dark:
hash: v1.k4693efd2.84cfc28bedc22a6728ba4ecf69274ab03537faeed5333775991716bf0b5f439e.fHsD0XKPnud-oDM4PLJ7edCbMnVcW6uVMdMHBYp3K08
hash: v1.k4693efd2.df4a1a4764e1de7b69905814b09c30d9b43f705f5e66c7ad83829c567d4faa1a.jthufoX_KPbbxScjj3pPBA4lEtT9Ym6zUCH-v1759GM
loops-loopslistview--with-builder-sessions--light:
hash: v1.k4693efd2.fb6c9b85bd660a6be4645cf7ba8fe3ab08a169e3823e5354325b7d7ecbe13aba.4FbKrdxiUN1PZOGwFa_kl7Rpz3Z8DnTVNJvUM_3FIp0
hash: v1.k4693efd2.eb65c38bb0de7efedbc0d7a6c27a6695528e2e6b60949617e7d22335b1e68c8b.fJr9uykvc8S7LI_ASXk5KRqxXCwIqh6ld9zfzs4shP0
scouts-scoutsfleetlist--filtered-to-you--dark:
hash: v1.k4693efd2.a3af6e76ef36598eaa347bedc4430878ed62754660166398e0576a9978cd084b.x3Ky-O6HOw0srWdOmnnKJwFNpmGmQVWip0t7CwVaRXY
scouts-scoutsfleetlist--filtered-to-you--light:
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/renderer/desktop-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { agentUiModule } from "@posthog/ui/features/agent/agent.module";
import { authUiModule } from "@posthog/ui/features/auth/auth.module";
import { billingUiModule } from "@posthog/ui/features/billing/billing.module";
import { browserTabsUiModule } from "@posthog/ui/features/browser-tabs/browser-tabs.module";
import { taskActivityUiModule } from "@posthog/ui/features/canvas/task-activity/taskActivity.module";
import { cloneUiModule } from "@posthog/ui/features/clone/clone.module";
import { connectivityUiModule } from "@posthog/ui/features/connectivity/connectivity.module";
import { discordPresenceUiModule } from "@posthog/ui/features/discord-presence/discordPresence.module";
Expand All @@ -36,6 +37,7 @@ export function registerDesktopContributions(): void {
authUiModule,
autoresearchCoreModule,
billingUiModule,
taskActivityUiModule,
taskThreadCoreModule,
browserTabsUiModule,
cloneUiModule,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/web-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ import {
BROWSER_TABS_CLIENT,
type BrowserTabsClient,
} from "@posthog/ui/features/browser-tabs/browserTabsClient";
import { taskActivityUiModule } from "@posthog/ui/features/canvas/task-activity/taskActivity.module";
import {
REVIEW_HOST,
type ReviewHost,
Expand Down Expand Up @@ -779,6 +780,7 @@ container.bind(REVIEW_HOST).toConstantValue(webReviewHost);
// (notificationsUiModule) is resolved by SessionService on task events and by
// the settings test harness; it needs these three providers.
container.load(notificationsUiModule);
container.load(taskActivityUiModule);
container.bind(NOTIFICATIONS_SERVICE).toConstantValue(webNotifications);
container
.bind(NOTIFICATION_SETTINGS_PROVIDER)
Expand Down
50 changes: 50 additions & 0 deletions packages/api-client/src/posthog-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ import type {
SuggestedReviewersArtefact,
SuggestedReviewerWriteEntry,
Task,
TaskActivityMarkReadResult,
TaskActivityPage,
TaskActivityReadMarker,
TaskChannel,
TaskMention,
TaskRun,
Expand Down Expand Up @@ -2506,6 +2509,53 @@ export class PostHogAPIClient {
return (await response.json()) as TaskMention[];
}

// Tasks the current user is involved in (created, mentioned, or messaged),
// one row per task, newest activity first.
async getTaskActivity(options?: {
before?: string;
beforeId?: string;
}): Promise<TaskActivityPage> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/task_activity/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
if (options?.before && options.beforeId) {
url.searchParams.set("before", options.before);
url.searchParams.set("before_id", options.beforeId);
}
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) {
throw new Error(`Failed to fetch task activity: ${response.statusText}`);
}
return (await response.json()) as TaskActivityPage;
}

// Read state is per task, so callers name the tasks the user has seen rather than
// clearing the whole feed.
async markTaskActivityRead(
activities: TaskActivityReadMarker[],
): Promise<TaskActivityMarkReadResult> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/task_activity/mark_read/`;
const response = await this.api.fetcher.fetch({
method: "post",
url: new URL(`${this.api.baseUrl}${urlPath}`),
path: urlPath,
overrides: {
body: JSON.stringify({ activities }),
},
});
if (!response.ok) {
throw new Error(
`Failed to mark task activity read: ${response.statusText}`,
);
}
return (await response.json()) as TaskActivityMarkReadResult;
}

async getTaskThreadMessages(taskId: string): Promise<TaskThreadMessage[]> {
const teamId = await this.getTeamId();
const urlPath = `/api/projects/${teamId}/tasks/${taskId}/thread_messages/`;
Expand Down
68 changes: 68 additions & 0 deletions packages/core/src/canvas/taskActivity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { TaskActivity, UserBasic } from "@posthog/shared/domain-types";
import { describe, expect, it } from "vitest";
import { toTaskActivityItems } from "./taskActivity";

const ann: UserBasic = {
id: 2,
uuid: "ann-uuid",
email: "ann@posthog.com",
first_name: "Ann",
};

function activity(overrides: Partial<TaskActivity> = {}): TaskActivity {
return {
id: "activity-1",
task_id: "t1",
task_title: "Task t1",
channel_id: "c1",
channel_name: "general",
activity_at: "2026-07-01T10:00:00Z",
activity_kind: "mention",
snippet: "ping @[Me](me@posthog.com)",
latest_author: ann,
latest_message_id: "m1",
is_unread: true,
...overrides,
};
}

describe("toTaskActivityItems", () => {
it("maps the authoritative activity and unread state", () => {
expect(toTaskActivityItems([activity()])).toEqual([
{
id: "activity-1",
taskId: "t1",
taskTitle: "Task t1",
channelId: "c1",
channelName: "general",
activityAt: "2026-07-01T10:00:00Z",
activityKind: "mention",
snippet: "ping @[Me](me@posthog.com)",
author: ann,
messageId: "m1",
isUnread: true,
},
]);
});

it("labels untitled tasks and tolerates missing optional values", () => {
const [item] = toTaskActivityItems([
activity({
task_title: "",
channel_id: null,
channel_name: null,
latest_author: null,
latest_message_id: null,
activity_kind: "created",
snippet: "",
}),
]);
expect(item).toMatchObject({
taskTitle: "Untitled task",
channelId: null,
channelName: null,
author: null,
messageId: null,
});
});
});
48 changes: 48 additions & 0 deletions packages/core/src/canvas/taskActivity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type {
TaskActivity,
TaskActivityKind,
UserBasic,
} from "@posthog/shared/domain-types";

/**
* The Activity feed — tasks the current user is involved in (created, mentioned
* in, or messaged in) — as served by the backend task-activity index
* (`getTaskActivity`). One row per task, newest activity first; the client only
* maps DTOs to items.
*/

export interface TaskActivityItem {
id: string;
taskId: string;
taskTitle: string;
/** Backend channel (tasks product Channel UUID); null for channel-less tasks. */
channelId: string | null;
/** Backend channel name, for the "#channel" label. */
channelName: string | null;
activityAt: string;
activityKind: TaskActivityKind;
/** Content of the message tied to the latest activity; empty for created rows. */
snippet: string;
author: UserBasic | null;
messageId: string | null;
isUnread: boolean;
}

/** Map activity DTOs (already newest-first from the backend) to feed items. */
export function toTaskActivityItems(
activity: readonly TaskActivity[],
): TaskActivityItem[] {
return activity.map((row) => ({
id: row.id,
taskId: row.task_id,
taskTitle: row.task_title || "Untitled task",
channelId: row.channel_id ?? null,
channelName: row.channel_name ?? null,
activityAt: row.activity_at,
activityKind: row.activity_kind,
snippet: row.snippet,
author: row.latest_author ?? null,
messageId: row.latest_message_id ?? null,
isUnread: row.is_unread,
}));
}
31 changes: 31 additions & 0 deletions packages/core/src/sessions/sessionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6758,6 +6758,37 @@ export class SessionService {
return () => {};
}

public async watchCreatedCloudTask(task: Task): Promise<void> {
const run = task.latest_run;
if (run?.environment !== "cloud") return;

const authStatus = await this.getAuthCredentialsStatus();
if (authStatus.kind !== "ready") return;

this.updateSessionTaskTitle(
task.id,
task.title || task.description || "Cloud Task",
);
this.watchCloudTask(
task.id,
run.id,
authStatus.auth.apiHost,
authStatus.auth.projectId,
undefined,
run.log_url,
typeof run.state?.initial_permission_mode === "string"
? run.state.initial_permission_mode
: undefined,
run.runtime_adapter === "codex" ? "codex" : "claude",
run.model ?? undefined,
task.description ?? undefined,
undefined,
run.status,
run.reasoning_effort ?? undefined,
run.state,
);
}

private logReconcileSkipOnce(
taskId: string,
reason: string,
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/task-detail/taskCreationSaga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const host = mockHost as unknown as ITaskCreationHost;
const sessionService = {
connectToTask: vi.fn(),
disconnectFromTask: vi.fn(),
watchCreatedCloudTask: vi.fn(),
rememberInitialCloudPrompt: vi.fn(),
markTaskCreationInFlight: vi.fn(),
} as unknown as SessionService;
Expand Down Expand Up @@ -186,6 +187,9 @@ describe("TaskCreationSaga", () => {
pendingUserArtifactIds: undefined,
});
expect(sendRunCommandMock).not.toHaveBeenCalled();
expect(sessionService.watchCreatedCloudTask).toHaveBeenCalledWith(
startedTask,
);
expect(onTaskReady).toHaveBeenCalledTimes(1);
expect(onTaskReady.mock.calls[0][0].task.latest_run?.branch).toBe(
"release/remembered-branch",
Expand Down Expand Up @@ -633,6 +637,9 @@ describe("TaskCreationSaga", () => {
// Warm-activated at create time: no fresh run is created or started.
expect(createTaskRunMock).not.toHaveBeenCalled();
expect(startTaskRunMock).not.toHaveBeenCalled();
expect(sessionService.watchCreatedCloudTask).toHaveBeenCalledWith(
warmActivatedTask,
);
});

it("suppresses warm reuse when attachments exist but no warm lease is known", async () => {
Expand Down
12 changes: 8 additions & 4 deletions packages/core/src/task-detail/taskCreationSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,11 @@ export class TaskCreationSaga extends Saga<
);
}

if (!hasProvisioning && !shouldStartCloudRun && this.deps.onTaskReady) {
this.deps.onTaskReady({ task, workspace });
if (!hasProvisioning && !shouldStartCloudRun) {
if (!taskId && workspaceMode === "cloud") {
await this.deps.sessionService.watchCreatedCloudTask(task);
}
this.deps.onTaskReady?.({ task, workspace });
}

if (hasProvisioning) {
Expand Down Expand Up @@ -469,8 +472,9 @@ export class TaskCreationSaga extends Saga<
},
});

if (!hasProvisioning && this.deps.onTaskReady) {
this.deps.onTaskReady({ task, workspace });
if (!hasProvisioning) {
await this.deps.sessionService.watchCreatedCloudTask(task);
this.deps.onTaskReady?.({ task, workspace });
}
}

Expand Down
Loading
Loading