import { useState, type ReactElement } from 'react'; import type { PendingApproval } from './use-chat-connection'; import { MAX_COMMAND_MESSAGE_CHARS } from './limits'; import { asNonEmptyString, asString } from './runtime-guards'; import type { CommandManifest, SlashCommandApprovalResultPayload, SlashCommandResultPayload, } from '@/lib/chat-contract'; /** Stable fallback copy shown for a failed command only when the server's * own guarded, non-empty `message` (e.g. "Unknown model") is absent or * malformed — the structured contract reason itself is otherwise shown * directly, never a raw thrown exception, stack trace, or object value. */ const COMMAND_FAILURE_COPY = 'Command failed.'; /** Render-site defense-in-depth: `use-chat-connection.ts` already bounds a * stored command:result message at ingestion, but this component must never * assume every caller went through that path — bounding again here means a * hostile/oversized message can never force an unbounded render. */ function boundMessage(value: string): string { return value.length > MAX_COMMAND_MESSAGE_CHARS ? value.slice(0, MAX_COMMAND_MESSAGE_CHARS) : value; } interface CommandsPanelProps { manifest: CommandManifest | null; results: SlashCommandResultPayload[]; approval: SlashCommandApprovalResultPayload | null; pendingApproval: PendingApproval | null; hasConversation: boolean; onExecute: (input: { command: string; args?: string }) => void; onApprove: (input: { command: string; args?: string }) => void; onRunApproved: () => void; } export function CommandsPanel({ manifest, results, approval, pendingApproval, hasConversation, onExecute, onApprove, onRunApproved, }: CommandsPanelProps): ReactElement { const [command, setCommand] = useState(''); const [args, setArgs] = useState(''); // Defense-in-depth: the reducer already normalizes success/approvalId // before storing `approval`, but a matching command string alone must // never be trusted here either — require the literal boolean `true` and a // non-empty string approvalId, not merely truthy values. const canRunApproved = approval?.success === true && typeof approval.approvalId === 'string' && approval.approvalId.length > 0 && !!pendingApproval && pendingApproval.command === approval.command; // A manifest arrives from the server as untyped JSON at runtime — guard // both collections before mapping so a malformed manifest cannot throw. const commands = Array.isArray(manifest?.commands) ? manifest.commands : []; const skills = Array.isArray(manifest?.skills) ? manifest.skills : []; return (
{commands.length > 0 ? ( ) : null} {skills.length > 0 ? ( ) : null}
setCommand(event.target.value)} placeholder="command" /> setArgs(event.target.value)} placeholder="args (optional)" />
{approval ? (
{/* A successful approval shows stable client copy only — never the server-controlled approval.message or echoed approval.command as the primary confirmation. The frozen local pendingApproval below (not this line) is the sole authoritative statement of what will run. A denial, by contrast, is not an execution authority and safely surfaces the guarded structured reason the server gave (e.g. "Not authorized"), falling back to a stable copy only when absent/malformed. */} {approval.success ? 'Approved.' : asNonEmptyString(approval.message, 'Denied.')} {canRunApproved && pendingApproval ? ( <> {/* Authoritative frozen local command+args — what the click below will actually emit. The server's `approval` above is display-only and must never be trusted to represent the executed payload. */} Will run: /{pendingApproval.command}{' '} {pendingApproval.args ? pendingApproval.args : '(no args)'} ) : null}
) : null} {results.length > 0 ? ( ) : null}
); }