Skip to content

Local Discovery

The scene-builder automatically detects AI services running on your machine and presents them as connectable signal sources.

How It Works

Browser (scene-builder)

   │  discoverLocalServices()          browser-side probes
   │──► httpProbe(:1234)               → LM Studio
   │──► httpProbe(:11434)              → Ollama
   │──► wsProbe(:18789)               → OpenClaw
   │──► relativeProbe(server SSE)      → Claude Code (via MCP server)
   │──► navigator.requestMIDIAccess()  → MIDI devices

   │  upsertLocalSources(services)
   │  (sync into signal-source-state)

All probes run in parallel with Promise.allSettled() and a 2s timeout. Services that don't respond are marked available: false.

Probed Services

ServiceProbePortProtocolSource ID
Claude CodeSSE via MCP server (/__signals__/stream)--sselocal:claude-code
OpenClawWebSocket probe18789openclawlocal:openclaw
LM StudioHTTP GET /v1/models1234openailocal:lm-studio
OllamaHTTP GET /v1/models11434openailocal:ollama

LM Studio and Ollama probes also fetch the list of available models from their /v1/models endpoint.

Claude Code signal relay

Claude Code signals flow through the MCP server, not directly to the browser:

Claude Code hook → sajou-emit → POST /api/signal [MCP server]

Browser ← EventSource(/__signals__/stream) ← SSE broadcast

The probe checks if the MCP server's /__signals__/stream endpoint is reachable (via serverUrl()). If the server isn't available (isServerAvailable() === false), the probe is skipped — no server means no signal relay.

This works in both dev mode (Vite proxies to the server) and production (Tauri/static, browser connects to the server URL directly).

Source Categories

Sources are split into two categories:

  • LOCAL -- auto-discovered, ephemeral. Rebuilt on every scan. Fixed identity colors to prevent visual drift across sessions.
  • REMOTE -- manually added by the user. Persisted to localStorage. Default URL: wss://test.sajou.dev/signals.

The chip bar UI displays both sections with a separator. Unavailable local sources appear grayed out (opacity 0.35) and are non-clickable.

Scan Lifecycle

scanAndSyncLocal() runs:

  • At application startup (after initServerConnection() completes)
  • When the user clicks the Rescan button (rotate-cw icon)
  • Every 30 seconds (periodic rescan)
  • On MIDI device hot-plug events

The function:

  1. Probes local services + browser MIDI devices in parallel
  2. Calls upsertLocalSources() to sync results into signal-source-state
  3. Auto-fills the OpenClaw token if available
  4. Auto-connects Claude Code and OpenClaw (if token is present)

upsertLocalSources()

Syncs discovered services with the source list:

  • New services → create source entry with appropriate protocol and color
  • Disappeared services → mark as "unavailable" (not deleted)
  • Already connected → never touched (won't disconnect a live connection)

OpenClaw Token Auto-Fill

When OpenClaw is detected:

  1. Tauri desktop: reads ~/.openclaw/openclaw.json directly via Rust command
  2. Vite dev mode: client calls GET /api/openclaw/token (reads ~/.openclaw/openclaw.jsongateway.auth.token)
  3. Production browser: silently returns null (no filesystem access)
  4. If token found and source has no key: pre-fill apiKey and mark tokenAutoFilled: true

The popover also has a "Paste from config" button for manual re-fetch.

MIDI Discovery

Browser-side MIDI detection runs alongside service probes:

  • Uses navigator.requestMIDIAccess() (Web MIDI API)
  • Each MIDI input port becomes a local source with protocol "midi"
  • initMIDIHotPlug() registers a statechange listener that triggers scanAndSyncLocal() on plug/unplug
  • Note: Web MIDI API is not available in WKWebView (Safari/Tauri on macOS) — MIDI sources won't appear in the Tauri desktop app

Transport Protocols

ProtocolTransportUsed By
sseServer-Sent Events (via MCP server)Claude Code
websocketWebSocket (raw JSON)Generic remote sources
openclawWebSocket + handshakeOpenClaw gateway
openaiHTTP + CORS proxyLM Studio, Ollama
anthropicHTTP + CORS proxyAnthropic API
midiWeb MIDI APIMIDI controllers

OpenAI and Anthropic protocols route through the Vite CORS proxy (/__proxy/?target=...) in dev mode, or through tauri-plugin-http in the desktop app, to avoid browser CORS restrictions.

Key Files

FileRole
state/local-discovery.tsClient-side probes, scan lifecycle, token fetch
state/signal-source-state.tsSource store, upsert logic, categories
state/server-config.tsserverUrl() — resolves API paths to MCP server
views/signal-connection.tsTransport connection logic, connectLocalSSE()
midi/midi-discovery.tsMIDI device detection + hot-plug