Documentation
Desktop UI
SolidJS workspaces, a local API client, and a Tauri shell that owns native integration.
Application architecture
The desktop application combines a SolidJS webview, Vite, Three.js, and a Tauri v2 Rust shell. The webview owns screens and interaction state. The Bun sidecar owns API handlers, database access, AI sessions, and project services. The Rust process owns the window, menus, tray, operating-system dialogs, keychain access, updates, and sidecar lifecycle.
apps/desktop/src/main.tsx mounts the application. app/App.tsx establishes the API connection and supplies shared state; app/routes.tsx maps URLs to lazy page modules. Feature screens live in features/. The presence of a route does not establish the availability of its external services: billing needs Stripe configuration and AI needs a configured provider.
Startup and connection recovery
- The shell starts the bundled sidecar with a generated launch token and application data and migration directories.
- The sidecar binds an ephemeral port on
127.0.0.1, waits for its health route to answer, and writes a JSONreadyevent containing the port. - The webview invokes
get_sidecar_endpointand creates an API client and managed Effect runtime. - A successful health request creates application state and mounts the router and shell.
The webview allows 150 seconds for connection recovery, with a 30-second timeout per attempt and a 750-millisecond retry pause. Failed attempts discard the cached endpoint so a restarted sidecar can supply a new port. The Rust supervisor waits 60 seconds for each ready handshake and permits one automatic restart. Retry becomes available when the webview connection budget expires.
Browser preview uses an in-memory API
Outside Tauri, a browser without VITE_SIDECAR_URL uses src/testing/fake-api.ts. Preview data is local to the running page; unsupported collections return HTTP 501. To connect a browser to a real sidecar, provide both VITE_SIDECAR_URL and VITE_SIDECAR_TOKEN when starting Vite.
Screen routes
| Area | Routes and modules |
|---|---|
| Projects | /projects, /projects/create, /projects/:id, and /projects/:id/history; screens in features/projects/screens. |
| Design and manufacturing | /cad mounts CadPage; /cam mounts CamPage. Saved drawings and toolpaths have separate detail routes. |
| Resources | /library, /materials, /tools, /machine, and their detail pages use library feature modules. Components have list and detail routes. |
| Assistant | /ai and the docked assistant panel share AI feature state. The AI activity toggles the panel. |
| Settings | /settings, /settings/companion, MCP settings, and subscription settings. |
| Account and collaboration | Auth, profile, organization, invitation, chat, marketplace, plugin, costs, analytics, and administration routes each load feature modules. |
Use app/routes.tsx for exact parameter names and pages/ to locate the screen mounted at a URL. Route adapters are intentionally small; read the screen and its API calls when diagnosing an empty view or a failed action.
Source organization
app/: startup, router, global API client, native integration, shell, and keyboard shortcuts.pages/: route adapters importing or re-exporting screens.features/: product areas with screens, components, hooks, state, and domain-specific helpers.shared/: reusable controls, shell components, application state, query caching, and command history.viewport/: canvas drawing, snapping, tool gestures, Three.js rendering, meshing, and playback.i18n/,styles/, andtesting/: translations, global styles, and test fixtures.
The desktop depends on @cadcamfun/api for the HTTP contract and on @cadcamfun/core for geometry and CAM operations. Database persistence remains behind the sidecar API. Local editor history is distinct from persisted drawing or toolpath versions and from project version control.
Native shell
The production window is 1440 × 900 by default, with a 960 × 600 minimum. Tauri stages the frontend from apps/desktop/dist, a target-specific sidecar from src-tauri/binaries, and generated migrations as resources. It registers .cadcam, .nc, and .gcode file associations. Opening a supported file or launching a second instance forwards file paths to the existing window.
src-tauri/src/sidecar.rs supervises the child process. Closing the app writes a {"event":"shutdown"} line to the child's stdin and waits briefly before reaping it; the child also observes stdin closure. The launch token is 32 random bytes, hex-encoded as 64 characters, and sidecar log lines that may contain credentials are redacted. Logs are separate from stdout, which carries the ready/error handshake. Native secrets are accessed through src-tauri/src/secrets.rs using the operating-system keychain.
On macOS, the shell registers the Liquid Glass plugin. The webview follows the glass setting and falls back when the effect is unavailable. The App Store configuration disables the private API option; the regular configuration enables it. These are separate build configurations.
Security and persistence boundaries
The production content policy permits local assets, loopback API connections, data/blob images, and WebAssembly evaluation. It does not grant arbitrary remote script execution. Native permissions are scoped in src-tauri/capabilities; the development policy additionally permits Vite and its websocket.
open_path canonicalizes paths, limits them to allowed user/application directories, and rejects executable extensions such as .exe, .app, .sh, .bat, .msi, and .jar. .. segments and symlinks cannot escape the allowed folders because both sides are canonicalized. File picking and saving use native dialogs and validation. A launch token grants access to the local application API; treat it separately from a companion token or a project-scoped AI bridge token.
UI settings, API query state, editor commands, stored project versions, and native secrets have different owners. Diagnose each through its actual service rather than assuming all application state is stored in SQLite.
Feature integration
CAD and CAM feature modules compose reusable viewport components with their own panels, stores, and save flows. The assistant panel receives project and editor context and streams tool events through AI feature state. Resource screens use the API client instead of reaching directly into the database.
For a UI change, follow the route adapter to its feature screen, then inspect the feature store or hook that sends the request. Reuse shared components only when their existing contract matches the interaction. Keep pure geometry in the core package and native operating-system behavior in the Rust shell.
Route loading and errors
Each route uses Solid's lazy import boundary. The table includes a catch-all page, /500, /offline, and legal routes. A direct browser navigation must be served through the Vite development server or the packaged webview so the client router can resolve the URL.
The preview API implements a limited set of collections and can deliberately inject request failures in tests. A successful preview render demonstrates UI behavior against those fixtures; verify persistence, permissions, streaming, and external integrations with the live sidecar.
Native command interface
| Command | Purpose |
|---|---|
get_sidecar_endpoint | Wait for the supervised sidecar and return its base URL and launch token. The wait covers one restart: two 60-second ready timeouts plus 5 seconds (125 seconds). |
get_logs_dir, open_path | Resolve the application log directory as a string, and open an existing absolute path with the operating-system opener. Paths must resolve under the app data, log, Documents, Downloads, or Desktop folder. |
secret_get, secret_set, secret_delete | Access native keychain entries under the com.cadcamfun.app service. Keys are 1–128 bytes of ASCII letters, digits, and . _ - : (for example ai.anthropic.apiKey); secret_get returns null for a missing entry. |
pick_file, save_file | pick_file({ filters: [{ name, extensions }] }) returns the chosen path or null on cancel. save_file({ defaultName, contents }) writes UTF-8 text to the chosen path and returns it, or null on cancel; directories and executable extensions are rejected. |
set_recent_projects | Refresh the tray's recent-project menu. |
check_for_updates, install_update | Request an updater check, or install a pending update. install_update accepts an optional unsavedWork flag (default false) so an install does not interrupt an open editor. |
The invoke handler is registered in src-tauri/src/lib.rs. Capabilities and generated command permissions also control availability; command registration alone does not establish webview access. Command errors are serialized as { kind, message } with kinds such as sidecar, invalid_path, io, dialog, and path; no command panics.
Native events
The shell emits sidecar-status for lifecycle changes, open-file with supported paths, menu for menu selection, and update://status for updater progress. Tray recent-project selections are represented by tray.recent.<projectId> menu identifiers. The shell's menu and tray modules define their complete identifier sets.
Listeners should be scoped to their component or application lifecycle and removed on cleanup. The webview must re-resolve the endpoint after a sidecar restart rather than continuing to use the previous port.
Development and verification
bun run --filter @cadcamfun/desktop dev
bun run desktop:dev
bun run --filter @cadcamfun/desktop typecheck
bun test apps/desktop/src
bun run build:desktop The first command runs a browser preview. desktop:dev compiles the real sidecar and stages migrations before starting Tauri. The desktop package's tauri:dev script instead creates a stub binary only when no binary exists; use it for isolated shell work and check which binary is present before interpreting its API responses.
The desktop build script runs vite build only; it does not compile the sidecar or package a Tauri bundle. Use bun scripts/build.ts for the full release pipeline (see Release).
Preferences and settings
The shared settings store persists a validated record under cadcamfun.settings in webview local storage. Missing, invalid, or unreadable fields fall back independently. A full or blocked storage area does not stop editing, but preference changes may fail to persist. Browser preview preferences belong to the browser origin and are separate from native webview preferences.
| Setting | Accepted values/default | Operational effect |
|---|---|---|
| Theme | light, dark, system; default system | System mode follows operating-system color-scheme changes. |
| Display units | mm or in; default mm | Controls presentation; core drawing coordinates remain millimetres. |
| Decimals | Integer 0–6; default 3 | Length presentation precision. |
| Default post | grbl, linuxcnc, smoothwb, gcode, fanuc; default grbl | A selected output grammar is a preference, not proof that the machine is configured or verified. |
| Drawing tolerance | Validated core tolerance; default 0.01 mm | Shared precision preference used by drawing/toolpath consumers. |
| Snap state | Boolean; default enabled | Shared status preference. Individual editors own their snapping behavior; the standalone Editor2D currently enables its grid snap internally. |
| Glass | Boolean; default enabled | Requests macOS window material when the native host supports it. |
The Settings screen has Profile, Account, Security, Subscription, Notifications, AI, Devices, and Language tabs. The Devices tab embeds the companion gateway panel. Language settings include the glass toggle. The common locale store currently validates the default locale; do not infer additional translation coverage from a list of display labels.
Save and reopen a CAM program
- Generate or load NC code in CAM and open the save/export dialog from its existing entry point.
- For a file export, choose G-code or the portable toolpath format. G-code downloads the current NC text; the toolpath choice writes a structured
.cam.jsonsnapshot. - For application persistence, choose an existing project, a new project, or update the already saved program. Provide the program name and the required destination fields.
- When updating, the save flow records the previous complete program as a toolpath version before replacing the current data.
- Open the saved item at its toolpath detail route to inspect or restore versions.
The save summary includes NC lines, bytes, moves, tool identifiers, operation names, stock, units, work offset, and verification identity when that evidence belongs to the current NC. A modified NC text can make previously computed evidence stale. Review the current code/evidence relationship before relying on the summary.
If creating a new project succeeds but a later save step fails, the modal retains the created destination for retries and includes its identifier in the error. Retrying within that modal avoids creating the same project again. Do not assume the save flow is one atomic cross-resource transaction.
File export and project persistence are different operations
A downloaded G-code file does not create a project toolpath row. A portable snapshot carries richer state than plain NC. Verify a saved program by reopening it from the application, and verify an export by inspecting the downloaded file.
Keyboard and failure diagnosis
Cmd/Ctrl+K opens the command palette and Cmd/Ctrl+J toggles the assistant panel. Shell shortcuts are ignored when focus is in an input, textarea, select, or content-editable element. Editor canvases have their own keyboard handling. Several shell command callbacks, including Save/Export in the generic shortcut table, are placeholders; use the feature's actual save/export controls instead of assuming every listed accelerator performs a document operation.
| Observed behavior | Check |
|---|---|
| Startup waits, then Retry appears | Sidecar error/ready output, target binary, migration resource, and application logs. A cached port may belong to a prior process. |
| A screen renders but requests return 501 | Browser fixture mode. Start the real sidecar and pass both Vite endpoint variables. |
| Desktop data differs from a manual script | The data directory and token of each process; a filename alone does not identify the database. |
| A native command is unavailable | Invoke registration, capability permissions, operating-system support, and whether the code runs inside Tauri. |
| Settings revert after relaunch | Storage availability and validation. Invalid stored fields fall back to defaults. |
| Saved program evidence differs from NC | Whether NC was edited after generation. Recompute the appropriate verification for the actual text/program. |
Implementation anchors: shared/state/settings.ts, app/shortcuts.ts, features/settings/screens/settings.tsx, and features/cam/components/SaveCamModal.tsx.
Native event payloads
| Event | Payload | Notes |
|---|---|---|
sidecar-status | { state: "starting" }, { state: "ready", port }, { state: "restarting" }, { state: "failed", message } | After restarting, call get_sidecar_endpoint again; the port changes. |
update://status | status: checking, upToDate (version), available (version, notes, date, critical), downloading (downloaded, total), ready (version), failed (message); plus dismissible | A critical update and a pending restart are not dismissible. A failed check is never shown as a blocking error. |
menu | Menu item ID, such as file.new, file.open, file.save, view.devtools, help.documentation | Tray items use tray.quit and tray.recent.<projectId>. |
open-file | Paths of .cadcam, .nc, or .gcode files | Sent when the OS opens a file with the app or a second instance is launched. |
install_update can be refused with a typed reason: unsavedWork ("the editor has unsaved work; apply the update once it is saved"), noneAvailable (nothing pending, or already installed), or failed (download or signature check failed). It installs only the update that the last check announced.
import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event"
const { baseUrl, token } = await invoke<{ baseUrl: string; token: string }>("get_sidecar_endpoint")
const path = await invoke<string | null>("pick_file", {
filters: [{ name: "G-code", extensions: ["nc", "gcode"] }],
}) // null when the user cancels
await invoke("save_file", { defaultName: "part.nc", contents: gcodeText })
const unlisten = await listen<{ state: string; port?: number }>("sidecar-status", (e) => {
if (e.payload.state === "ready") reconnect()
})
// later: unlisten()
try {
await invoke("install_update", { unsavedWork: editorIsDirty })
} catch (error) {
// { kind, message }: the update variant carries the refusal (unsavedWork | noneAvailable | failed)
} get_sidecar_endpoint returns { baseUrl, token } (serialized in camelCase from SidecarEndpoint). Errors from every command arrive as the rejected value of invoke, shaped { kind, message }.
Keyboard shortcuts
| Shortcut | Action | Group |
|---|---|---|
Cmd/Ctrl+K | Open the command palette | navigation |
Cmd/Ctrl+Shift+P | New project | navigation |
Cmd/Ctrl+Z / Cmd/Ctrl+Shift+Z | Undo / redo | edit |
Cmd/Ctrl+S, Cmd/Ctrl+Shift+E | Save, export (shell placeholders; use the feature's own controls) | edit |
Cmd/Ctrl+0 | Zoom to fit | view |
Cmd/Ctrl+= / Cmd/Ctrl+- | Zoom in / out | view |
Cmd/Ctrl+J | Toggle the assistant panel | ai |
Cmd applies on macOS and Ctrl elsewhere. Each shortcut's ID is also its translation key suffix (for example settings.shortcut.palette), and the Settings screen renders this same table, so the documentation and the app cannot drift apart.
Every screen route
The router in apps/desktop/src/app/routes.tsx declares 68 routes, each loaded lazily from apps/desktop/src/pages/ (dynamic segments are _id_.tsx files). Grouped by first segment:
| Area | Routes |
|---|---|
* | * |
500 | /500 |
admin | /admin/analytics /admin/feature-flags |
ai | /ai |
analytics | /analytics/history /analytics |
auth | /auth/forgot-password /auth/magic-link /auth/magic-link/verify /auth/reset-password /auth/signin /auth/signup /auth/two-factor /auth/verify-email /auth/verify |
cad | /cad |
cam | /cam |
costs | /costs |
components | /components/:id /components |
drawings | /drawings/:id |
/ | / |
invitations | /invitations/:token |
legal | /legal/privacy /legal/terms /legal/third-party-licenses |
library | /library |
machine | /machine/:id /machine |
marketplace | /marketplace /marketplace/items/:id /marketplace/purchase/success /marketplace/purchases /marketplace/seller/dashboard |
materials | /materials/:id /materials |
mcp | /mcp/directory /mcp/servers/:id |
offline | /offline |
organizations | /organizations/:id /organizations/:id/chat/:conversationId /organizations/:id/chat /organizations/:id/chat/new-group /organizations/:id/chat/new /organizations/create /organizations |
plugins | /plugins/:id /plugins |
pricing | /pricing |
profile | /profile /profile/settings |
projects | /projects/:id/history /projects/:id /projects/create /projects |
settings | /settings /settings/companion /settings/mcp/config-editor /settings/mcp/edit/:id /settings/mcp /settings/subscription |
third-party-licenses | /third-party-licenses |
toolpaths | /toolpaths/:id /toolpaths |
tools | /tools/:id /tools |
waitlist | /waitlist |
* renders the 404 page and /500 the error page. /offline renders the offline screen; like the auth, invitation, 404, 500 and purchase-success pages it uses the standalone layout without the app chrome. Because every page is a lazy import, a screen's code is only loaded on first visit; a failed chunk load surfaces as a route error, not a blank app.