feat: show tool approval in Awaiting Replies + Allow All button

- PendingRepliesBlock: show "user — awaiting approval" entry when
  pendingApprovals exist, with amber pulse dot and ShieldQuestion icon
- ToolApprovalSheet: add grey "Allow all" button (separated by divider)
  right of Allow/Deny — enables autoAllowAll setting instantly
This commit is contained in:
iliya 2026-03-21 12:31:51 +02:00
parent 73661c80a4
commit d92ab08416
2 changed files with 102 additions and 18 deletions

View file

@ -104,6 +104,7 @@ const RESPOND_TIMEOUT_MS = 10_000;
export const ToolApprovalSheet: React.FC = () => {
const pendingApprovals = useStore((s) => s.pendingApprovals);
const respondToToolApproval = useStore((s) => s.respondToToolApproval);
const updateToolApprovalSettings = useStore((s) => s.updateToolApprovalSettings);
const teams = useStore((s) => s.teams);
const selectedTeamName = useStore((s) => s.selectedTeamName);
const { isLight } = useTheme();
@ -281,6 +282,32 @@ export const ToolApprovalSheet: React.FC = () => {
>
Deny
</button>
<div className="mx-1 h-4 w-px" style={{ backgroundColor: 'var(--color-border)' }} />
<button
type="button"
onClick={() => void updateToolApprovalSettings({ autoAllowAll: true })}
className="rounded-md px-2.5 py-1.5 text-[10px] transition-colors"
style={{
color: 'var(--color-text-muted)',
backgroundColor: 'var(--color-surface-raised)',
}}
onMouseEnter={(e) => {
Object.assign(e.currentTarget.style, {
color: 'var(--color-text-secondary)',
backgroundColor: 'var(--color-border-emphasis)',
});
}}
onMouseLeave={(e) => {
Object.assign(e.currentTarget.style, {
color: 'var(--color-text-muted)',
backgroundColor: 'var(--color-surface-raised)',
});
}}
>
Allow all
</button>
</div>
{pendingApprovals.length > 1 && (
<span className="text-[11px] text-[var(--color-text-muted)]">

View file

@ -1,6 +1,7 @@
import { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import {
agentAvatarUrl,
@ -9,7 +10,7 @@ import {
} from '@renderer/utils/memberHelpers';
import { nameColorSet } from '@renderer/utils/projectColor';
import { formatDistanceToNowStrict } from 'date-fns';
import { Users } from 'lucide-react';
import { ShieldQuestion, Users } from 'lucide-react';
import type { ResolvedTeamMember } from '@shared/types';
@ -32,6 +33,7 @@ export const PendingRepliesBlock = ({
onMemberClick,
}: PendingRepliesBlockProps): React.JSX.Element | null => {
const { isLight } = useTheme();
const pendingApprovals = useStore((s) => s.pendingApprovals);
const colorMap = buildMemberColorMap(members);
const memberPending = Object.entries(pendingRepliesByMember)
.map(([name, sentAtMs]) => ({
@ -49,7 +51,17 @@ export const PendingRepliesBlock = ({
teamName: entry.teamName,
sentAtMs: entry.sentAtMs,
}));
const pending = [...memberPending, ...teamPending].sort((a, b) => b.sentAtMs - a.sentAtMs);
// Tool approvals awaiting user response
const userPending = pendingApprovals.map((a) => ({
kind: 'user' as const,
toolName: a.toolName,
sentAtMs: new Date(a.receivedAt).getTime(),
}));
const pending = [...memberPending, ...teamPending, ...userPending].sort(
(a, b) => b.sentAtMs - a.sentAtMs
);
if (pending.length === 0) return null;
@ -137,45 +149,90 @@ export const PendingRepliesBlock = ({
);
}
const colors = nameColorSet(entry.teamName, isLight);
if (entry.kind === 'team') {
const colors = nameColorSet(entry.teamName, isLight);
return (
<article
key={`pending-reply:team:${entry.teamName}:${entry.sentAtMs}`}
className="activity-card-enter-animate overflow-hidden rounded-md"
style={{
backgroundColor: CARD_BG,
border: CARD_BORDER_STYLE,
borderLeft: `3px solid ${colors.border}`,
}}
>
<div className="flex items-center gap-2 px-3 py-2">
<span className="relative inline-flex shrink-0 items-center justify-center rounded-full bg-[var(--color-surface-raised)] p-1">
<Users size={12} style={{ color: colors.border }} />
<span className="absolute -bottom-0.5 -right-0.5 flex size-2.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" />
<span className="relative inline-flex size-full rounded-full bg-emerald-500" />
</span>
</span>
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
style={{
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
}}
title={entry.teamName}
>
{entry.teamName}
</span>
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
external team
</span>
<span
className="min-w-0 flex-1 truncate text-[10px]"
style={{ color: CARD_ICON_MUTED }}
title="Cross-team message sent, awaiting reply"
>
awaiting reply
</span>
<span className="shrink-0 text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{since}
</span>
</div>
</article>
);
}
// User tool approval pending
return (
<article
key={`pending-reply:team:${entry.teamName}:${entry.sentAtMs}`}
key={`pending-reply:user:${entry.sentAtMs}`}
className="activity-card-enter-animate overflow-hidden rounded-md"
style={{
backgroundColor: CARD_BG,
border: CARD_BORDER_STYLE,
borderLeft: `3px solid ${colors.border}`,
borderLeft: '3px solid var(--color-text-muted)',
}}
>
<div className="flex items-center gap-2 px-3 py-2">
<span className="relative inline-flex shrink-0 items-center justify-center rounded-full bg-[var(--color-surface-raised)] p-1">
<Users size={12} style={{ color: colors.border }} />
<ShieldQuestion size={12} style={{ color: 'var(--color-text-muted)' }} />
<span className="absolute -bottom-0.5 -right-0.5 flex size-2.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" />
<span className="relative inline-flex size-full rounded-full bg-emerald-500" />
<span className="absolute inline-flex size-full animate-ping rounded-full bg-amber-400 opacity-70" />
<span className="relative inline-flex size-full rounded-full bg-amber-500" />
</span>
</span>
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
style={{
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
backgroundColor: 'var(--color-surface-raised)',
color: 'var(--color-text-secondary)',
border: '1px solid var(--color-border-emphasis)',
}}
title={entry.teamName}
>
{entry.teamName}
</span>
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
external team
user
</span>
<span
className="min-w-0 flex-1 truncate text-[10px]"
style={{ color: CARD_ICON_MUTED }}
title="Cross-team message sent, awaiting reply"
title={`Tool approval: ${entry.toolName}`}
>
awaiting reply
awaiting approval
</span>
<span className="shrink-0 text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{since}