Skip to content

feat(chat): add Claude Code as local chat provider#2668

Open
vibegui wants to merge 13 commits intomainfrom
vibegui/claude-code-chat
Open

feat(chat): add Claude Code as local chat provider#2668
vibegui wants to merge 13 commits intomainfrom
vibegui/claude-code-chat

Conversation

@vibegui
Copy link
Contributor

@vibegui vibegui commented Mar 11, 2026

CleanShot 2026-03-12 at 19 19 20 CleanShot 2026-03-12 at 19 20 09

Summary

  • When running locally with Claude Code installed, detects it via Bun.which("claude") and offers it as a model option in the chat UI
  • Routes messages through the Claude Agent SDK subprocess instead of OpenRouter — uses local Claude auth (no API key needed)
  • Adds "Claude Code" entry with "Local" badge at the top of the model selector (opt-in, not auto-selected)
  • Handles edge cases: no OpenRouter connection, empty model list, abort/cancellation

Changes

  • apps/mesh/src/api/routes/auth.tsclaudeCodeAvailable in AuthConfig
  • apps/mesh/src/api/routes/decopilot/claude-code-provider.ts — New SDK adapter (stream_event + assistant message handling)
  • apps/mesh/src/api/routes/decopilot/routes.ts — Fork stream handler for claude-code sentinel
  • apps/mesh/src/api/routes/decopilot/schemas.ts — Accept claude-code provider
  • apps/mesh/src/web/components/chat/select-model.tsx — Claude Code UI entry + guards
  • apps/mesh/src/web/components/chat/context.tsx — Handle claude-code connectionId in model state

Test plan

  • Start dev server locally with Claude Code installed (which claude)
  • Open chat UI — model selector should show "Claude Code" with "Local" badge
  • Select Claude Code and send a message — should stream response
  • Open model selector dialog — should not error when no OpenRouter connection exists
  • Test switching between Claude Code and OpenRouter models
  • Test cancellation mid-stream

🤖 Generated with Claude Code


Summary by cubic

Adds Claude Code as a local chat provider (Opus/Sonnet/Haiku) streamed via @anthropic-ai/claude-agent-sdk, plus a Connect Studio flow with per‑connection cards for one‑click setup of Mesh tools and GitHub MCP. Auto‑wires the MCP endpoint with a short‑lived API key during Claude Code chats and skips org model permissions for this local provider.

  • New Features

    • Local provider: static claude-code:* models in the factory/registry; server routes by models.thinking.provider === "claude-code"; /config exposes claudeCodeAvailable.
    • Streaming via @anthropic-ai/claude-agent-sdk with cancel, start/finish-step markers, real‑time thinking/text deltas (includePartialMessages), assistant text fallback de‑dupe, and usage/cost passthrough.
    • Connect Studio: status/POST/DELETE endpoints; auto‑registers deco-studio MCP; modal with per‑connection cards (Claude Code and GitHub via local gh token), auth info, connect/disconnect, and a sidebar entry.
  • Bug Fixes

    • Unified model/provider flow; fixed model selector init and tiering for synthetic claude-code models; handled empty connections and replaced connectionId sentinel with provider detection.
    • Removed Cursor; added CLI timeouts; small UI fixes; logos/types include claude-code; org settings now support one‑click Claude Code with better loading states.
    • Fixed GitHub status detection in Connect Studio (gh auth status + gh api user) and compacted cards; rewrote self‑connection MCP URL to localhost for reliable loopback.

Written for commit 8366d56. Summary will update on new commits.

@github-actions
Copy link
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 11, 2026

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 2.168.1-alpha.1
🎉 Patch 2.168.1
❤️ Minor 2.169.0
🚀 Major 3.0.0

Current version: 2.168.0

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/api/routes/decopilot/routes.ts">

<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:297">
P2: Validate Claude Code availability before starting the run/saving messages; currently an unavailable provider still creates a failed run and persists the user message.</violation>
</file>

<file name="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts">

<violation number="1" location="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts:33">
P1: Multi-turn conversation context is lost. `messagesToPrompt` strips the `role` from all messages and concatenates their text, so prior assistant replies are indistinguishable from user messages. Add role prefixes (e.g., `User:` / `Assistant:`) or only pass the last user message as the prompt.</violation>

<violation number="2" location="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts:215">
P1: Text will be duplicated in the output. The `stream_event` handler already writes incremental text deltas, but the `assistant` handler then re-emits the full completed text from `message.message.content`. Remove the text extraction from the `assistant` case since it's already delivered via stream events, or gate it so it only fires when no stream events were received.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

/**
* Convert chat messages to a prompt string for the Claude Agent SDK.
*/
function messagesToPrompt(messages: ChatMessage[]): string {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Multi-turn conversation context is lost. messagesToPrompt strips the role from all messages and concatenates their text, so prior assistant replies are indistinguishable from user messages. Add role prefixes (e.g., User: / Assistant:) or only pass the last user message as the prompt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/claude-code-provider.ts, line 33:

<comment>Multi-turn conversation context is lost. `messagesToPrompt` strips the `role` from all messages and concatenates their text, so prior assistant replies are indistinguishable from user messages. Add role prefixes (e.g., `User:` / `Assistant:`) or only pass the last user message as the prompt.</comment>

<file context>
@@ -0,0 +1,277 @@
+/**
+ * Convert chat messages to a prompt string for the Claude Agent SDK.
+ */
+function messagesToPrompt(messages: ChatMessage[]): string {
+  const parts: string[] = [];
+
</file context>
Fix with Cubic

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/api/routes/decopilot/routes.ts">

<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:315">
P2: Guard the Claude Code sentinel model id before forwarding it to the SDK; otherwise restored sessions can pass `"claude-code"` as a concrete model value.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@vibegui vibegui force-pushed the vibegui/claude-code-chat branch from 9b795cb to 8002f8b Compare March 12, 2026 19:36
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/web/components/chat/select-model.tsx">

<violation number="1" location="apps/mesh/src/web/components/chat/select-model.tsx:1070">
P1: Selecting a Claude Code model never tags the request with `connectionId: "claude-code"`, so the backend will not take the local Claude Code streaming path.</violation>
</file>

<file name="apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx">

<violation number="1" location="apps/mesh/src/web/components/chat/no-llm-binding-empty-state.tsx:57">
P2: Also guard the in-flight state here so repeated clicks do not trigger duplicate connect requests.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/api/routes/decopilot/routes.ts">

<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:158">
P1: Don't trust `models.thinking.provider` to decide whether to skip model-permission checks.</violation>

<violation number="2" location="apps/mesh/src/api/routes/decopilot/routes.ts:321">
P2: This passes `credentialId` into a `connectionId` field, so Claude Code responses emit the wrong metadata shape for follow-up requests.</violation>
</file>

<file name="apps/mesh/src/web/components/settings-modal/pages/org-ai-providers.tsx">

<violation number="1" location="apps/mesh/src/web/components/settings-modal/pages/org-ai-providers.tsx:334">
P2: Gate the Claude Code connect path on actual CLI availability; right now it can create a unusable "connected" provider on servers where Claude Code is unavailable.</violation>
</file>

<file name="apps/mesh/src/ai-providers/factory.ts">

<violation number="1" location="apps/mesh/src/ai-providers/factory.ts:8">
P2: Keep the Claude Code model variants in one shared source of truth; this duplicate list can drift from the runtime SDK mapping.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

import { PROVIDERS } from "./registry";

/** Static model list for the Claude Code local provider. */
const CLAUDE_CODE_MODEL_LIST: ModelInfo[] = [
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Keep the Claude Code model variants in one shared source of truth; this duplicate list can drift from the runtime SDK mapping.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/ai-providers/factory.ts, line 8:

<comment>Keep the Claude Code model variants in one shared source of truth; this duplicate list can drift from the runtime SDK mapping.</comment>

<file context>
@@ -4,6 +4,40 @@ import type { ModelListCache } from "./model-list-cache";
 import { PROVIDERS } from "./registry";
 
+/** Static model list for the Claude Code local provider. */
+const CLAUDE_CODE_MODEL_LIST: ModelInfo[] = [
+  {
+    providerId: "claude-code",
</file context>
Fix with Cubic

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/api/routes/decopilot/routes.ts">

<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:875">
P2: Use `getUserId(ctx)` here instead of reading `ctx.auth.user` directly; otherwise API-key-authenticated requests are rejected by the new Connect Studio endpoints.

(Based on your team's feedback about using `getUserId` for mixed user/API-key auth.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/mesh/src/web/components/connect-studio-modal.tsx">

<violation number="1" location="apps/mesh/src/web/components/connect-studio-modal.tsx:83">
P1: This disconnect flow reports success without revoking the generated full-access API key, so Claude Code access persists after “Disconnect”.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


app.delete("/:org/decopilot/connect-studio", async (c) => {
const ctx = c.get("meshContext");
if (!ctx.auth?.user?.id) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Use getUserId(ctx) here instead of reading ctx.auth.user directly; otherwise API-key-authenticated requests are rejected by the new Connect Studio endpoints.

(Based on your team's feedback about using getUserId for mixed user/API-key auth.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/routes.ts, line 875:

<comment>Use `getUserId(ctx)` here instead of reading `ctx.auth.user` directly; otherwise API-key-authenticated requests are rejected by the new Connect Studio endpoints.

(Based on your team's feedback about using `getUserId` for mixed user/API-key auth.) </comment>

<file context>
@@ -799,61 +830,80 @@ export function createDecopilotRoutes(deps: DecopilotDeps) {
+
+  app.delete("/:org/decopilot/connect-studio", async (c) => {
+    const ctx = c.get("meshContext");
+    if (!ctx.auth?.user?.id) {
+      throw new HTTPException(401, { message: "Authentication required" });
+    }
</file context>
Fix with Cubic

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts">

<violation number="1" location="apps/mesh/src/api/routes/decopilot/claude-code-provider.ts:372">
P2: This replays the full `thinking` content even after `thinking_delta` events were already streamed, so Claude Code reasoning can show up duplicated in the chat UI.</violation>
</file>

<file name="apps/mesh/src/api/routes/decopilot/routes.ts">

<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:794">
P2: `gh auth status --json` is missing the required field list, so GitHub status detection will always fall back to disconnected.</violation>

<violation number="2" location="apps/mesh/src/api/routes/decopilot/routes.ts:843">
P1: This creates a permanent wildcard API key but the disconnect/failure paths never revoke it, leaving active internal credentials behind.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


if (target === "claude-code") {
// Create API key for the MCP endpoint
const apiKey = await ctx.boundAuth.apiKey.create({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This creates a permanent wildcard API key but the disconnect/failure paths never revoke it, leaving active internal credentials behind.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/routes/decopilot/routes.ts, line 843:

<comment>This creates a permanent wildcard API key but the disconnect/failure paths never revoke it, leaving active internal credentials behind.</comment>

<file context>
@@ -811,63 +835,78 @@ export function createDecopilotRoutes(deps: DecopilotDeps) {
+
+    if (target === "claude-code") {
+      // Create API key for the MCP endpoint
+      const apiKey = await ctx.boundAuth.apiKey.create({
+        name: "studio-connect-claude-code",
+        permissions: { "*": ["*"] },
</file context>
Fix with Cubic

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mesh/src/api/routes/decopilot/routes.ts">

<violation number="1" location="apps/mesh/src/api/routes/decopilot/routes.ts:796">
P2: Don't return early on missing `gh` auth here; it hides an already-registered GitHub MCP and flips the modal back to `Connect`.

(Based on your team's feedback about checking MCP authentication state first.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

vibegui and others added 13 commits March 14, 2026 07:56
When running locally with Claude Code installed, offer it as a model
option in the chat UI. Routes messages through the Claude Agent SDK
subprocess instead of OpenRouter, using the user's local Claude auth.

- Add `claudeCodeAvailable` to AuthConfig (detected via Bun.which)
- New claude-code-provider adapter wrapping @anthropic-ai/claude-agent-sdk
- Fork decopilot stream endpoint for claude-code connectionId sentinel
- Add Claude Code entry in model selector with "Local" badge
- Handle empty connections gracefully when only Claude Code is available

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract text from assistant message content blocks (SDK emits full
  messages, not stream_event deltas)
- Add start-step/finish-step markers for proper AI SDK status tracking
- Add Opus/Sonnet/Haiku model variants in the selector
- Pass selected model to SDK query options
- Fix selectedConnectionId initialization to skip claude-code sentinel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Connect Studio modal with one-click Claude Code / Cursor setup
- Server endpoint generates API key and runs `claude mcp add-json` or writes Cursor config
- Status endpoint checks if IDE is already connected
- Auto-wire MCP endpoint when using Claude Code as local chat provider
- Support mcpHeaders in Claude Agent SDK http transport

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…boarding

- Fix missing closing brace in model selector JSX
- Fix AiProviderModel type mismatches for Claude Code synthetic models
- Remove stale useAllowedModels import
- Add Claude Code card with "Local" badge to the AI provider onboarding screen
- Card shows connection status and one-click connect via CLI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove custom Claude Code UI from model selector and onboarding.
Claude Code now uses the same ConnectionModelList → groupByTier →
ModelTierSection pipeline as every other provider. Models are returned
from factory.ts via a static list, classified into tiers via prefixes,
and server detection uses thinking.provider instead of broken
connectionId field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove Cursor support (not working), show Claude Code auth info
(email, org, subscription) when connected, add disconnect button,
and fix refresh icon positioning. Add timeouts to all CLI spawns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Handle thinking_delta events from the Claude Agent SDK to stream
reasoning content in real-time. Track content block types to route
thinking vs text deltas correctly. Prevent duplicate text from
assistant message fallback when stream events are available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactor Connect Studio modal to show each connection as a self-contained
card with its own connect/disconnect buttons. Add GitHub connection that
uses the local `gh` CLI token to register GitHub's MCP in Claude Code.

Also enable `includePartialMessages` in the Claude Agent SDK to get
real-time thinking/text streaming events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GitHub status now uses `gh auth status` (plain) + `gh api user --jq .login`
instead of `gh auth status --json` which requires explicit field names.
Simplify ConnectionCard to single-row layout with inline toggle button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The self MCP connection URL is persisted in the DB using BASE_URL
(e.g. las-vegas.localhost) which is unresolvable from Node.js/Bun
runtime. Parse the stored URL and replace hostname/port/protocol
with getInternalUrl() (localhost:PORT) so loopback calls always succeed,
regardless of what origin is stored in the database.

Also fix dev-worktree.ts to not set BASE_URL in local mode,
allowing the server to default to localhost:PORT.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vibegui vibegui force-pushed the vibegui/claude-code-chat branch from fd36ce0 to 8366d56 Compare March 14, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant