From 13ce8119067ead38cde7ec262f265af6f1c1551f Mon Sep 17 00:00:00 2001 From: Flownium <157689911+itsflownium@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:57:18 +1000 Subject: [PATCH] fix: show desktop approval fallback (#46548) --- .../assistant-ui/tool-approval.test.tsx | 30 +++++++++- .../components/assistant-ui/tool-approval.tsx | 56 +++++++++++++++++-- .../src/components/prompt-overlays.tsx | 15 ++--- apps/desktop/src/store/prompts.ts | 11 ++++ 4 files changed, 97 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx b/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx index 007eeff831b..db8debd85c6 100644 --- a/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +++ b/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx @@ -1,4 +1,4 @@ -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import type { HermesGateway } from '@/hermes' @@ -6,7 +6,7 @@ import { $gateway } from '@/store/gateway' import { $approvalRequest, clearAllPrompts, setApprovalRequest } from '@/store/prompts' import { $activeSessionId } from '@/store/session' -import { PendingToolApproval } from './tool-approval' +import { PendingApprovalFallback, PendingToolApproval } from './tool-approval' import type { ToolPart } from './tool-fallback-model' // Radix's DropdownMenu touches pointer-capture + scrollIntoView, which jsdom @@ -130,4 +130,30 @@ describe('PendingToolApproval', () => { expect(await screen.findByRole('menuitem', { name: /Allow this session/ })).toBeTruthy() expect(screen.queryByRole('menuitem', { name: /Always allow/ })).toBeNull() }) + + it('renders a floating fallback when no pending tool row is mounted', () => { + setRequest('rm /tmp/hermes_approval_test.txt') + const { container } = render() + const fallback = container.querySelector('[data-slot="tool-approval-fallback"]') + + expect(fallback).not.toBeNull() + expect(within(fallback as HTMLElement).getByRole('button', { name: /Run/ })).toBeTruthy() + expect(within(fallback as HTMLElement).getByRole('button', { name: /Reject/ })).toBeTruthy() + }) + + it('hides the floating fallback once the inline approval bar is mounted', async () => { + setRequest('rm /tmp/hermes_approval_test.txt') + + const { container } = render( + <> + + + + ) + + await waitFor(() => { + expect(container.querySelector('[data-slot="tool-approval-inline"]')).not.toBeNull() + expect(container.querySelector('[data-slot="tool-approval-fallback"]')).toBeNull() + }) + }) }) diff --git a/apps/desktop/src/components/assistant-ui/tool-approval.tsx b/apps/desktop/src/components/assistant-ui/tool-approval.tsx index d355fda77fc..3a0bf75af5e 100644 --- a/apps/desktop/src/components/assistant-ui/tool-approval.tsx +++ b/apps/desktop/src/components/assistant-ui/tool-approval.tsx @@ -15,11 +15,17 @@ import { import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { useI18n } from '@/i18n' import { triggerHaptic } from '@/lib/haptics' -import { ChevronDown, Loader2 } from '@/lib/icons' +import { AlertCircle, ChevronDown, Loader2 } from '@/lib/icons' import { cn } from '@/lib/utils' import { $gateway } from '@/store/gateway' import { notifyError } from '@/store/notifications' -import { $approvalRequest, type ApprovalRequest, clearApprovalRequest } from '@/store/prompts' +import { + $approvalInlineVisible, + $approvalRequest, + type ApprovalRequest, + clearApprovalRequest, + registerApprovalInlineAnchor +} from '@/store/prompts' import type { ToolPart } from './tool-fallback-model' @@ -48,12 +54,47 @@ export const PendingToolApproval: FC<{ part: ToolPart }> = ({ part }) => { return null } - return + return +} + +const InlineApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => { + useEffect(() => registerApprovalInlineAnchor(), []) + + return +} + +export const PendingApprovalFallback: FC = () => { + const { t } = useI18n() + const request = useStore($approvalRequest) + const inlineVisible = useStore($approvalInlineVisible) + + if (!request || inlineVisible) { + return null + } + + return ( +
+
+
+ + {t.assistant.approval.jumpToApproval} + {request.description && ( + {request.description} + )} +
+ +
+
+ ) } const isMac = typeof navigator !== 'undefined' && /Mac|iP(hone|ad|od)/.test(navigator.platform) -const ApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => { +const ApprovalBar: FC<{ request: ApprovalRequest; surface: 'floating' | 'inline' }> = ({ request, surface }) => { const { t } = useI18n() const copy = t.assistant.approval const gateway = useStore($gateway) @@ -99,7 +140,7 @@ const ApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => { setSubmitting(null) } }, - [busy, gateway, request.sessionId] + [busy, copy.gatewayDisconnected, copy.sendFailed, gateway, request.sessionId] ) // ⌘/Ctrl+Enter → Run, Esc → Reject. @@ -126,7 +167,10 @@ const ApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => { }, [confirmAlways, respond]) return ( -
+