From 0c0c30859431570dbcee43386496b89572737eb5 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Fri, 20 Feb 2026 15:52:53 -0600 Subject: [PATCH] Make toast notifications more informative - Server: Add bodySnippet, identifier, issueTitle to comment_added activity details so the UI can show comment content - Client: Show comment snippet in comment toasts instead of just "posted a comment on PAP-39" - Client: Add agent title/role as body text in agent status toasts - Client: Show trigger detail in run status toasts for context PAP-31 Co-Authored-By: Claude Opus 4.6 --- server/src/routes/issues.ts | 14 +++++++-- ui/src/context/LiveUpdatesProvider.tsx | 42 ++++++++++++++++++-------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index 47217fe1..6577e574 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -302,7 +302,12 @@ export function issueRoutes(db: Db, storage: StorageService) { action: "issue.comment_added", entityType: "issue", entityId: issue.id, - details: { commentId: comment.id }, + details: { + commentId: comment.id, + bodySnippet: comment.body.slice(0, 120), + identifier: issue.identifier, + issueTitle: issue.title, + }, }); } @@ -557,7 +562,12 @@ export function issueRoutes(db: Db, storage: StorageService) { action: "issue.comment_added", entityType: "issue", entityId: currentIssue.id, - details: { commentId: comment.id }, + details: { + commentId: comment.id, + bodySnippet: comment.body.slice(0, 120), + identifier: currentIssue.identifier, + issueTitle: currentIssue.title, + }, }); // Merge all wakeups from this comment into one enqueue per agent to avoid duplicate runs. diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index 8c211b17..ae7c4842 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -173,9 +173,14 @@ function buildActivityToast( } const commentId = readString(details?.commentId); + const bodySnippet = readString(details?.bodySnippet); return { - title: `${actor} posted a comment on ${issue.ref}`, - body: issue.title ? truncate(issue.title, 96) : undefined, + title: `${actor} commented on ${issue.ref}`, + body: bodySnippet + ? truncate(bodySnippet.replace(/^#+\s*/m, "").replace(/\n/g, " "), 96) + : issue.title + ? truncate(issue.title, 96) + : undefined, tone: "info", action: { label: `View ${issue.ref}`, href: issue.href }, dedupeKey: `activity:${action}:${entityId}:${commentId ?? "na"}`, @@ -185,6 +190,8 @@ function buildActivityToast( function buildAgentStatusToast( payload: Record, nameOf: (id: string) => string | null, + queryClient: QueryClient, + companyId: string, ): ToastInput | null { const agentId = readString(payload.agentId); const status = readString(payload.status); @@ -199,8 +206,13 @@ function buildAgentStatusToast( ? `${name} is idle` : `${name} errored`; + const agents = queryClient.getQueryData(queryKeys.agents.list(companyId)); + const agent = agents?.find((a) => a.id === agentId); + const body = agent?.title ?? undefined; + return { title, + body, tone, action: { label: "View agent", href: `/agents/${agentId}` }, dedupeKey: `agent-status:${agentId}:${status}`, @@ -217,20 +229,26 @@ function buildRunStatusToast( if (!runId || !agentId || !status || !TERMINAL_RUN_STATUSES.has(status)) return null; const error = readString(payload.error); + const triggerDetail = readString(payload.triggerDetail); const name = nameOf(agentId) ?? `Agent ${shortId(agentId)}`; const tone = status === "succeeded" ? "success" : status === "cancelled" ? "warn" : "error"; - const title = - status === "succeeded" - ? `${name} run succeeded` - : status === "failed" - ? `${name} run failed` - : status === "timed_out" - ? `${name} run timed out` - : `${name} run cancelled`; + const statusLabel = + status === "succeeded" ? "succeeded" + : status === "failed" ? "failed" + : status === "timed_out" ? "timed out" + : "cancelled"; + const title = `${name} run ${statusLabel}`; + + let body: string | undefined; + if (error) { + body = truncate(error, 100); + } else if (triggerDetail) { + body = `Trigger: ${triggerDetail}`; + } return { title, - body: error ? truncate(error, 100) : undefined, + body, tone, ttlMs: status === "succeeded" ? 5000 : 7000, action: { label: "View run", href: `/agents/${agentId}/runs/${runId}` }, @@ -388,7 +406,7 @@ function handleLiveEvent( queryClient.invalidateQueries({ queryKey: queryKeys.org(expectedCompanyId) }); const agentId = readString(payload.agentId); if (agentId) queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId) }); - const toast = buildAgentStatusToast(payload, nameOf); + const toast = buildAgentStatusToast(payload, nameOf, queryClient, expectedCompanyId); if (toast) gatedPushToast(gate, pushToast, "agent-status", toast); return; }