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 0d13371afee39..007eeff831b3c 100644 --- a/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +++ b/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx @@ -84,6 +84,19 @@ describe('PendingToolApproval', () => { expect($approvalRequest.get()).toBeNull() }) + it('reveals the full command inline when the Command toggle is clicked', () => { + const longCommand = 'python -c "' + 'x'.repeat(400) + '"' + setRequest(longCommand) + render() + + // Collapsed by default: the full command is not in the DOM yet. + expect(screen.queryByText(longCommand)).toBeNull() + + fireEvent.click(screen.getByRole('button', { name: /Command/ })) + + expect(screen.getByText(longCommand)).toBeTruthy() + }) + it('sends choice "deny" on Reject', async () => { const request = mockGateway() setRequest() diff --git a/apps/desktop/src/components/assistant-ui/tool-approval.tsx b/apps/desktop/src/components/assistant-ui/tool-approval.tsx index 6a3dc6c0d9c52..d355fda77fc9f 100644 --- a/apps/desktop/src/components/assistant-ui/tool-approval.tsx +++ b/apps/desktop/src/components/assistant-ui/tool-approval.tsx @@ -16,6 +16,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { useI18n } from '@/i18n' import { triggerHaptic } from '@/lib/haptics' import { 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' @@ -60,9 +61,15 @@ const ApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => { // "Always allow" persists the pattern to ~/.hermes/config.yaml permanently, so // it goes through a confirm step rather than firing straight from the menu. const [confirmAlways, setConfirmAlways] = useState(false) + // The pending tool row only shows a single truncated line of the command, and + // a pending row can't be expanded (no result yet), so the full command was + // previously only reachable via the "Always allow" modal. Let the user reveal + // it inline instead — "expand, Run" (2 clicks) rather than the modal dance. + const [showCommand, setShowCommand] = useState(false) const busy = submitting !== null // false when the backend won't honor a permanent allow (tirith warning) → hide "Always allow". const allowPermanent = request.allowPermanent !== false + const hasCommand = request.command.trim().length > 0 const respond = useCallback( async (choice: ApprovalChoice) => { @@ -119,70 +126,89 @@ const ApprovalBar: FC<{ request: ApprovalRequest }> = ({ request }) => { }, [confirmAlways, respond]) return ( -
-
+
+
+
+ + + + + + + + void respond('session')}>{copy.allowSession} + {allowPermanent && ( + { + // Defer one tick so the menu fully unmounts before the dialog + // mounts — otherwise Radix's focus-return races the dialog and + // dismisses it via onInteractOutside. + setTimeout(() => setConfirmAlways(true), 0) + }} + > + {copy.alwaysAllowMenu} + + )} + void respond('deny')} variant="destructive"> + {copy.reject} + + + +
+ - - - - - - - void respond('session')}>{copy.allowSession} - {allowPermanent && ( - { - // Defer one tick so the menu fully unmounts before the dialog - // mounts — otherwise Radix's focus-return races the dialog and - // dismisses it via onInteractOutside. - setTimeout(() => setConfirmAlways(true), 0) - }} - > - {copy.alwaysAllowMenu} - - )} - void respond('deny')} variant="destructive"> - {copy.reject} - - - + + {hasCommand && ( + + )}
- + {showCommand && hasCommand && ( +
+          {request.command.trim()}
+        
+ )} {copy.alwaysTitle} - - {copy.alwaysDescription(request.description)} - + {copy.alwaysDescription(request.description)} {request.command.trim() && ( diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index a0cfdbb08b795..66749c96f889d 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -1665,6 +1665,7 @@ export const en: Translations = { gatewayDisconnected: 'Hermes gateway is not connected', sendFailed: 'Could not send approval response', run: 'Run', + command: 'Command', moreOptions: 'More approval options', allowSession: 'Allow this session', alwaysAllowMenu: 'Always allow…', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 0ae343586fddc..4b27e74f0c831 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -1798,6 +1798,7 @@ export const ja = defineLocale({ gatewayDisconnected: 'Hermes ゲートウェイが接続されていません', sendFailed: '承認応答を送信できませんでした', run: '実行', + command: 'コマンド', moreOptions: 'その他の承認オプション', allowSession: 'このセッションで許可', alwaysAllowMenu: '常に許可…', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 592fe2bfa2ceb..ae4cd10679819 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -1325,6 +1325,7 @@ export interface Translations { gatewayDisconnected: string sendFailed: string run: string + command: string moreOptions: string allowSession: string alwaysAllowMenu: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index 058ad3fb3c2a5..9e78292acaabc 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -1759,6 +1759,7 @@ export const zhHant = defineLocale({ gatewayDisconnected: 'Hermes 閘道未連線', sendFailed: '無法傳送核准回應', run: '執行', + command: '指令', moreOptions: '更多核准選項', allowSession: '允許本工作階段', alwaysAllowMenu: '一律允許…', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index de6f467ab617a..7b18ded488f04 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -1844,6 +1844,7 @@ export const zh: Translations = { gatewayDisconnected: 'Hermes 网关未连接', sendFailed: '无法发送审批响应', run: '运行', + command: '命令', moreOptions: '更多审批选项', allowSession: '允许本会话', alwaysAllowMenu: '始终允许…',