Skip to content

v0.4–v0.6.1: guided make front door, auto-update, Studio, and a browser port - #3

Open
toyuvalo wants to merge 1 commit into
levkropp:mainfrom
toyuvalo:feat/studio-web-port-and-portability
Open

toyuvalo wants to merge 1 commit into
levkropp:mainfrom
toyuvalo:feat/studio-web-port-and-portability

Conversation

@toyuvalo

@toyuvalo toyuvalo commented Sep 8, 2026

Copy link
Copy Markdown

Hi Lev — I've been building on ClawSCAD for a while and this is everything from my fork, offered back. It's a big diff, so the short version first: the guided "pick a type, describe it, press one button" front door, auto-update, the v0.6 Studio, and a browser port of the Make view. Take all of it, some of it, or just the parts you like — happy to split this into smaller PRs if that's easier to review.

It's squashed into one commit deliberately: my fork's history is mostly auto-commits with useless subjects, and the commit message here is structured by feature area instead.


What's in it

Guided make (v0.4) A print-type grid is the front door — screws & hardware, brackets, boxes, replacement parts, figures… Pick one, describe the thing in a sentence, press the button. The type sets walls, tolerances, resolution and modelling approach; every one of those controls is still there, demoted to a Fine-tune row. Obvious things go straight to a model; matters of taste generate reference pictures and ask "is this the thing?" before spending ten minutes on a mesh. The app says which it chose and why, in one sentence, and you can override either way.
Studio (v0.6) That flow moved out of the 280 px rail above the terminal into a full-window view, switched from the header: Make and Workbench. Thirteen switchable tool groups (Dimensions, Hardware, Fit & tolerance, Mounting, Strength, Material…) for saying what a sentence can't carry; each compiles into three separate things — constraints Claude reads, words the image prompt gets, and CLI flags.
Auto-update (v0.5) electron-updater against GitHub releases: background download, install on quit, never mid-render. Only a staged update is announced — a notification you can't act on is noise. Plus scripts/release.ps1, which exists for a specific reason (below).
Browser port (v0.6.1) web/ serves the Make view over HTTP, running renderer/studio.js unmodified. Zero new dependencies — node:http + SSE. Good for driving it from a phone or tablet.
Exports 3MF is now the default export because it's the only one that preserves per-part colour; --backend=Manifold is used automatically when the resolved OpenSCAD supports it.
Stability Unhandled error events on the chokidar watcher and spawned children were killing the main process and orphaning children; render faults are now split from model faults so an environment problem doesn't read as "Claude wrote bad code"; detectKind missed import() when a division shared its line; on-disk checkpoints are adopted rather than ignored.
Dependencies 63 Dependabot alerts and a DOMPurify advisory resolved; Electron 42 → 43. The electron-rebuild postinstall is gone — node-pty is N-API, so it was doing nothing but failing on Windows, and scripts/check-native-deps.js now fails the test run if a non-N-API native dep is ever added.

One bug you'll want regardless of the rest

The bundled OpenSCAD has never been found on Windows. The snapshot zip carries a top-level OpenSCAD-<snapshot>-x86-64/ folder, so download-openscad.js leaves the binary at vendors/openscad-win/OpenSCAD-…/openscad.exe, while main.js's openscadBundledPath() looks for vendors/openscad-win/openscad.exe. electron-builder copies that tree into the installer verbatim, so a shipped Windows build carries 65 MB of OpenSCAD it can't resolve and then tells the user to go install one.

This predates my fork — it's in main today. The download now flattens a single-directory archive and leaves an already-flat one alone; the rule is exported so it can be exercised without a 65 MB download. It's a self-contained commit's worth of change if you'd rather take just that.

How I found it: the five Render Pipeline specs were failing on this machine, I fixed the vendor path, and they went green.

The browser port, specifically

web/entry.js plays renderer.js's exact role and web/api-shim.js speaks fetch + EventSource where preload.js speaks ipcRenderer. main/tools.js, main/categories.js and main/composer.js grew Electron-free exports, so the server reuses the same catalog, the same userData-override rules and the same state-merge semantics rather than a second implementation that drifts within a release.

Two things worth knowing:

  • Flow A never needed a pty. claude -p --permission-mode acceptEdits runs headless and exits. The .scad it produced is found by diffing workspace .scad mtimes, not by trusting the model to report a path.
  • Customize never rewrites the file. It parses OpenSCAD's own Customizer syntax and applies values with -D. Every .scad is an immutable checkpoint, and a UI that edited them to "customize" them would be the easiest possible way to break that promise. The preview is a server-side render, so it can't drift from the artifact you download.

What doesn't work is stated on screen rather than faked: no three.js viewport, no terminal, no checkpoint tree. The server has no authentication of its own — it binds loopback and expects an authenticating proxy in front. Path handling is the security boundary: every filesystem-touching request is resolved, realpath'd and containment-checked against the workspace rather than ..-filtered, because an absolute path on another drive and a symlink out of the tree both contain no ... No CORS header is ever sent.

Why scripts/release.ps1 exists

electron-builder --publish always has a race that publishes a release with no latest.yml: two publishers both create the release, GitHub 422s one, and that rejection throws before writeUpdateInfoFiles() runs. The release page looks completely normal; every updater client is blind to it, permanently. The script decouples build from publish, asserts the .exe/.blockmap/latest.yml agree on version, path and sha512, uploads one file at a time, then fetches the published manifest back over HTTP before claiming success.

Related: I removed the tags: triggers and release-upload steps from the three build workflows. They uploaded their own .exe over the published one — a different binary, so its hash no longer matched the manifest, breaking that update for every client while the release script had already printed green. CI still builds on main and PRs for verification. scripts/release.ps1 refuses to publish if a colliding workflow is ever added back. All of this is written up in docs/releasing.md.

Your call

  • build.publish in package.json — I've pointed it at levkropp/ClawSCAD. electron-updater resolves the newest release in a repo, not the newest release of a product, so this has to be your repo or the updater feeds your users someone else's builds. Nothing publishes until you run npm run release, and the app itself needs no token (public repo).
  • The default workspace changed to ~/clawscad-workspace on every platform. It used to prefer E:\ then D:\ on Windows, which was my disk layout and not anything true of a fresh install. Order is now $CLAWSCAD_WORKSPACE → the workspace last opened, if it still exists → ~, so nobody's existing install starts up empty.
  • claw-gen is an external CLI and the image/mesh pipeline is optional — the app hardcodes nothing about providers and says "no generation pipeline configured" rather than failing quietly. Everything else works without it.
  • docs/configuration.md is new: every env var, flag and override file in one table, so nothing about a particular machine is compiled in. docs/generation-pipeline.md documents the claw-gen contract — the backends JSON, the four actions, the NDJSON events — so the Generate panel is an integration point anyone can satisfy rather than a pointer to a CLI you can't get.
  • The README keeps your licence wording (MIT, see LICENSE). My fork had restated it as something else in the README while LICENSE itself stayed MIT; that's not a fork's call to make, so it's reverted here.

Testing

  • npm run test:harness — 328 assertions, pure Node, no Electron: routing, tool compilation, preset merge, composer state, the Customizer parser, render-fault classification, plus an error-handler survival harness that extracts the real function bodies out of main.js at run time.
  • npx playwright test — 160 specs driving the real app.

Both green on Windows on this branch: 328/328 harness assertions, 155 Playwright specs passing plus the 5 Render Pipeline specs that were failing until the vendor-path fix above — 160/160 with a resolvable OpenSCAD.

The two parser rules that silently produce a wrong model rather than an error — the module/function cutoff, and -D quoting — are mutation-proven: removing either turns specific checks red.

Happy to rework any of this, and no hard feelings if the direction isn't what you want for the project.

Squashed from a fork's development history so the diff reads as one change
rather than 30 auto-commits. Five releases of work, plus a portability pass so
none of it assumes the machine it was written on.

GUIDED MAKE (v0.4). A print-type grid is the front door: pick what you're
making, describe it in a sentence, press one button. The type sets walls,
tolerances, resolution and modelling approach; those controls are still there,
demoted to a Fine-tune row. Obvious things (an M4 standoff 20 mm long) go
straight to a model; matters of taste generate reference pictures and ask "is
this the thing?" before spending ten minutes on a mesh. The app always says
which it chose and why, and you can override either way.

STUDIO (v0.6). That flow moved out of a 280 px scrolling rail above the
terminal into a full-window view, switched from the header: Make and Workbench.
Thirteen switchable tool groups (Dimensions, Hardware, Fit & tolerance,
Mounting, Text, Strength, Material, ...) compile into three separate things —
constraints Claude reads, words the image prompt gets, and claw-gen flags. The
routing decision (renderer/route.js) is a pure function with its own harness.

AUTO-UPDATE (v0.5). electron-updater against GitHub releases: background
download, install on quit, never mid-render. Only a staged update is
announced — checking and failed checks stay silent, since a notification you
cannot act on is noise. scripts/release.ps1 publishes in two phases because
`electron-builder --publish always` has a race that ships a release with no
latest.yml, which every updater client is blind to, permanently and silently.
The script builds with --publish never, asserts the .exe/.blockmap/latest.yml
agree on version, path and sha512, uploads with gh one file at a time, then
fetches the published manifest back over HTTP before claiming success. CI no
longer publishes: exactly one publisher may own an update feed. Full reasoning
in docs/releasing.md.

BROWSER PORT (v0.6.1). web/ serves the Make view over HTTP, running
renderer/studio.js UNMODIFIED — web/entry.js plays renderer.js's role and
web/api-shim.js speaks fetch + EventSource where preload.js speaks ipcRenderer.
Zero new dependencies: node:http and SSE. main/tools.js, main/categories.js and
main/composer.js grew Electron-free exports so the server reuses the same
catalog, the same userData-override rules and the same state-merge semantics
rather than a second implementation that drifts.

  - Flow A never needed a pty: `claude -p --permission-mode acceptEdits` runs
    headless and exits. The .scad it produced is found by diffing workspace
    .scad mtimes, never by trusting the model to report a path.
  - Customize parses OpenSCAD's own Customizer syntax and applies values with
    -D, so a checkpoint is never rewritten to "customize" it. The preview is a
    server-side render, so it cannot drift from the artifact you download.
  - What does not work is stated on screen, never faked: no viewport, no
    terminal, no checkpoint tree.
  - It binds loopback and has NO authentication of its own. Path handling is
    the security boundary — every filesystem-touching request is resolved,
    realpath'd and containment-checked against the workspace rather than
    dot-dot filtered, and no CORS header is ever sent.

FIXED: THE BUNDLED OPENSCAD WAS NEVER FOUND ON WINDOWS. The snapshot zip
carries a top-level OpenSCAD-<snapshot>-x86-64/ folder, so download-openscad.js
left the binary at vendors/openscad-win/OpenSCAD-.../openscad.exe while
main.js's openscadBundledPath() looks for vendors/openscad-win/openscad.exe.
electron-builder copies that tree into the installer verbatim, so a shipped
Windows build carried 65 MB of OpenSCAD it could not resolve and then told the
user to go and install one. The download now flattens a single-directory
archive and leaves an already-flat one alone; the rule is exported so it can be
exercised without a 65 MB download. This predates the rest of this change.

PORTABILITY. Nothing about one machine is compiled in any more:

  - The default workspace is ~/clawscad-workspace on every platform. It used to
    prefer E:\ then D:\ on Windows, which is one disk layout rather than
    anything true of a fresh install. Order is now $CLAWSCAD_WORKSPACE, else
    the workspace last opened if it still exists, else ~ — so an existing
    install keeps opening its own work instead of silently starting empty.
  - $CLAWSCAD_CLAUDE_BIN (and --claude on the server) for a Claude CLI the
    probe cannot find.
  - build.publish points at this repo. It is both the upload target and the
    update feed, and electron-updater resolves the newest release IN A REPO,
    not the newest release of a product — so a fork must repoint it or ship an
    app that updates itself into someone else's builds.
  - docs/configuration.md lists every environment variable, flag and override
    file in one place; docs/releasing.md replaces a private handoff note; and
    docs/generation-pipeline.md documents the claw-gen CLI contract — the
    backends JSON, the four actions, the NDJSON event stream — so the Generate
    panel is an integration point anyone can satisfy rather than a pointer to
    a CLI that is not publicly released.
  - The web port's deployment story is now "put an authenticating proxy in
    front" with three worked examples, rather than naming the one tunnel it
    happened to be developed against.

The README keeps this project's own licence wording (MIT, see LICENSE) — the
fork had restated it, and that is not a fork's call to make.

TESTS. 328 assertions in `npm run test:harness` (pure Node, no Electron:
route, tools, preset merge, composer state, scad-params, render-fault
classification, an error-handler survival harness) plus 160 Playwright specs
driving the real app. The parser rules that silently produce a wrong model
rather than an error — the module/function cutoff, -D quoting — are
mutation-proven: removing either turns specific checks red.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant