Phone + watch operator surfaces

Companion apps for the operator loop

The same agents, beads, chat, choices, summaries, logs and release controls, on your phone and your wrist.

Two families: iPhone + Apple Watch, and Android + Wear OS (shipped through Play internal testing). The watch apps are not remotes for a single button; they handle choices, the inbox, project drill-downs, agent and bead actions, complications and voice input.

Open, inspect, act Status, chat, agents, beads, feed, timeline, logs, links, summaries, releases and settings.
Watch-first triage Choices, inbox alerts, status and complications at a glance.
Safe when reconnecting The last good state renders immediately and is replaced in place once the live stream catches up.
Android Status: service health, cluster topology and agents by state Android Chat: agents posting progress, with code in a message Android Agents: grouped by project with running, attention and terminal filters Android Flows: a running feature-delivery flow and recent runs
The Android app (release v1.2.1882) against the repository's isolated QA fixture peer.
Wear OS home: Agents and Chat tiles, an overview card and Status Wear OS agents: blocked and failed agents first, with their beads Wear OS chat: recent agent messages
The Wear OS app connected directly to the same fixture peer.

The four apps

iPhone

SwiftUI app at parity with Android, web and the TUI. Adds Live Activities and the Dynamic Island, Siri / App Intents, a share extension, widgets (pending App Store provisioning), SSH tunnels with on-device keys, and an embedded in-process daemon. Source: companion/ios.

Apple Watch

Relays through the paired iPhone by default, with direct Wi-Fi setup in progress. Interactive choices and chat in the Smart Stack, DM quick-reply, Pico chat input and complications.

Android

Kotlin / Compose. Status, chat, agents, beads and every other surface; live agent terminals; group chat; SSH tunnels with multi-port forwarding; and an embedded daemon mode. Source: companion/android.

Wear OS

Talks to the daemon directly or with a profile imported from the phone. Project menus, voice compose, agent and bead actions, tiles, six complications, and a full-screen agent terminal over SSH.

Install and update path

Android companion + Wear OS builds

APK assets are attached to project releases.

The live Android lane publishes the phone and Wear apps as separate Play apps on internal-testing tracks: com.cacophony.companion to internal, and com.cacophony.companion.wear to wear:internal. Wear version codes use a dedicated +1,000,000 lane so the two apps cannot collide.

Install the phone companion first, then install the Wear app from the paired phone+wear Play internal-testing flow or your usual Wear OS install path. The legacy APK lookup remains useful for local QA and release assets, but the smooth operator update path is the Play internal-testing rollout.

Need help choosing between Play testing tracks, internal app sharing, and a private F-Droid repo? See the Android distribution comparison.

Release cadence The Android release helper companion/android/scripts/release-to-play.sh builds both app bundles, verifies the signing certificate, uploads the phone AAB to internal, uploads the watch AAB to wear:internal, and commits the Play edit with rollout status by default.

Android installation checklist

  1. Join the private Play internal-testing track for the phone app and the Wear app.
  2. Install or update the phone companion from Play first.
  3. Install or update the Wear app from the paired watch flow, Play on-device, or a deliberate ADB QA install.
  4. Open phone Settings → Watch App, tap Send to watch, then use watch Settings → Import from phone if the watch has not imported the profile automatically.
  5. If the watch looks stale after a token or endpoint rotation, use phone Reset watch sync state and watch Refresh complications.

For a fast daemon connection, an Android caco://connect deep link opens the phone companion straight into a one-command daemon connect instead of entering the endpoint and token in connection settings by hand.

Opt-in location privacy

Private Home/Work travel zones

The Android phone companion can classify the operator as Home, Work, in transit, or unknown. This feature is off by default. Enabling it and setting Home or Work is an explicit foreground action that requests location permission and a fresh device fix.

Exact locations stay on the phone. Home/Work centers are encrypted with an Android Keystore-backed AES-GCM key and stored in the app's no-backup area. The daemon and wearable surfaces receive only the coarse zone, observation time, bounded expiry, and confidence — never coordinates, addresses, network names, or a location trail.
  • Revoke Home / Revoke Work deletes that encrypted zone and immediately checks the remaining coarse state.
  • Delete all travel data removes the ciphertext and Keystore key, disables reporting, and attempts one final unknown report.
  • Re-enrollment is required when Android reports that secure storage was reset or invalidated; there is no plaintext recovery or backup fallback.
  • Unavailable permission or signal leaves the zone unchanged and reports unknown rather than guessing.

See the canonical Android private travel guide for registration, radius, check-now, revoke, delete, and re-enrollment details.

Architecture

Three tiers, with no changes to the daemon.

Three tiers with no daemon changes. The daemon host serves its usual REST API, SSE stream and audio endpoints, and can optionally run a ttyd bridge that exposes caco choices tui over WebSocket. The phone app reaches it over HTTPS and WSS on the LAN or a VPN: it subscribes to SSE, shows native choices, can open the ttyd terminal in a WebView, and relays events to the watch over WatchConnectivity on Apple devices or the Wear Data Layer on Android. The watch app is a structured-data client with native screens and voice input; it never renders a terminal.
Phone and watchThe phone talks to the daemon; the watch talks to the phone and never renders a terminal.

ttyd Bridge Setup

The ttyd bridge is optional but enables the full caco choices tui experience in a phone WebView.

Prerequisites

Install ttyd from a package manager you trust:

# macOS
brew install ttyd

# NixOS / nix-env
nix-env -iA nixpkgs.ttyd

# Debian/Ubuntu
sudo apt install ttyd

Avoid copy/pasting unpinned source builds that install directly into system paths for routine setup. If you must build ttyd yourself, pin a release tag or commit and verify the upstream release before installing.

Quick Start

# Start the ttyd bridge (uses the convenience command)
caco choices serve-ttyd

# Custom local-only port
caco choices serve-ttyd --port 8080 --bind 127.0.0.1

# Read-only mode (recommended unless you need phone input)
caco choices serve-ttyd --writable false

CLI Reference

FlagDefaultDescription
--port7682Port for the ttyd HTTP/WebSocket server
--bind127.0.0.1Bind address. Keep loopback unless the bridge is behind trusted TLS/auth on a private network.
--writabletrueAllow terminal input from the WebView. Prefer --writable false for read-only monitoring.

The ttyd bridge is stateless — all choice state lives in the daemon. Restart freely without data loss.

Manual ttyd Setup

Alternatively, run ttyd directly:

ttyd --port 7682 --interface 127.0.0.1 caco choices tui

Phone App

Platforms

Android build / QA shell Android companion build and QA commands use the Nix-provided gradle binary; the repository does not check in ./gradlew. For focused unit-test validation, prefer the lightweight nix develop .#android-validation --command bash -c 'cd companion/android && gradle :app:testDebugUnitTest --tests ...' shell so queued jobs avoid emulator/F-Droid dependencies. Use the full nix develop .#android --command bash -c 'cd companion/android && gradle :app:assembleDebug' shell for APK builds, screenshot/emulator QA, and release work. Avoid bash -lc in queued Android jobs because a login shell can reset the Nix dev-shell PATH. From companion/android, nix develop .#android --command ... and the shorter nix develop . --command ... select the full shell.

Features

FeatureDescription
Choices firstActive choices resolve with a tap or a voice reply. Pending choices outrank routine inbox items: a red badge and an urgent banner stay up until every choice is answered.
InboxChoices, messages, broadcasts and speech in tabs, with swipe to archive.
Status and fleetNode telemetry, service health, cluster topology, and agents grouped by project with lifecycle filters and actions: stop, pause, resume, restart, nudge and spawn.
BeadsBoard rows sorted by priority and recency, status filters, detail and edit (including moving a bead to another project), and Take / Release / Start agent actions.
ChatProject, group and global chat with direct, broadcast and speak modes, voice input, per-agent channel focus, threaded replies, and visible pending/sent state.
Agent sessionsLive agent terminals (Termux renderer on Android) and structured Pico sessions; dormant on-demand agents offer an explicit Wake & send.
Feed and timelineNewest-first activity with the same filters as caco-web.
Everything elsePlans, summaries, nodes, files, links, releases, logs, actions, the merge queue and settings, in list/detail panes that split side by side on wide screens.
NotificationsChannels for choices, messages, alerts and speech, with inline reply, choice actions, quiet hours and per-channel toggles.
Live stateHydrates from the UI snapshot, then follows the SSE stream; the last good snapshot renders immediately on a cold start or while reconnecting.
Apple extrasLive Activity and Dynamic Island for choices and chat, Siri / App Intents shortcuts, a share extension for filing beads, and widgets.
ConnectivityQR connect, self-managed SSH tunnels with on-device keys, and an embedded in-process daemon mode.
ThemesNord and E-ink palettes in light and dark, an OLED true-black option, and AA contrast throughout.
StorageBearer tokens live in the Keychain (iOS) or EncryptedSharedPreferences (Android); Android app data is excluded from backups.

Connection Setup

  1. Open the companion app on your phone
  2. Go to Settings
  3. Enter the daemon host, port (default 11100, the daemon API rather than caco-web's 11180 or ttyd's 7680/7681) and bearer token, or scan a connection QR code
  4. Tap Connect

Connection QR codes carry {"host":…,"port":11100,"token":…} as JSON or as a caco://connect?host=…&port=…&token=… URL; both apps accept either.

The bearer token at $CACOPHONY_DIR/tokens/node.token grants broad local daemon access. Enter it only on devices you trust, keep it out of screenshots and shared notes, and rotate it if the phone is lost. Narrower companion-scoped tokens are not available yet.

Network Requirements

The phone must be on the same network (LAN or VPN) as the daemon host. The daemon is not exposed to the internet.

Watch App

Platforms

Features

FeatureDescription
ChoicesThe current choice with crown or rotary navigation; tap an option to resolve it, or dictate a freeform reply.
Glanceable screensCompact views of status, activity, inbox, chat, agents, beads, projects, the merge queue, logs, crons, releases and more, fetched directly from the daemon.
AlertsHaptics on a new choice, with actionable buttons.
Complications and tilesA pending-choice count on the watch face; a Wear OS tile shows the question and its first two options for inline answers.
Live ActivitywatchOS Smart Stack for interactive choices, chat streams and DM quick-reply.
TerminalsWear OS can open a full-screen live agent terminal over an SSH port forward, with keyboard or dictation input.

Interaction

Connectivity

The Apple Watch now has two explicit connection modes:

Relay via iPhone (default): Watch ←→ Phone ←→ Daemon
Direct Wi-Fi (in progress): Watch ←→ Daemon

Offline and stale-state behavior

Daemon API Surface

The companion app consumes these existing endpoints (no daemon changes required):

EndpointMethodPurpose
/api/v1/choicesGETList active choices
/api/v1/choices/listGETList choices with status filter
/api/v1/choices/{id}GETGet single choice by ID
/api/v1/choices/resolvePOSTResolve a choice
/api/v1/ui/streamGETSSE event stream (agents, beads, feed, choices, services)
/api/v1/ui/snapshotGETInitial state snapshot for Android AppStateStore; Android caches the last parseable response locally and no longer performs eager all-endpoint pull-sync after connect
/api/v1/operator-inboxGETOperator inbox messages
/api/v1/feedGETPaginated event feed
/api/v1/agents/{id}/stopPOSTStop an agent
/api/v1/agents/{id}/pausePOSTPause an agent
/api/v1/agents/{id}/resumePOSTResume an agent
/api/v1/agents/{id}/restartPOSTRestart an agent
/api/v1/agents/{id}/nudgePOSTSend a message to an agent
/api/v1/nodesGETList cluster nodes (status dashboard)
/api/v1/actionsGETList project actions
/api/v1/actions/{name}/runPOSTExecute a project action
/api/v1/messages/chatGETGlobal chat history
/api/v1/projects/{p}/messages/chatGETProject chat history
/api/v1/projects/{p}/messages/sendPOSTSend direct message
/api/v1/messages/broadcastPOSTBroadcast message
/api/v1/projects/{p}/messages/speakPOSTSpeak via TTS

Project Structure

companion/
├── ios/
│   ├── CacophonyCompanion/          # Phone app (SwiftUI)
│   │   └── Sources/
│   │       ├── App/                 # App entry point
│   │       ├── Connection/          # Daemon SSE + REST client
│   │       ├── Choices/             # Choices data model + views
│   │       ├── WebView/             # ttyd WKWebView surface
│   │       └── WatchRelay/          # WatchConnectivity bridge
│   └── CacophonyCompanionWatch/     # Watch app (SwiftUI)
│       └── Sources/
│           ├── App/                 # Watch app + state manager
│           ├── Views/               # Choices + idle views
│           ├── Notifications/       # Actionable notifications
│           └── Complications/       # Complication widget
└── android/
    ├── app/                         # Phone app (Kotlin/Compose)
    │   └── src/
    │       ├── main/java/.../
    │       │   ├── connection/      # SSE + REST client (ConnectionManager)
    │       │   ├── choices/         # Choices data model (ChoicesStore)
    │       │   ├── notifications/   # Android notification channels + builders
    │       │   ├── relay/           # Wear Data Layer bridge (WearRelay)
    │       │   ├── state/           # SSE-driven app state (AppStateStore, Models)
    │       │   └── ui/
    │       │       ├── actions/     # Actions list + execution screen
    │       │       ├── agents/      # Agents fleet list + detail screen
    │       │       ├── beads/       # Beads list + detail screens
    │       │       ├── chat/        # Chat surface with send mode selector
    │       │       ├── choices/     # Choices screen
    │       │       ├── inbox/       # Tabbed inbox (Choices/Messages/Broadcasts/Speech/All)
    │       │       ├── settings/    # Connection + notification settings
    │       │       ├── status/      # Node telemetry + service health dashboard
    │       │       └── theme/       # Nord theme (Compose)
    │       └── test/java/.../       # Unit + E2E tests
    │           ├── ActionsScreenTest
    │           ├── AgentsScreenTest
    │           ├── AppStateStoreTest
    │           ├── ChatScreenTest
    │           ├── DaemonApiE2eTest
    │           ├── FullAppTest
    │           ├── NotificationManagerTest
    │           ├── StatusScreenTest
    │           └── TestDaemonServer
    ├── scripts/
    │   └── test-against-daemon.sh   # Live daemon integration test runner
    └── wearable/                    # Watch app (Compose for Wear)
        └── src/main/java/.../
            ├── choices/             # Watch choices view
            └── tiles/               # Wear OS tile layout

Security