openclaw/ui/src/ui/views/exec-approval.ts
John Schneider 5389f26d51 UI: fix exec approval dialog scrolling with long commands
Make exec approval content scrollable when commands are too long to fit
on screen. Previously, long commands (like Python scripts) would push
the action buttons off-screen, making it impossible to approve/deny.

Changes:
- Wrap command/meta content in scrollable `.exec-approval-content` div
- Add max-height constraint to card (viewport - padding)
- Use flexbox layout to keep header and buttons always visible
- Content area scrolls independently in the middle

Fixes issue where users couldn't see approval buttons for long exec requests.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-12 10:41:54 -04:00

92 lines
3.0 KiB
TypeScript

import { html, nothing } from "lit";
import type { AppViewState } from "../app-view-state.ts";
function formatRemaining(ms: number): string {
const remaining = Math.max(0, ms);
const totalSeconds = Math.floor(remaining / 1000);
if (totalSeconds < 60) {
return `${totalSeconds}s`;
}
const minutes = Math.floor(totalSeconds / 60);
if (minutes < 60) {
return `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
return `${hours}h`;
}
function renderMetaRow(label: string, value?: string | null) {
if (!value) {
return nothing;
}
return html`<div class="exec-approval-meta-row"><span>${label}</span><span>${value}</span></div>`;
}
export function renderExecApprovalPrompt(state: AppViewState) {
const active = state.execApprovalQueue[0];
if (!active) {
return nothing;
}
const request = active.request;
const remainingMs = active.expiresAtMs - Date.now();
const remaining = remainingMs > 0 ? `expires in ${formatRemaining(remainingMs)}` : "expired";
const queueCount = state.execApprovalQueue.length;
return html`
<div class="exec-approval-overlay" role="dialog" aria-live="polite">
<div class="exec-approval-card">
<div class="exec-approval-header">
<div>
<div class="exec-approval-title">Exec approval needed</div>
<div class="exec-approval-sub">${remaining}</div>
</div>
${
queueCount > 1
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
: nothing
}
</div>
<div class="exec-approval-content">
<div class="exec-approval-command mono">${request.command}</div>
<div class="exec-approval-meta">
${renderMetaRow("Host", request.host)}
${renderMetaRow("Agent", request.agentId)}
${renderMetaRow("Session", request.sessionKey)}
${renderMetaRow("CWD", request.cwd)}
${renderMetaRow("Resolved", request.resolvedPath)}
${renderMetaRow("Security", request.security)}
${renderMetaRow("Ask", request.ask)}
</div>
${
state.execApprovalError
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
: nothing
}
</div>
<div class="exec-approval-actions">
<button
class="btn primary"
?disabled=${state.execApprovalBusy}
@click=${() => state.handleExecApprovalDecision("allow-once")}
>
Allow once
</button>
<button
class="btn"
?disabled=${state.execApprovalBusy}
@click=${() => state.handleExecApprovalDecision("allow-always")}
>
Always allow
</button>
<button
class="btn danger"
?disabled=${state.execApprovalBusy}
@click=${() => state.handleExecApprovalDecision("deny")}
>
Deny
</button>
</div>
</div>
</div>
`;
}