mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-03 18:38:35 +02:00
feat: add approval queue UX with position labels, queue preview, and count badges
- ChatPane: show 'Approval 1 of N' label, queued-approvals accordion preview, queue count badge in header, and hint text for queue advancement - Sidebar: show '+N queued' count next to 'Awaiting approval' badge - ActivityPanel: show total approval count in header badge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -184,6 +184,8 @@ export function ActivityPanel({
|
||||
|
||||
const isBusy = session.status === 'running';
|
||||
const hasPendingApproval = session.pendingApproval?.status === 'pending';
|
||||
const queuedCount = (session.pendingApprovalQueue ?? []).filter((a) => a.status === 'pending').length;
|
||||
const totalApprovalCount = (hasPendingApproval ? 1 : 0) + queuedCount;
|
||||
const toolsDisabled = isBusy || projectIsScratchpad;
|
||||
const accent = modeAccent[pattern.mode] ?? modeAccent.single;
|
||||
const hasTools = mcpServers.length > 0 || lspProfiles.length > 0;
|
||||
@@ -200,7 +202,9 @@ export function ActivityPanel({
|
||||
{hasPendingApproval ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<ShieldAlert className="size-3 text-amber-400" />
|
||||
<span className="text-[9px] font-semibold uppercase tracking-wider text-amber-400">Approval</span>
|
||||
<span className="text-[9px] font-semibold uppercase tracking-wider text-amber-400">
|
||||
Approval{totalApprovalCount > 1 ? `s (${totalApprovalCount})` : ''}
|
||||
</span>
|
||||
</span>
|
||||
) : isBusy ? (
|
||||
<span className="size-1.5 animate-pulse rounded-full bg-blue-400" />
|
||||
|
||||
@@ -228,13 +228,18 @@ function ApprovalBanner({
|
||||
approval,
|
||||
onResolve,
|
||||
isResolving,
|
||||
position,
|
||||
total,
|
||||
}: {
|
||||
approval: PendingApprovalRecord;
|
||||
onResolve: (decision: ApprovalDecision) => void;
|
||||
isResolving: boolean;
|
||||
position?: number;
|
||||
total?: number;
|
||||
}) {
|
||||
const kindLabel = approval.kind === 'final-response' ? 'Final response review' : 'Tool call approval';
|
||||
const hasMessages = approval.messages && approval.messages.length > 0;
|
||||
const showPosition = position !== undefined && total !== undefined && total > 1;
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-amber-500/30 bg-amber-500/5 px-4 py-3">
|
||||
@@ -247,6 +252,11 @@ function ApprovalBanner({
|
||||
<span className="rounded-full bg-amber-500/15 px-2 py-0.5 text-[9px] font-semibold uppercase tracking-wider text-amber-400">
|
||||
{kindLabel}
|
||||
</span>
|
||||
{showPosition && (
|
||||
<span className="rounded-full bg-zinc-800 px-2 py-0.5 text-[9px] font-semibold tabular-nums text-zinc-400">
|
||||
{position} of {total}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Agent / permission context */}
|
||||
@@ -301,11 +311,62 @@ function ApprovalBanner({
|
||||
<X className="size-3" />
|
||||
Reject
|
||||
</button>
|
||||
{showPosition && (
|
||||
<span className="ml-auto text-[10px] text-zinc-600">
|
||||
Next approval will appear after this one is resolved
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Queued approvals preview ──────────────────────────────── */
|
||||
|
||||
function QueuedApprovalsList({ approvals }: { approvals: PendingApprovalRecord[] }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900/40 px-3 py-2">
|
||||
<button
|
||||
className="flex w-full items-center gap-2 text-left"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
type="button"
|
||||
>
|
||||
<ShieldCheck className="size-3 text-zinc-500" />
|
||||
<span className="text-[11px] font-medium text-zinc-400">
|
||||
{approvals.length} queued approval{approvals.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`ml-auto size-3 text-zinc-600 transition-transform ${expanded ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mt-2 space-y-1.5 border-t border-zinc-800/60 pt-2">
|
||||
{approvals.map((approval) => {
|
||||
const kindLabel = approval.kind === 'final-response' ? 'response' : 'tool';
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-md bg-zinc-800/40 px-2.5 py-1.5"
|
||||
key={approval.id}
|
||||
>
|
||||
<ShieldAlert className="size-3 shrink-0 text-zinc-600" />
|
||||
<span className="min-w-0 flex-1 truncate text-[11px] text-zinc-400">{approval.title}</span>
|
||||
<span className="shrink-0 rounded-full bg-zinc-800 px-1.5 py-0.5 text-[8px] font-semibold uppercase tracking-wider text-zinc-500">
|
||||
{kindLabel}
|
||||
</span>
|
||||
{approval.agentName && (
|
||||
<span className="shrink-0 text-[10px] text-zinc-600">{approval.agentName}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── ChatPane ──────────────────────────────────────────────── */
|
||||
|
||||
interface ChatPaneProps {
|
||||
@@ -340,6 +401,8 @@ export function ChatPane({
|
||||
|
||||
const isSessionBusy = session.status === 'running';
|
||||
const pendingApproval = session.pendingApproval?.status === 'pending' ? session.pendingApproval : undefined;
|
||||
const queuedApprovals = (session.pendingApprovalQueue ?? []).filter((a) => a.status === 'pending');
|
||||
const totalPendingCount = (pendingApproval ? 1 : 0) + queuedApprovals.length;
|
||||
const isScratchpad = isScratchpadProject(project);
|
||||
const primaryAgent = pattern.agents[0];
|
||||
const selectedModel = primaryAgent ? findModel(primaryAgent.model, availableModels) : undefined;
|
||||
@@ -446,6 +509,11 @@ export function ChatPane({
|
||||
<div className="flex items-center gap-1.5 text-[12px] font-medium text-amber-400">
|
||||
<ShieldAlert className="size-3.5" />
|
||||
Awaiting approval
|
||||
{queuedApprovals.length > 0 && (
|
||||
<span className="rounded-full bg-amber-500/15 px-1.5 py-0.5 text-[10px] tabular-nums">
|
||||
+{queuedApprovals.length} queued
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isSessionBusy && !pendingApproval && <span className="size-2 animate-pulse rounded-full bg-blue-400" />}
|
||||
@@ -579,12 +647,17 @@ export function ChatPane({
|
||||
<div className="mx-auto max-w-3xl">
|
||||
{/* Pending approval banner */}
|
||||
{pendingApproval && (
|
||||
<div className="mb-3">
|
||||
<div className="mb-3 space-y-2">
|
||||
<ApprovalBanner
|
||||
approval={pendingApproval}
|
||||
isResolving={isResolvingApproval}
|
||||
onResolve={(decision) => void handleResolveApproval(decision)}
|
||||
position={totalPendingCount > 1 ? 1 : undefined}
|
||||
total={totalPendingCount > 1 ? totalPendingCount : undefined}
|
||||
/>
|
||||
{queuedApprovals.length > 0 && (
|
||||
<QueuedApprovalsList approvals={queuedApprovals} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -168,6 +168,7 @@ function SessionItem({
|
||||
const isRunning = session.status === 'running';
|
||||
const isError = session.status === 'error';
|
||||
const hasPendingApproval = session.pendingApproval?.status === 'pending';
|
||||
const queuedCount = (session.pendingApprovalQueue ?? []).filter((a) => a.status === 'pending').length;
|
||||
const mode = pattern?.mode ?? 'single';
|
||||
const visual = modeVisuals[mode];
|
||||
const ModeIcon = visual.icon;
|
||||
@@ -273,7 +274,7 @@ function SessionItem({
|
||||
{hasPendingApproval && (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-medium text-amber-400">
|
||||
<span className="size-1.5 rounded-full bg-amber-400 animate-pulse" />
|
||||
Awaiting approval
|
||||
Awaiting approval{queuedCount > 0 && ` (+${queuedCount})`}
|
||||
</span>
|
||||
)}
|
||||
{isError && (
|
||||
|
||||
Reference in New Issue
Block a user