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