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: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

# 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 (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":

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-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:

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.