Latest gateway events.
+ No events yet.
`
+ ? html`
${props.eventLog.map(
diff --git a/ui/src/ui/views/exec-approval.test.ts b/ui/src/ui/views/exec-approval.test.ts
index a21fda1dd2d..c25542490db 100644
--- a/ui/src/ui/views/exec-approval.test.ts
+++ b/ui/src/ui/views/exec-approval.test.ts
@@ -2,6 +2,8 @@
import { nothing, render } from "lit";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { i18n } from "../../i18n/index.ts";
+import { createStorageMock } from "../../test-helpers/storage.ts";
import type { AppViewState } from "../app-view-state.ts";
import { type OpenClawModalDialog } from "../components/modal-dialog.ts";
import type { ExecApprovalRequest } from "../controllers/exec-approval.ts";
@@ -101,17 +103,22 @@ function createExecState(
}
describe("approval and confirmation modals", () => {
- beforeEach(() => {
+ beforeEach(async () => {
installDialogPolyfill();
+ vi.stubGlobal("localStorage", createStorageMock());
+ await i18n.setLocale("en");
container = document.createElement("div");
document.body.append(container);
});
- afterEach(() => {
+ afterEach(async () => {
render(nothing, container);
container.remove();
+ await i18n.setLocale("en");
restoreDescriptor("showModal", showModalDescriptor);
restoreDescriptor("close", closeDescriptor);
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
vi.restoreAllMocks();
});
@@ -160,6 +167,48 @@ describe("approval and confirmation modals", () => {
expect(handleExecApprovalDecision).not.toHaveBeenCalled();
});
+ it("renders exec approval chrome from the active locale", async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date("2026-04-29T00:00:00.000Z"));
+ await i18n.setLocale("zh-CN");
+ const active: ExecApprovalRequest = {
+ id: "approval-1",
+ kind: "exec",
+ request: {
+ command: "pnpm check:changed",
+ host: "gateway",
+ agentId: "main",
+ sessionKey: "main",
+ cwd: "/tmp/project",
+ resolvedPath: "/tmp/project",
+ security: "workspace-write",
+ ask: "on-request",
+ },
+ createdAtMs: Date.now(),
+ expiresAtMs: Date.now() + 61_000,
+ };
+ const queued: ExecApprovalRequest = {
+ ...active,
+ id: "approval-2",
+ createdAtMs: Date.now() + 1,
+ expiresAtMs: Date.now() + 62_000,
+ };
+
+ render(
+ renderExecApprovalPrompt(createExecState({ execApprovalQueue: [active, queued] })),
+ container,
+ );
+
+ expect(container.textContent).toContain("需要 Exec 审批");
+ expect(container.textContent).toContain("1m 后过期");
+ expect(container.textContent).toContain("2 个待处理");
+ expect(container.textContent).toContain("主机");
+ expect(container.textContent).toContain("代理");
+ expect(container.textContent).toContain("允许一次");
+ expect(container.textContent).toContain("始终允许");
+ expect(container.textContent).toContain("拒绝");
+ });
+
it("uses the shared modal primitive for gateway URL confirmation and cancels on Escape", async () => {
const handleGatewayUrlCancel = vi.fn();
render(
diff --git a/ui/src/ui/views/exec-approval.ts b/ui/src/ui/views/exec-approval.ts
index 6c640d12ebf..ff5b2fed8ab 100644
--- a/ui/src/ui/views/exec-approval.ts
+++ b/ui/src/ui/views/exec-approval.ts
@@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { formatApprovalDisplayPath } from "../../../../src/infra/approval-display-paths.ts";
+import { t } from "../../i18n/index.ts";
import type { AppViewState } from "../app-view-state.ts";
import "../components/modal-dialog.ts";
import type {
@@ -35,13 +36,15 @@ function renderExecBody(request: ExecApprovalRequestPayload) {
return html`
${request.command}
- ${renderMetaRow("Host", request.host)} ${renderMetaRow("Agent", request.agentId)}
- ${renderMetaRow("Session", request.sessionKey)}
- ${renderMetaRow("CWD", request.cwd, {
+ ${renderMetaRow(t("execApproval.labels.host"), request.host)}
+ ${renderMetaRow(t("execApproval.labels.agent"), request.agentId)}
+ ${renderMetaRow(t("execApproval.labels.session"), request.sessionKey)}
+ ${renderMetaRow(t("execApproval.labels.cwd"), request.cwd, {
path: true,
})}
- ${renderMetaRow("Resolved", request.resolvedPath, { path: true })}
- ${renderMetaRow("Security", request.security)} ${renderMetaRow("Ask", request.ask)}
+ ${renderMetaRow(t("execApproval.labels.resolved"), request.resolvedPath, { path: true })}
+ ${renderMetaRow(t("execApproval.labels.security"), request.security)}
+ ${renderMetaRow(t("execApproval.labels.ask"), request.ask)}
`;
}
@@ -54,9 +57,10 @@ ${active.pluginDescription}`
: nothing}
- ${renderMetaRow("Severity", active.pluginSeverity)}
- ${renderMetaRow("Plugin", active.pluginId)} ${renderMetaRow("Agent", active.request.agentId)}
- ${renderMetaRow("Session", active.request.sessionKey)}
+ ${renderMetaRow(t("execApproval.labels.severity"), active.pluginSeverity)}
+ ${renderMetaRow(t("execApproval.labels.plugin"), active.pluginId)}
+ ${renderMetaRow(t("execApproval.labels.agent"), active.request.agentId)}
+ ${renderMetaRow(t("execApproval.labels.session"), active.request.sessionKey)}
`;
}
@@ -68,12 +72,15 @@ export function renderExecApprovalPrompt(state: AppViewState) {
}
const request = active.request;
const remainingMs = active.expiresAtMs - Date.now();
- const remaining = remainingMs > 0 ? `expires in ${formatRemaining(remainingMs)}` : "expired";
+ const remaining =
+ remainingMs > 0
+ ? t("execApproval.expiresIn", { time: formatRemaining(remainingMs) })
+ : t("execApproval.expired");
const queueCount = state.execApprovalQueue.length;
const isPlugin = active.kind === "plugin";
const title = isPlugin
- ? (active.pluginTitle ?? "Plugin approval needed")
- : "Exec approval needed";
+ ? (active.pluginTitle ?? t("execApproval.pluginApprovalNeeded"))
+ : t("execApproval.execApprovalNeeded");
const titleId = "exec-approval-title";
const descriptionId = "exec-approval-description";
const handleCancel = () => {
@@ -90,7 +97,9 @@ export function renderExecApprovalPrompt(state: AppViewState) {
${remaining}
${queueCount > 1
- ? html`
+ ${t("execApproval.pending", { count: String(queueCount) })}
+
`
: nothing}