Imported from ThingEdu/thingblock-desktop (
AGENTS.md). Install upstream withnpx skills add ThingEdu/thingblock-desktop. Copyright stays with the author.
Agent Guide: thingblock-desktop
What this is
thingblock-desktop is the Tauri (Rust) desktop shell that packages the two existing
ThingBlock projects into one installable app:
thingblock-editor— the web editor UI, rendered in the Tauri webview.thingblock-link— the local Rust helper (WebSocket server +arduino-cligRPC client) that compiles and flashes firmware. It runs as a bundled sidecar process, not as a library.
The webview loads the editor and talks to the link helper over WebSocket on ws://localhost:3030
— exactly the contract the editor already uses in browser mode. The desktop shell adds no new
protocol; it just bundles both halves, spawns the sidecar on launch, and gives users a single
double-click install instead of "run the helper, then open the web editor."
This repo stays small. It is the shell and the glue: window setup, sidecar lifecycle, bundler config. It does not vendor the editor or the link source — see the layout below.
How the three repos relate
The three are independent git repos checked out as siblings on disk:
<parent>/
├── thingblock-editor/ # web UI (npm monorepo)
├── thingblock-link/ # Rust WS helper binary (built as a sidecar)
└── thingblock-desktop/ # THIS repo — Tauri shell
thingblock-desktop reaches out to the siblings rather than embedding them:
- The editor is consumed as a built frontend: dev points at its webpack dev server
(
http://localhost:8601), release points atfrontend/— a staged copy of its production build (thingblock-editor/packages/scratch-gui/build) that this repo owns (see "The frontend contract").frontendDistpoints at the staged copy, never at the editor's build directly. - The link is consumed as a prebuilt sidecar binary:
cargo build --releasein the link repo, then its binary is staged intosrc-tauri/binaries/(bundled via TauriexternalBin). Its runtime data — thethingblock-resource/pack, the host-platformarduino-cli, and the arduino config seed (arduino-cli.yaml+ adata/bundle with thearduino:avrcore pre-installed, underresources/arduino/, mirroring the link repo'sscripts/bundle-data.sh) — is staged intosrc-tauri/resources/(arduino-cliunderresources/bin/, a directory resource so the Tauriresourcesmap is identical on every platform) and bundled via Tauriresources. The resource pack is produced by the editor workspace@thingblock/thingblock-resourceand staged from itsdist/thingblock-resource/output — the link repo's own copy is a gitignored dev convenience, not the source of truth.scripts/stage-sidecar.sh(macOS/Linux) andscripts/stage-sidecar.ps1(Windows) do the staging;beforeBuildCommand/beforeDevCommandrun the right one automatically.
Do not copy editor or link source into this repo, and do not add them as git submodules. They are released independently; this shell pins to built artifacts.
The link contract (don't drift from it)
- The sidecar is spawned with
--port 3030(the link's default WS port). The webview connects tows://localhost:3030. If this port ever changes, it must change on both sides in lockstep — it is a contract with the editor, not a local detail. - On Windows the webview is Chromium (WebView2), so the editor's HTTP calls into the link
(
/resources,/api/…) are cross-address-space: the frontend is served fromhttp://tauri.localhost, a scheme-intercepted response with no source IP, which Chromium scores as more public than loopback.additionalBrowserArgsintauri.conf.jsondisables those checks (LocalNetworkAccessChecksis the Chromium 142 permission gate; the twoPrivateNetworkAccess*names are its preflight-based predecessor). WebSockets are not gated, which is why the WS pipe works while resource loading fails. Two constraints when editing that string: setting it replaces wry's entire default arg string, so the threems*names must stay; and it belongs intauri.conf.json, nottauri.windows.conf.json, because platform configs merge by RFC 7386 and awindowsarray there would replace the whole window definition. - The resource packs no longer travel over that HTTP route:
stage-frontendships them inside the frontend, so the editor reads them from its own origin and no address-space check applies (see "The frontend contract"). The remaining cross-origin call isGET /api/platforms/{id}(getPlatformStatus, the core-install flow). Move that read onto the WS envelope and theadditionalBrowserArgsflag can come out — which it eventually must, since Microsoft intends to remove it onceCoreWebView2PermissionKindgains LNA values. - The link resolves its
thingblock-resource/pack andarduino-clifrom paths we pass it (--resource-root,--arduino-cli). The shell resolves both fromBaseDirectory::Resourceand passes them on spawn, so the link finds the bundled copies instead of its compile-time dev paths. Without--arduino-cli, the link falls back to its in-treearduino-cli-binaries/, which only exists on a dev checkout — so a packaged build must always pass it. - The link's arduino-cli daemon runs against
arduino-cli.yaml+ adata/bundle in the dir we pass as--config-dir(without it, the link falls back to its compile-time crate root — dev only, same as--arduino-cli). arduino-cli writes into that dir (downloads, on-demand core installs), and the Tauri resource dir is read-only when installed, so the shell seeds a writable per-user copy underlocal_data_dir()/ThingBlock(e.g.%LOCALAPPDATA%\ThingBlockon Windows) and passes that: the yaml is overwritten every launch, thedata/bundle (~260 MB, avr core pre-installed) is copied only when absent. The dir is deliberately short — the esp32 GCC toolchains resolve C++ multilib headers through include paths deep enough that a longer base (like the app'scom.thingblock.desktopdata dir) exceeds Windows' 260-char MAX_PATH and breaks compiles withbits/c++config.h: No such file or directory. The seed copy and sidecar spawn run on a background task so first launch doesn't block window creation. - The link owns its own tray icon and event loop as a standalone helper. When run as a sidecar it is a separate process from the Tauri window; that is expected.
- On window close, the shell requests a graceful sidecar shutdown by writing to its stdin (content
is irrelevant — any stdin activity is the signal) and waits briefly for it to exit before falling
back to a hard kill. This rides stdin rather than the WS server so it keeps working even if the
server is unhealthy, and behaves identically across Linux/macOS/Windows unlike an OS signal. See
watch_stdin_for_shutdownin the link repo'ssrc/ui/tray.rs.
The frontend contract (don't drift from it)
scripts/stage-frontend.{sh,ps1}copies the editor's production build intofrontend/(gitignored) and drops thethingblock-resourcepack in beside it, so the packs are served from the editor's own origin — no CORS, no address-space check, on any platform. It runs afterbuild:editorinbeforeBuildCommand, because it consumes that build's output.- Never stage into the editor's own tree.
thingblock-editoris a backend-agnostic client that also fronts the cloud service; a pack baked into its build would make the pack editor-versioned instead of service-versioned and leave desktop-only bytes in an artifact meant to be reused. This repo owningfrontend/is what keeps that boundary. - The editor learns the pack base from
globalThis.__THINGBLOCK_RESOURCE_BASE__, injected into the stagedindex.htmlby the staging script (the editor'slink-controller.jsreads it and passes it toLinkClientasresourceBase). Injected into the staged copy, not built into the editor, for the same reason. Unset in dev —beforeDevCommanddoes no frontend staging, so dev falls back to the link's own/resourcesroute, which works there because the dev origin (http://localhost:8601) is itself loopback.
Build, run, lint
Standard Rust toolchain (edition 2024) for the shell; npm for the Tauri CLI and frontend glue.
npm run tauri dev # run the desktop app against the editor dev server + link sidecar
npm run dist # production installer (= tauri build): stages sidecar, builds the editor
npm run dist:debug # faster unoptimized bundle for local testing
cargo fmt --manifest-path src-tauri/Cargo.toml
cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
dev/dist run stage:sidecar and the editor build automatically (via
beforeDevCommand/beforeBuildCommand). Installers land in
src-tauri/target/release/bundle/<format>/ (Linux: deb, rpm).
On a low-RAM machine the link's release build can be OOM-killed at full parallelism; prefix commands
with CARGO_BUILD_JOBS=2 to cap it.
Only the host platform is staged (one ~36 MB arduino-cli, plus the ~320 MB arduino config
seed — its core install arduino:avr downloads ~50 MB on the first staging run and is skipped
once seeded); cross-platform packaging is a CI concern. Per-platform bundle targets live in tauri.<platform>.conf.json (Tauri auto-merges them):
base tauri.conf.json is Linux (deb/rpm), tauri.macos.conf.json is dmg, and
tauri.windows.conf.json is nsis plus PowerShell build hooks. Windows builds via
stage-sidecar.ps1 (the sidecar binary and arduino-cli carry .exe; lib.rs resolves the
.exe name under cfg!(windows)), so no Git Bash is required on Windows.
CI: .github/workflows/release.yml builds macOS (aarch64-apple-darwin), Windows
(x86_64-pc-windows-msvc), and Linux (x86_64-unknown-linux-gnu → deb, rpm) on a version
tag (v*) and drafts a GitHub Release with the installers (workflow_dispatch builds without
releasing and uploads them as artifacts). The Linux job apt-installs webkit2gtk/GTK.
Artifacts are currently unsigned (Gatekeeper/SmartScreen warnings expected).
Agent defaults
Use these unless the user asks otherwise:
- Keep changes minimal and scoped to the request. Don't refactor, add features, or restyle code you weren't asked to touch.
- This shell is a standalone app, not a published library — restructure internals freely. The contracts to preserve are external: the WS port/protocol with the editor, and the sidecar + resource-dir packaging layout the link expects.
- Comments explain the current code, not its history. If something is counterintuitive, explain why it is correct now.
- Fix root causes, not symptoms. Don't add fallbacks or validation for states that cannot happen.
- When fixing a bug, add a failing test first, then fix until it and the rest of the suite pass.
- Surface invalid states explicitly — prefer an explicit
Err/panic!with a useful message over silent failure. Log actionable context viatracing:warn!for recoverable states,error!for invalid required data. - Validate only at boundaries — process spawn/exit of the sidecar, and anything crossing the webview IPC. Trust internal code.
Conventions
- Commits follow Conventional Commits.
- Keep
Cargo.tomlandpackage.jsonsections in alphabetical order. - Design docs live in
.agents/docs/(mirrors the link repo).
Before submitting changes
- Scope: changes confined to the request; nothing extra added.
- Build clean:
npm run tauri build(or at leastcargo build --manifest-path src-tauri/Cargo.toml). - Lint clean:
cargo clippy ... -D warnings,cargo fmt --check. - Docs in sync: if you change a convention, the WS port, or the packaging layout, update this file accordingly.
- Commit format: Conventional Commits.