From f113e1a6b62f1a63417b2620ff7e83ef240528de Mon Sep 17 00:00:00 2001 From: Jonathan Borgwing Date: Sat, 18 Jul 2026 10:21:32 -0400 Subject: [PATCH] feat(adapters): name SDK-embedded hosts instead of mislabeling as Claude Code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Claude adapter watches ~/.claude/projects and hardcoded every session as "Claude Code". But Synara, T3 Code, and other Claude Agent SDK hosts write their journals into that same store, so their sessions showed up as "Claude Code" in the app. Attribute the host truthfully using two fields already present in the journal the daemon reads — no new watchers, no polling, no process inspection, and no host/IDE files touched: - entrypoint: cli -> "Claude Code", claude-desktop -> "Claude Desktop", sdk-* -> an embedded host. - For SDK hosts, name from cwd when it runs under a known host home (~/.synara -> "Synara", ~/.t3 -> "T3 Code", ~/.cursor -> "Cursor"); else fall back to the honest generic "Claude Agent SDK". In-place SDK sessions (cwd is the plain repo) have no on-disk host marker and show "Claude Agent SDK" — truthful, just not host-named. Missing/unknown entrypoint keeps the historical "Claude Code" default. Co-Authored-By: Claude Opus 4.8 --- crates/mb-adapters/src/claude.rs | 95 +++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/crates/mb-adapters/src/claude.rs b/crates/mb-adapters/src/claude.rs index f788f80..810f317 100644 --- a/crates/mb-adapters/src/claude.rs +++ b/crates/mb-adapters/src/claude.rs @@ -21,6 +21,7 @@ use crate::{AdapterEvent, AdapterTx}; struct Fingerprint { state: AgentState, title: String, + app: String, } pub fn spawn_claude_adapter(tx: AdapterTx) { @@ -61,6 +62,7 @@ pub fn spawn_claude_adapter(tx: AdapterTx) { let fp = Fingerprint { state: session.state, title: session.title.clone(), + app: session.app.clone(), }; let mut map = seen_cb.lock().unwrap(); if map.get(&session.id) == Some(&fp) { @@ -80,6 +82,11 @@ fn parse_claude_session(path: &std::path::Path) -> Option { let mut id: Option = None; let mut title: Option = None; let mut state = AgentState::Idle; + // Host discriminators, both already present in the journal we're reading — + // no extra I/O. `entrypoint` separates the CLI/desktop from SDK-embedded + // hosts; `cwd` names the host when it runs under a known host home. + let mut entrypoint: Option = None; + let mut cwd: Option = None; for line in text.lines() { let line = line.trim(); @@ -98,6 +105,13 @@ fn parse_claude_session(path: &std::path::Path) -> Option { id.get_or_insert_with(|| sid.to_string()); } + if let Some(ep) = value.get("entrypoint").and_then(|v| v.as_str()) { + entrypoint.get_or_insert_with(|| ep.to_string()); + } + if let Some(c) = value.get("cwd").and_then(|v| v.as_str()) { + cwd.get_or_insert_with(|| c.to_string()); + } + let msg = value.get("message").cloned().unwrap_or(Value::Null); let role = msg .get("role") @@ -141,13 +155,55 @@ fn parse_claude_session(path: &std::path::Path) -> Option { Some(SessionStatus { id: format!("claude:{id_raw}"), - app: "Claude Code".into(), + app: claude_app_label(entrypoint.as_deref(), cwd.as_deref()), title, state, updated_at_ms, }) } +/// Truthful host label for a Claude-SDK session journal. +/// +/// The `~/.claude/projects` store is shared by the interactive CLI, Claude +/// Desktop, and every app that embeds the Claude Agent SDK (Synara, T3 Code, +/// …), so a hardcoded "Claude Code" mislabels the SDK hosts. `entrypoint` +/// separates the categories; for SDK-embedded sessions we name the host from +/// `cwd` when it runs under a known host home, else fall back to the honest +/// generic label. Everything is derived from fields already in the journal — +/// no process inspection, no extra I/O. +fn claude_app_label(entrypoint: Option<&str>, cwd: Option<&str>) -> String { + match entrypoint { + Some("cli") => "Claude Code".into(), + Some("claude-desktop") => "Claude Desktop".into(), + Some(ep) if ep.starts_with("sdk") => { + host_from_cwd(cwd).unwrap_or("Claude Agent SDK").into() + } + // Missing/unknown entrypoint: keep the historical default. + _ => "Claude Code".into(), + } +} + +/// Map a session `cwd` to the embedding host when it lives under that host's +/// home directory. Purely path-based, so it names the host for worktree +/// sessions without touching the host at all. +fn host_from_cwd(cwd: Option<&str>) -> Option<&'static str> { + let cwd = cwd?; + let home = std::env::var("HOME").ok()?; + // (home-relative dir, display name) — extend as more SDK hosts appear. + const HOSTS: &[(&str, &str)] = &[ + (".synara", "Synara"), + (".t3", "T3 Code"), + (".cursor", "Cursor"), + ]; + for (dir, name) in HOSTS { + let prefix = format!("{home}/{dir}/"); + if cwd.starts_with(&prefix) { + return Some(name); + } + } + None +} + fn extract_text(value: &Value) -> Option { if let Some(s) = value.get("content").and_then(|v| v.as_str()) { return Some(s.to_string()); @@ -223,5 +279,42 @@ mod tests { let session = parse_claude_session(&path).unwrap(); assert_eq!(session.id, "claude:s1"); assert_eq!(session.title, "Wire the menu bar tray icon"); + // No entrypoint in the journal → historical default. + assert_eq!(session.app, "Claude Code"); + } + + #[test] + fn labels_host_from_entrypoint_and_cwd() { + // Interactive CLI and the desktop app are named directly. + assert_eq!(claude_app_label(Some("cli"), None), "Claude Code"); + assert_eq!( + claude_app_label(Some("claude-desktop"), None), + "Claude Desktop" + ); + + // SDK-embedded host with no locating cwd → honest generic label, + // never a false "Claude Code". + assert_eq!( + claude_app_label(Some("sdk-ts"), Some("/Users/me/dev/repo")), + "Claude Agent SDK" + ); + assert_eq!(claude_app_label(Some("sdk-cli"), None), "Claude Agent SDK"); + + // SDK-embedded host running under a known host home → named host. + let home = std::env::var("HOME").unwrap(); + assert_eq!( + claude_app_label( + Some("sdk-ts"), + Some(&format!("{home}/.synara/worktrees/Foo")) + ), + "Synara" + ); + assert_eq!( + claude_app_label(Some("sdk-cli"), Some(&format!("{home}/.t3/worktrees/Bar"))), + "T3 Code" + ); + + // Missing entrypoint stays back-compatible. + assert_eq!(claude_app_label(None, None), "Claude Code"); } }