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)
This commit is contained in:
parent
569b3b3f5b
commit
d01bce2101
3 changed files with 60 additions and 12 deletions
|
|
@ -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 => (
|
||||
<div className="flex items-center gap-2 px-3 py-1" style={{ opacity: 0.45 }}>
|
||||
<span className="size-2 shrink-0 rounded-full" style={{ backgroundColor: colors.border }} />
|
||||
|
|
@ -282,6 +288,7 @@ const NoiseRow = ({
|
|||
<span className="text-[11px]" style={{ color: CARD_ICON_MUTED }}>
|
||||
{label}
|
||||
</span>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -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 <Check size={12} className="text-emerald-400" />;
|
||||
}
|
||||
if (resolved === false) {
|
||||
return <X size={12} className="text-red-400" />;
|
||||
}
|
||||
const isPending = pendingApprovals.some((a) => a.requestId === requestId);
|
||||
if (isPending) {
|
||||
return <Clock size={12} className="animate-pulse text-amber-400" />;
|
||||
}
|
||||
// Not in pending and not resolved — already handled before we started tracking
|
||||
return <Check size={12} className="text-emerald-400/50" />;
|
||||
}, [structured, pendingApprovals, resolvedApprovals]);
|
||||
|
||||
// Noise messages: minimal inline row
|
||||
if (noiseLabel) {
|
||||
return <NoiseRow name={message.from} label={noiseLabel} colors={colors} />;
|
||||
return (
|
||||
<NoiseRow name={message.from} label={noiseLabel} colors={colors} icon={permissionIcon} />
|
||||
);
|
||||
}
|
||||
|
||||
const messageType =
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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<string, boolean>;
|
||||
toolApprovalSettings: ToolApprovalSettings;
|
||||
updateToolApprovalSettings: (patch: Partial<ToolApprovalSettings>) => Promise<void>;
|
||||
respondToToolApproval: (
|
||||
|
|
@ -896,6 +898,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
|||
deletedTasks: [],
|
||||
deletedTasksLoading: false,
|
||||
pendingApprovals: [],
|
||||
resolvedApprovals: new Map(),
|
||||
toolApprovalSettings: loadToolApprovalSettings(),
|
||||
|
||||
// Messages panel UI state
|
||||
|
|
@ -2201,11 +2204,16 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (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}`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue