From a49bd9b4a8f2c8ebdf043bce383e9c5b724afa41 Mon Sep 17 00:00:00 2001 From: Shy Alter Date: Fri, 31 Jul 2026 12:17:49 +0200 Subject: [PATCH] feat: name the task a comment belongs to so mentions reach its feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A comment on an artifact or canvas points at an id that lives in a run's JSON, not in a table the server can join against, so nothing on the backend could work out which task the comment belonged to. Mentions on those comments therefore reached people by email and by the web inbox but never by this app's Activity page — the one surface where the comment is actually readable. The task id now rides along in item_context, which is the only piece the server was missing. It is client-supplied, so the backend checks it against the team before acting on it, and re-checks visibility when the feed is read. Needs the matching PostHog/posthog change to have any effect; harmless before it lands, since item_context is a free-form JSON field. Generated-By: PostHog Code Task-Id: ec8afa68-0d3b-417a-b9aa-4804b7429d76 --- packages/core/src/comments/anchors.ts | 3 +++ .../canvas/components/TaskCommentsList.tsx | 7 +++++-- .../sessions/components/ArtifactPreview.tsx | 2 +- .../features/sessions/components/useComments.ts | 17 ++++++++++++++--- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/core/src/comments/anchors.ts b/packages/core/src/comments/anchors.ts index 99c1bc6ba5..57e6293fd4 100644 --- a/packages/core/src/comments/anchors.ts +++ b/packages/core/src/comments/anchors.ts @@ -59,6 +59,9 @@ export type CommentAnchor = z.infer; export const commentContextSchema = z.object({ anchor: commentAnchorSchema, threadState: z.enum(["resolved", "open"]).optional(), + // The task the commented resource belongs to. Artifact and canvas ids live in a run's + // JSON rather than a table, so the server can't get back to the task without being told. + taskId: z.string().optional(), }); export type CommentContext = z.infer; diff --git a/packages/ui/src/features/canvas/components/TaskCommentsList.tsx b/packages/ui/src/features/canvas/components/TaskCommentsList.tsx index 19d64f7c68..51c34d4984 100644 --- a/packages/ui/src/features/canvas/components/TaskCommentsList.tsx +++ b/packages/ui/src/features/canvas/components/TaskCommentsList.tsx @@ -123,6 +123,7 @@ function ResourceThreadRow({ thread, source, root, + taskId, members, selected, pulsing, @@ -132,13 +133,14 @@ function ResourceThreadRow({ thread: TaskCommentThread; source: CommentSource; root: ResourceComment; + taskId: string; members: UserBasic[]; selected: boolean; pulsing: boolean; resolution?: HighlightResolution; onOpen: () => void; }) { - const createComment = useCreateComment(source.target); + const createComment = useCreateComment(source.target, taskId); const setResolved = useSetCommentResolved(source.target); return ( @@ -278,7 +280,7 @@ export function TaskCommentsList({ const taskTarget = useMemo(() => taskCommentTarget(task.id), [task.id]); const taskSourceKey = commentTargetKey(taskTarget); - const createTaskComment = useCreateComment(taskTarget); + const createTaskComment = useCreateComment(taskTarget, task.id); const threads = useMemo(() => { const reviewByUrl = new Map(prReviews.byUrl); @@ -531,6 +533,7 @@ export function TaskCommentsList({ thread={thread} source={thread.origin.source} root={thread.origin.root} + taskId={task.id} members={members} selected={thread.id === focusedThreadId} pulsing={thread.id === pulseThreadId} diff --git a/packages/ui/src/features/sessions/components/ArtifactPreview.tsx b/packages/ui/src/features/sessions/components/ArtifactPreview.tsx index 2a9a1fe6ec..96c9c43394 100644 --- a/packages/ui/src/features/sessions/components/ArtifactPreview.tsx +++ b/packages/ui/src/features/sessions/components/ArtifactPreview.tsx @@ -108,7 +108,7 @@ export function ArtifactPreview({ ); const commentsQuery = useCommentsQuery(commentTarget); const { members } = useOrgMembers(); - const createComment = useCreateComment(commentTarget); + const createComment = useCreateComment(commentTarget, taskId); const requestCommentFocus = useCommentNavigationStore( (state) => state.requestCommentFocus, ); diff --git a/packages/ui/src/features/sessions/components/useComments.ts b/packages/ui/src/features/sessions/components/useComments.ts index 21b9d08637..874b7feeb4 100644 --- a/packages/ui/src/features/sessions/components/useComments.ts +++ b/packages/ui/src/features/sessions/components/useComments.ts @@ -133,15 +133,26 @@ export function useCommentsForTargetsQuery( }); } -export function useCreateComment(target: CommentTarget) { +/** + * @param taskId Names the task the resource belongs to, so a mention on it reaches that + * task's activity feed — the server can't resolve an artifact or canvas id back to a task. + */ +export function useCreateComment(target: CommentTarget, taskId?: string) { const service = useService(SESSION_SERVICE); const queryClient = useQueryClient(); const caches = commentCachesCoveringTarget(target); + const contextWithTask = (context: unknown) => + taskId ? { ...(context as Record), taskId } : context; return useMutation({ mutationFn: ( request: Omit, - ) => service.createResourceComment({ ...request, ...target }), + ) => + service.createResourceComment({ + ...request, + ...target, + context: contextWithTask(request.context), + }), onMutate: async (request) => { await queryClient.cancelQueries(caches); const previous = queryClient.getQueriesData(caches); @@ -151,7 +162,7 @@ export function useCreateComment(target: CommentTarget) { content: request.content, created_at: new Date().toISOString(), item_id: target.itemId, - item_context: request.context, + item_context: contextWithTask(request.context), scope: target.scope, source_comment: request.sourceCommentId ?? null, completed_at: null,