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 = () => { export const ToolApprovalSheet: React.FC = () => {
const pendingApprovals = useStore((s) => s.pendingApprovals); const pendingApprovals = useStore((s) => s.pendingApprovals);
const respondToToolApproval = useStore((s) => s.respondToToolApproval); const respondToToolApproval = useStore((s) => s.respondToToolApproval);
const updateToolApprovalSettings = useStore((s) => s.updateToolApprovalSettings);
const teams = useStore((s) => s.teams); const teams = useStore((s) => s.teams);
const selectedTeamName = useStore((s) => s.selectedTeamName); const selectedTeamName = useStore((s) => s.selectedTeamName);
const { isLight } = useTheme(); const { isLight } = useTheme();
@ -281,6 +282,32 @@ export const ToolApprovalSheet: React.FC = () => {
> >
Deny Deny
</button> </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> </div>
{pendingApprovals.length > 1 && ( {pendingApprovals.length > 1 && (
<span className="text-[11px] text-[var(--color-text-muted)]"> <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 { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors'; import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme'; import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store';
import { formatAgentRole } from '@renderer/utils/formatAgentRole'; import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { import {
agentAvatarUrl, agentAvatarUrl,
@ -9,7 +10,7 @@ import {
} from '@renderer/utils/memberHelpers'; } from '@renderer/utils/memberHelpers';
import { nameColorSet } from '@renderer/utils/projectColor'; import { nameColorSet } from '@renderer/utils/projectColor';
import { formatDistanceToNowStrict } from 'date-fns'; import { formatDistanceToNowStrict } from 'date-fns';
import { Users } from 'lucide-react'; import { ShieldQuestion, Users } from 'lucide-react';
import type { ResolvedTeamMember } from '@shared/types'; import type { ResolvedTeamMember } from '@shared/types';
@ -32,6 +33,7 @@ export const PendingRepliesBlock = ({
onMemberClick, onMemberClick,
}: PendingRepliesBlockProps): React.JSX.Element | null => { }: PendingRepliesBlockProps): React.JSX.Element | null => {
const { isLight } = useTheme(); const { isLight } = useTheme();
const pendingApprovals = useStore((s) => s.pendingApprovals);
const colorMap = buildMemberColorMap(members); const colorMap = buildMemberColorMap(members);
const memberPending = Object.entries(pendingRepliesByMember) const memberPending = Object.entries(pendingRepliesByMember)
.map(([name, sentAtMs]) => ({ .map(([name, sentAtMs]) => ({
@ -49,7 +51,17 @@ export const PendingRepliesBlock = ({
teamName: entry.teamName, teamName: entry.teamName,
sentAtMs: entry.sentAtMs, 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; 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 ( return (
<article <article
key={`pending-reply:team:${entry.teamName}:${entry.sentAtMs}`} key={`pending-reply:user:${entry.sentAtMs}`}
className="activity-card-enter-animate overflow-hidden rounded-md" className="activity-card-enter-animate overflow-hidden rounded-md"
style={{ style={{
backgroundColor: CARD_BG, backgroundColor: CARD_BG,
border: CARD_BORDER_STYLE, 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"> <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"> <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 -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="absolute inline-flex size-full animate-ping rounded-full bg-amber-400 opacity-70" />
<span className="relative inline-flex size-full rounded-full bg-emerald-500" /> <span className="relative inline-flex size-full rounded-full bg-amber-500" />
</span> </span>
</span> </span>
<span <span
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide" className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
style={{ style={{
backgroundColor: getThemedBadge(colors, isLight), backgroundColor: 'var(--color-surface-raised)',
color: colors.text, color: 'var(--color-text-secondary)',
border: `1px solid ${colors.border}40`, border: '1px solid var(--color-border-emphasis)',
}} }}
title={entry.teamName}
> >
{entry.teamName} user
</span>
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
external team
</span> </span>
<span <span
className="min-w-0 flex-1 truncate text-[10px]" className="min-w-0 flex-1 truncate text-[10px]"
style={{ color: CARD_ICON_MUTED }} style={{ color: CARD_ICON_MUTED }}
title="Cross-team message sent, awaiting reply" title={`Tool approval: ${entry.toolName}`}
> >
awaiting reply awaiting approval
</span> </span>
<span className="shrink-0 text-[10px]" style={{ color: CARD_ICON_MUTED }}> <span className="shrink-0 text-[10px]" style={{ color: CARD_ICON_MUTED }}>
{since} {since}