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
| Service | Probe | Port | Protocol | Source ID |
|---|---|---|---|---|
| Claude Code | SSE via MCP server (/__signals__/stream) | -- | sse | local:claude-code |
| OpenClaw | WebSocket probe | 18789 | openclaw | local:openclaw |
| LM Studio | HTTP GET /v1/models | 1234 | openai | local:lm-studio |
| Ollama | HTTP GET /v1/models | 11434 | openai | local: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 broadcastThe 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:
- Probes local services + browser MIDI devices in parallel
- Calls
upsertLocalSources()to sync results into signal-source-state - Auto-fills the OpenClaw token if available
- 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:
- Tauri desktop: reads
~/.openclaw/openclaw.jsondirectly via Rust command - Vite dev mode: client calls
GET /api/openclaw/token(reads~/.openclaw/openclaw.json→gateway.auth.token) - Production browser: silently returns null (no filesystem access)
- If token found and source has no key: pre-fill
apiKeyand marktokenAutoFilled: 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 astatechangelistener that triggersscanAndSyncLocal()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
| Protocol | Transport | Used By |
|---|---|---|
sse | Server-Sent Events (via MCP server) | Claude Code |
websocket | WebSocket (raw JSON) | Generic remote sources |
openclaw | WebSocket + handshake | OpenClaw gateway |
openai | HTTP + CORS proxy | LM Studio, Ollama |
anthropic | HTTP + CORS proxy | Anthropic API |
midi | Web MIDI API | MIDI 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
| File | Role |
|---|---|
state/local-discovery.ts | Client-side probes, scan lifecycle, token fetch |
state/signal-source-state.ts | Source store, upsert logic, categories |
state/server-config.ts | serverUrl() — resolves API paths to MCP server |
views/signal-connection.ts | Transport connection logic, connectLocalSSE() |
midi/midi-discovery.ts | MIDI device detection + hot-plug |