import { useState } from 'react'; import { Check, FolderOpen, ShieldOff } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api/client'; import { Button } from '@/components/ui/button'; import type { ToolCall, ToolResult } from '@/api/types'; // v1.13.17-cross-repo-reads. Renders an inline allow/deny picker for a // paused request_read_access tool call. Mirrors AskUserInputCard's pending // vs answered render dance: // - Pending: server pre-stamps a sentinel tool_result with output=null. // The card shows path + reason and lets the user pick Allow or Deny. // - Answered: the eventual WS tool_result frame carries the actual // decision string ("granted: " or "denied" or "denied: "). // The card flips to a read-only summary line. // // Tool name discrimination lives in MessageList.flatten/group — anything // with tc.name === 'request_read_access' bypasses grouping and renders this // card directly. interface Props { toolCall: ToolCall; toolResult: ToolResult | null; chatId: string; } interface ParsedArgs { path: string; reason: string; } function parseArgs(raw: unknown): ParsedArgs | null { if (!raw || typeof raw !== 'object') return null; const obj = raw as { path?: unknown; reason?: unknown }; if (typeof obj.path !== 'string' || obj.path.length === 0) return null; if (typeof obj.reason !== 'string' || obj.reason.length === 0) return null; return { path: obj.path, reason: obj.reason }; } function decisionVariant(output: unknown): 'granted' | 'denied' | 'unknown' { if (typeof output !== 'string') return 'unknown'; if (output.startsWith('granted:')) return 'granted'; if (output === 'denied' || output.startsWith('denied:')) return 'denied'; return 'unknown'; } export function RequestReadAccessCard({ toolCall, toolResult, chatId }: Props) { const args = parseArgs(toolCall.args); if (!args) { return (
request_read_access: malformed tool args
); } // Non-null output means the WS tool_result frame arrived (or the row was // re-fetched from history). const answered = toolResult && toolResult.output !== null; if (answered) { return ; } return ; } function PendingView({ args, toolCallId, chatId, }: { args: ParsedArgs; toolCallId: string; chatId: string; }) { const [submitting, setSubmitting] = useState<'allow' | 'deny' | null>(null); async function decide(decision: 'allow' | 'deny') { if (submitting) return; setSubmitting(decision); try { await api.chats.grantReadAccess(chatId, toolCallId, decision); // Card stays mounted; the incoming WS tool_result frame swaps it to // AnsweredView via the parent prop change. } catch (err) { toast.error(err instanceof Error ? err.message : 'request failed'); setSubmitting(null); } } return (
Read-access request
Path
{args.path}
Reason
{args.reason}
Allow grants the agent read access to the matching repository root for the rest of this session. Revoke any time from the session settings.
); } function AnsweredView({ args, output }: { args: ParsedArgs; output: unknown }) { const variant = decisionVariant(output); const text = typeof output === 'string' ? output : 'unknown'; return (
{variant === 'granted' ? ( <> Read access granted ) : variant === 'denied' ? ( <> Read access denied ) : ( <> Read access request — unknown result )}
Path
{args.path}
{variant === 'granted' && (
Granted root
{text.replace(/^granted:\s*/, '')}
)} {variant === 'denied' && text !== 'denied' && (
{text.replace(/^denied:\s*/, '')}
)}
); }