Skip to content

Commit 8e174e5

Browse files
Add Claude Code host template from live probe (#2653)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 49910c0 commit 8e174e5

12 files changed

Lines changed: 364 additions & 4 deletions

File tree

1.43 KB
Loading

mcpjam-inspector/client/src/components/chat-v2/__tests__/LoadingIndicatorContent.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ describe("LoadingIndicatorContent", () => {
8888
expect(screen.getByTestId("loading-indicator-claude")).toBeInTheDocument();
8989
});
9090

91+
it("renders the CLI spinner (not the Claude mascot) for Claude Code hosts", () => {
92+
render(
93+
<ChatboxHostStyleProvider value="claude-code">
94+
<LoadingIndicatorContent />
95+
</ChatboxHostStyleProvider>,
96+
);
97+
98+
expect(
99+
screen.getByTestId("loading-indicator-claude-code-cli"),
100+
).toBeInTheDocument();
101+
expect(
102+
screen.queryByTestId("loading-indicator-claude"),
103+
).not.toBeInTheDocument();
104+
});
105+
91106
it("renders the GPT pulse for ChatGPT-style chatbox hosts", () => {
92107
render(
93108
<ChatboxHostStyleProvider value="chatgpt">
@@ -144,4 +159,9 @@ describe("inline streaming footer host helpers", () => {
144159
expect(usesMcpjamInlineStreamingFooter("mcpjam")).toBe(true);
145160
expect(usesMcpjamInlineStreamingFooter("claude")).toBe(false);
146161
});
162+
163+
it("excludes Claude Code from the Claude mark footer (CLI agent, own spinner)", () => {
164+
expect(usesClaudeInlineStreamingFooter("claude-code")).toBe(false);
165+
expect(usesMcpjamInlineStreamingFooter("claude-code")).toBe(false);
166+
});
147167
});

mcpjam-inspector/client/src/components/chat-v2/shared/loading-indicator-content.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export function usesClaudeInlineStreamingFooter(
4242
return (
4343
hostStyle != null &&
4444
hostStyle !== "mcpjam" &&
45+
// Claude Code borrows the "claude" visual family for bubble styling but
46+
// is a terminal agent — it shows its own CLI spinner indicator (via the
47+
// generic LoadingIndicatorContent path), not the claude.ai mark painted
48+
// beneath the assistant bubble. Same opt-out shape as "mcpjam".
49+
hostStyle !== "claude-code" &&
4550
getChatboxHostFamily(hostStyle) === "claude"
4651
);
4752
}

mcpjam-inspector/client/src/components/hosts/redesigned/canvas/HostCapabilityMatrix.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { memo, type CSSProperties, type ReactNode } from "react";
22
import { cn } from "@/lib/utils";
33
import claudeLogo from "/claude_logo.png";
4+
import claudeCodeLogo from "/claude_code_logo.png";
45
import openaiLogo from "/openai_logo.png";
56
import cursorLogo from "/cursor_logo.png";
67
import codexLogo from "/codex-logo.svg";
@@ -28,6 +29,8 @@ function getClientLogo(
2829
): string | null {
2930
const haystack = `${clientInfoName ?? ""} ${hostName ?? ""}`.toLowerCase();
3031
if (haystack.includes("mcpjam") || haystack.includes("mcp-jam")) return mcpjamLogo;
32+
if (haystack.includes("claude-code") || haystack.includes("claude code"))
33+
return claudeCodeLogo;
3134
if (haystack.includes("claude")) return claudeLogo;
3235
if (haystack.includes("cursor")) return cursorLogo;
3336
if (haystack.includes("codex")) return codexLogo;

mcpjam-inspector/client/src/components/hosts/redesigned/focus/AppsExtensionTab.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,9 +393,11 @@ function appsToJson(draft: HostConfigInputV2): AppsDoc {
393393
hostCapabilitiesOverride: draft.hostCapabilitiesOverride,
394394
}) as Record<string, unknown>;
395395

396-
if (Object.keys(effectiveCaps).length > 0) {
397-
doc.hostCapabilities = effectiveCaps;
398-
}
396+
// Always emit, even when empty — mirrors `hostContext` above. An empty
397+
// `{}` is a meaningful advertise ("this host offers no app capabilities",
398+
// e.g. the Claude Code CLI template's explicit override), so hiding it
399+
// would make an intentional empty look like an omission.
400+
doc.hostCapabilities = effectiveCaps;
399401

400402
// sandbox — proxy iframe configuration. Maps to `mcpProfile.apps.sandbox`
401403
// in storage and the "Sandbox proxy iframe" card in the matrix. Spec

mcpjam-inspector/client/src/index.css

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,71 @@
683683
}
684684
}
685685

686+
/* Claude Code CLI spinner — the braille "working" cycle (⠋⠙⠹…) ora and
687+
most terminal tools use. Claude Code borrows Claude's chat surface but
688+
is a terminal agent, so its busy state is a monospace spinner, not the
689+
claude.ai mascot. Colored MCPJam orange via `text-primary` on the
690+
element (see the component); the glyph uses `currentColor` so it inherits
691+
that. `::before` content is a discrete (non-interpolated) property, so
692+
each keyframe snaps to its frame; an engine without content animation
693+
simply shows the static base glyph. */
694+
.claude-code-cli-indicator {
695+
display: inline-flex;
696+
align-items: center;
697+
}
698+
699+
.claude-code-cli-indicator__spinner::before {
700+
content: "⠋";
701+
display: inline-block;
702+
width: 1ch;
703+
text-align: center;
704+
animation: claude-code-cli-spinner 1s linear infinite;
705+
}
706+
707+
.claude-code-cli-indicator__label {
708+
margin-left: 0.45em;
709+
}
710+
711+
@keyframes claude-code-cli-spinner {
712+
0% {
713+
content: "⠋";
714+
}
715+
10% {
716+
content: "⠙";
717+
}
718+
20% {
719+
content: "⠹";
720+
}
721+
30% {
722+
content: "⠸";
723+
}
724+
40% {
725+
content: "⠼";
726+
}
727+
50% {
728+
content: "⠴";
729+
}
730+
60% {
731+
content: "⠦";
732+
}
733+
70% {
734+
content: "⠧";
735+
}
736+
80% {
737+
content: "⠇";
738+
}
739+
90% {
740+
content: "⠏";
741+
}
742+
}
743+
744+
@media (prefers-reduced-motion: reduce) {
745+
.claude-code-cli-indicator__spinner::before {
746+
content: "⠿";
747+
animation: none;
748+
}
749+
}
750+
686751
@keyframes progress-indeterminate {
687752
0% {
688753
transform: translateX(-100%);

mcpjam-inspector/client/src/lib/client-styles/__tests__/registry.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
22
import {
33
CHATGPT_HOST_STYLE,
44
CLAUDE_HOST_STYLE,
5+
CLAUDE_CODE_HOST_STYLE,
56
CODEX_HOST_STYLE,
67
COPILOT_HOST_STYLE,
78
DEFAULT_HOST_STYLE,
@@ -23,6 +24,7 @@ describe("host-styles registry", () => {
2324
expect(findHostStyle("chatgpt")).toBe(CHATGPT_HOST_STYLE);
2425
expect(findHostStyle("copilot")).toBe(COPILOT_HOST_STYLE);
2526
expect(findHostStyle("codex")).toBe(CODEX_HOST_STYLE);
27+
expect(findHostStyle("claude-code")).toBe(CLAUDE_CODE_HOST_STYLE);
2628
});
2729

2830
it("returns undefined for unknown ids", () => {
@@ -44,6 +46,7 @@ describe("host-styles registry", () => {
4446
expect(isKnownHostStyleId("chatgpt")).toBe(true);
4547
expect(isKnownHostStyleId("copilot")).toBe(true);
4648
expect(isKnownHostStyleId("codex")).toBe(true);
49+
expect(isKnownHostStyleId("claude-code")).toBe(true);
4750
expect(isKnownHostStyleId("unknown")).toBe(false);
4851
expect(isKnownHostStyleId(42)).toBe(false);
4952
expect(isKnownHostStyleId(null)).toBe(false);
@@ -56,14 +59,16 @@ describe("host-styles registry", () => {
5659
expect(ids).toContain("chatgpt");
5760
expect(ids).toContain("copilot");
5861
expect(ids).toContain("codex");
62+
expect(ids).toContain("claude-code");
5963
// MCPJam ships first so the default-fallback host appears at the top
6064
// of pickers.
6165
expect(ids.indexOf("mcpjam")).toBeLessThan(ids.indexOf("claude"));
6266
expect(ids.indexOf("claude")).toBeLessThan(ids.indexOf("chatgpt"));
6367
// Copilot ships after Cursor (registration order in BUILT_IN_HOST_STYLES).
6468
expect(ids.indexOf("chatgpt")).toBeLessThan(ids.indexOf("copilot"));
65-
// Codex ships last (registered after Copilot in BUILT_IN_HOST_STYLES).
6669
expect(ids.indexOf("copilot")).toBeLessThan(ids.indexOf("codex"));
70+
// Claude Code ships last (registered after Codex in BUILT_IN_HOST_STYLES).
71+
expect(ids.indexOf("codex")).toBeLessThan(ids.indexOf("claude-code"));
6772
});
6873

6974
it("registers custom host styles for project-defined hosts", () => {

mcpjam-inspector/client/src/lib/client-styles/built-ins.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import claudeLogo from "/claude_logo.png";
2+
import claudeCodeLogo from "/claude_code_logo.png";
23
import openaiLogo from "/openai_logo.png";
34
import cursorLogo from "/cursor_logo.png";
45
import copilotLogo from "/copilot_logo.png";
@@ -30,6 +31,7 @@ import {
3031
getMcpJamStyleVariables,
3132
} from "@/config/mcpjam-client-context";
3233
import { ClaudeMarkIndicator } from "./indicators/claude-mark";
34+
import { ClaudeCodeCliIndicator } from "./indicators/claude-code-cli";
3335
import { ChatGptDotIndicator } from "./indicators/chatgpt-dot";
3436
import { CursorShineIndicator } from "./indicators/cursor-shine";
3537
import { CopilotPulseIndicator } from "./indicators/copilot-pulse";
@@ -250,6 +252,36 @@ export const CLAUDE_HOST_STYLE: HostStyleDefinition = {
250252
},
251253
};
252254

255+
// Claude Code is a terminal agent with no chat chrome of its own, so it
256+
// borrows Claude's desktop chat surface wholesale (style variables, fonts,
257+
// background, MCP profile) and only differs in brand identity: its own
258+
// label, logo, and a CLI spinner busy-state instead of the claude.ai
259+
// mascot. Mirrors how CODEX_HOST_STYLE borrows ChatGPT's surface.
260+
//
261+
// Capabilities reuse Claude's preset here, but the "claude-code" template
262+
// (`client-templates.ts`) overrides hostCapabilities to `{}` since the CLI
263+
// renders no MCP Apps — the style preset is just the fallback if a host
264+
// ever clears that override.
265+
export const CLAUDE_CODE_HOST_STYLE: HostStyleDefinition = {
266+
id: "claude-code",
267+
mcp: {
268+
protocolOverride: UIType.MCP_APPS,
269+
platform: CLAUDE_DESKTOP_PLATFORM,
270+
fontCss: CLAUDE_DESKTOP_FONT_CSS,
271+
mcpAppsCapabilities: MCP_APPS_FULL_SURFACE,
272+
resolveStyleVariables: getClaudeDesktopStyleVariables,
273+
},
274+
chatUi: {
275+
label: "Claude Code",
276+
shortLabel: "Claude Code-style host",
277+
pickerDescription: "Anthropic Claude Code CLI chrome",
278+
logoSrc: claudeCodeLogo,
279+
family: "claude",
280+
resolveChatBackground: (theme) => CLAUDE_DESKTOP_CHAT_BACKGROUND[theme],
281+
loadingIndicator: ClaudeCodeCliIndicator,
282+
},
283+
};
284+
253285
export const CHATGPT_HOST_STYLE: HostStyleDefinition = {
254286
id: "chatgpt",
255287
mcp: {
@@ -469,4 +501,5 @@ export const BUILT_IN_HOST_STYLES: readonly HostStyleDefinition[] = [
469501
CURSOR_HOST_STYLE,
470502
COPILOT_HOST_STYLE,
471503
CODEX_HOST_STYLE,
504+
CLAUDE_CODE_HOST_STYLE,
472505
];

mcpjam-inspector/client/src/lib/client-styles/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type {
1616
export {
1717
CHATGPT_HOST_STYLE,
1818
CLAUDE_HOST_STYLE,
19+
CLAUDE_CODE_HOST_STYLE,
1920
CODEX_HOST_STYLE,
2021
COPILOT_HOST_STYLE,
2122
CURSOR_HOST_STYLE,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { render } from "@testing-library/react";
2+
import { describe, expect, it } from "vitest";
3+
import { ClaudeCodeCliIndicator } from "../claude-code-cli";
4+
5+
describe("ClaudeCodeCliIndicator", () => {
6+
it("renders the CLI spinner node with the 'Thinking' label", () => {
7+
const { getByTestId } = render(<ClaudeCodeCliIndicator />);
8+
const node = getByTestId("loading-indicator-claude-code-cli");
9+
expect(node).toBeTruthy();
10+
expect(node.textContent).toContain("Thinking");
11+
});
12+
13+
it("binds the braille-spinner keyframe via class", () => {
14+
// Spinner frames resolve from `@keyframes claude-code-cli-spinner` in
15+
// `index.css`, driven off `.claude-code-cli-indicator__spinner::before`.
16+
// Assert the class wiring so a refactor can't silently drop it.
17+
const { container } = render(<ClaudeCodeCliIndicator />);
18+
expect(
19+
container.querySelector(".claude-code-cli-indicator__spinner"),
20+
).not.toBeNull();
21+
});
22+
23+
it("forwards className to the outer wrapper", () => {
24+
const { container } = render(
25+
<ClaudeCodeCliIndicator className="custom-test-class" />,
26+
);
27+
const wrapper = container.firstChild as HTMLElement;
28+
expect(wrapper.classList.contains("custom-test-class")).toBe(true);
29+
});
30+
31+
it("declares an aria-live region so the thinking state is announced", () => {
32+
const { container } = render(<ClaudeCodeCliIndicator />);
33+
const live = container.querySelector("[aria-live='polite']");
34+
expect(live).not.toBeNull();
35+
// The spinner glyph is aria-hidden, so the sr-only "Thinking" carries
36+
// the state for assistive tech.
37+
expect(container.querySelector(".sr-only")?.textContent).toBe("Thinking");
38+
});
39+
});

0 commit comments

Comments
 (0)