Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7.0 KiB
Approval checkpoints UX handover
What changed in the backend
Approval checkpoints are now queued per session instead of failing when a second approval request arrives before the first one is resolved.
This fixes the previous backend limitation where multi-agent patterns such as Sequential Trio Review could throw:
Session "<id>" already has a pending approval.
The queue is backend-only in this change. The current frontend can keep working because the active approval is still exposed through the existing session.pendingApproval field.
Queue semantics
- A session still exposes one active approval through
session.pendingApproval. - Additional approvals waiting behind it are exposed through
session.pendingApprovalQueue. session.pendingApprovalQueuedoes not include the active approval.- Queue order is the order in which approvals were requested.
resolveSessionApproval(...)still resolves only the active approval.- When the active approval is resolved, the next queued approval automatically becomes the new
session.pendingApproval. - If the run fails while approvals remain queued, the backend rejects and clears the remaining queued approvals.
- If Eryx restarts while approvals are pending or queued, the backend rejects all of them and marks the session/run as errored.
Current shared model
Pattern approval policy
This is unchanged from the earlier backend work:
type ApprovalCheckpointKind = 'tool-call' | 'final-response';
interface ApprovalCheckpointRule {
kind: ApprovalCheckpointKind;
agentIds?: string[]; // omitted or empty = all agents
}
interface ApprovalPolicy {
rules: ApprovalCheckpointRule[];
}
Session approval state
The active approval + queued approvals are now represented as:
interface PendingApprovalMessageRecord {
id: string;
authorName: string;
content: string;
}
interface PendingApprovalRecord {
id: string;
kind: 'tool-call' | 'final-response';
status: 'pending' | 'approved' | 'rejected';
requestedAt: string;
resolvedAt?: string;
agentId?: string;
agentName?: string;
toolName?: string;
permissionKind?: string;
title: string;
detail?: string;
messages?: PendingApprovalMessageRecord[];
}
Attached to SessionRecord as:
pendingApproval?: PendingApprovalRecord;
pendingApprovalQueue?: PendingApprovalRecord[];
Interpretation:
pendingApproval= the approval the user can act on right nowpendingApprovalQueue= additional pending approvals waiting behind it
For final-response, pendingApproval.messages contains the unpublished assistant output preview.
For tool-call, messages is usually absent and the useful context is agentName, toolName, and permissionKind.
IPC surface
No new IPC was added for queueing.
The renderer still resolves approvals through:
resolveSessionApproval({
sessionId: string;
approvalId: string;
decision: 'approved' | 'rejected';
}): Promise<WorkspaceState>
Important behavior:
- this resolves the active approval only
- if the user somehow attempts to resolve a queued approval directly, the backend rejects it
- after a successful resolution, the next queued approval may immediately become active in the returned workspace state
Timeline behavior
The run timeline still uses approval events.
Each approval request gets its own event keyed by approvalId.
That means queued approvals now show up as multiple approval events in the same run when applicable. The active/queued distinction is in session state, not in the timeline event type.
Backend files changed for queue support
src/shared/domain/approval.ts- added queue-state helpers and normalization
src/shared/domain/session.ts- added
pendingApprovalQueue
- added
src/shared/domain/sessionLibrary.ts- duplicated sessions now clear queued approvals too
src/main/persistence/workspaceRepository.ts- normalizes legacy single-approval data into active + queue state
src/main/EryxAppService.ts- enqueues approval requests instead of throwing
- advances the queue on resolution
- rejects all queued approvals on restart / failure cleanup
What the current frontend already does
The current UI should remain functionally compatible because it already reads session.pendingApproval and renders a single active approval banner/card.
That means:
- users can still resolve approvals one at a time
- the next queued approval should appear automatically after resolving the current one
- no urgent UI fix is required just to keep the app working
Next UX steps for the frontend agent
These are the next improvements the UX agent should make on top of the new backend queue.
1. Show queue size next to the active approval
Files to inspect:
src/renderer/components/ChatPane.tsxsrc/renderer/components/ActivityPanel.tsxsrc/renderer/components/Sidebar.tsx
Recommended UX:
- if
session.pendingApprovalQueue?.length > 0, show a compact badge like:1 queued2 queued
- keep the active approval clearly distinguished from queued approvals
2. Clarify that the visible approval is only the active one
In the active approval card/banner, add copy such as:
Approval 1 of 3Next approval will appear after this one is resolved
This will make automatic queue advancement feel intentional instead of surprising.
3. Add an optional queued-approvals preview
Files to inspect:
src/renderer/components/ChatPane.tsxsrc/renderer/components/ActivityPanel.tsx
Recommended UX:
- an accordion or compact list for
session.pendingApprovalQueue - each queued item can show:
- title
- kind
- agent name
- permission kind / tool name when available
Important:
- queued approvals are read-only
- only the active
pendingApprovalshould show Approve / Reject buttons
4. Handle queue transitions smoothly
When the active approval is resolved and the next one becomes active:
- avoid jarring layout jumps
- keep scroll position stable
- consider a subtle transition so the user understands the queue advanced
5. Improve error copy for queued-approval edge cases
Possible backend outcomes the UI should explain well:
- active approval was resolved, then the run failed before the queue fully drained
- app restarted and the whole queue was rejected/interrupted
- user tried to resolve an approval that is no longer active
Recommended UX:
- inline error or toast
- refresh from returned workspace state
- preserve visibility into which approval is now active
6. Optionally surface queue state in the timeline
The timeline already records all approval events, but the UI could add:
- an “active” vs “queued” distinction when an approval event is still pending
- badges or copy that explain multiple approval checkpoints exist in the same run
This is optional because the core queue behavior is already represented in backend state.
Validation commands
Backend queue changes should be validated with:
bun run typecheckbun testbun run sidecar:testbun run build