* feat(desktop): add Retina DMG background tooling * feat(desktop): customize the macOS DMG layout * ci(desktop): validate DMG background assets * fix(desktop): adjust DMG Applications icon position * ci(desktop): drop redundant DMG artwork validation from publish workflow Tauri's beforeBuildCommand already runs dmg:background (with its own validation) at the start of the build/sign/notarize step, and the release/beta config overlays do not override the build section, so this step duplicated work the publish job performs anyway. PR-time coverage lives in desktop-test.yml. Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com> --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
14 KiB
Desktop App Example
Tauri desktop shell + Bun sidecar backend + Next.js UI for running and inspecting Cline chat sessions.
Dev Commands
From apps/examples/desktop-app/:
bun run dev:headless- Next.js UI (http://localhost:3125) and sidecar backend with a fresh shared approval credentialbun run dev:web- Next.js UI only (approval-gated tools requiredev:headlessor the native app)bun run dev:sidecar- sidecar backend only (approval-gated tools requiredev:headlessor the native app)bun run dev- Tauri desktop devbun run build- build web assetsbun run build:sidecar- build the Bun sidecar bundlebun run build:sidecar:bin- compile the Bun sidecar into a local binarybun run build:binary- build desktop binarybun run package:desktop- package the current OS desktop app intodist/desktop/bun run typecheck- TypeScript check
Customizing the macOS Install Window
The drag-to-Applications window is configured by bundle.macOS.dmg in
src-tauri/tauri.conf.json. Its artwork comes
from the PNG sources in src-tauri/dmg/; the
background.gen.tiff Finder actually renders is a gitignored build artifact
regenerated from them on every build.
- The current source artwork is
640x400. Exportbackground.pngat 1x andbackground@2x.pngat 2x. - Currently the app icons are centered at
(140, 200)and the Applications folder centered at(500, 200). If updating artwork, updateappPositionandapplicationFolderPositionto reposition the app icons. - Build with
bun run build:binary. Before compiling, the build validates both PNG dimensions, combines them withtiffutilinto the Retina-awaresrc-tauri/dmg/background.gen.tiff, and verifies the TIFF contains the expected 1x and 2x representations. Runbun run dmg:backgroundto do just that step, e.g. to sanity-check new artwork without a full build. The DMG is written beneathsrc-tauri/target/release/bundle/dmg/.
Run bun run test:dmg-background for the cross-platform checks covering the
committed PNG dimensions and TIFF validation logic.
The configured 640x432 Finder window is intentionally 32 points taller than
the 640x400 background. That extra height matches the Finder chrome in the
currently verified packaged layout; re-check it after material macOS or Finder
changes. The project deliberately uses a multi-resolution TIFF even though
Tauri's documented background formats are PNG, JPG, and GIF: Finder renders
both the 1x and 2x representations from a single background file. Re-check the
packaged DMG after upgrading Tauri in case its background validation changes.
Login Shell PATH Resolution
Apps launched from Finder/the Dock inherit launchd's minimal PATH
(/usr/bin:/bin:/usr/sbin:/sbin), not the one your shell profiles build, so
agent-run commands would miss Homebrew-installed tools like gh even though
they work fine from a terminal. At startup the sidecar asks the user's login
shell — read from the account database via getpwuid, falling back to
$SHELL — for its PATH and merges it into process.env.PATH, which every
agent-spawned child (run_commands, MCP servers) inherits. Only PATH is
imported, deliberately; other login-environment variables (SSH_AUTH_SOCK,
API keys, JAVA_HOME-style tool roots) are not pulled in. Set
CLINE_SIDECAR_SKIP_SHELL_PATH=1 to disable. Implementation and details:
sidecar/shell-path.ts.
Web Visual System
The framework-neutral color, typography, radius, and navigation contract lives
in the internal @cline/ui workspace
package. Other Cline web surfaces can take only its tokens or opt into the
Tailwind adapter and shared base styles without depending on the desktop
runtime. See webview/styles/README.md for the
desktop integration notes.
Releases & Auto-Updates
Releases are built, signed, notarized, and published by the desktop-publish
GitHub workflow as a single universal macOS DMG — one download that runs
natively on both Apple Silicon and Intel (macOS picks the matching slice at
launch, so users never choose an architecture). The step-by-step flow (version
bumps, changelog, tag, repo secrets) lives in the publish-desktop skill
(.cline/skills/publish-desktop/SKILL.md).
Installed apps auto-update via the Tauri updater: they poll the rolling
desktop-latest release's latest.json on launch and every 2 hours, install
updates in the background, and prompt for a restart. Two things must never be
lost: the desktop-latest release/tag (its feed URL is baked into shipped
apps) and the updater private key (TAURI_SIGNING_PRIVATE_KEY — without it,
shipped apps can't verify new updates).
There is also a beta channel ("Cline Beta", a separate app that installs
side by side with stable) cut from the desktop-experimental branch and
served by the rolling desktop-beta release — the same never-delete rule
applies to it. The experimental-branch process and beta release flow live in
EXPERIMENTAL.md.
Shareable Desktop Packages (manual fallback)
Tauri desktop bundles are OS-specific, so build each package on the target OS:
- macOS:
bun run package:desktop:mac - Windows:
bun run package:desktop:windows - Linux:
bun run package:desktop:linux
The macOS package script refuses to create a shareable package unless Developer ID signing and notarization credentials are configured. This prevents the common Gatekeeper failure where a downloaded unsigned build appears damaged on a teammate's Mac.
Set either APPLE_CERTIFICATE or APPLE_SIGNING_IDENTITY, plus one notarization credential set before packaging macOS:
APPLE_ID,APPLE_PASSWORD,APPLE_TEAM_IDAPPLE_API_KEYorAPPLE_API_KEY_PATH,APPLE_API_KEY_ID,APPLE_API_ISSUER
For local-only macOS testing, use bun run package:desktop:mac --allow-unsigned-mac. That ad-hoc signs the .app and strips quarantine attributes, but it is not suitable for a downloaded build shared with teammates.
macOS signing & notarization, step by step
One-time keychain setup:
- Get the Developer ID Application identity from your team admin. A
.ceralone is not enough — you need the private key. If the admin generated the CSR, have them export the identity from Keychain Access as a.p12and import it:security import BeeCertificates.p12 -k ~/Library/Keychains/login.keychain-db -T /usr/bin/codesign -T /usr/bin/security - If
security find-identity -v -p codesigningstill reports0 valid identities, the Apple intermediate CA is missing. Install it:curl -O https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer && security import DeveloperIDG2CA.cer -k ~/Library/Keychains/login.keychain-db - Re-run
security find-identity -v -p codesigning— it should now listDeveloper ID Application: <Team Name> (<TEAMID>). That exact quoted string is yourAPPLE_SIGNING_IDENTITY. - Get an App Store Connect API key from the admin: the
AuthKey_<KEYID>.p8file, the Key ID, and the Issuer ID (a UUID from App Store Connect → Users and Access → Integrations). This is used for notarization only — nothing is published.
Per-build:
export APPLE_SIGNING_IDENTITY="Developer ID Application: <Team Name> (<TEAMID>)"
export APPLE_API_KEY="<KEYID>" # Tauri reads APPLE_API_KEY (the Key ID); APPLE_API_KEY_ID alone silently skips notarization
export APPLE_API_KEY_PATH="/path/to/AuthKey_<KEYID>.p8"
export APPLE_API_ISSUER="<issuer UUID>"
bun run package:desktop:mac
The first signing run pops a keychain dialog — enter your macOS login password and click Always Allow. Notarization uploads the app to Apple's automated malware scan (typically 2–10 minutes) and staples the ticket. Artifacts land in dist/desktop/; share the .dmg. The DMG name takes its version from src-tauri/tauri.conf.json, the zip name from package.json — bump both.
Do not remove src-tauri/entitlements.plist or the bundle.macOS.entitlements reference in tauri.conf.json: notarization requires the hardened runtime, which breaks the Bun-compiled sidecar (SharedArrayBuffer is not defined, surfacing in-app as "desktop backend endpoint not ready") unless the JIT entitlements are present.
Runtime Overview
Startup flow:
- Tauri starts a persistent local desktop backend and keeps only native window/file-picker/open-path responsibilities.
- The desktop backend starts the Bun sidecar, which discovers or starts the
canonical shared Cline Hub and exposes one websocket transport (
/transport) for desktop commands, queries, and pushed events. - The React app uses
lib/desktop-client.tsand no longer imports@tauri-apps/api/coredirectly in feature code. - Tool approval updates are pushed from the backend instead of polled from the UI.
- Session process context resolves
workspaceRootfrom git root and uses that same path as defaultcwdfor chat runtime and git operations unless explicitly overridden.
Desktop transport envelope:
- Request:
{ "type": "command", "id": string, "command": string, "args"?: object } - Response:
{ "type": "response", "id": string, "ok": boolean, "result"?: unknown, "error"?: string } - Event:
{ "type": "event", "event": { "name": string, "payload": unknown } }
Settings: Routine
- The Settings sidebar includes a
Routineview for hub-backed automations. Routinelists all RPC schedules and shows status (enabled,nextRunAt, active execution).- From the UI you can open a create form and add, pause/resume, trigger-now, and delete schedules.
- The view is wired to the same scheduler APIs used by
cline schedulethrough Tauri commands andscripts/routine-schedules.ts.
Key Files
src-tauri/src/main.rs- Tauri shell lifecycle, backend launch, and native-only commandssidecar/index.ts- persistent Bun sidecar and Hub-daemon entry dispatchsidecar/chat-session.ts- shared-Hub chat session adapterwebview/lib/desktop-client.ts- typed desktop websocket clientwebview/hooks/use-chat-session.ts- UI chat session state + backend subscriptionswebview/lib/chat-schema.ts- chat message schema used by the UIwebview/components/views/settings/routine-view.tsx- Routine schedules UI
Data + Storage
- Session artifacts are written under
~/.cline/data/sessions/<sessionId>/(orCLINE_SESSION_DATA_DIR). - Canonical replay/export artifact:
<sessionId>.messages.json. <sessionId>.messages.jsonis expected to contain ordered messages plus assistantmodelInfoandmetrics(including cache token fields when provided by the model runtime).<sessionId>.hooks.jsonlis observability/debug telemetry and should not be required for normal history replay/export flows.- Full v1 schema for the persisted messages file, including failure/retry semantics and golden fixtures, is documented in
packages/core/docs/messages-contract-v1.md.
Sidecar observability
The desktop sidecar sends SDK telemetry through the same configured OpenTelemetry
pipeline used by the CLI and writes structured runtime logs to
~/.cline/data/logs/code.log by default. Telemetry continues to honor the global
opt-out setting exposed in the desktop settings UI. The sidecar truncates stale
logs and rotates the active file before it exceeds 50 MiB.
Logging can be configured with the same environment variables as the CLI:
CLINE_LOG_ENABLED=0disables file logging.CLINE_LOG_LEVELsets the Pino level (for example,debugorwarn).CLINE_LOG_PATHoverrides the log destination.CLINE_LOG_NAMEoverrides the logger name.
In a development webview, sidecar voice-input diagnostics are also streamed to
the webview console as [desktop:voice-input] entries. Production builds can
enable the same console stream with NEXT_PUBLIC_CLINE_DEBUG_LOGS=1 at build
time, or at runtime from DevTools with
localStorage.setItem("cline.debugLogs", "1") followed by a reload. Diagnostic
events include the selected provider/model and sanitized endpoint, but never
credentials, request headers, recorded audio, or transcript contents.
Troubleshooting
- If live updates stall, verify the desktop backend websocket is connected and
chat_eventmessages are arriving. - Tauri restarts the desktop backend if the sidecar process exits and kills it on app teardown.
- Chat sends now preflight provider credentials. If a provider that requires API-key auth is selected without a key, the UI blocks the turn with a clear error message instead of starting a hanging session.
- If a turn completes with
finishReason=errorbefore any assistant content is produced, the UI now adds an explicit error chat message so failed turns are visible in the transcript. - If package changes are not reflected, rebuild SDK packages (
bun run build:sdk). The next desktop or CLI Hub connection will reuse a compatible running Hub or replace an incompatible one through the shared discovery path. - Provider settings updates are patch-style: only fields you edit are changed. Unset fields are preserved instead of being cleared.
- Speech input requires an enabled provider whose models.dev metadata identifies
a dedicated
audio-to-textmodel, or the built-in ElevenLabs provider with its Scribe v2 model. Choose the voice input provider and model explicitly under Settings → Models → Voice input. That selection is stored separately from the chat model asmodes.voiceInputin~/.cline/data/settings/providers.json; provider credentials remain in their existing provider entry and never enter the webview. ElevenLabs uses its native/v1/speech-to-textAPI. Text-to-speech models withoutput: ["audio"]are not used for microphone transcription. - Streaming transcription models, such as Vercel AI Gateway's
openai/gpt-realtime-whisper, update the composer while the user speaks. The sidecar mints a short-lived transcription token; the long-lived gateway credential is never sent to the webview. Batch models such asopenai/whisper-1continue to transcribe after recording stops.