diff --git a/plugins/_office/api/office_session.py b/plugins/_office/api/office_session.py index 69691c24d..b78a799d7 100644 --- a/plugins/_office/api/office_session.py +++ b/plugins/_office/api/office_session.py @@ -1,7 +1,8 @@ from __future__ import annotations from helpers.api import ApiHandler, Request -from plugins._office.helpers import document_store, libreoffice, libreoffice_desktop, markdown_sessions +from plugins._desktop.helpers import desktop_session +from plugins._office.helpers import document_store, libreoffice, markdown_sessions class OfficeSession(ApiHandler): @@ -14,6 +15,7 @@ class OfficeSession(ApiHandler): if action == "home": return {"ok": True, "path": document_store.default_open_path(context_id)} if action == "desktop": + # Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session. return self._desktop() if action == "close": closed = document_store.close_session( @@ -58,30 +60,47 @@ class OfficeSession(ApiHandler): if action == "renamed": return self._renamed(input, context_id) if action == "desktop_save": + # Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session. return self._desktop_save(input) if action == "desktop_sync": + # Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session. return self._desktop_sync(input) if action == "desktop_state": + # Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session. return self._desktop_state(input) if action == "desktop_shutdown": + # Compatibility only. New Desktop callers use /plugins/_desktop/desktop_session. return self._desktop_shutdown(input) return {"ok": False, "error": f"Unsupported office session action: {action}"} async def _open_document(self, doc: dict, input: dict, request: Request) -> dict: mode = "edit" if str(input.get("mode") or "edit").lower() == "edit" else "view" - store_session = document_store.create_session( - doc["file_id"], - user_id=str(input.get("user_id") or "agent-zero-user"), - permission="write" if mode == "edit" else "read", - origin=self._origin(request), - ) - if str(doc.get("extension") or "").lower() in libreoffice_desktop.OFFICIAL_EXTENSIONS: - desktop = libreoffice_desktop.get_manager().open(doc, refresh=input.get("refresh") is True) + if str(doc.get("extension") or "").lower() in desktop_session.OFFICIAL_EXTENSIONS: + if input.get("open_in_desktop") is not True: + return { + "ok": True, + "requires_desktop": True, + "file_id": doc["file_id"], + "title": doc["basename"], + "extension": doc["extension"], + "path": doc["path"], + "text": "", + "document": _public_doc(doc), + "version": document_store.item_version(doc), + "mode": mode, + } + store_session = document_store.create_session( + doc["file_id"], + user_id=str(input.get("user_id") or "agent-zero-user"), + permission="write" if mode == "edit" else "read", + origin=self._origin(request), + ) + desktop = desktop_session.get_manager().open(doc, refresh=input.get("refresh") is True) if not desktop.get("available"): document_store.close_session(session_id=store_session["session_id"]) return { "ok": False, - "error": desktop.get("error") or desktop.get("reason") or "Official LibreOffice desktop session is unavailable.", + "error": desktop.get("error") or desktop.get("reason") or "Desktop session is unavailable.", "desktop": desktop, "libreoffice": libreoffice.collect_status(), } @@ -100,6 +119,12 @@ class OfficeSession(ApiHandler): "store_session_id": store_session["session_id"], "mode": mode, } + store_session = document_store.create_session( + doc["file_id"], + user_id=str(input.get("user_id") or "agent-zero-user"), + permission="write" if mode == "edit" else "read", + origin=self._origin(request), + ) try: editor = markdown_sessions.get_manager().open(doc, sid="") except ValueError as exc: @@ -135,8 +160,8 @@ class OfficeSession(ApiHandler): except Exception as exc: return {"ok": False, "error": str(exc)} desktop = None - if str(updated.get("extension") or "").lower() in libreoffice_desktop.OFFICIAL_EXTENSIONS: - desktop = libreoffice_desktop.get_manager().retarget_document(file_id, updated) + if str(updated.get("extension") or "").lower() in desktop_session.OFFICIAL_EXTENSIONS: + desktop = desktop_session.get_manager().retarget_document(file_id, updated) return { "ok": True, "document": _public_doc(updated), @@ -146,7 +171,7 @@ class OfficeSession(ApiHandler): } def _desktop(self) -> dict: - desktop = libreoffice_desktop.get_manager().ensure_system_desktop() + desktop = desktop_session.get_manager().ensure_system_desktop() if not desktop.get("available"): return { "ok": False, @@ -155,7 +180,7 @@ class OfficeSession(ApiHandler): "libreoffice": libreoffice.collect_status(), } document = { - "file_id": libreoffice_desktop.SYSTEM_FILE_ID, + "file_id": desktop_session.SYSTEM_FILE_ID, "path": desktop["path"], "basename": desktop["title"], "title": desktop["title"], @@ -167,7 +192,7 @@ class OfficeSession(ApiHandler): "ok": True, "session_id": desktop["session_id"], "desktop_session_id": desktop["session_id"], - "file_id": libreoffice_desktop.SYSTEM_FILE_ID, + "file_id": desktop_session.SYSTEM_FILE_ID, "title": desktop["title"], "extension": "desktop", "path": desktop["path"], @@ -183,24 +208,24 @@ class OfficeSession(ApiHandler): session_id = str(input.get("desktop_session_id") or input.get("session_id") or "").strip() if not session_id: return {"ok": False, "error": "desktop_session_id is required."} - return libreoffice_desktop.get_manager().save( + return desktop_session.get_manager().save( session_id, file_id=str(input.get("file_id") or ""), ) def _desktop_sync(self, input: dict) -> dict: - return libreoffice_desktop.get_manager().sync( + return desktop_session.get_manager().sync( session_id=str(input.get("desktop_session_id") or input.get("session_id") or ""), file_id=str(input.get("file_id") or ""), ) def _desktop_state(self, input: dict) -> dict: include_screenshot = bool(input.get("include_screenshot") is True) - return libreoffice_desktop.get_manager().state(include_screenshot=include_screenshot) + return desktop_session.get_manager().state(include_screenshot=include_screenshot) def _desktop_shutdown(self, input: dict) -> dict: save_first = input.get("save_first") is not False - return libreoffice_desktop.get_manager().shutdown_system_desktop( + return desktop_session.get_manager().shutdown_system_desktop( save_first=save_first, source=str(input.get("source") or "api"), ) diff --git a/plugins/_office/extensions/webui/lib/document-actions.js b/plugins/_office/extensions/webui/lib/document-actions.js index 2b26c9f95..f9abba1ba 100644 --- a/plugins/_office/extensions/webui/lib/document-actions.js +++ b/plugins/_office/extensions/webui/lib/document-actions.js @@ -2,6 +2,9 @@ import { createActionButton, copyToClipboard, } from "/components/messages/action-buttons/simple-action-buttons.js"; +import { ensureModalOpen } from "/js/modals.js"; +import { open as openSurface } from "/js/surfaces.js"; +import { store as officeStore } from "/plugins/_office/webui/office-store.js"; function basename(path = "") { const value = String(path || "").split("?")[0].split("#")[0]; @@ -33,10 +36,8 @@ export function documentFromLog(args = {}, result = {}) { }; } -export async function openOfficeCanvas(kvps = {}) { - const canvas = globalThis.Alpine?.store?.("rightCanvas") - || (await import("/components/canvas/right-canvas-store.js")).store; - await canvas?.open?.("office", { +export async function openDocumentInDesktop(kvps = {}) { + await openSurface("desktop", { path: kvps.path || "", file_id: kvps.file_id || "", refresh: true, @@ -44,6 +45,33 @@ export async function openOfficeCanvas(kvps = {}) { }); } +export async function openDocumentArtifact(kvps = {}) { + if (usesDesktop(kvps)) { + await openDocumentInDesktop(kvps); + return; + } + await ensureModalOpen("/plugins/_office/webui/main.html"); + await officeStore.openSession?.({ + path: kvps.path || "", + file_id: kvps.file_id || "", + refresh: true, + source: "message-action", + }); +} + +function usesDesktop(doc = {}) { + const format = String(doc.format || doc.extension || "").toLowerCase(); + return ["odt", "ods", "odp", "docx", "xlsx", "pptx"].includes(format); +} + +function desktopActionLabel(doc = {}) { + const format = String(doc.format || doc.extension || "").toLowerCase(); + if (["odt", "docx"].includes(format)) return "Edit in Writer"; + if (["ods", "xlsx"].includes(format)) return "Edit in Calc"; + if (["odp", "pptx"].includes(format)) return "Edit in Impress"; + return "Open Document"; +} + export function downloadDocument(doc = {}) { const path = String(doc.path || ""); if (!path) return; @@ -59,7 +87,8 @@ export function buildDocumentFileActionButtons(document = {}) { const hasTarget = Boolean(document?.path || document?.file_id); const buttons = []; if (hasTarget) { - buttons.push(createActionButton("dock_to_right", "Open in canvas", () => openOfficeCanvas(document))); + const icon = usesDesktop(document) ? "desktop_windows" : "article"; + buttons.push(createActionButton(icon, desktopActionLabel(document), () => openDocumentArtifact(document))); } if (document?.path) { buttons.push( diff --git a/plugins/_office/extensions/webui/set_messages_after_loop/auto-open-document-results.js b/plugins/_office/extensions/webui/set_messages_after_loop/auto-open-document-results.js index 755073992..360233cda 100644 --- a/plugins/_office/extensions/webui/set_messages_after_loop/auto-open-document-results.js +++ b/plugins/_office/extensions/webui/set_messages_after_loop/auto-open-document-results.js @@ -1,14 +1,18 @@ +import { store as officeStore } from "/plugins/_office/webui/office-store.js"; +import { ensureModalOpen } from "/js/modals.js"; +import { open as openSurface } from "/js/surfaces.js"; + const SYNC_WINDOW_MS = 10 * 60 * 1000; -const DESKTOP_OFFICE_FORMATS = new Set(["odt", "ods", "odp", "docx", "xlsx", "pptx"]); +const DESKTOP_DOCUMENT_EXTENSIONS = new Set(["odt", "ods", "odp", "docx", "xlsx", "pptx"]); const syncedDocumentResults = new Set(); -export default async function syncDocumentResultsIntoOpenCanvas(context) { +export default async function syncDocumentResultsIntoOpenOfficeModal(context) { if (!context?.results?.length || context.historyEmpty) return; for (const { args } of context.results) { const payload = getDocumentPayload(args); if (getToolName(payload) !== "document_artifact") continue; - if (!shouldSyncOpenOfficeCanvas(args, payload)) continue; + if (!shouldSyncOpenOfficeModal(args, payload)) continue; const document = payload.document && typeof payload.document === "object" ? payload.document : {}; const path = payload.path || document.path || ""; @@ -25,14 +29,16 @@ export default async function syncDocumentResultsIntoOpenCanvas(context) { if (syncedDocumentResults.has(key)) continue; syncedDocumentResults.add(key); - if (!isOfficeCanvasOrModalOpen() && shouldColdOpenOfficeCanvas(payload, document)) { - await openOfficeCanvasFromResult({ path, file_id: fileId }); + if (shouldOpenDocumentUiFromResult(payload, document)) { + globalThis.setTimeout(() => { + void openDocumentUiFromResult({ path, file_id: fileId }, payload, document); + }, 0); continue; } globalThis.setTimeout(async () => { - if (!isOfficeCanvasOrModalOpen()) return; - const office = globalThis.Alpine?.store?.("office"); + if (!isOfficeModalOpen()) return; + const office = officeStore; if (!office || isDirtySameDocument(office, { path, file_id: fileId })) return; await office.openSession?.({ path, @@ -63,8 +69,14 @@ function pickPayloadFields(args = {}) { "tool_name", "action", "canvas_surface", + "extension", "file_id", "format", + "open_canvas", + "open_document", + "open_desktop", + "open_in_canvas", + "open_in_desktop", "path", "version", "last_modified", @@ -78,54 +90,62 @@ function getToolName(payload = {}) { return String(payload._tool_name || payload.tool_name || "").trim(); } -function shouldSyncOpenOfficeCanvas(args = {}, payload = {}) { +function shouldSyncOpenOfficeModal(args = {}, payload = {}) { if (!isFresh(args.timestamp, payload.last_modified || payload.document?.last_modified)) return false; const action = String(payload.action || "").trim().toLowerCase().replace("-", "_"); return ["create", "open", "edit", "restore_version"].includes(action); } -function shouldColdOpenOfficeCanvas(payload = {}, document = {}) { - const action = String(payload.action || "").trim().toLowerCase().replace("-", "_"); - if (!["create", "open"].includes(action)) return false; - return DESKTOP_OFFICE_FORMATS.has(documentFormat(payload, document)); +function shouldOpenDocumentUiFromResult(payload = {}, document = {}) { + if (!isExplicitDocumentUiRequest(payload)) return false; + return Boolean(documentExtension(payload, document)); } -async function openOfficeCanvasFromResult(document = {}) { - const canvas = globalThis.Alpine?.store?.("rightCanvas") - || (await import("/components/canvas/right-canvas-store.js")).store; - await canvas?.open?.("office", { - path: document.path || "", - file_id: document.file_id || "", +function isExplicitDocumentUiRequest(payload = {}) { + const action = String(payload.action || "").trim().toLowerCase().replace("-", "_"); + return action === "open" + || truthy(payload.open_in_canvas) + || truthy(payload.open_canvas) + || truthy(payload.open_document) + || truthy(payload.open_in_desktop) + || truthy(payload.open_desktop); +} + +async function openDocumentUiFromResult(target = {}, payload = {}, document = {}) { + if (isDesktopDocument(payload, document)) { + await openSurface("desktop", { + path: target.path || "", + file_id: target.file_id || "", + refresh: true, + source: "tool-result-open", + }); + return; + } + + await ensureModalOpen("/plugins/_office/webui/main.html"); + await officeStore.openSession?.({ + path: target.path || "", + file_id: target.file_id || "", refresh: true, - source: "tool-result-sync", + source: "tool-result-open", }); } -function documentFormat(payload = {}, document = {}) { +function isDesktopDocument(payload = {}, document = {}) { + return DESKTOP_DOCUMENT_EXTENSIONS.has(documentExtension(payload, document)); +} + +function documentExtension(payload = {}, document = {}) { return String( payload.format || payload.extension || document.extension - || extensionOf(payload.path || document.path || ""), - ).trim().toLowerCase().replace(/^\./, ""); + || document.format + || "", + ).toLowerCase(); } -function extensionOf(path = "") { - const name = String(path || "").split("?")[0].split("#")[0].split("/").filter(Boolean).pop() || ""; - const index = name.lastIndexOf("."); - return index >= 0 ? name.slice(index + 1) : ""; -} - -function isOfficeCanvasAlreadyOpen() { - const canvas = globalThis.Alpine?.store?.("rightCanvas"); - return Boolean(canvas?.isOpen && canvas?.activeSurfaceId === "office"); -} - -function isOfficeCanvasOrModalOpen() { - return Boolean(isOfficeCanvasAlreadyOpen() || isOfficeModalAlreadyOpen()); -} - -function isOfficeModalAlreadyOpen() { +function isOfficeModalOpen() { return Boolean( globalThis.isModalOpen?.("/plugins/_office/webui/main.html") || globalThis.isModalOpen?.("plugins/_office/webui/main.html") @@ -143,6 +163,13 @@ function isDirtySameDocument(office, document = {}) { ); } +function truthy(value) { + if (value === true) return true; + if (value === false || value == null) return false; + if (typeof value === "number") return value !== 0; + return ["1", "true", "yes", "y", "on"].includes(String(value).trim().toLowerCase()); +} + function isFresh(...timestamps) { const now = Date.now(); for (const value of timestamps) { diff --git a/plugins/_office/helpers/artifact_editor.py b/plugins/_office/helpers/artifact_editor.py index bbec68e1e..78d6b66ea 100644 --- a/plugins/_office/helpers/artifact_editor.py +++ b/plugins/_office/helpers/artifact_editor.py @@ -141,9 +141,9 @@ def _refresh_open_editor_sessions(file_id: str) -> None: # Direct artifact edits should never fail just because no canvas is open. pass try: - from plugins._office.helpers import libreoffice_desktop + from plugins._desktop.helpers import desktop_session - libreoffice_desktop.get_manager().refresh_document(file_id) + desktop_session.get_manager().refresh_document(file_id) except Exception: pass diff --git a/plugins/_office/helpers/canvas_context.py b/plugins/_office/helpers/canvas_context.py index cd4f0b604..395194269 100644 --- a/plugins/_office/helpers/canvas_context.py +++ b/plugins/_office/helpers/canvas_context.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any -from plugins._office.helpers import desktop_state +from plugins._desktop.helpers import desktop_state from plugins._office.helpers import document_store @@ -13,7 +13,7 @@ def build_context(max_items: int = 6) -> str: return desktop_context lines = [ - "These document artifacts have active canvas sessions. Content is omitted; load skill `office-artifacts` for edit workflow, then use document_artifact:read before content-sensitive edits.", + "These document artifacts have active document sessions. Content is omitted; load skill `document-artifacts` for edit workflow, then use document_artifact:read before content-sensitive edits.", ] for doc in documents: lines.append(format_document_line(doc)) @@ -46,5 +46,5 @@ def build_desktop_context() -> str: return ( "[DESKTOP STATE]\n" f"- unavailable={exc}\n" - "- next=Open the Desktop canvas manually, then run plugins/_office/skills/linux-desktop/scripts/desktopctl.sh observe --json." + "- next=Open the Desktop surface manually, then run plugins/_desktop/skills/linux-desktop/scripts/desktopctl.sh observe --json." ) diff --git a/plugins/_office/helpers/document_store.py b/plugins/_office/helpers/document_store.py index f357bb7e0..dbdb8843b 100644 --- a/plugins/_office/helpers/document_store.py +++ b/plugins/_office/helpers/document_store.py @@ -40,7 +40,7 @@ ODF_MIMETYPES = { "odp": "application/vnd.oasis.opendocument.presentation", } -STATE_DIR = Path(files.get_abs_path("usr", "plugins", PLUGIN_NAME, "documents")) +STATE_DIR = Path(files.get_abs_path("usr", PLUGIN_NAME, "documents")) DB_PATH = STATE_DIR / "documents.sqlite3" BACKUP_DIR = STATE_DIR / "backups" WORKDIR = Path(files.get_abs_path("usr", "workdir")) diff --git a/plugins/_office/helpers/libreoffice.py b/plugins/_office/helpers/libreoffice.py index 92e29fd25..d5d4862cf 100644 --- a/plugins/_office/helpers/libreoffice.py +++ b/plugins/_office/helpers/libreoffice.py @@ -36,9 +36,9 @@ def collect_status() -> dict[str, Any]: "message": "LibreOffice is available." if soffice else "LibreOffice is not installed in this runtime.", } try: - from plugins._office.helpers import libreoffice_desktop + from plugins._desktop.helpers import desktop_session - status["desktop"] = libreoffice_desktop.collect_desktop_status() + status["desktop"] = desktop_session.collect_desktop_status() except Exception as exc: status["desktop"] = {"ok": False, "healthy": False, "error": str(exc)} return status diff --git a/plugins/_office/plugin.yaml b/plugins/_office/plugin.yaml index b1397b6aa..965925a8e 100644 --- a/plugins/_office/plugin.yaml +++ b/plugins/_office/plugin.yaml @@ -1,6 +1,6 @@ name: _office title: LibreOffice -description: Markdown writing and ODF-first LibreOffice document artifacts in the right canvas. +description: Markdown writing and ODF-first LibreOffice document artifacts. version: "0.1" settings_sections: - developer diff --git a/plugins/_office/prompts/agent.system.tool.document_artifact.md b/plugins/_office/prompts/agent.system.tool.document_artifact.md index baa29d947..3c51ab3e0 100644 --- a/plugins/_office/prompts/agent.system.tool.document_artifact.md +++ b/plugins/_office/prompts/agent.system.tool.document_artifact.md @@ -1,15 +1,17 @@ ### document_artifact -create/open/read/edit reusable document artifacts in the Agent Zero canvas +create/open/read/edit reusable document artifacts in Agent Zero formats: md odt ods odp docx xlsx pptx default format: md methods: create open read edit inspect export version_history restore_version status common args: method action kind title format content path file_id +optional UI intent args: open_in_canvas open_in_desktop `method` is accepted as an alias for action when the tool_name has no suffix -tool results save or update artifacts only; they do not open the canvas automatically -created/updated artifacts are shown with explicit Download and Open in canvas message actions -ODF is first-class for LibreOffice: use ODT for Writer, ODS for Spreadsheet/Calc, and ODP for Presentation/Impress unless the user explicitly requests Microsoft compatibility +create/read/edit results save or update artifacts only; they do not open a surface automatically unless the user explicitly asks to open the document UI +use action `open`, `open_in_canvas: true`, or `open_in_desktop: true` only when the user explicitly asks to open the document/editor/Desktop +created/updated artifacts are shown with explicit Download, Open Document, or Desktop edit message actions +ODF is first-class for LibreOffice: use ODT for Writer, ODS for Spreadsheet/Calc, and ODP for Presentation/Impress unless the user explicitly requests OOXML compatibility DOCX/XLSX/PPTX are compatibility formats, not defaults XLSX charts: use edit operation `create_chart` with `chart` object instead of code execution for embedded spreadsheet charts when an embedded chart is required chart types: line bar column pie area scatter stock ohlc candlestick ODS/XLSX create/edit tabular content: CSV, TSV, Markdown tables, or rows arrays become real spreadsheet cells -for nontrivial document artifact work, load skill `office-artifacts` or the specific Markdown/Writer/Calc/Impress skill first +for nontrivial document artifact work, load skill `document-artifacts` or the specific Markdown/Writer/Calc/Impress skill first diff --git a/plugins/_office/tools/document_artifact.py b/plugins/_office/tools/document_artifact.py index e2b9de21a..c0f5d2006 100644 --- a/plugins/_office/tools/document_artifact.py +++ b/plugins/_office/tools/document_artifact.py @@ -28,10 +28,22 @@ class DocumentArtifact(Tool): chart: Any = None, slides: Any = None, max_chars: int | str = 12000, + open_in_canvas: bool = False, + open_in_desktop: bool = False, method: str = "", **kwargs: Any, ) -> Response: action = str(action or method or self.method or "status").strip().lower().replace("-", "_") + open_in_canvas = _truthy( + open_in_canvas + or kwargs.get("open_canvas") + or kwargs.get("open_document") + ) + open_in_desktop = _truthy( + open_in_desktop + or kwargs.get("open_desktop") + or kwargs.get("desktop") + ) try: if action == "create": doc = document_store.create_document( @@ -56,10 +68,22 @@ class DocumentArtifact(Tool): message=f"document_artifact create failed: {validation.get('error')}", break_loop=False, ) - return self._document_response("Created document artifact.", doc, action=action) + return self._document_response( + "Created document artifact.", + doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if action == "open": doc = self._document_from_input(file_id=file_id, path=path) - return self._document_response("Opened document artifact.", doc, action=action) + return self._document_response( + "Opened document artifact.", + doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if action in {"read", "extract"}: doc = self._document_from_input(file_id=file_id, path=path) payload = { @@ -68,7 +92,13 @@ class DocumentArtifact(Tool): "document": self._public_doc(doc), "content": artifact_editor.read_artifact(doc, max_chars=int(max_chars or 12000)), } - return self._json_response(payload, doc=doc, action="read") + return self._json_response( + payload, + doc=doc, + action="read", + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if action in {"edit", "update", "patch"}: doc = self._document_from_input(file_id=file_id, path=path) updated_doc, payload = artifact_editor.edit_artifact( @@ -85,20 +115,44 @@ class DocumentArtifact(Tool): **kwargs, ) payload["document"] = self._public_doc(updated_doc) - return self._json_response(payload, doc=updated_doc, action="edit") + return self._json_response( + payload, + doc=updated_doc, + action="edit", + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if action == "inspect": doc = self._document_from_input(file_id=file_id, path=path) - return self._json_response({"ok": True, "action": action, "document": self._public_doc(doc)}, doc=doc, action=action) + return self._json_response( + {"ok": True, "action": action, "document": self._public_doc(doc)}, + doc=doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if action == "version_history": doc = self._document_from_input(file_id=file_id, path=path) versions = document_store.version_history(doc["file_id"]) - return self._json_response({"ok": True, "action": action, "versions": versions}, doc=doc, action=action) + return self._json_response( + {"ok": True, "action": action, "versions": versions}, + doc=doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if action == "restore_version": if version_id is None or str(version_id).strip() == "": return Response(message="version_id is required for restore_version.", break_loop=False) doc = self._document_from_input(file_id=file_id, path=path) restored = document_store.restore_version(doc["file_id"], int(version_id)) - return self._document_response("Restored document artifact version.", restored, action=action) + return self._document_response( + "Restored document artifact version.", + restored, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if action == "export": doc = self._document_from_input(file_id=file_id, path=path) target_format = str(kwargs.get("target_format") or kwargs.get("export_format") or "").lower().lstrip(".") @@ -111,13 +165,30 @@ class DocumentArtifact(Tool): "path": document_store.display_path(result["path"]), "document": self._public_doc(doc), } - return self._json_response(payload, doc=doc, action=action) + return self._json_response( + payload, + doc=doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) return Response( message=f"document_artifact export failed: {result.get('error')}", break_loop=False, - additional=self._additional(doc, action=action), + additional=self._additional( + doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ), ) - return self._document_response("Document artifact export path is ready.", doc, action=action) + return self._document_response( + "Document artifact export path is ready.", + doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if action == "status": return self._json_response({"ok": True, "action": action, "status": libreoffice.collect_status()}, action=action) return Response(message=f"Unknown document_artifact action: {action}", break_loop=False) @@ -143,28 +214,75 @@ class DocumentArtifact(Tool): def _context_id(self) -> str: return self.agent.context.id if self.agent and self.agent.context else "" - def _document_response(self, message: str, doc: dict[str, Any], action: str = "") -> Response: + def _document_response( + self, + message: str, + doc: dict[str, Any], + action: str = "", + *, + open_in_canvas: bool = False, + open_in_desktop: bool = False, + ) -> Response: payload = {"ok": True, "action": action, "message": message, "document": self._public_doc(doc)} return Response( message=json.dumps(payload, indent=2, ensure_ascii=False), break_loop=False, - additional=self._additional(doc, action=action), + additional=self._additional( + doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ), ) - def _json_response(self, payload: dict[str, Any], doc: dict[str, Any] | None = None, action: str = "") -> Response: + def _json_response( + self, + payload: dict[str, Any], + doc: dict[str, Any] | None = None, + action: str = "", + *, + open_in_canvas: bool = False, + open_in_desktop: bool = False, + ) -> Response: return Response( message=json.dumps(payload, indent=2, ensure_ascii=False, default=str), break_loop=False, - additional=self._additional(doc, action=action) if doc else {"_tool_name": self.name, "canvas_surface": "office", "action": action}, + additional=self._additional( + doc, + action=action, + open_in_canvas=open_in_canvas, + open_in_desktop=open_in_desktop, + ) if doc else { + "_tool_name": self.name, + "canvas_surface": "office", + "action": action, + "open_in_canvas": bool(open_in_canvas), + "open_in_desktop": bool(open_in_desktop), + }, ) - def _additional(self, doc: dict[str, Any] | None, action: str = "") -> dict[str, Any]: + def _additional( + self, + doc: dict[str, Any] | None, + action: str = "", + *, + open_in_canvas: bool = False, + open_in_desktop: bool = False, + ) -> dict[str, Any]: if not doc: - return {"_tool_name": self.name, "canvas_surface": "office", "action": action} + return { + "_tool_name": self.name, + "canvas_surface": "office", + "action": action, + "open_in_canvas": bool(open_in_canvas), + "open_in_desktop": bool(open_in_desktop), + } return { "_tool_name": self.name, "canvas_surface": "office", "action": action, + "open_in_canvas": bool(open_in_canvas), + "open_in_desktop": bool(open_in_desktop), "file_id": doc["file_id"], "title": doc["basename"], "format": doc["extension"], @@ -183,3 +301,13 @@ class DocumentArtifact(Tool): "last_modified": doc["last_modified"], "exists": Path(doc["path"]).exists(), } + + +def _truthy(value: Any) -> bool: + if isinstance(value, bool): + return value + if value is None: + return False + if isinstance(value, (int, float)): + return value != 0 + return str(value).strip().lower() in {"1", "true", "yes", "y", "on"} diff --git a/plugins/_office/webui/main.html b/plugins/_office/webui/main.html index 8248c6d03..dcf7d21c0 100644 --- a/plugins/_office/webui/main.html +++ b/plugins/_office/webui/main.html @@ -1,12 +1,6 @@ - + - Desktop + Documents diff --git a/plugins/_office/webui/office-panel.html b/plugins/_office/webui/office-panel.html index 095d17ad5..ac46cec7d 100644 --- a/plugins/_office/webui/office-panel.html +++ b/plugins/_office/webui/office-panel.html @@ -5,123 +5,130 @@ -
+