Skip to content
CADCAMFUN
Docs

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

  1. The shell starts the bundled sidecar with a generated launch token and application data and migration directories.
  2. The sidecar binds an ephemeral port on 127.0.0.1, waits for its health route to answer, and writes a JSON ready event containing the port.
  3. The webview invokes get_sidecar_endpoint and creates an API client and managed Effect runtime.
  4. 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

AreaRoutes 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 collaborationAuth, 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/, and testing/: 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

CommandPurpose
get_sidecar_endpointWait 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_pathResolve 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_deleteAccess 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_filepick_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_projectsRefresh the tray's recent-project menu.
check_for_updates, install_updateRequest 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

Run from the repository root
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.

SettingAccepted values/defaultOperational effect
Themelight, dark, system; default systemSystem mode follows operating-system color-scheme changes.
Display unitsmm or in; default mmControls presentation; core drawing coordinates remain millimetres.
DecimalsInteger 0–6; default 3Length presentation precision.
Default postgrbl, linuxcnc, smoothwb, gcode, fanuc; default grblA selected output grammar is a preference, not proof that the machine is configured or verified.
Drawing toleranceValidated core tolerance; default 0.01 mmShared precision preference used by drawing/toolpath consumers.
Snap stateBoolean; default enabledShared status preference. Individual editors own their snapping behavior; the standalone Editor2D currently enables its grid snap internally.
GlassBoolean; default enabledRequests 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

  1. Generate or load NC code in CAM and open the save/export dialog from its existing entry point.
  2. 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.json snapshot.
  3. 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.
  4. When updating, the save flow records the previous complete program as a toolpath version before replacing the current data.
  5. 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 behaviorCheck
Startup waits, then Retry appearsSidecar 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 501Browser fixture mode. Start the real sidecar and pass both Vite endpoint variables.
Desktop data differs from a manual scriptThe data directory and token of each process; a filename alone does not identify the database.
A native command is unavailableInvoke registration, capability permissions, operating-system support, and whether the code runs inside Tauri.
Settings revert after relaunchStorage availability and validation. Invalid stored fields fall back to defaults.
Saved program evidence differs from NCWhether 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

EventPayloadNotes
sidecar-status{ state: "starting" }, { state: "ready", port }, { state: "restarting" }, { state: "failed", message }After restarting, call get_sidecar_endpoint again; the port changes.
update://statusstatus: checking, upToDate (version), available (version, notes, date, critical), downloading (downloaded, total), ready (version), failed (message); plus dismissibleA critical update and a pending restart are not dismissible. A failed check is never shown as a blocking error.
menuMenu item ID, such as file.new, file.open, file.save, view.devtools, help.documentationTray items use tray.quit and tray.recent.<projectId>.
open-filePaths of .cadcam, .nc, or .gcode filesSent 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.
Calling native commands from the webview
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

ShortcutActionGroup
Cmd/Ctrl+KOpen the command palettenavigation
Cmd/Ctrl+Shift+PNew projectnavigation
Cmd/Ctrl+Z / Cmd/Ctrl+Shift+ZUndo / redoedit
Cmd/Ctrl+S, Cmd/Ctrl+Shift+ESave, export (shell placeholders; use the feature's own controls)edit
Cmd/Ctrl+0Zoom to fitview
Cmd/Ctrl+= / Cmd/Ctrl+-Zoom in / outview
Cmd/Ctrl+JToggle the assistant panelai
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:

AreaRoutes
**
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.

In 3D

Desktop process topology Drag to orbit · scroll to zoom · right-drag to pan
Processes at run time. The Tauri shell spawns the sidecar and waits for its {”event”:”ready”,”port”:N} line on stdout (sidecar.rs); the webview then talks HTTP to 127.0.0.1 on that port with the bearer token. The sidecar owns the SQLite database, the embedded nikcli AI host (CADCAMFUN_AI_HOST=nikcli, the default) and plugins; the mobile app reaches it through companion pairing.