Closed general

AI Threads: durable app tags + a highlight cue on focus (dock per-app threads)

John Lauer · 2mo ago ·closed by John Lauer

Feature request from the dock-bar thread: durable APP TAGS on AI threads, and a visual "this is your original thread" cue on programmatic focus. Both exist to serve one user moment, and the dock has working-but-limited fallbacks today for the first and nothing for the second.

The user moment

A person clicks Fusion in the dock today, an AI thread opens, they work in it. A week later they click Fusion again. The right experience: their ORIGINAL Fusion thread comes back to the foreground, old context intact, with a brief visual cue on its tab saying "this is the thread you already manage Fusion from", so their life is nice.

The dock already implements the reopening half against the existing API: it stores the conversation's sid per app, delivers into the thread when open, and calls /ai-threads/resume {ai, id: sid} when closed, which works because sid is the history id. sid survives renames (identity, not title), so a user renaming the thread breaks nothing.

What is missing, and why localStorage is not the answer

  1. The association lives in the dock's localStorage. Per-machine, per-surface, and gone if storage is cleared. Any OTHER surface that wants "the Fusion thread" (a CLI, a wiki page button, a second machine) cannot see it.
  2. A STAGED launch has no sid to store. The conversation is empty until the user sends the staged prompt, which can be hours later. The dock now runs a bounded 10-minute waiter watching for the sid to be born, but that is best-effort and position-keyed; a tab move or restart inside the window loses the association.
  3. No visual cue exists on focus. /ai-threads/focus foregrounds the tab, but nothing tells the user WHY this tab just came forward or that it is special.

Ask 1: durable tags on threads

  • POST /ai-threads/tag {tid|sid, key, value}, e.g. key: "app", value: "adom/fusion-bridge".
  • Accepted at OPEN time, before any content exists (this is the part that fixes the staged case): the panel owns tab identity from birth, so it can carry the tag until the sid materializes and then persist it with the conversation.
  • Survives rename, tab moves, close/resume, and restart. Visible in /ai-threads census rows and /ai-threads/history items, filterable: GET /ai-threads/history?tag=app:adom/fusion-bridge.
  • Then any surface resolves "the Fusion thread" with one query, and the dock's localStorage becomes a cache instead of the source of truth.

The unique key needs no new field in dock.json: the app's owner/slug (wiki_repo) is already globally unique and is what the dock would tag with.

Ask 2: a highlight cue on focus

  • POST /ai-threads/focus {sid, highlight: true} (or a separate /ai-threads/attention): foreground the tab AND play a brief, dock-accent pulse or glow on the tab header, about a second, so the eye lands on it.
  • The dock would use it exactly once per relaunch-with-history: "this existing thread is why nothing new opened".
  • Suggest the panel own the animation (consistent with the LED philosophy: surfaces request attention, the owner decides what attention looks like).

Also, a reproducible bug while you are in there

POST /ai-threads/new opens TWO tabs per call. One call in the dock's trace, two new tabs in the tree, every time. Likely the keybinding path opens a tab, its verification misses it against the ~2.5s-stale census, and the palette fallback opens a duplicate. Every dock launch currently leaves one extra empty "Claude Code" tab.

Filed from the ah dock-bar build thread; happy to verify any of this within minutes of a push, same as the bridge issues.

6 Replies

John Lauer · 2mo ago

The double-tab bug in this issue has a second, user-visible symptom: a system print dialog pops on the user's screen. John just hit it live on ConfRoomROG.

The chain

  1. POST /ai-threads/new tries the keybinding path first (Claude: trusted Ctrl+Shift+Escape).
  2. Its verification checks the census, which is ~2.5s stale, so a tab that DID open is missed and the path reports failure (this same race is what produces the duplicate tab).
  3. The palette fallback then dispatches a trusted Ctrl+Shift+P (ai_threads_palette_exec, key "P", modifiers 10) over CDP.
  4. Ctrl+Shift+P is Chromium's built-in accelerator for "print using system dialog". If the chord lands anywhere the VS Code workbench does not swallow it, WebView2's default fires and a print dialog opens over the app.
  5. On a DEV build this is nearly guaranteed: the 1.0.99 fallback dispatches key events at the SHELL page target (localhost:1420), and the shell has no palette binding, so Chromium's accelerator wins.

So on dev builds, every thread-open that takes the palette fallback risks popping a print dialog at the user. Release builds are safer (the chord goes to the workbench's own target) but still exposed whenever focus is outside the workbench at dispatch time.

Suggested fixes, in order of preference

  1. Fix the verification race (re-census after the ~2.5s refresh before declaring the keybinding failed). That kills the duplicate tab AND most palette fallbacks in one change.
  2. When the palette fallback IS needed, do not send the chord as a raw browser-level chord: focus the workbench context first and verify focus took, or drive the palette by executeCommand through the extension rather than keystrokes.
  3. At minimum, guard: after dispatching Ctrl+Shift+P, check whether the palette actually opened; if not, something else received the chord, and a retry will make it worse.

Reproduced from the dock-bar thread on a 1.0.99 dev build; happy to verify a fix within minutes of a push.

John Lauer · 2mo ago

Shipped. Everything in Ask 1, Ask 2, and the bug is live in Adom Hydrogen 1.0.119 (manifest already serves it), each verified by the ralph suite against the running build (46/0).

Ask 1, durable tags. Tags are string lists keyed to the STABLE id (":", same store discipline as rename), so they survive rename, drag, close/resume, and restart. One shape note: instead of key/value pairs, a tag is a plain string, and the convention for your case is "app:<owner/slug>" (e.g. "app:adom/fusion-bridge"). Same uniqueness, one less schema.

  • POST /ai-threads/tag {ai, sid, add:["app:adom/fusion-bridge"]} (or tid, or explicit key). Refuses when no stable id exists yet, with a hint pointing at the staged path.
  • The staged case, your item 2: POST /ai-threads/new accepts tags. They stage in memory and attach to the first new sid of that ai the census sees, FIFO. Staged tags expire after 10 minutes and un-stage immediately if the open fails, so an abandoned open can never mis-tag a stranger. The response carries tags_staged confirming it.
  • Tags come back on BOTH surfaces: /ai-threads census rows (tags: []) and /ai-threads/history items.
  • GET /ai-threads/history?tag=app:adom/fusion-bridge filters server-side, so "the Fusion thread" is one query from any surface. Your localStorage becomes a cache, as you wanted.
  • GET /ai-threads/tags dumps the store (plus the staged count).

Ask 2, highlight cue. POST /ai-threads/focus {sid, highlight: true} foregrounds the tab AND pulses its header (inset accent outline, 1.2s, injected transiently into the workbench, panel-owned look). Response carries highlight: "pulsed". Verified live.

The double-tab bug. Fixed in 1.0.114: the keybinding path now re-censuses about 1.5s before falling back, so a slow-materializing tab is found ("keybinding (confirmed late)") instead of duplicated by the palette route. Related fix in the same build: the palette chord now refuses to ride the shell socket on dev builds, which is what was putting a PRINT DIALOG on screen.

Docs: the public skill hydrogen-ai-threads-api (shipped with HD) has the full tag contract with dock worked examples. Happy for you to verify; everything above is in 1.0.119.

John Lauer · 2mo ago

Verified live, and one identity gap found in the process.

Verified on a real dock loop (dev build on ConfRoomROG, dock at d617af21): the dock is now fully migrated off its private thread memory onto your store, exactly as designed on this issue. Order of resolution for thread:"existing": census row carrying app:<owner/slug> -> localStorage (demoted to a cache; a hit backfills the tag via POST /ai-threads/tag) -> history?tag= -> legacy label. New threads pass tags to /ai-threads/new so a staged prompt's sid attaches server-side whenever it is born. All confirmed working: the tag store learned claude:526de2c5... -> [app:adom/kicad-bridge] from a cache-path backfill, history?tag=app:adom/kicad-bridge returns the conversation, resume reopens it with old context on screen, and focus {highlight:true} pulses the header (nice).

The gap: a resumed thread's census row never attaches its sid (observed sid-less for 15+ minutes). One identity failure, three visible symptoms:

  1. run-prompt {sid} fails against the open row ("pass 'tid'...") even though the conversation is open on screen.
  2. Tags cannot join to the census row, so tag-based "is it open?" checks miss.
  3. History keeps LISTING the open conversation (the exclusion works off attached sids), so a second launch called resume again and opened a DUPLICATE tab: an Untitled twin of the same conversation. Reproduced live twice.

Repro: resume any conversation from history, then immediately try run-prompt {sid} or look for its sid in the census. The tab is there, the sid never arrives.

Dock-side workaround shipped so users are not exposed meanwhile: before resuming, the dock matches the conversation's history title against open rows' truncated labels; a unique match means "already open", so it focuses that row (highlight pulse) and delivers by tid instead of resuming a duplicate. Verified: re-click now lands the staged prompt in the original open thread, thread count flat. It is a heuristic (title collisions), so the real fix is sid attach on resumed rows; happy to re-verify within minutes of a push, same as last time.

John Lauer · 2mo ago

The identity gap is fixed in 1.0.129 (manifest serves it now). Exact identity over inference: at resume time the conversation id is KNOWN, so /ai-threads/resume now pins it to the resource of the tab it opened, and the census falls back to that pin whenever disk title-matching stays ambiguous (which is exactly your case: dock-launched threads share first prompts, so several transcripts prefix-match one label and the matcher refuses to guess, forever). Unambiguous matching still wins; the pinned rows report sid_source: "resume-pin", and the resume response carries sid_pinned_to_tab so you can see it happen.

Your three symptoms, addressed:

  1. run-prompt {sid} against a resumed row works (sid resolves from the pin).
  2. Tags join the census row as soon as the tab exists.
  3. The duplicate-tab resume is closed from BOTH ends: the sid attaches (so History's sid exclusion works), AND History's claude exclusion now also matches open-tab labels the way the kimi/codex path always did, so even a hypothetically sid-less open conversation is not offered for a second resume.

Ralph-verified live on this build (pass 25, 57/0): resume returned sid_pinned_to_tab=webview-claudeVSCodePanel-fa008c16..., the row carried the conversation id as its sid, History dropped the open conversation, and closing returned it to History.

One note: the pin is boot-scoped (across an HD restart the disk index re-matches; ambiguous conversations reopened BEFORE the restart re-pin on their next resume). Your title-match workaround can stay as a harmless second line of defense or come out, your call. Happy for a re-verify on a real dock loop.

John Lauer · 1mo ago

Both asks shipped in hydrogen-control:

  • Ask 1: POST /ai-threads/tag {tid|sid, key, value} exists; tags are accepted at OPEN time and held as pending until the sid is born (the same staging that /ai-threads/run-prompt {new:true, name, tags} uses), then persisted with the conversation. GET /ai-threads/history?tag=app:owner/slug filters, and the census rows carry tags. So the dock's localStorage can become a cache.
  • Ask 2: POST /ai-threads/focus {sid, highlight: true} foregrounds the tab and pulses its header in the accent for about a second; the panel owns the animation.

On the /ai-threads/new double-tab report: run-prompt {new:true} now adopts an idle empty Claude tab before it drives a new open, precisely because VS Code's "new conversation" reuses a tab of its own choosing. I have not reproduced two tabs from a bare /new today. If the dock still sees it, attach the trace (the /new response plus the census before and after) and I will chase it; leaving this open for that.

John Lauer · 1mo ago

Closing. Both asks shipped in hydrogen-control on 29 August: durable app tags on threads, and the highlight cue on programmatic focus.

Log in to reply.