← Commit history

Delete skills/hydrogen-desktop-sse/SKILL.md

John Lauer ·1fe7e1196f ·1mo ago ·parent e841b62
1 file changed −241
skills/hydrogen-desktop-sse/SKILL.mddeleted−241
@@ -1,241 +0,0 @@-----name: hydrogen-desktop-sse-description: How Hydrogen's local workspace API + SSE works (a.k.a. the workspace API) — replaces Carbon for workspace commands so the workspace's adom-cli controls Hydrogen, not cloud Hydrogen. The workspace is the WSL2 Adom-Workspace distro. Covers the full pipeline, routing, env vars, field format differences, and hard-won lessons. Trigger words — Hydrogen SSE, workspace API, local API, adom-cli workspace, add-tab, move-tab, webview tab, EventSource, workspace_updated, ADOM_CARBON_URL, ADOM_HYDROGEN_URL.------# Hydrogen — Local Workspace API + SSE--## Architecture--```-Workspace adom-cli (the WSL2 Adom-Workspace distro)-    → http://127.0.0.1:47083 (Hydrogen's proxy)-    → Hydrogen handles workspace commands locally + broadcasts SSE-    → Hydrogen frontend receives SSE, re-fetches workspace, renders--Cloud Docker's adom-cli (unchanged)-    → carbon.adom.inc → Hydrogen web-```--Hydrogen runs its own workspace API on port 47083 (reverse proxy, default — see PortConfig) so the workspace's adom-cli controls Hydrogen's UI, not cloud Hydrogen. No cross-talk between environments. (Under WSL2 the workspace reaches Hydrogen at `127.0.0.1:47083` — mirrored networking shares loopback with the Windows host.)--## Port 47083 routing (the hydrogen-proxy / reverse-proxy)--All requests hit port 47083 (reverse proxy, default — see PortConfig). The router in `handle_request()` decides:--| Path pattern | Handler | Where |-|---|---|---|-| `/api/workspaces/editor/{owner}/{repo}/current/*` | Local workspace API | In-memory |-| `/api/panels/webview/*`, `/api/panels/sandbox/*` | Local panel API | SSE broadcast |-| `/containers/*` | Local container discovery | Canned response |-| `/proxy/{port}/{path}` | Workspace proxy | TCP forward to the workspace (WSL2 distro) |-| Everything else (`/api/*`, etc.) | Carbon proxy | HTTPS to carbon.adom.inc |--The dev file server on port 1420 forwards `/api/*` directly to `handle_request()` (same function, not TCP — supports SSE streaming via BoxBody).--## SSE Connection--**Endpoint:** `GET /api/workspaces/editor/{owner}/{repo}/current/events`--Uses `tokio::sync::broadcast` channel. On connect sends `{"type":"connected","connectionId":"hydrogen-local"}`. Workspace mutations broadcast `{"type":"workspace_updated"}`.--**Frontend connects** in `PanelWorkspaceComponent.svelte` → `setupSSE()`. In Tauri mode, `$page.params` may be empty (adapter-static), so it falls back to `owner=local, repo=workspace`.--**Critical lesson:** The `onopen` handler PUTs the workspace state. This broadcasts `workspace_updated`, which the frontend re-fetches. Set `skipNextSSESync = true` before the PUT to prevent the feedback loop that kills the connection.--## Container Discovery--adom-cli calls `/containers/{slug}` to get owner/repo. The slug comes from parsing `VSCODE_PROXY_URI`'s hostname — split on `-`, take the LAST segment.--**Response format** (must match Carbon's exactly):-```json-{-  "repository": {-    "owner": { "name": "jlauer12" },-    "name": "workspace"-  },-  "slug": "hdlocal",-  "status": "running"-}-```--NOT `{"owner": "jlauer12", "repository": "workspace"}` — adom-cli reads `container["repository"]["owner"]["name"]`.--## Workspace Env Vars--Set by the WSL2 workspace-creation code when the distro is created:--| Var | Value | Why |-|---|---|---|-| `VSCODE_PROXY_URI` | `http://hydrogen-hdlocal/proxy/{{port}}/` | Slug = `hdlocal` (last segment after `-`) |-| `ADOM_CARBON_URL` | `http://127.0.0.1:47083` | Container discovery + non-workspace API |-| `ADOM_HYDROGEN_URL` | `http://127.0.0.1:47083` | Workspace mutations (adom-cli sends these HERE, not to Carbon) |-| `ADOM_DESKTOP_MODE` | `local` | Tells adom-cli it's a local environment |--Under WSL2, `127.0.0.1` reaches the Windows host directly — mirrored networking shares loopback with the host, so the same `127.0.0.1:<port>` works from inside the distro and on Windows. The canonical code-server port Hydrogen exposes on the host is 7380.--**Also set in adom-cli config** (persists across sessions):-```bash-adom-cli config set-url http://127.0.0.1:47083-adom-cli config set-token <session-token>-```-Plus manually write `hydrogen_url` to `~/.config/adom-cli/config.json` (no CLI command for it).--**API key** injected at `/var/run/adom/api-key` by bootstrap via `GET "$(cat ~/.adom/hydrogen-control-url)/auth-token"` (the control URL is `http://127.0.0.1:<dynamic>`).--## Workspace Tree Format--The frontend serializes splits with `first`/`second`, NOT `children`:-```json-{-  "type": "split",-  "id": "desktop-root-split",-  "direction": "horizontal",-  "ratio": 0.5,-  "first": { "type": "leaf", "id": "pane-left", ... },-  "second": { "type": "leaf", "id": "pane-right", ... }-}-```--All tree traversal functions (`find_leaf_path`, `find_split_path`, `resolve_path`) must handle BOTH `children` array and `first`/`second` keys.--## REST Endpoint Field Names (adom-cli vs Hydrogen)--adom-cli sends different field names than you might expect:--| Endpoint | adom-cli sends | Hydrogen must read |-|---|---|---|-| POST /tabs | `{panelId, panelType, displayName, initialState}` | Construct tab with UUID, merge initialState into panelState |-| POST /moves | `{sourcePanelId, tabId, targetPanelId}` | NOT `sourcePanel`/`targetPanel` |-| DELETE /tabs | `{panelId, tabId}` or `{name}` | Support both |--## Smoke Test (from inside the workspace)--```bash-# 1. Probe — confirms SSE alive-adom-cli hydrogen probe-# → browser_connected: true, sse_connections: 1--# 2. Get workspace-adom-cli hydrogen workspace get--# 3. Add tab-adom-cli hydrogen workspace add-tab \-  --panel-id desktop-pane-right \-  --panel-type adom/a1b2c3d4-0031-4000-a000-000000000031 \-  --display-name "Test Page" \-  --initial-state '{"url":"https://wiki.adom.inc/adom/adom-shotlog"}'--# 4. Move tab-adom-cli hydrogen workspace move-tab \-  --from-panel-id desktop-pane-right \-  --tab-id <tab-id-from-step-3> \-  --to-panel-id desktop-pane-left--# 5. Screenshot to verify-adom-bridge hd_screenshot-```--## What Broke and Why--### SSE never connected on fresh launch-`setupSSE()` checks `$page.params.owner` and `$page.params.repo`. In Tauri mode with adapter-static, these are empty. **Fix:** fall back to `owner=local, repo=workspace` when in Tauri mode.--### SSE connected then immediately disconnected-The `onopen` handler PUTs workspace state → broadcasts `workspace_updated` → frontend re-fetches → gets the state it just PUT → re-applies → triggers autosave → another PUT → loop. **Fix:** set `skipNextSSESync = true` before the `onopen` PUT.--### adom-cli "Could not get owner from container info"-Three stacked issues:-1. `VSCODE_PROXY_URI` was `http://localhost:8080/...` → slug parsed as `8080` → called `/containers/8080` → Carbon 404-2. Container info response had flat `{"owner": "..."}` but adom-cli reads `container["repository"]["owner"]["name"]`-3. `ADOM_HYDROGEN_URL` wasn't set → workspace commands went to `hydrogen.adom.inc` instead of Hydrogen--### add-tab returned "Missing 'tab' in body"-Hydrogen expected `{"panelId", "tab": {id, panelType, panelState}}` but adom-cli sends `{"panelId", "panelType", "displayName", "initialState"}` — flat format. **Fix:** accept both, construct tab with UUID for the flat format.--### move-tab returned "Source/target panel not found"-Two issues:-1. Handler read `sourcePanel`/`targetPanel` but adom-cli sends `sourcePanelId`/`targetPanelId`-2. `find_leaf_path` only traversed `children` array, not `first`/`second` keys--### Webview tabs were blank + opened in native browser-`add_FrameNavigationStarting` was blocking ALL external URLs in child iframes, including legitimate webview panel content. **Fix:** stop blocking frame navigations entirely — webview panels need external URLs. URL interception for window.open still works via `on_new_window`.--### Google showed "blocked" icon-Not an Hydrogen bug. Google sends `X-Frame-Options: SAMEORIGIN` which prevents iframe embedding. Sites that allow embedding (adom.inc, wiki) load fine.--## Port Discovery — How Any Client Finds Hydrogen--All Hydrogen ports are dynamic (configurable via `ports.json`). The ONE constant is the **discovery port: 47080**. It never changes.--```-GET http://127.0.0.1:47080/discover-```--Returns:-```json-{-  "ok": true,-  "code_server": 7380,-  "proxy": 47083,-  "control": 47084,-  "cdp": 47085,-  "hostname": "hd.localhost"-}-```--> **Relay ports:** under the WSL2 runtime the relay listens **same-port 8765-> (WS) / 8766 (HTTP)** inside the distro and is reached same-port via mirrored-> loopback (or via `/proxy/`). There are **no host 47081/47082 listeners**.-> Clients discover the relay ports from `~/.adom/cli-relay-ports.json` (fallback-> 8765/8766), not from `/discover`.--### Who uses it--| Client | What it discovers | How |-|---|---|---|-| adom-bridge relay | Control API port (was hardcoded 9001) | `curl :47080/discover` → use `control` field |-| adom-cli in the workspace (the WSL2 distro) | Proxy + Hydrogen URL | Already set via `ADOM_CARBON_URL` env var at workspace creation |-| External tools / diagnostics | Any port | Hit discovery, get the full map |-| Cloud Docker (this container) | All Hydrogen ports | `curl http://<desktop-ip>:47080/discover` via relay |--### Implementation--Tiny `std::net::TcpListener` on port 47080 in `hydrogen-app/src/lib.rs`. Runs on a plain OS thread (no tokio). Reads `get_runtime_ports()` on each request. Returns raw HTTP response with JSON body.--```rust-std::thread::spawn(move || {-    let listener = std::net::TcpListener::bind("127.0.0.1:47080").unwrap();-    for stream in listener.incoming() {-        let ports = hd_control::get_runtime_ports();-        let body = serde_json::json!({ "ok": true, ...ports });-        // write HTTP/1.1 200 OK + JSON-    }-});-```--### Port defaults (PortConfig)--| Service | Default Port | User-visible? | Workspace Port |-|---|---|---|---|-| Discovery | **47080** (FIXED) | No | n/a |-| Code-server | 7380 | Yes (in webview URLs) | 8080 |-| Proxy | 47083 | No | n/a |-| Control API | 47084 | No | n/a |-| CDP | 47085 | No | n/a |-| Relay WS | — (same-port under WSL2) | No | 8765 |-| Relay HTTP | — (same-port under WSL2) | No | 8766 |--Config stored at `%APPDATA%\hydrogen-desktop\ports.json`. On first launch, defaults are written without scanning. Workspace-side ports (8080, 8765, 8766) are always fixed — only host-side ports are dynamic. The relay (8765/8766) is reached **same-port** under WSL2 (mirrored loopback) — no host 47081/47082 mapping; discover it from `~/.adom/cli-relay-ports.json` (fallback 8765/8766).--## Files--| File | What |-|---|---|-| `src-tauri/crates/hydrogen-proxy/src/lib.rs` | Workspace API, SSE, Carbon proxy, tree mutations (~1500 lines) |-| `src-tauri/crates/hydrogen-control/src/lib.rs` | `/auth-token` endpoint, `/ports` endpoint, `/setup/*` routes |-| `src-tauri/crates/hydrogen-control/src/ports.rs` | PortConfig struct, load/save/resolve, DISCOVERY_PORT constant |-| `src-tauri/crates/hydrogen-app/src/lib.rs` | Discovery server on :47080, port resolution at startup |-| `src/lib/stores/portStore.ts` | Frontend Svelte store, `initPorts()`, `controlUrl()`, `codeServerUrl()` |-| `src/lib/components/editor/workspaces/PanelWorkspaceComponent.svelte` | Frontend SSE setup, autosave, workspace re-fetch |-| `scripts/hydrogen-bootstrap.sh` | Sets adom-cli config, injects API key, uses `HD_CONTROL_PORT` env var |