Add approval continuation and multi-step tools
This commit is contained in:
+90
-54
@@ -165,6 +165,7 @@ function appendApprovalTerminal(article, eventPayload) {
|
||||
const command = formatToolCommand(action.tool || payload.tool, action.args || {});
|
||||
terminal?.classList.add("is-waiting");
|
||||
if (terminal && payload.approval_id) terminal.dataset.approvalId = payload.approval_id;
|
||||
if (terminal && eventPayload.task_id) terminal.dataset.taskId = eventPayload.task_id;
|
||||
if (status) status.textContent = "approval";
|
||||
if (body) {
|
||||
body.textContent = [
|
||||
@@ -207,6 +208,7 @@ function inlineApprovalButton(label, action, tone = "") {
|
||||
async function resolveInlineApproval(button) {
|
||||
const terminal = button.closest(".tool-terminal");
|
||||
const approvalId = terminal?.dataset.approvalId;
|
||||
const taskId = terminal?.dataset.taskId;
|
||||
const action = button.dataset.inlineApprovalAction;
|
||||
if (!terminal || !approvalId || !action) return;
|
||||
|
||||
@@ -226,6 +228,9 @@ async function resolveInlineApproval(button) {
|
||||
if (status) status.textContent = decision;
|
||||
if (body) body.textContent = `${command}\n\n${decision}: ${humanApprovalDecision(action)}`;
|
||||
actions?.remove();
|
||||
if (taskId) {
|
||||
await continueAfterInlineApproval(terminal.closest(".message"), taskId, approvalId);
|
||||
}
|
||||
}
|
||||
|
||||
function humanApprovalDecision(action) {
|
||||
@@ -320,8 +325,8 @@ function parseSseBlock(block) {
|
||||
return {name: event.name, data: JSON.parse(event.data)};
|
||||
}
|
||||
|
||||
async function streamChat(payload, onEvent) {
|
||||
const response = await fetch("/v1/chat/stream", {
|
||||
async function streamSse(url, payload, onEvent) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(payload),
|
||||
@@ -350,6 +355,85 @@ async function streamChat(payload, onEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
async function streamChat(payload, onEvent) {
|
||||
await streamSse("/v1/chat/stream", payload, onEvent);
|
||||
}
|
||||
|
||||
async function handleAssistantStreamEvent(pending, name, data, context) {
|
||||
if (data.task_id) context.taskId = data.task_id;
|
||||
if (name === "task_created") {
|
||||
context.taskId = data.task_id;
|
||||
setStatus("#task-status", data.task_id, "warn");
|
||||
return;
|
||||
}
|
||||
if (name === "reasoning_delta") {
|
||||
pending.querySelector(".message-meta span").textContent = "reasoning";
|
||||
appendInlineReasoning(pending, data.delta || "");
|
||||
return;
|
||||
}
|
||||
if (name === "tool_call_started") {
|
||||
pending.querySelector(".message-meta span").textContent = "tool";
|
||||
appendToolTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "tool_call_finished") {
|
||||
pending.querySelector(".message-meta span").textContent = "tool";
|
||||
updateToolTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "tool_approval_requested") {
|
||||
pending.querySelector(".message-meta span").textContent = "approval";
|
||||
appendApprovalTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "content_delta") {
|
||||
if (!context.contentStarted) {
|
||||
context.contentStarted = true;
|
||||
setMessagePending(pending, "");
|
||||
}
|
||||
pending.querySelector(".message-meta span").textContent = "answering";
|
||||
appendMessageText(pending, data.delta || "");
|
||||
return;
|
||||
}
|
||||
if (name === "done") {
|
||||
if (!context.contentStarted) {
|
||||
setMessagePending(pending, data.final_response || "No final content returned.");
|
||||
}
|
||||
pending.querySelector(".message-meta span").textContent = data.status;
|
||||
setStatus("#task-status", data.task_id, data.status === "completed" ? "ok" : "warn");
|
||||
finishInlineReasoning(pending, data.reasoning_content);
|
||||
await refreshEvents(data.task_id);
|
||||
return;
|
||||
}
|
||||
if (name === "error") {
|
||||
throw new Error(data.error || "Stream failed.");
|
||||
}
|
||||
}
|
||||
|
||||
async function continueAfterInlineApproval(article, taskId, approvalId) {
|
||||
if (!article || state.running) return;
|
||||
state.running = true;
|
||||
document.querySelector("#run").disabled = true;
|
||||
setStatus("#task-status", taskId, "warn");
|
||||
const context = {taskId, contentStarted: false};
|
||||
try {
|
||||
await streamSse(
|
||||
`/v1/tasks/${encodeURIComponent(taskId)}/continue/stream`,
|
||||
{approval_id: approvalId},
|
||||
async ({name, data}) => handleAssistantStreamEvent(article, name, data, context),
|
||||
);
|
||||
} catch (error) {
|
||||
setMessagePending(article, error.message);
|
||||
article.querySelector(".message-meta span").textContent = "failed";
|
||||
setStatus("#task-status", "failed", "bad");
|
||||
await refreshEvents(taskId);
|
||||
} finally {
|
||||
state.running = false;
|
||||
document.querySelector("#run").disabled = false;
|
||||
document.querySelector("#message")?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (state.running) return;
|
||||
const input = document.querySelector("#message");
|
||||
@@ -362,8 +446,7 @@ async function sendMessage() {
|
||||
addMessage("user", message, "submitted");
|
||||
input.value = "";
|
||||
const pending = addMessage("assistant", "", "thinking", {reasoning: true});
|
||||
let taskId = "";
|
||||
let contentStarted = false;
|
||||
const context = {taskId: "", contentStarted: false};
|
||||
|
||||
try {
|
||||
await streamChat({
|
||||
@@ -371,61 +454,14 @@ async function sendMessage() {
|
||||
workspace: document.querySelector("#workspace").value,
|
||||
debug: document.querySelector("#debug").checked,
|
||||
}, async ({name, data}) => {
|
||||
if (data.task_id) taskId = data.task_id;
|
||||
if (name === "task_created") {
|
||||
taskId = data.task_id;
|
||||
setStatus("#task-status", taskId, "warn");
|
||||
return;
|
||||
}
|
||||
if (name === "reasoning_delta") {
|
||||
pending.querySelector(".message-meta span").textContent = "reasoning";
|
||||
appendInlineReasoning(pending, data.delta || "");
|
||||
return;
|
||||
}
|
||||
if (name === "tool_call_started") {
|
||||
pending.querySelector(".message-meta span").textContent = "tool";
|
||||
appendToolTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "tool_call_finished") {
|
||||
pending.querySelector(".message-meta span").textContent = "tool";
|
||||
updateToolTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "tool_approval_requested") {
|
||||
pending.querySelector(".message-meta span").textContent = "approval";
|
||||
appendApprovalTerminal(pending, data);
|
||||
return;
|
||||
}
|
||||
if (name === "content_delta") {
|
||||
if (!contentStarted) {
|
||||
contentStarted = true;
|
||||
setMessagePending(pending, "");
|
||||
}
|
||||
pending.querySelector(".message-meta span").textContent = "answering";
|
||||
appendMessageText(pending, data.delta || "");
|
||||
return;
|
||||
}
|
||||
if (name === "done") {
|
||||
if (!contentStarted) {
|
||||
setMessagePending(pending, data.final_response || "No final content returned.");
|
||||
}
|
||||
pending.querySelector(".message-meta span").textContent = data.status;
|
||||
setStatus("#task-status", data.task_id, data.status === "completed" ? "ok" : "warn");
|
||||
finishInlineReasoning(pending, data.reasoning_content);
|
||||
await refreshEvents(data.task_id);
|
||||
return;
|
||||
}
|
||||
if (name === "error") {
|
||||
throw new Error(data.error || "Stream failed.");
|
||||
}
|
||||
await handleAssistantStreamEvent(pending, name, data, context);
|
||||
});
|
||||
} catch (error) {
|
||||
if (!taskId) input.value = message;
|
||||
if (!context.taskId) input.value = message;
|
||||
setMessagePending(pending, error.message);
|
||||
pending.querySelector(".message-meta span").textContent = "failed";
|
||||
setStatus("#task-status", "failed", "bad");
|
||||
if (taskId) await refreshEvents(taskId);
|
||||
if (context.taskId) await refreshEvents(context.taskId);
|
||||
} finally {
|
||||
state.running = false;
|
||||
document.querySelector("#run").disabled = false;
|
||||
|
||||
Reference in New Issue
Block a user