From 8aa3294f06b889491a86ada801d3c69d9ca8e82e Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Wed, 22 Apr 2026 19:02:31 +0200 Subject: [PATCH] fix(aibridge): track Charm Crush client and session ID (#24630) *Disclaimer: implemented by a Coder Agent using Claude Opus 4.6* Porting https://github.com/coder/aibridge/pull/277 to coder/coder after the [aibridge code move](https://github.com/coder/coder/pull/24190). ## Summary Fixes client detection and session ID tracking for the [Charm Crush](https://github.com/charmbracelet/crush) AI coding client. ## Changes ### Bug fix: User-Agent matching The actual Crush user-agent is `Charm-Crush/{version} (https://charm.land/crush)` (hyphenated), but `GuessClient` only checked for `charm crush/` (space-separated). After lowercasing, `Charm-Crush/0.2.0` becomes `charm-crush/0.2.0`, which did not match the `charm crush/` prefix. Now matches both formats for backwards compatibility. ### Session ID tracking Adds an explicit `ClientCrush` case to `GuessSessionID`. Crush does not currently send a session ID header to upstream AI providers, so this returns `nil` (consistent with how `ClientZed`, `ClientRoo`, and `ClientCursor` are handled). ### Tests - Added `charm_crush_hyphen` test case for `GuessClient` using the real user-agent format. - Added `crush_returns_empty` test case for `GuessSessionID`. --- aibridge/client.go | 2 +- aibridge/client_test.go | 7 ++++++- aibridge/session.go | 2 ++ aibridge/session_test.go | 5 +++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/aibridge/client.go b/aibridge/client.go index 3e9e277bec..68caffdd30 100644 --- a/aibridge/client.go +++ b/aibridge/client.go @@ -51,7 +51,7 @@ func GuessClient(r *http.Request) Client { return ClientRoo case strings.HasPrefix(userAgent, "coder-agents/"): return ClientCoderAgents - case strings.HasPrefix(userAgent, "charm crush/"): + case strings.HasPrefix(userAgent, "charm crush/") || strings.HasPrefix(userAgent, "charm-crush/"): return ClientCrush case r.Header.Get("x-cursor-client-version") != "": return ClientCursor diff --git a/aibridge/client_test.go b/aibridge/client_test.go index e3fa82866e..985c254a26 100644 --- a/aibridge/client_test.go +++ b/aibridge/client_test.go @@ -79,10 +79,15 @@ func TestGuessClient(t *testing.T) { wantClient: aibridge.ClientCoderAgents, }, { - name: "charm_crush", + name: "charm_crush_space", userAgent: "Charm Crush/0.1.11", wantClient: aibridge.ClientCrush, }, + { + name: "charm_crush_hyphen", + userAgent: "Charm-Crush/0.2.0 (https://charm.land/crush)", + wantClient: aibridge.ClientCrush, + }, { name: "cursor_x_cursor_client_version", userAgent: "connect-es/1.6.1", diff --git a/aibridge/session.go b/aibridge/session.go index 34c45d2158..a97fdaef2a 100644 --- a/aibridge/session.go +++ b/aibridge/session.go @@ -75,6 +75,8 @@ func GuessSessionID(client Client, r *http.Request) *string { return cleanRef(r.Header.Get("X-KILOCODE-TASKID")) case ClientCoderAgents: return cleanRef(r.Header.Get("X-Coder-Chat-Id")) + case ClientCrush: + return nil // Crush does not send a session ID header. case ClientRoo: return nil // RooCode doesn't send a session ID. case ClientCursor: diff --git a/aibridge/session_test.go b/aibridge/session_test.go index 7592dc5c54..90b27ce705 100644 --- a/aibridge/session_test.go +++ b/aibridge/session_test.go @@ -182,6 +182,11 @@ func TestGuessSessionID(t *testing.T) { name: "coder_agents_without_chat_id", client: aibridge.ClientCoderAgents, }, + // Crush. + { + name: "crush_returns_empty", + client: aibridge.ClientCrush, + }, // Roo. { name: "roo_returns_empty",