name: hd-editor-state description: The Editor State Machine (ESM) — HOW to know the true state of the VS Code editor and the Claude Code conversations inside Hydrogen Desktop, and the HARD typing invariants. Use BEFORE typing into a Claude conversation, driving setup-step threads, diagnosing "state unknown", empty split panels, tab spam, prompts landing in the wrong conversation, or any Claude tab/webview confusion. Trigger words — editor state, /editor/state, claude tab state, conversation state, which tab is active, empty editor group, split panel, double-type, typed into two boxes, prompt in wrong conversation, census, safe to type, stray text, disk verified, msg_count wrong, workbench unknown.

hd-editor-state — the Editor State Machine

ONE authoritative read: GET /editor/state on HD's control API. Everything below is what it returns and the rules for acting on it. Full design: docs/EDITOR-STATE-MACHINE.md (hd repo).

The layered truth (never blend layers)

Layer Question Source Trust
L0 lifecycle starting / editor-up / ready port probes (7380, :8821, CDP) absolute
L1 layout+conversations groups, widths, empty panels, tab census, focused tab workbench DOM + tab bar high (VS Code chrome is stable)
L2 extension what commands can I invoke the extension's contributed-command registry + fingerprint absolute (it IS the API)
L3 conversations_disk turns, first prompt, generating, delivered the jsonl transcripts absolute — works for DISPOSED background tabs
L4 census compose boxes + submit buttons visible NOW DOM probes across ALL contexts only under the invariants below

The HARD invariants (John 2026-07-17 — non-negotiable)

  1. Never type unless census.safe_to_type == true (exactly 1 visible input across ALL webview contexts — not just the focused one).
  2. Freshness oracle is the TITLE: title == 'Untitled' = never submitted. NEVER use msg_count for freshness — it counts welcome-screen DOM nodes (reads 5 on a pristine tab).
  3. After a submit, run the stray-text audit: POST /editor/audit-prompt {prefix} must return contexts_containing: 0. 1+ = the prompt is ALSO sitting in another conversation's box (double-type) — clear it, fail loudly, never ignore.
  4. Delivery is proven on DISK, not in the DOM: the prompt must be the first user turn of exactly ONE ~/.claude/projects/*/*.jsonl. 0 = not delivered; 2+ = double-send.
  5. Background tabs are unreadable (VS Code disposes their webviews): loaded:false, null ctx/compose. To read one, activate it first (/claude/tabs/activate {index:<ordinal>} — by ORDINAL, never by label: every Claude tab is labeled 'Claude Code').
  6. normalize_needed: truePOST /editor/normalize before showing the user anything (an empty split group steals 50% of their editor; populated 2-group layouts are preserved).
  7. Prefer registered commands over DOM clicks: extension.commands lists them (claude-vscode.newConversation, .focus, .editor.open, ...). Run via the in-distro :8821/command API. Commands survive extension updates; selectors don't.
  8. Extension drift: extension.fingerprint changes when Claude Code updates — re-verify your assumptions (anchors, commands) before trusting flows after a fingerprint change.

Quick recipes

  • Am I safe to type? GET /editor/statecensus.safe_to_type.
  • Did my prompt double-type? POST /editor/audit-prompt {"prefix":"<first words>"} → want 0 after submit.
  • Which conversation is generating? conversations_disk[*].generating (disk truth, any tab).
  • Fix a half-width Claude tab / dead empty panel? POST /editor/normalize.
  • Bars: layout.bars reports activity / primary sidebar / secondary (auxiliary) bar / bottom panel visibility. A Claude conversation living in the SECONDARY bar is a second compose box (census=2). Close bars deterministically: POST /editor/bars {"primary":"close","secondary":"close"} (read -> toggle-on-mismatch -> verify; never a blind toggle).