From d01bce2101121943d504309a66479bf4ef82af31 Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 28 Mar 2026 15:52:12 +0200 Subject: [PATCH] feat(team): show approval status icons on permission_request noise rows - Add resolvedApprovals Map to store (request_id -> allowed/denied) - Record result in respondToToolApproval and autoResolved events - NoiseRow accepts optional icon prop - Permission request rows show: check (approved), x (denied), clock (pending), or faded check (pre-existing/unknown) --- .../components/team/activity/ActivityItem.tsx | 36 ++++++++++++++++++- src/renderer/store/index.ts | 18 ++++++---- src/renderer/store/slices/teamSlice.ts | 18 +++++++--- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/renderer/components/team/activity/ActivityItem.tsx b/src/renderer/components/team/activity/ActivityItem.tsx index f9bf3dd6..11d2ddce 100644 --- a/src/renderer/components/team/activity/ActivityItem.tsx +++ b/src/renderer/components/team/activity/ActivityItem.tsx @@ -16,6 +16,7 @@ import { } from '@renderer/constants/cssVariables'; import { getTeamColorSet, getThemedBorder } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; +import { useStore } from '@renderer/store'; import { getMessageTypeLabel, getStructuredMessageSummary, @@ -47,12 +48,15 @@ import { import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity'; import { AlertTriangle, + Check, ChevronRight, + Clock, Command, ListPlus, Maximize2, RefreshCw, Reply, + X, } from 'lucide-react'; import { ReplyQuoteBlock } from './ReplyQuoteBlock'; @@ -269,10 +273,12 @@ const NoiseRow = ({ name, label, colors, + icon, }: { name: string; label: string; colors: TeamColorSet; + icon?: React.ReactNode; }): React.JSX.Element => (
@@ -282,6 +288,7 @@ const NoiseRow = ({ {label} + {icon}
); @@ -594,9 +601,36 @@ export const ActivityItem = memo( ]); const summaryText = useMemo(() => extractMarkdownPlainText(rawSummary), [rawSummary]); + // Permission request status icon (check/x/clock) + const pendingApprovals = useStore((s) => s.pendingApprovals); + const resolvedApprovals = useStore((s) => s.resolvedApprovals); + const permissionIcon = useMemo(() => { + if (!structured) return null; + const type = typeof structured.type === 'string' ? structured.type : null; + if (type !== 'permission_request') return null; + const requestId = typeof structured.request_id === 'string' ? structured.request_id : null; + if (!requestId) return null; + + const resolved = resolvedApprovals.get(requestId); + if (resolved === true) { + return ; + } + if (resolved === false) { + return ; + } + const isPending = pendingApprovals.some((a) => a.requestId === requestId); + if (isPending) { + return ; + } + // Not in pending and not resolved — already handled before we started tracking + return ; + }, [structured, pendingApprovals, resolvedApprovals]); + // Noise messages: minimal inline row if (noiseLabel) { - return ; + return ( + + ); } const messageType = diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts index 5f7dd39a..26f2c6b9 100644 --- a/src/renderer/store/index.ts +++ b/src/renderer/store/index.ts @@ -683,12 +683,18 @@ export function initializeNotificationListeners(): () => void { const cleanup = api.teams.onToolApprovalEvent((_event: unknown, data: unknown) => { const event = data as ToolApprovalEvent; if ('autoResolved' in event && event.autoResolved) { - // Timeout or auto-allow resolved in main — remove from UI - useStore.setState((s) => ({ - pendingApprovals: s.pendingApprovals.filter( - (a) => !(a.runId === event.runId && a.requestId === event.requestId) - ), - })); + // Timeout or auto-allow resolved in main — remove from UI and record result + const allowed = event.reason !== 'timeout_deny'; + useStore.setState((s) => { + const next = new Map(s.resolvedApprovals); + next.set(event.requestId, allowed); + return { + pendingApprovals: s.pendingApprovals.filter( + (a) => !(a.runId === event.runId && a.requestId === event.requestId) + ), + resolvedApprovals: next, + }; + }); } else if ('dismissed' in event && event.dismissed) { const dismiss = event; useStore.setState((s) => ({ diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index 2a452f6e..45f06569 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -687,6 +687,8 @@ export interface TeamSlice { subscribeProvisioningProgress: () => void; unsubscribeProvisioningProgress: () => void; pendingApprovals: ToolApprovalRequest[]; + /** Resolved permission approvals: request_id → allowed (true/false). Used for noise row icons. */ + resolvedApprovals: Map; toolApprovalSettings: ToolApprovalSettings; updateToolApprovalSettings: (patch: Partial) => Promise; respondToToolApproval: ( @@ -896,6 +898,7 @@ export const createTeamSlice: StateCreator = (set, deletedTasks: [], deletedTasksLoading: false, pendingApprovals: [], + resolvedApprovals: new Map(), toolApprovalSettings: loadToolApprovalSettings(), // Messages panel UI state @@ -2201,11 +2204,16 @@ export const createTeamSlice: StateCreator = (set, try { await api.teams.respondToToolApproval(teamName, runId, requestId, allow, message); // Remove ONLY after successful IPC, by runId+requestId pair - set((s) => ({ - pendingApprovals: s.pendingApprovals.filter( - (a) => !(a.runId === runId && a.requestId === requestId) - ), - })); + set((s) => { + const next = new Map(s.resolvedApprovals); + next.set(requestId, allow); + return { + pendingApprovals: s.pendingApprovals.filter( + (a) => !(a.runId === runId && a.requestId === requestId) + ), + resolvedApprovals: next, + }; + }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); logger.error(`respondToToolApproval failed for ${teamName}/${requestId}: ${msg}`);