Hydrogen Desktop Bootstrap (apps + skills)
Public Made by Adomby adom
Layer Hydrogen Desktop's workspace onto a standard Adom workstation.
name: hd-claude-management description: > First-class API for managing the Claude Code conversations inside Hydrogen Desktop: list/open/close/switch conversation tabs, read each conversation's full state (title, message count, compose box, and the on-disk session GUID), type messages into a SPECIFIC conversation (instant or with a human typing animation) and submit them, and turn on Remote Control to get a claude.ai cloud URL so the user can view+drive that exact conversation from claude.ai or the Claude mobile app. Every conversation is addressed by a stable execution-context id (deterministic, background, no foreground/focus needed). Also explains how a live conversation is matched to its ~/.claude/projects/*.jsonl log. Trigger words: claude tabs, claude conversations, list conversations, open claude tab, close claude tab, switch claude tab, new conversation, type into claude, inject prompt, remote control, claude.ai url, view on mobile, session id, conversation guid, jsonl, claude session, context_id, multi-tab claude, conversation management.
HD Claude Conversation Management
Hydrogen Desktop runs Claude Code as webview tabs inside VS Code. This skill is the first-class API for managing those conversations programmatically — from HD's control API, from the user's AI inside the WSL2 workspace, or from setup steps.
All endpoints are on the HD control API (a dynamic localhost port). From inside the
workspace, examples below use $HD for http://127.0.0.1:<port> (find the port in the HD
log / port discovery). From an external context (a cloud container), call the same
endpoints with the AD verb adom-desktop hd_api '{"path":"/claude/…","method":"POST","body":{…}}'
— it auto-discovers the port. Everything here works in the background — no window focus,
no foreground, no keystroke stealing.
The core idea: address conversations by CONTEXT ID, list from the TAB BAR
Each Claude conversation is its own webview execution context with a stable integer id. That id — not the tab index, not the title, not visibility — is how you reliably target one conversation (type into it, remote-control it). VS Code keeps multiple webviews "visible" at once, so visibility/tab-index are NOT reliable addresses; the context id is.
But VS Code also disposes the webview of a background tab, so a context id only exists for
loaded tabs. That's why GET /claude/conversations merges three sources into one view:
the tab bar (every open conversation + which is active — what you see), the live webview
contexts (rich data for loaded tabs), and the on-disk session logs (the GUIDs). Each row
carries a loaded flag; loaded:false means activate the tab first to read its live details.
GET /claude/conversations — everything in one call
curl -s $HD/claude/conversations
{
"ok": true,
"count": 4,
"conversations": [
{
"ordinal": 3, // its position in the tab bar
"active": true, // the currently-focused tab
"loaded": true, // webview is live → rich fields are populated
"context_id": 7, // STABLE address for inject/remote-control (null if not loaded)
"title": "Calculate 10 times 10", // authoritative title (from the session log when matched)
"tab_label": "Calculate 10 times 10", // raw tab label (may be ellipsis-truncated)
"compose_text": "", // what's typed but not yet sent (null if not loaded)
"msg_count": 16, // null if not loaded
"session_id": "566cec21-…", // on-disk log GUID (read ~/.claude/projects/*/<id>.jsonl)
"rc_active": false, // Remote Control on? (null if not loaded)
"remote_control_url": null // claude.ai cloud URL when rc_active
},
{ "ordinal": 1, "active": false, "loaded": false, "context_id": null,
"title": "Find the capital of France", "tab_label": "Find the capital of Fran…",
"compose_text": null, "msg_count": null, "session_id": "b352055b-…", "rc_active": null, "remote_control_url": null }
]
}
- Merged view: every tab is listed (from the tab bar).
loaded:truerows have live fields (context_id,compose_text,msg_count,rc_active);loaded:falserows are background tabs VS Code disposed — activate them (/claude/tabs/activate) to read those. Even a disposed tab resolves itssession_idwhen its title matches the on-disk log. - A conversation is
Untitledwithsession_id: nulluntil its first message is submitted — then Claude names it and writes the session log. ?with_session=falseskips the disk read (faster; omitssession_id).
GET /claude/contexts is the lighter version (just the loaded contexts: context_id + compose_text).
Open / close / switch tabs
# Open a fresh conversation (new tab)
curl -s -X POST $HD/claude/new-conversation
# Open the Claude panel if it isn't open
curl -s -X POST $HD/claude/open
# List raw editor tabs (label/active/dirty)
curl -s $HD/claude/tabs
# Close ALL conversation tabs (also removes the leftover empty editor group)
curl -s -X POST $HD/claude/tabs/close-all
# Switch the visible tab by index or label substring (background)
curl -s -X POST $HD/claude/tabs/activate -d '{"index":1}'
curl -s -X POST $HD/claude/tabs/activate -d '{"label":"Eiffel"}'
# Remove a stale EMPTY editor group (the dead blank panel) WITHOUT collapsing a
# real 2-group left/right layout. (merge-groups is a back-compat alias.)
curl -s -X POST $HD/claude/tabs/close-empty-groups
Type a message into a SPECIFIC conversation
POST /claude/inject types into the conversation you name with context_index. Two speeds:
# INSTANT paste (fast) — no typing animation. Best for slash commands and scripted input.
curl -s -X POST $HD/claude/inject -d '{"text":"summarize this repo","context_index":16,"typed":false,"submit":true}'
# SLOW human typing animation (the "watch the AI type" effect), then submit.
curl -s -X POST $HD/claude/inject -d '{"text":"hello there","context_index":16,"typed":true,"delay_ms":50,"submit":true}'
# Type but DON'T send (leave it sitting in the compose box):
curl -s -X POST $HD/claude/inject -d '{"text":"draft…","context_index":16,"typed":false,"submit":false}'
typed:true= char-by-char with ±50% jitter (avgdelay_ms);false= instant paste.submit:true= press Enter;false= leave in the box.- Omit
context_indexONLY when a single conversation is open. With more than one conversation open, omitting it can type your text into several compose boxes at once (it falls back to a visibility heuristic that isn't reliable when the window isn't foreground). When in doubt, pass it. - Targeting by
context_indexis deterministic — it lands in exactly that tab even when it's not the foreground one and even with many tabs open (no fan-out into other tabs). This is the safe default: list/claude/conversations, take the row withactive:true(or the tab you want), use itscontext_id, then inject.
Typical "open a new tab and ask a question":
curl -s -X POST $HD/claude/new-conversation
CTX=$(curl -s $HD/claude/contexts | jq '.contexts[-1].context_id')
curl -s -X POST $HD/claude/inject -d "{\"text\":\"what does this project do?\",\"context_index\":$CTX,\"typed\":true,\"submit\":true}"
⭐ Remote Control → view the conversation on claude.ai / mobile
This is the big one. Turn on Remote Control for a conversation and HD returns a claude.ai cloud URL. Open it on claude.ai in any browser or in the Claude mobile app to see and drive that exact conversation in the cloud — not tied to HD. The user can be on their phone editing the same conversation.
curl -s -X POST $HD/claude/remote-control -d '{"context_index":7}'
{
"ok": true,
"rc_active": true,
"toggled": true, // false + already_in_desired_state:true if it was already on
"context_id": 7,
"remote_control_url": "https://claude.ai/code/session_014koKPJxmjYpnX2SjRRqrD3"
}
- ⚠️
/remote-controlis a TOGGLE. This endpoint detects the current state first and only toggles when needed — re-calling it on an already-on session is a no-op that returns the existing URL (it will NOT turn it off; doing that blindly archives the cloud session). {enable:false}turns Remote Control off.- The
session_…id in the URL is the cloud session id — distinct from the local on-disksession_idGUID. The cloud URL is the shareable one for claude.ai / mobile. {name}→/remote-control <name>to label the remote session.- Each conversation's current state is also reported as
rc_activein/claude/conversations. - Opening it on the user's machine (external/cloud agent): get the URL, then open it in their
Chrome and screenshot to confirm — all via AD verbs (no synthetic keystrokes):
desktop_find_window {titleContains:"Claude Code - Google Chrome"}→desktop_screenshot_window {hwnd}. To type INTO the claude.ai page, use the native-browser bridgenbrowser_eval {tabId, expression}(the composer is shadow-DOM; do NOT blind-type with SendInput). See thedriving-hd-remotelyguide on the Hydrogen Desktop wiki page.
How a live conversation maps to its on-disk log (JSONL)
Claude Code stores each conversation as ~/.claude/projects/<cwd>/<uuid>.jsonl containing
sessionId, aiTitle (the generated title), and every message. The session GUID is NOT
exposed in the conversation's webview DOM, so HD correlates a live conversation to its log by
matching the first user message (exact + unique; present in both the DOM and the JSONL).
On a match, /claude/conversations fills in session_id and uses the log's aiTitle as the
authoritative title. (Matching on the title alone is unreliable — the rendered title and the
persisted aiTitle can differ.)
Read a conversation's full transcript from the workspace:
cat ~/.claude/projects/*/<session_id>.jsonl
Why this is valuable
- Multi-conversation orchestration: open N conversations, drive each independently by context id, read each one's state — all from a script or an agent, in the background.
- Cloud + mobile continuity: Remote Control hands you a claude.ai URL so a conversation started in HD continues on the phone / web. Huge for working away from the desk.
- Deterministic + safe: context-id addressing never types into the wrong tab; the empty blank panel is cleaned up automatically; a real 2-group left/right layout is preserved.
- Auditable: every conversation maps to an on-disk JSONL you can read, diff, or archive.
Endpoint quick reference
| Endpoint | Method | What it does |
|---|---|---|
/claude/conversations |
GET | Full per-conversation data incl. session_id (the canonical list) |
/claude/contexts |
GET | Light list: context_id + compose_text |
/claude/tabs |
GET | Raw editor tabs (label/active/dirty) |
/claude/new-conversation |
POST | Open a fresh conversation tab |
/claude/open |
POST | Open the Claude panel if closed |
/claude/tabs/close-all |
POST | Close all tabs + remove the leftover empty group |
/claude/tabs/activate |
POST | Switch visible tab by {index} or {label} |
/claude/tabs/close-empty-groups |
POST | Remove stale empty editor groups (preserves populated) |
/claude/inject |
POST | Type into {context_index} — {typed,submit,delay_ms} |
/claude/remote-control |
POST | Turn on RC for {context_index} → returns remote_control_url |
/claude/cross-eval |
POST | (Diagnostic) run JS across all workbench/webview contexts |
These are the same endpoints HD's own setup steps use to open Claude and inject the first prompt — so behavior is identical whether a human, setup, or the workspace AI drives them.
---
name: hd-claude-management
description: >
First-class API for managing the Claude Code conversations inside Hydrogen Desktop:
list/open/close/switch conversation tabs, read each conversation's full state (title,
message count, compose box, and the on-disk session GUID), type messages into a SPECIFIC
conversation (instant or with a human typing animation) and submit them, and turn on
Remote Control to get a claude.ai cloud URL so the user can view+drive that exact
conversation from claude.ai or the Claude mobile app. Every conversation is addressed by a
stable execution-context id (deterministic, background, no foreground/focus needed). Also
explains how a live conversation is matched to its ~/.claude/projects/*.jsonl log.
Trigger words: claude tabs, claude conversations, list conversations, open claude tab,
close claude tab, switch claude tab, new conversation, type into claude, inject prompt,
remote control, claude.ai url, view on mobile, session id, conversation guid, jsonl,
claude session, context_id, multi-tab claude, conversation management.
---
# HD Claude Conversation Management
Hydrogen Desktop runs Claude Code as webview tabs inside VS Code. This skill is the
**first-class API** for managing those conversations programmatically — from HD's control
API, from the user's AI inside the WSL2 workspace, or from setup steps.
All endpoints are on the **HD control API** (a dynamic localhost port). From inside the
workspace, examples below use `$HD` for `http://127.0.0.1:<port>` (find the port in the HD
log / port discovery). From an **external** context (a cloud container), call the same
endpoints with the AD verb `adom-desktop hd_api '{"path":"/claude/…","method":"POST","body":{…}}'`
— it auto-discovers the port. Everything here works in the **background** — no window focus,
no foreground, no keystroke stealing.
## The core idea: address conversations by CONTEXT ID, list from the TAB BAR
Each Claude conversation is its own webview **execution context** with a **stable integer id**.
That id — not the tab index, not the title, not visibility — is how you reliably *target* one
conversation (type into it, remote-control it). VS Code keeps multiple webviews "visible" at
once, so visibility/tab-index are NOT reliable addresses; the context id is.
But VS Code also **disposes the webview of a background tab**, so a context id only exists for
*loaded* tabs. That's why `GET /claude/conversations` **merges three sources** into one view:
the **tab bar** (every open conversation + which is active — what you see), the **live webview
contexts** (rich data for loaded tabs), and the **on-disk session logs** (the GUIDs). Each row
carries a `loaded` flag; `loaded:false` means activate the tab first to read its live details.
## GET /claude/conversations — everything in one call
```bash
curl -s $HD/claude/conversations
```
```jsonc
{
"ok": true,
"count": 4,
"conversations": [
{
"ordinal": 3, // its position in the tab bar
"active": true, // the currently-focused tab
"loaded": true, // webview is live → rich fields are populated
"context_id": 7, // STABLE address for inject/remote-control (null if not loaded)
"title": "Calculate 10 times 10", // authoritative title (from the session log when matched)
"tab_label": "Calculate 10 times 10", // raw tab label (may be ellipsis-truncated)
"compose_text": "", // what's typed but not yet sent (null if not loaded)
"msg_count": 16, // null if not loaded
"session_id": "566cec21-…", // on-disk log GUID (read ~/.claude/projects/*/<id>.jsonl)
"rc_active": false, // Remote Control on? (null if not loaded)
"remote_control_url": null // claude.ai cloud URL when rc_active
},
{ "ordinal": 1, "active": false, "loaded": false, "context_id": null,
"title": "Find the capital of France", "tab_label": "Find the capital of Fran…",
"compose_text": null, "msg_count": null, "session_id": "b352055b-…", "rc_active": null, "remote_control_url": null }
]
}
```
- **Merged view:** every tab is listed (from the tab bar). `loaded:true` rows have live fields
(`context_id`, `compose_text`, `msg_count`, `rc_active`); `loaded:false` rows are background tabs
VS Code disposed — **activate them** (`/claude/tabs/activate`) to read those. Even a disposed tab
resolves its `session_id` when its title matches the on-disk log.
- A conversation is **`Untitled`** with **`session_id: null`** until its first message is submitted —
then Claude names it and writes the session log.
- `?with_session=false` skips the disk read (faster; omits `session_id`).
`GET /claude/contexts` is the lighter version (just the *loaded* contexts: `context_id` + `compose_text`).
## Open / close / switch tabs
```bash
# Open a fresh conversation (new tab)
curl -s -X POST $HD/claude/new-conversation
# Open the Claude panel if it isn't open
curl -s -X POST $HD/claude/open
# List raw editor tabs (label/active/dirty)
curl -s $HD/claude/tabs
# Close ALL conversation tabs (also removes the leftover empty editor group)
curl -s -X POST $HD/claude/tabs/close-all
# Switch the visible tab by index or label substring (background)
curl -s -X POST $HD/claude/tabs/activate -d '{"index":1}'
curl -s -X POST $HD/claude/tabs/activate -d '{"label":"Eiffel"}'
# Remove a stale EMPTY editor group (the dead blank panel) WITHOUT collapsing a
# real 2-group left/right layout. (merge-groups is a back-compat alias.)
curl -s -X POST $HD/claude/tabs/close-empty-groups
```
## Type a message into a SPECIFIC conversation
`POST /claude/inject` types into the conversation you name with `context_index`. Two speeds:
```bash
# INSTANT paste (fast) — no typing animation. Best for slash commands and scripted input.
curl -s -X POST $HD/claude/inject -d '{"text":"summarize this repo","context_index":16,"typed":false,"submit":true}'
# SLOW human typing animation (the "watch the AI type" effect), then submit.
curl -s -X POST $HD/claude/inject -d '{"text":"hello there","context_index":16,"typed":true,"delay_ms":50,"submit":true}'
# Type but DON'T send (leave it sitting in the compose box):
curl -s -X POST $HD/claude/inject -d '{"text":"draft…","context_index":16,"typed":false,"submit":false}'
```
- `typed`: `true` = char-by-char with ±50% jitter (avg `delay_ms`); `false` = instant paste.
- `submit`: `true` = press Enter; `false` = leave in the box.
- Omit `context_index` ONLY when a single conversation is open. With **more than one** conversation
open, omitting it can type your text into **several compose boxes at once** (it falls back to a
visibility heuristic that isn't reliable when the window isn't foreground). When in doubt, pass it.
- Targeting by `context_index` is deterministic — it lands in exactly that tab even when it's
not the foreground one and even with many tabs open (no fan-out into other tabs). **This is the
safe default: list `/claude/conversations`, take the row with `active:true` (or the tab you want),
use its `context_id`, then inject.**
Typical "open a new tab and ask a question":
```bash
curl -s -X POST $HD/claude/new-conversation
CTX=$(curl -s $HD/claude/contexts | jq '.contexts[-1].context_id')
curl -s -X POST $HD/claude/inject -d "{\"text\":\"what does this project do?\",\"context_index\":$CTX,\"typed\":true,\"submit\":true}"
```
## ⭐ Remote Control → view the conversation on claude.ai / mobile
This is the big one. Turn on Remote Control for a conversation and HD returns a **claude.ai
cloud URL**. Open it on claude.ai in any browser or in the Claude **mobile app** to see and
drive that exact conversation in the cloud — not tied to HD. The user can be on their phone
editing the same conversation.
```bash
curl -s -X POST $HD/claude/remote-control -d '{"context_index":7}'
```
```jsonc
{
"ok": true,
"rc_active": true,
"toggled": true, // false + already_in_desired_state:true if it was already on
"context_id": 7,
"remote_control_url": "https://claude.ai/code/session_014koKPJxmjYpnX2SjRRqrD3"
}
```
- ⚠️ **`/remote-control` is a TOGGLE.** This endpoint **detects** the current state first and only
toggles when needed — re-calling it on an already-on session is a **no-op** that returns the
existing URL (it will NOT turn it off; doing that blindly archives the cloud session).
- `{enable:false}` turns Remote Control **off**.
- The `session_…` id in the URL is the **cloud** session id — distinct from the local on-disk
`session_id` GUID. The cloud URL is the shareable one for claude.ai / mobile.
- `{name}` → `/remote-control <name>` to label the remote session.
- Each conversation's current state is also reported as `rc_active` in `/claude/conversations`.
- **Opening it on the user's machine (external/cloud agent):** get the URL, then open it in their
Chrome and screenshot to confirm — all via AD verbs (no synthetic keystrokes):
`desktop_find_window {titleContains:"Claude Code - Google Chrome"}` → `desktop_screenshot_window
{hwnd}`. To type INTO the claude.ai page, use the native-browser bridge `nbrowser_eval
{tabId, expression}` (the composer is shadow-DOM; do NOT blind-type with SendInput). See the
`driving-hd-remotely` guide on the Hydrogen Desktop wiki page.
## How a live conversation maps to its on-disk log (JSONL)
Claude Code stores each conversation as `~/.claude/projects/<cwd>/<uuid>.jsonl` containing
`sessionId`, `aiTitle` (the generated title), and every message. **The session GUID is NOT
exposed in the conversation's webview DOM**, so HD correlates a live conversation to its log by
matching the **first user message** (exact + unique; present in both the DOM and the JSONL).
On a match, `/claude/conversations` fills in `session_id` and uses the log's `aiTitle` as the
authoritative `title`. (Matching on the title alone is unreliable — the rendered title and the
persisted `aiTitle` can differ.)
Read a conversation's full transcript from the workspace:
```bash
cat ~/.claude/projects/*/<session_id>.jsonl
```
## Why this is valuable
- **Multi-conversation orchestration:** open N conversations, drive each independently by
context id, read each one's state — all from a script or an agent, in the background.
- **Cloud + mobile continuity:** Remote Control hands you a claude.ai URL so a conversation
started in HD continues on the phone / web. Huge for working away from the desk.
- **Deterministic + safe:** context-id addressing never types into the wrong tab; the empty
blank panel is cleaned up automatically; a real 2-group left/right layout is preserved.
- **Auditable:** every conversation maps to an on-disk JSONL you can read, diff, or archive.
## Endpoint quick reference
| Endpoint | Method | What it does |
|---|---|---|
| `/claude/conversations` | GET | Full per-conversation data incl. `session_id` (the canonical list) |
| `/claude/contexts` | GET | Light list: `context_id` + `compose_text` |
| `/claude/tabs` | GET | Raw editor tabs (label/active/dirty) |
| `/claude/new-conversation` | POST | Open a fresh conversation tab |
| `/claude/open` | POST | Open the Claude panel if closed |
| `/claude/tabs/close-all` | POST | Close all tabs + remove the leftover empty group |
| `/claude/tabs/activate` | POST | Switch visible tab by `{index}` or `{label}` |
| `/claude/tabs/close-empty-groups` | POST | Remove stale empty editor groups (preserves populated) |
| `/claude/inject` | POST | Type into `{context_index}` — `{typed,submit,delay_ms}` |
| `/claude/remote-control` | POST | Turn on RC for `{context_index}` → returns `remote_control_url` |
| `/claude/cross-eval` | POST | (Diagnostic) run JS across all workbench/webview contexts |
These are the same endpoints HD's own setup steps use to open Claude and inject the first
prompt — so behavior is identical whether a human, setup, or the workspace AI drives them.