Fix Office artifact creation and canvas closing

Add an explicit close button to the right canvas toolbar, next to the undock control, and cover its label, handler, and ordering in the canvas regression test. Treat document_artifact tool_args.method as an action alias so calls like method=create with format=xlsx create workbooks instead of falling back to LibreOffice status. Add regression coverage for the exact XLSX creation shape.

Alessandro committed May 2, 2026 at 14:07 UTC baac20f7a474233c79ede13cd73fd852416abe73
4 files changed +73 -1
plugins/_office/tools/document_artifact.py
+2 -1
@@ -28,9 +28,10 @@ class DocumentArtifact(Tool):
28 chart: Any = None,
29 slides: Any = None,
30 max_chars: int | str = 12000,
31 + method: str = "",
32 **kwargs: Any,
33 ) -> Response:
33 - action = str(action or self.method or "status").strip().lower().replace("-", "_")
34 + action = str(action or method or self.method or "status").strip().lower().replace("-", "_")
35 try:
36 if action == "create":
37 doc = document_store.create_document(
tests/test_office_canvas_setup.py
+5
@@ -300,6 +300,11 @@ def test_right_canvas_requires_explicit_open_and_is_absent_on_mobile():
300 assert "if (this.isMobileMode)" in canvas_store
301 assert "shouldRender()" in canvas_store
302 assert "$store.rightCanvas.shouldRender()" in canvas_html
303 + assert 'title="Open as window"' in canvas_html
304 + assert 'title="Close canvas"' in canvas_html
305 + assert 'aria-label="Close canvas"' in canvas_html
306 + assert "@click=\"$store.rightCanvas.close()\"" in canvas_html
307 + assert canvas_html.index('title="Open as window"') < canvas_html.index('title="Close canvas"')
308 assert "body.right-canvas-mobile-mode .right-canvas" in canvas_css
309 assert "display: none !important" in canvas_css
310 assert "autoOpenOfficeCanvas" not in handler
tests/test_office_document_store.py
+57
@@ -1,5 +1,6 @@
1 from __future__ import annotations
2
3 +import asyncio
4 import importlib.util
5 import json
6 import os
@@ -127,6 +128,62 @@ def test_xlsx_and_pptx_creation_and_direct_edits_still_work(office_state):
128 assert deck_read["slides"][1]["title"] == "Next"
129
130
131 +def test_document_artifact_accepts_method_alias_for_xlsx_create(office_state, monkeypatch):
132 + tool_module = types.ModuleType("helpers.tool")
133 +
134 + class Response:
135 + def __init__(self, message, break_loop, additional=None):
136 + self.message = message
137 + self.break_loop = break_loop
138 + self.additional = additional
139 +
140 + class Tool:
141 + def __init__(self, agent, name, method, args, message, loop_data, **kwargs):
142 + self.agent = agent
143 + self.name = name
144 + self.method = method
145 + self.args = args
146 + self.message = message
147 + self.loop_data = loop_data
148 +
149 + tool_module.Response = Response
150 + tool_module.Tool = Tool
151 + monkeypatch.setitem(sys.modules, "helpers.tool", tool_module)
152 + spec = importlib.util.spec_from_file_location(
153 + "test_document_artifact_tool",
154 + PROJECT_ROOT / "plugins" / "_office" / "tools" / "document_artifact.py",
155 + )
156 + document_artifact_module = importlib.util.module_from_spec(spec)
157 + assert spec and spec.loader
158 + spec.loader.exec_module(document_artifact_module)
159 + DocumentArtifact = document_artifact_module.DocumentArtifact
160 +
161 + tool = DocumentArtifact(
162 + agent=None,
163 + name="document_artifact",
164 + method=None,
165 + args={},
166 + message="",
167 + loop_data=None,
168 + )
169 +
170 + response = asyncio.run(
171 + tool.execute(
172 + method="create",
173 + kind="document",
174 + title="New Excel Workbook",
175 + format="xlsx",
176 + content="Sheet1\n",
177 + )
178 + )
179 + payload = json.loads(response.message)
180 +
181 + assert payload["action"] == "create"
182 + assert payload["document"]["extension"] == "xlsx"
183 + assert Path(payload["document"]["path"]).name == "New Excel Workbook.xlsx"
184 + assert Path(document_store._path_from_a0(payload["document"]["path"])).exists()
185 +
186 +
187 def test_odt_is_not_advertised_and_returns_clear_unsupported_response(office_state):
188 prompt = (PROJECT_ROOT / "plugins" / "_office" / "prompts" / "agent.system.tool.document_artifact.md").read_text(
189 encoding="utf-8",
webui/components/canvas/right-canvas.html
+9
@@ -98,6 +98,15 @@
98 >
99 <span class="material-symbols-outlined">open_in_new</span>
100 </button>
101 + <button
102 + type="button"
103 + class="right-canvas-icon-button right-canvas-close-button"
104 + title="Close canvas"
105 + aria-label="Close canvas"
106 + @click="$store.rightCanvas.close()"
107 + >
108 + <span class="material-symbols-outlined">close</span>
109 + </button>
110 <x-extension id="right-canvas-toolbar-end"></x-extension>
111 </div>
112 </header>