diff --git a/src/main/AryxAppService.ts b/src/main/AryxAppService.ts index ff7d21d..1d252d1 100644 --- a/src/main/AryxAppService.ts +++ b/src/main/AryxAppService.ts @@ -1199,6 +1199,7 @@ export class AryxAppService extends EventEmitter { permissionKind: event.permissionKind, title: event.title, detail: event.detail, + permissionDetail: event.permissionDetail, }; } diff --git a/src/renderer/components/chat/ApprovalBanner.tsx b/src/renderer/components/chat/ApprovalBanner.tsx index e2fa210..2964456 100644 --- a/src/renderer/components/chat/ApprovalBanner.tsx +++ b/src/renderer/components/chat/ApprovalBanner.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Bot, Check, ChevronDown, Loader2, ShieldAlert, ShieldCheck, X } from 'lucide-react'; import { MarkdownContent } from '@renderer/components/MarkdownContent'; +import { permissionDetailSummary, PermissionDetailView } from '@renderer/components/chat/PermissionDetailView'; import type { ApprovalDecision, PendingApprovalRecord } from '@shared/domain/approval'; /* ── ApprovalBanner ────────────────────────────────────────── */ @@ -47,9 +48,11 @@ export function ApprovalBanner({ {approval.permissionKind && Permission: {approval.permissionKind}} - {approval.detail && ( -

{approval.detail}

- )} + {approval.permissionDetail + ? + : approval.detail && ( +

{approval.detail}

+ )} @@ -134,7 +137,9 @@ export function QueuedApprovalsList({ approvals }: { approvals: PendingApprovalR key={approval.id} > - {approval.title} + + {(approval.permissionDetail && permissionDetailSummary(approval.permissionDetail)) || approval.title} + {kindLabel} diff --git a/src/renderer/components/chat/PermissionDetailView.tsx b/src/renderer/components/chat/PermissionDetailView.tsx new file mode 100644 index 0000000..8d8aa0b --- /dev/null +++ b/src/renderer/components/chat/PermissionDetailView.tsx @@ -0,0 +1,295 @@ +import { useState } from 'react'; +import { + AlertTriangle, + BookOpen, + ChevronDown, + ExternalLink, + FileEdit, + FileText, + Globe, + Server, + Terminal, +} from 'lucide-react'; + +import type { PermissionDetail } from '@shared/contracts/sidecar'; + +export function PermissionDetailView({ detail }: { detail: PermissionDetail }) { + switch (detail.kind) { + case 'shell': + return ; + case 'write': + return ; + case 'read': + return ; + case 'mcp': + return ; + case 'url': + return ; + case 'memory': + return ; + case 'custom-tool': + return ; + case 'hook': + return ; + default: + return null; + } +} + +export function permissionDetailSummary(detail: PermissionDetail): string | undefined { + switch (detail.kind) { + case 'shell': + return detail.command ? truncate(detail.command, 80) : undefined; + case 'write': + return detail.fileName; + case 'read': + return detail.path; + case 'url': + return detail.url; + case 'mcp': + return detail.serverName + ? `${detail.serverName} → ${detail.toolTitle ?? ''}` + : detail.toolTitle; + case 'memory': + return detail.subject; + case 'custom-tool': + return detail.toolDescription ? truncate(detail.toolDescription, 80) : undefined; + case 'hook': + return detail.hookMessage ? truncate(detail.hookMessage, 80) : undefined; + default: + return undefined; + } +} + +/* ── Kind-specific renderers ────────────────────────────────── */ + +function ShellDetail({ detail }: { detail: PermissionDetail }) { + return ( +
+ {detail.intention && } + {detail.warning && ( +
+ + {detail.warning} +
+ )} + {detail.command && } + {detail.possiblePaths && detail.possiblePaths.length > 0 && ( + + )} + {detail.possibleUrls && detail.possibleUrls.length > 0 && ( + + )} +
+ ); +} + +function WriteDetail({ detail }: { detail: PermissionDetail }) { + return ( +
+ {detail.intention && } + {detail.fileName && ( +
+ + {detail.fileName} +
+ )} + {detail.diff && } + {!detail.diff && detail.newFileContents && ( + + )} +
+ ); +} + +function ReadDetail({ detail }: { detail: PermissionDetail }) { + return ( +
+ {detail.intention && } + {detail.path && ( +
+ + {detail.path} +
+ )} +
+ ); +} + +function McpDetail({ detail }: { detail: PermissionDetail }) { + return ( +
+
+ {detail.serverName && ( + + + {detail.serverName} + + )} + {detail.toolTitle && {detail.toolTitle}} + {detail.readOnly && ( + + read-only + + )} +
+ {detail.args && Object.keys(detail.args).length > 0 && ( + + )} +
+ ); +} + +function UrlDetail({ detail }: { detail: PermissionDetail }) { + return ( +
+ {detail.intention && } + {detail.url && ( +
+ + {detail.url} + +
+ )} +
+ ); +} + +function MemoryDetail({ detail }: { detail: PermissionDetail }) { + return ( +
+ {detail.subject && ( +
+ + {detail.subject} +
+ )} + {detail.fact && ( +

+ {detail.fact} +

+ )} + {detail.citations && ( +

+ Source: {detail.citations} +

+ )} +
+ ); +} + +function CustomToolDetail({ detail }: { detail: PermissionDetail }) { + return ( +
+ {detail.toolDescription && ( +

{detail.toolDescription}

+ )} + {detail.args && Object.keys(detail.args).length > 0 && ( + + )} +
+ ); +} + +function HookDetail({ detail }: { detail: PermissionDetail }) { + return ( +
+ {detail.hookMessage && ( +
+ + {detail.hookMessage} +
+ )} + {detail.args && Object.keys(detail.args).length > 0 && ( + + )} +
+ ); +} + +/* ── Shared primitives ──────────────────────────────────────── */ + +function IntentionLine({ text }: { text: string }) { + return

{text}

; +} + +function CommandBlock({ text }: { text: string }) { + return ( +
+      {text}
+    
+ ); +} + +function DiffBlock({ text }: { text: string }) { + const lines = text.split('\n'); + return ( + +
+        {lines.map((line, i) => {
+          let color = 'text-zinc-400';
+          if (line.startsWith('+')) color = 'text-emerald-400';
+          else if (line.startsWith('-')) color = 'text-red-400';
+          else if (line.startsWith('@@')) color = 'text-blue-400';
+          return (
+            
+ {line} +
+ ); + })} +
+
+ ); +} + +function CollapsibleCode({ + label, + text, + children, + defaultExpanded = false, +}: { + label: string; + text: string; + children?: React.ReactNode; + defaultExpanded?: boolean; +}) { + const [expanded, setExpanded] = useState(defaultExpanded); + + return ( +
+ + {expanded && ( +
+ {children ?? ( +
+              {text}
+            
+ )} +
+ )} +
+ ); +} + +function MetaList({ label, items }: { label: string; items: string[] }) { + return ( +
+ {label}:{' '} + {items.join(', ')} +
+ ); +} + +function truncate(text: string, maxLength: number): string { + return text.length <= maxLength ? text : `${text.slice(0, maxLength)}…`; +} diff --git a/src/shared/contracts/sidecar.ts b/src/shared/contracts/sidecar.ts index 1354a03..a51b5ff 100644 --- a/src/shared/contracts/sidecar.ts +++ b/src/shared/contracts/sidecar.ts @@ -181,6 +181,30 @@ export interface AgentActivityEvent { toolName?: string; } +export interface PermissionDetail { + kind: string; + intention?: string; + command?: string; + warning?: string; + possiblePaths?: string[]; + possibleUrls?: string[]; + hasWriteFileRedirection?: boolean; + fileName?: string; + diff?: string; + newFileContents?: string; + path?: string; + serverName?: string; + toolTitle?: string; + args?: Record; + readOnly?: boolean; + url?: string; + subject?: string; + fact?: string; + citations?: string; + toolDescription?: string; + hookMessage?: string; +} + export interface ApprovalRequestedEvent { type: 'approval-requested'; requestId: string; @@ -193,6 +217,7 @@ export interface ApprovalRequestedEvent { permissionKind?: string; title: string; detail?: string; + permissionDetail?: PermissionDetail; } export interface CommandErrorEvent { diff --git a/src/shared/domain/approval.ts b/src/shared/domain/approval.ts index 84078aa..ea14eef 100644 --- a/src/shared/domain/approval.ts +++ b/src/shared/domain/approval.ts @@ -1,3 +1,5 @@ +import type { PermissionDetail } from '@shared/contracts/sidecar'; + export type ApprovalCheckpointKind = 'tool-call' | 'final-response'; export type ApprovalStatus = 'pending' | 'approved' | 'rejected'; export type ApprovalDecision = Exclude; @@ -35,6 +37,7 @@ export interface PendingApprovalRecord { title: string; detail?: string; messages?: PendingApprovalMessageRecord[]; + permissionDetail?: PermissionDetail; } export interface PendingApprovalState { @@ -290,6 +293,7 @@ export function normalizePendingApproval( title, detail: normalizeOptionalString(approval?.detail), messages: normalizePendingApprovalMessages(approval?.messages), + permissionDetail: approval?.permissionDetail, }; }