Add inline approval controls to chat

This commit is contained in:
mirivlad 2026-05-20 01:07:06 +08:00
parent 72abeae2e8
commit 2d3a047548
2 changed files with 103 additions and 2 deletions

View File

@ -151,19 +151,87 @@ function updateToolTerminal(article, eventPayload) {
function appendApprovalTerminal(article, eventPayload) { function appendApprovalTerminal(article, eventPayload) {
const payload = eventPayload.payload || eventPayload; const payload = eventPayload.payload || eventPayload;
const action = payload.action || {};
appendToolTerminal(article, { appendToolTerminal(article, {
payload: { payload: {
index: payload.index, index: payload.index,
tool: payload.tool, tool: payload.tool,
args: payload.action?.args || {}, args: action.args || {},
}, },
}); });
const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`); const terminal = article?.querySelector(`.tool-terminal[data-tool-index="${payload.index || ""}"]`);
const body = terminal?.querySelector(".tool-terminal-body"); const body = terminal?.querySelector(".tool-terminal-body");
const status = terminal?.querySelector(".tool-terminal-status"); const status = terminal?.querySelector(".tool-terminal-status");
const command = formatToolCommand(action.tool || payload.tool, action.args || {});
terminal?.classList.add("is-waiting"); terminal?.classList.add("is-waiting");
if (terminal && payload.approval_id) terminal.dataset.approvalId = payload.approval_id;
if (status) status.textContent = "approval"; if (status) status.textContent = "approval";
if (body) body.textContent += `\n\napproval required\n${payload.reason || ""}`; if (body) {
body.textContent = [
body.textContent,
"",
"approval required",
command,
payload.reason || "",
].filter(Boolean).join("\n");
}
terminal?.append(createInlineApprovalActions(payload.approval_id, command));
}
function createInlineApprovalActions(approvalId, command) {
const actions = document.createElement("div");
actions.className = "tool-approval-actions";
actions.dataset.command = command || "tool action";
actions.append(
inlineApprovalButton("Allow once", "allow_once"),
inlineApprovalButton("Allow forever", "allow_forever"),
inlineApprovalButton("Deny", "deny", "danger"),
);
if (!approvalId) {
actions.querySelectorAll("button").forEach((button) => {
button.disabled = true;
});
}
return actions;
}
function inlineApprovalButton(label, action, tone = "") {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.dataset.inlineApprovalAction = action;
if (tone) button.dataset.tone = tone;
return button;
}
async function resolveInlineApproval(button) {
const terminal = button.closest(".tool-terminal");
const approvalId = terminal?.dataset.approvalId;
const action = button.dataset.inlineApprovalAction;
if (!terminal || !approvalId || !action) return;
terminal.querySelectorAll("[data-inline-approval-action]").forEach((item) => {
item.disabled = true;
});
await jsonFetch(`/v1/approvals/${approvalId}/${action}`, {method: "POST"});
const status = terminal.querySelector(".tool-terminal-status");
const body = terminal.querySelector(".tool-terminal-body");
const actions = terminal.querySelector(".tool-approval-actions");
const command = actions?.dataset.command || terminal.querySelector(".tool-terminal-title")?.textContent || "tool action";
const decision = action === "deny" ? "denied" : "allowed";
terminal.classList.toggle("is-error", action === "deny");
terminal.classList.remove("is-waiting");
if (status) status.textContent = decision;
if (body) body.textContent = `${command}\n\n${decision}: ${humanApprovalDecision(action)}`;
actions?.remove();
}
function humanApprovalDecision(action) {
if (action === "allow_once") return "разрешено один раз";
if (action === "allow_forever") return "разрешено навсегда";
return "запрещено";
} }
function setMessagePending(article, text) { function setMessagePending(article, text) {
@ -403,6 +471,11 @@ function bindChat() {
setStatus("#task-status", "none"); setStatus("#task-status", "none");
}); });
document.querySelector("#messages")?.addEventListener("click", (event) => { document.querySelector("#messages")?.addEventListener("click", (event) => {
const approvalButton = event.target.closest("[data-inline-approval-action]");
if (approvalButton) {
resolveInlineApproval(approvalButton).catch(console.error);
return;
}
const button = event.target.closest(".message-reasoning-toggle"); const button = event.target.closest(".message-reasoning-toggle");
if (button) toggleInlineReasoning(button); if (button) toggleInlineReasoning(button);
}); });

View File

@ -538,6 +538,34 @@ dd {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.tool-approval-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 10px 12px 12px;
border-top: 1px solid #1e293b;
background: #111827;
}
.tool-approval-actions button {
border: 0;
border-radius: 7px;
padding: 8px 10px;
background: #2563eb;
color: #ffffff;
font-size: 12px;
font-weight: 800;
}
.tool-approval-actions button[data-tone="danger"] {
background: #b42318;
}
.tool-approval-actions button:disabled {
cursor: wait;
opacity: 0.65;
}
.message-meta { .message-meta {
display: flex; display: flex;
align-items: center; align-items: center;