mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-19 07:59:34 +00:00
Automatic canvas handoffs
- Auto-open Office and Browser canvas surfaces from fresh tool results, including history/result messages. - Preserve Browser target IDs when focusing a canvas session from tool output. - Convert substantial response-style artifacts into Office documents at runtime, without relying only on prompt compliance. - Attach Office artifact metadata to the completed response log so the canvas opens without leaving a dangling Processing group. - Polish Office UX by removing the inactive version-history action, showing only the healthy dot, and improving Collabora blank-load recovery with browser state cleanup. - Deduplicate auto-open events and ignore stale results.
This commit is contained in:
parent
370ac9b878
commit
f1b014feb3
10 changed files with 882 additions and 35 deletions
|
|
@ -11,6 +11,8 @@ import {
|
|||
} from "/js/messages.js";
|
||||
|
||||
const BROWSER_MODAL = "/plugins/_browser/webui/main.html";
|
||||
const AUTO_OPEN_WINDOW_MS = 10 * 60 * 1000;
|
||||
const autoOpenedBrowsers = new Set();
|
||||
|
||||
export default async function registerBrowserToolHandler(extData) {
|
||||
if (extData?.tool_name === "browser") {
|
||||
|
|
@ -18,6 +20,69 @@ export default async function registerBrowserToolHandler(extData) {
|
|||
}
|
||||
}
|
||||
|
||||
async function openBrowserCanvas(payload = {}) {
|
||||
const canvas = globalThis.Alpine?.store?.("rightCanvas")
|
||||
|| (await import("/components/canvas/right-canvas-store.js")).store;
|
||||
if (canvas) {
|
||||
await canvas.open("browser", payload);
|
||||
return;
|
||||
}
|
||||
if (window.ensureModalOpen) {
|
||||
await window.ensureModalOpen(BROWSER_MODAL);
|
||||
return;
|
||||
}
|
||||
await window.openModal?.(BROWSER_MODAL);
|
||||
}
|
||||
|
||||
function parseBrowserResult(content) {
|
||||
if (!content || typeof content !== "string") return {};
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed && typeof parsed === "object" ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function browserIdFromResult(result = {}, kvps = {}) {
|
||||
return (
|
||||
result.id
|
||||
|| result.browser_id
|
||||
|| result.state?.id
|
||||
|| result.last_interacted_browser_id
|
||||
|| kvps.browser_id
|
||||
|| null
|
||||
);
|
||||
}
|
||||
|
||||
function isFreshToolMessage(timestamp) {
|
||||
const value = Number(timestamp);
|
||||
if (!Number.isFinite(value) || value <= 0) return true;
|
||||
const messageMs = value > 10_000_000_000 ? value : value * 1000;
|
||||
return Math.abs(Date.now() - messageMs) <= AUTO_OPEN_WINDOW_MS;
|
||||
}
|
||||
|
||||
function shouldAutoOpenBrowser(args, result) {
|
||||
if (!isFreshToolMessage(args?.timestamp)) return false;
|
||||
const action = String(args?.kvps?.action || "").trim().toLowerCase().replace("-", "_");
|
||||
if (["list", "content", "detail", "close", "close_all"].includes(action)) return false;
|
||||
return Boolean(browserIdFromResult(result, args?.kvps || {}) || action === "open" || action === "navigate");
|
||||
}
|
||||
|
||||
function autoOpenBrowserCanvas(args, result) {
|
||||
if (!shouldAutoOpenBrowser(args, result)) return;
|
||||
const kvps = args?.kvps || {};
|
||||
const browserId = browserIdFromResult(result, kvps);
|
||||
const key = `${args.id || ""}:${kvps.action || ""}:${browserId || ""}:${result.currentUrl || result.state?.currentUrl || kvps.url || ""}`;
|
||||
const persistedKey = `a0.browser.autoOpened.${key}`;
|
||||
if (autoOpenedBrowsers.has(key) || sessionStorage.getItem(persistedKey)) return;
|
||||
autoOpenedBrowsers.add(key);
|
||||
sessionStorage.setItem(persistedKey, "1");
|
||||
requestAnimationFrame(() => {
|
||||
void openBrowserCanvas({ browserId, source: "tool" });
|
||||
});
|
||||
}
|
||||
|
||||
function drawBrowserTool({
|
||||
id,
|
||||
type,
|
||||
|
|
@ -28,27 +93,18 @@ function drawBrowserTool({
|
|||
agentno = 0,
|
||||
...additional
|
||||
}) {
|
||||
const args = arguments[0];
|
||||
const title = cleanStepTitle(heading);
|
||||
const displayKvps = { ...kvps };
|
||||
const headerLabels = [
|
||||
kvps?._tool_name && { label: kvps._tool_name, class: "tool-name-badge" },
|
||||
].filter(Boolean);
|
||||
const contentText = String(content ?? "");
|
||||
const browserResult = parseBrowserResult(contentText);
|
||||
const browserButton = createActionButton(
|
||||
"visibility",
|
||||
"Browser",
|
||||
() => {
|
||||
const canvas = globalThis.Alpine?.store?.("rightCanvas");
|
||||
if (canvas) {
|
||||
void canvas.open("browser");
|
||||
return;
|
||||
}
|
||||
if (window.ensureModalOpen) {
|
||||
void window.ensureModalOpen(BROWSER_MODAL);
|
||||
return;
|
||||
}
|
||||
void window.openModal?.(BROWSER_MODAL);
|
||||
},
|
||||
() => openBrowserCanvas({ browserId: browserIdFromResult(browserResult, kvps), source: "tool" }),
|
||||
);
|
||||
browserButton.setAttribute("title", "Open Browser");
|
||||
browserButton.setAttribute("aria-label", "Open Browser");
|
||||
|
|
@ -60,7 +116,7 @@ function drawBrowserTool({
|
|||
actionButtons.push(
|
||||
createActionButton("detail", "", () =>
|
||||
stepDetailStore.showStepDetail(
|
||||
buildDetailPayload(arguments[0], { headerLabels }),
|
||||
buildDetailPayload(args, { headerLabels }),
|
||||
),
|
||||
),
|
||||
createActionButton("speak", "", () => speechStore.speak(contentText)),
|
||||
|
|
@ -68,7 +124,7 @@ function drawBrowserTool({
|
|||
);
|
||||
}
|
||||
|
||||
return drawProcessStep({
|
||||
const result = drawProcessStep({
|
||||
id,
|
||||
title,
|
||||
code: "WWW",
|
||||
|
|
@ -76,6 +132,8 @@ function drawBrowserTool({
|
|||
kvps: displayKvps,
|
||||
content,
|
||||
actionButtons: actionButtons.filter(Boolean),
|
||||
log: arguments[0],
|
||||
log: args,
|
||||
});
|
||||
autoOpenBrowserCanvas(args, browserResult);
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,14 @@ export default async function registerBrowserSurface(canvas) {
|
|||
icon: "language",
|
||||
order: 10,
|
||||
modalPath: "/plugins/_browser/webui/main.html",
|
||||
async open() {
|
||||
async open(payload = {}) {
|
||||
const panel = await waitForElement('[data-surface-id="browser"] .browser-panel');
|
||||
const browser = globalThis.Alpine?.store?.("browserPage");
|
||||
if (panel && browser?.onOpen) {
|
||||
await browser.onOpen(panel, { mode: "canvas" });
|
||||
await browser.onOpen(panel, {
|
||||
mode: "canvas",
|
||||
browserId: payload.browserId || payload.browser_id || null,
|
||||
});
|
||||
}
|
||||
},
|
||||
async close() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
const AUTO_OPEN_WINDOW_MS = 10 * 60 * 1000;
|
||||
const BROWSER_MODAL = "/plugins/_browser/webui/main.html";
|
||||
const autoOpenedBrowsers = new Set();
|
||||
|
||||
export default async function autoOpenBrowserResults(context) {
|
||||
if (!context?.results?.length || context.historyEmpty) return;
|
||||
|
||||
for (const { args } of context.results) {
|
||||
const payload = getToolResultPayload(args);
|
||||
if (getToolName(payload) !== "browser") continue;
|
||||
|
||||
const result = parseMaybeJson(payload.tool_result) || {};
|
||||
if (!shouldAutoOpen(args, payload, result)) continue;
|
||||
|
||||
const browserId = getBrowserId(payload, result);
|
||||
const key = [
|
||||
args?.id || "",
|
||||
browserId || "",
|
||||
result.currentUrl || result.state?.currentUrl || payload.url || "",
|
||||
].join(":");
|
||||
const persistedKey = `a0.browser.autoOpened.${key}`;
|
||||
if (hasOpened(key, persistedKey)) continue;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
void openBrowserCanvas({ browserId, source: "tool-result" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getToolResultPayload(args = {}) {
|
||||
const topLevelPayload = pickPayloadFields(args);
|
||||
const contentPayload = parseMaybeJson(args.content);
|
||||
const kvpsPayload = parseMaybeJson(args.kvps);
|
||||
return {
|
||||
...topLevelPayload,
|
||||
...(contentPayload || {}),
|
||||
...(kvpsPayload || {}),
|
||||
};
|
||||
}
|
||||
|
||||
function pickPayloadFields(args = {}) {
|
||||
const payload = {};
|
||||
for (const key of [
|
||||
"_tool_name",
|
||||
"tool_name",
|
||||
"tool_result",
|
||||
"action",
|
||||
"browser_id",
|
||||
"browserId",
|
||||
"url",
|
||||
"last_modified",
|
||||
]) {
|
||||
if (args[key] != null && args[key] !== "") payload[key] = args[key];
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function getToolName(payload = {}) {
|
||||
return String(payload._tool_name || payload.tool_name || "").trim();
|
||||
}
|
||||
|
||||
function parseMaybeJson(value) {
|
||||
if (!value) return null;
|
||||
if (typeof value === "object") return value;
|
||||
if (typeof value !== "string") return null;
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.startsWith("{")) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
return parsed && typeof parsed === "object" ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldAutoOpen(args = {}, payload = {}, result = {}) {
|
||||
if (!isFresh(args.timestamp, payload.last_modified || result.last_modified)) return false;
|
||||
|
||||
const action = String(payload.action || "").trim().toLowerCase().replace("-", "_");
|
||||
if (["list", "content", "detail", "close", "close_all"].includes(action)) return false;
|
||||
|
||||
return Boolean(
|
||||
getBrowserId(payload, result)
|
||||
|| action === "open"
|
||||
|| action === "navigate"
|
||||
|| result.currentUrl
|
||||
|| result.state?.currentUrl,
|
||||
);
|
||||
}
|
||||
|
||||
function getBrowserId(payload = {}, result = {}) {
|
||||
return (
|
||||
result.id
|
||||
|| result.browser_id
|
||||
|| result.state?.id
|
||||
|| result.last_interacted_browser_id
|
||||
|| payload.browser_id
|
||||
|| payload.browserId
|
||||
|| null
|
||||
);
|
||||
}
|
||||
|
||||
function isFresh(timestamp, fallbackTimestamp) {
|
||||
const messageMs = toMs(timestamp) || toMs(fallbackTimestamp);
|
||||
if (!messageMs) return true;
|
||||
return Math.abs(Date.now() - messageMs) <= AUTO_OPEN_WINDOW_MS;
|
||||
}
|
||||
|
||||
function toMs(value) {
|
||||
if (value == null || value === "") return 0;
|
||||
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric) && numeric > 0) {
|
||||
return numeric > 10_000_000_000 ? numeric : numeric * 1000;
|
||||
}
|
||||
|
||||
const parsed = Date.parse(String(value));
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function hasOpened(key, persistedKey) {
|
||||
if (autoOpenedBrowsers.has(key)) return true;
|
||||
autoOpenedBrowsers.add(key);
|
||||
|
||||
try {
|
||||
if (sessionStorage.getItem(persistedKey)) return true;
|
||||
sessionStorage.setItem(persistedKey, "1");
|
||||
} catch {
|
||||
// Best-effort persistence; the in-memory guard still prevents repeat opens.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function openBrowserCanvas(payload = {}) {
|
||||
const canvas = globalThis.Alpine?.store?.("rightCanvas")
|
||||
|| (await import("/components/canvas/right-canvas-store.js")).store;
|
||||
if (canvas) {
|
||||
await canvas.open("browser", payload);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.ensureModalOpen) {
|
||||
await window.ensureModalOpen(BROWSER_MODAL);
|
||||
return;
|
||||
}
|
||||
await window.openModal?.(BROWSER_MODAL);
|
||||
}
|
||||
|
|
@ -299,6 +299,7 @@ const model = {
|
|||
async onOpen(element = null, options = {}) {
|
||||
this.loading = true;
|
||||
this.error = "";
|
||||
const requestedBrowserId = this.normalizeBrowserId(options.browserId ?? options.browser_id);
|
||||
this._mode = options?.mode === "modal" ? "modal" : "canvas";
|
||||
if (this._mode === "modal") {
|
||||
this.setupFloatingModal(element);
|
||||
|
|
@ -308,7 +309,7 @@ const model = {
|
|||
this.contextId = this.resolveContextId();
|
||||
try {
|
||||
await this.refreshStatus();
|
||||
await this.connectViewer();
|
||||
await this.connectViewer({ browserId: requestedBrowserId });
|
||||
} catch (error) {
|
||||
this.error = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue