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}`);