studio: agent window, a chat with a model that edits the scene
Deploy web Studio / deploy (push) Successful in 3m3s

✦ Agent in the Studio's top bar opens a floating chat window. The model
(any OpenAI-compatible endpoint with tool calling; default B4L's inference
server, https://inference.p0.contact/v1) gets the 15 MCP tools plus two page
tools (select_object, set_view) and the Studio's state (selection, level,
rotation) in its system prompt. Answers and reasoning stream in; tool calls
show as rows with arguments and results; object ids link to the object;
Stop ends a turn; Undo takes the agent's edits back. The window moves by
its title bar and resizes at its top-left corner.

- studio/agent.js: shared by both front ends (waits for studio.js's
  isokit:studio event); MCP over /mcp or isokit.mcp in the web Studio
- isokit studio: /api/agent/{config,models,chat} relay the calls on their
  own threads (ureq, chunked and flushed per read), so the key stays on the
  machine and the endpoint needs no CORS; settings from ISOKIT_LLM_URL /
  _KEY / _MODEL or agent.json in the user's config folder
- web Studio: calls the endpoint from the page with a key kept in the
  browser; build_web.py --agent-url names a same-site relay as default; the
  deploy builds with /llm/v1, which deploy/Caddyfile forwards to the
  inference server (its nginx refuses the CORS preflight)
- tools/mock_llm.py + tools/agent_checks.py: the agent window checks in both
  check_studio.py and check_web.py
This commit is contained in:
grabowski committed 2026-10-23 19:35:53 +07:00
1 parent c647f39059
commit 1a1b62d475
21 files changed
+1690 -25

No files matched your search

+11 -2
View File
@@ -99,6 +99,10 @@ def build_info():
return ""
def html_attr(s):
return s.replace("&", "&amp;").replace('"', "&quot;").replace("<", "&lt;")
def replace_once(s, old, new):
n = s.count(old)
assert n == 1, f"studio/index.html: expected {old!r} once, found {n}"
@@ -109,6 +113,7 @@ def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--out", default=str(ROOT / "dist" / "web"))
ap.add_argument("--skip-cargo", action="store_true", help="reuse the last WASM build (front-end changes only)")
ap.add_argument("--agent-url", help="default endpoint of the agent window, e.g. a relay on the same site (/llm/v1)")
a = ap.parse_args()
out = Path(a.out).resolve()
web = out / "assets" / "web"
@@ -134,7 +139,8 @@ def main():
print(" (wasm-opt not found: the .wasm stays unoptimised by binaryen, about 15% larger)")
for src, dst in [("viewer/isokit-view.js", "viewer"), ("viewer/isokit-view.css", "viewer"),
("studio/studio.js", "studio"), ("studio/studio.css", "studio"), ("studio/favicon.svg", "studio"),
("studio/studio.js", "studio"), ("studio/agent.js", "studio"), ("studio/studio.css", "studio"),
("studio/favicon.svg", "studio"),
("web/web.js", "web"), ("web/web.css", "web")]:
(out / "assets" / dst).mkdir(parents=True, exist_ok=True)
shutil.copy2(ROOT / src, out / "assets" / dst / Path(src).name)
@@ -150,7 +156,8 @@ def main():
build = build_info()
html = replace_once(html, '<title>isokit Studio</title>',
'<title>isokit Studio</title>\n<meta name="description" content="isokit Studio: edit isometric site plans in the browser.">'
+ (f'\n<meta name="isokit-build" content="{build}">' if build else ""))
+ (f'\n<meta name="isokit-build" content="{build}">' if build else "")
+ (f'\n<meta name="isokit-agent" content="{html_attr(a.agent_url)}">' if a.agent_url else ""))
html = replace_once(html, 'href="/assets/studio/favicon.svg"', f'href="assets/studio/favicon.svg?{v}"')
html = replace_once(html, 'href="/assets/viewer/isokit-view.css"', f'href="assets/viewer/isokit-view.css?{v}"')
html = replace_once(html, '<link rel="stylesheet" href="/assets/theme.css">', '<style id="iso-theme"></style>')
@@ -158,6 +165,8 @@ def main():
f'<link rel="stylesheet" href="assets/studio/studio.css?{v}">\n<link rel="stylesheet" href="assets/web/web.css?{v}">')
html = replace_once(html, 'href="/view"', 'href="#"')
html = replace_once(html, '<script src="/assets/viewer/isokit-view.js"></script>', f'<script src="assets/viewer/isokit-view.js?{v}"></script>')
# agent.js waits for studio.js, which web.js starts once the engine is loaded
html = replace_once(html, '<script src="/assets/studio/agent.js"></script>', f'<script src="assets/studio/agent.js?{v}"></script>')
html = replace_once(html, '<script src="/assets/studio/studio.js"></script>',
f'<script type="module" src="assets/web/web.js?{v}"></script>\n'
'<noscript>The isokit Studio needs JavaScript and WebAssembly.</noscript>')