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';
|
} from '@renderer/constants/cssVariables';
|
||||||
import { getTeamColorSet, getThemedBorder } from '@renderer/constants/teamColors';
|
import { getTeamColorSet, getThemedBorder } from '@renderer/constants/teamColors';
|
||||||
import { useTheme } from '@renderer/hooks/useTheme';
|
import { useTheme } from '@renderer/hooks/useTheme';
|
||||||
|
import { useStore } from '@renderer/store';
|
||||||
import {
|
import {
|
||||||
getMessageTypeLabel,
|
getMessageTypeLabel,
|
||||||
getStructuredMessageSummary,
|
getStructuredMessageSummary,
|
||||||
|
|
@ -47,12 +48,15 @@ import {
|
||||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Check,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Clock,
|
||||||
Command,
|
Command,
|
||||||
ListPlus,
|
ListPlus,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Reply,
|
Reply,
|
||||||
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { ReplyQuoteBlock } from './ReplyQuoteBlock';
|
import { ReplyQuoteBlock } from './ReplyQuoteBlock';
|
||||||
|
|
@ -269,10 +273,12 @@ const NoiseRow = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
colors,
|
colors,
|
||||||
|
icon,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
colors: TeamColorSet;
|
colors: TeamColorSet;
|
||||||
|
icon?: React.ReactNode;
|
||||||
}): React.JSX.Element => (
|
}): React.JSX.Element => (
|
||||||
<div className="flex items-center gap-2 px-3 py-1" style={{ opacity: 0.45 }}>
|
<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 }} />
|
<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 }}>
|
<span className="text-[11px]" style={{ color: CARD_ICON_MUTED }}>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -594,9 +601,36 @@ export const ActivityItem = memo(
|
||||||
]);
|
]);
|
||||||
const summaryText = useMemo(() => extractMarkdownPlainText(rawSummary), [rawSummary]);
|
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
|
// Noise messages: minimal inline row
|
||||||
if (noiseLabel) {
|
if (noiseLabel) {
|
||||||
return <NoiseRow name={message.from} label={noiseLabel} colors={colors} />;
|
return (
|
||||||
|
<NoiseRow name={message.from} label={noiseLabel} colors={colors} icon={permissionIcon} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageType =
|
const messageType =
|
||||||
|
|
|
||||||
|
|
@ -683,12 +683,18 @@ export function initializeNotificationListeners(): () => void {
|
||||||
const cleanup = api.teams.onToolApprovalEvent((_event: unknown, data: unknown) => {
|
const cleanup = api.teams.onToolApprovalEvent((_event: unknown, data: unknown) => {
|
||||||
const event = data as ToolApprovalEvent;
|
const event = data as ToolApprovalEvent;
|
||||||
if ('autoResolved' in event && event.autoResolved) {
|
if ('autoResolved' in event && event.autoResolved) {
|
||||||
// Timeout or auto-allow resolved in main — remove from UI
|
// Timeout or auto-allow resolved in main — remove from UI and record result
|
||||||
useStore.setState((s) => ({
|
const allowed = event.reason !== 'timeout_deny';
|
||||||
pendingApprovals: s.pendingApprovals.filter(
|
useStore.setState((s) => {
|
||||||
(a) => !(a.runId === event.runId && a.requestId === event.requestId)
|
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) {
|
} else if ('dismissed' in event && event.dismissed) {
|
||||||
const dismiss = event;
|
const dismiss = event;
|
||||||
useStore.setState((s) => ({
|
useStore.setState((s) => ({
|
||||||
|
|
|
||||||
|
|
@ -687,6 +687,8 @@ export interface TeamSlice {
|
||||||
subscribeProvisioningProgress: () => void;
|
subscribeProvisioningProgress: () => void;
|
||||||
unsubscribeProvisioningProgress: () => void;
|
unsubscribeProvisioningProgress: () => void;
|
||||||
pendingApprovals: ToolApprovalRequest[];
|
pendingApprovals: ToolApprovalRequest[];
|
||||||
|
/** Resolved permission approvals: request_id → allowed (true/false). Used for noise row icons. */
|
||||||
|
resolvedApprovals: Map<string, boolean>;
|
||||||
toolApprovalSettings: ToolApprovalSettings;
|
toolApprovalSettings: ToolApprovalSettings;
|
||||||
updateToolApprovalSettings: (patch: Partial<ToolApprovalSettings>) => Promise<void>;
|
updateToolApprovalSettings: (patch: Partial<ToolApprovalSettings>) => Promise<void>;
|
||||||
respondToToolApproval: (
|
respondToToolApproval: (
|
||||||
|
|
@ -896,6 +898,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
deletedTasks: [],
|
deletedTasks: [],
|
||||||
deletedTasksLoading: false,
|
deletedTasksLoading: false,
|
||||||
pendingApprovals: [],
|
pendingApprovals: [],
|
||||||
|
resolvedApprovals: new Map(),
|
||||||
toolApprovalSettings: loadToolApprovalSettings(),
|
toolApprovalSettings: loadToolApprovalSettings(),
|
||||||
|
|
||||||
// Messages panel UI state
|
// Messages panel UI state
|
||||||
|
|
@ -2201,11 +2204,16 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
try {
|
try {
|
||||||
await api.teams.respondToToolApproval(teamName, runId, requestId, allow, message);
|
await api.teams.respondToToolApproval(teamName, runId, requestId, allow, message);
|
||||||
// Remove ONLY after successful IPC, by runId+requestId pair
|
// Remove ONLY after successful IPC, by runId+requestId pair
|
||||||
set((s) => ({
|
set((s) => {
|
||||||
pendingApprovals: s.pendingApprovals.filter(
|
const next = new Map(s.resolvedApprovals);
|
||||||
(a) => !(a.runId === runId && a.requestId === requestId)
|
next.set(requestId, allow);
|
||||||
),
|
return {
|
||||||
}));
|
pendingApprovals: s.pendingApprovals.filter(
|
||||||
|
(a) => !(a.runId === runId && a.requestId === requestId)
|
||||||
|
),
|
||||||
|
resolvedApprovals: next,
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
logger.error(`respondToToolApproval failed for ${teamName}/${requestId}: ${msg}`);
|
logger.error(`respondToToolApproval failed for ${teamName}/${requestId}: ${msg}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue