isokit: web Studio (engine as WASM, projects in the browser or a live folder)

- isokit-wasm: wasm-bindgen build of the engine; a project in memory behind
  the same JSON API as `isokit studio`, plus the MCP tools
- isokit-project: Store trait (FsStore folder, MemStore memory) under a
  generic Workspace; Project::from_files; shared Studio API module (api.rs)
  used by serve.rs and the WASM build; web_time for clocks in the browser
- studio.js runs on a pluggable backend (window.IsoKitBackend); no Studio
  keys while a dialog is open
- web/: projects in IndexedDB, new/demo/delete, zip export/import (stored and
  deflated), folder import by drop or picker, folders opened live via File
  System Access with outside edits followed, code editor, page build and
  preview, two tabs in step, isokit.mcp() and WebMCP registration
- tools/build_web.py -> dist/web (static), tools/check_web.py (44 checks)
This commit is contained in:
grabowski committed 2026-10-22 17:00:42 +07:00
1 parent 564aeb75f6
commit 5eb60d2a0c
20 files changed
+1950 -178

No files matched your search

+10 -15
View File
@@ -6,6 +6,7 @@
use crate::check::check;
use crate::ops::Op;
use crate::page;
use crate::store::Store;
use crate::workspace::Workspace;
use serde_json::{json, Map, Value};
@@ -85,7 +86,7 @@ fn tool_error(msg: impl Into<String>) -> Value {
json!({ "content": [{ "type": "text", "text": msg.into() }], "isError": true })
}
fn boxes(ws: &Workspace) -> Map<String, Value> {
fn boxes<S: Store>(ws: &Workspace<S>) -> Map<String, Value> {
let p = &ws.project;
match p.render(&p.manifest.default_level(), 0, None, false) {
Ok(v) => v
@@ -97,7 +98,7 @@ fn boxes(ws: &Workspace) -> Map<String, Value> {
}
}
fn object_json(ws: &Workspace, id: &str, bx: &Map<String, Value>) -> Option<Value> {
fn object_json<S: Store>(ws: &Workspace<S>, id: &str, bx: &Map<String, Value>) -> Option<Value> {
let o = ws.project.doc.get(id)?;
let mut v = serde_json::to_value(o).ok()?;
if let Some(b) = bx.get(id) {
@@ -111,7 +112,7 @@ fn str_arg(a: &Map<String, Value>, k: &str) -> Option<String> {
}
/// Run one tool.
pub fn call_tool(ws: &mut Workspace, name: &str, a: &Map<String, Value>) -> Value {
pub fn call_tool<S: Store>(ws: &mut Workspace<S>, name: &str, a: &Map<String, Value>) -> Value {
ws.refresh();
match name {
"project_info" => {
@@ -287,18 +288,12 @@ pub fn call_tool(ws: &mut Workspace, name: &str, a: &Map<String, Value>) -> Valu
let out = str_arg(a, "out")
.or_else(|| p.manifest.build.out.clone())
.unwrap_or_else(|| "dist/index.html".into());
let path = p.dir.join(&out);
match page::static_page(p) {
Err(e) => tool_error(e.to_string()),
Ok(html) => {
if let Some(d) = path.parent() {
let _ = std::fs::create_dir_all(d);
}
match std::fs::write(&path, html) {
Ok(_) => text(json!({ "path": path.display().to_string() })),
Err(e) => tool_error(format!("{}: {e}", path.display())),
}
}
Ok(html) => match ws.write_file(&out, &html) {
Ok(path) => text(json!({ "path": path.display().to_string() })),
Err(e) => tool_error(e),
},
}
}
"undo" => match ws.undo() {
@@ -313,7 +308,7 @@ pub fn call_tool(ws: &mut Workspace, name: &str, a: &Map<String, Value>) -> Valu
}
}
fn render_warnings(ws: &Workspace) -> Vec<String> {
fn render_warnings<S: Store>(ws: &Workspace<S>) -> Vec<String> {
let p = &ws.project;
p.render(&p.manifest.default_level(), 0, None, false)
.map(|v| v.warnings)
@@ -321,7 +316,7 @@ fn render_warnings(ws: &Workspace) -> Vec<String> {
}
/// Handle one JSON-RPC message.
pub fn handle(ws: &mut Workspace, msg: &Value) -> Option<Value> {
pub fn handle<S: Store>(ws: &mut Workspace<S>, msg: &Value) -> Option<Value> {
let id = msg.get("id").cloned();
let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
let params = msg.get("params").cloned().unwrap_or(json!({}));