Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/albums-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"embla-carousel-wheel-gestures": "^8.1.0",
"sunpeak": "^0.18.2",
"sunpeak": "^0.18.4",
"tailwind-merge": "^3.5.0",
"zod": "^4.3.6"
},
Expand Down
34 changes: 19 additions & 15 deletions examples/albums-example/tests/e2e/albums.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,21 +222,25 @@ for (const host of hosts) {
});
});

test('should render content after switching from inline to pip', async ({ page }) => {
// Start in inline mode
await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host }));

const iframe = page.frameLocator('iframe').frameLocator('iframe');
await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible();

// Switch to PiP via sidebar
await page.locator('button:has-text("PiP")').click();

// Content should still be visible after the mode transition
await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible({
timeout: 5000,
});
});
// Claude doesn't support PiP — only run this test for hosts that have the button.
(host === 'claude' ? test.skip : test)(
'should render content after switching from inline to pip',
async ({ page }) => {
// Start in inline mode
await page.goto(createInspectorUrl({ simulation: 'show-albums', theme: 'dark', host }));

const iframe = page.frameLocator('iframe').frameLocator('iframe');
await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible();

// Switch to PiP via sidebar
await page.locator('button:has-text("PiP")').click();

// Content should still be visible after the mode transition
await expect(iframe.locator('button:has-text("Summer Slice")')).toBeVisible({
timeout: 5000,
});
}
);
});
});
}
10 changes: 10 additions & 0 deletions examples/albums-example/tests/live/albums.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ test('albums tool renders photo grid with correct styles', async ({ live }) => {
});
expect(containerStyles.overflow).toBe('hidden');

// Background: the app's root should have a resolved background color
// (from --color-background-primary or the CSS Canvas system color),
// not transparent. A transparent root would show the host container
// rather than the app's own styled background.
const rootBg = await app
.locator(':root')
.evaluate((el) => window.getComputedStyle(el).backgroundColor);
expect(rootBg).not.toBe('rgba(0, 0, 0, 0)');
expect(rootBg).toMatch(/^rgb/);

// Theme: text color has appropriate luminance in light mode
const textColor = await albumCard.evaluate((el) => window.getComputedStyle(el).color);
assertTextContrast(textColor);
Expand Down
2 changes: 1 addition & 1 deletion examples/carousel-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"embla-carousel-wheel-gestures": "^8.1.0",
"sunpeak": "^0.18.2",
"sunpeak": "^0.18.4",
"tailwind-merge": "^3.5.0",
"zod": "^4.3.6"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/map-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"mapbox-gl": "^3.20.0",
"sunpeak": "^0.18.2",
"sunpeak": "^0.18.4",
"tailwind-merge": "^3.5.0",
"zod": "^4.3.6"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/review-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"dependencies": {
"clsx": "^2.1.1",
"sunpeak": "^0.18.2",
"sunpeak": "^0.18.4",
"tailwind-merge": "^3.5.0",
"zod": "^4.3.6"
},
Expand Down
12 changes: 11 additions & 1 deletion packages/sunpeak/bin/lib/sandbox-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function generateProxyHtml(theme, platform) {
<head>
<meta name="color-scheme" content="${colorScheme}" />
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; }
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: transparent; }
iframe { border: none; width: 100%; height: 100%; display: block; }
</style>
</head>
Expand Down Expand Up @@ -168,6 +168,16 @@ iframe { border: none; width: 100%; height: 100%; display: block; }
return;
}

// Sync color-scheme on the inner iframe element when theme changes.
// This ensures prefers-color-scheme resolves correctly inside the app.
// Important: do NOT set color-scheme on the proxy's own document —
// changing it from the initial 'dark' causes Chrome to re-evaluate
// the CSS Canvas as opaque white, blocking the host's conversation
// background from showing through the transparent proxy.
if (data.method === 'ui/notifications/host-context-changed' && data.params && data.params.theme) {
if (innerFrame) innerFrame.style.colorScheme = data.params.theme;
}

// Forward all other messages to the inner iframe
if (innerWindow) {
try { innerWindow.postMessage(data, '*'); } catch(e) { /* detached */ }
Expand Down
2 changes: 1 addition & 1 deletion packages/sunpeak/src/claude/claude-conversation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('ClaudeConversation', () => {

expect(screen.getByTestId('fullscreen-content')).toBeInTheDocument();
expect(container.querySelector('footer')).toBeInTheDocument();
expect(screen.getAllByPlaceholderText('Reply to sunpeak...').length).toBeGreaterThan(0);
expect(screen.getAllByText('Reply to sunpeak...').length).toBeGreaterThan(0);
expect(screen.getByLabelText('Back')).toBeInTheDocument();
});

Expand Down
57 changes: 37 additions & 20 deletions packages/sunpeak/src/claude/claude-conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export function ClaudeConversation({
transform: 'translate(0)',
backgroundColor: 'var(--sim-bg-conversation, var(--color-background-primary))',
color: 'var(--color-text-primary)',
fontFamily: 'var(--font-sans)',
WebkitFontSmoothing: 'antialiased',
}}
>
{/* ─── Fullscreen chrome overlay ─── */}
Expand Down Expand Up @@ -139,18 +141,24 @@ export function ClaudeConversation({
}}
>
<div className="max-w-[48rem] mx-auto">
<input
type="text"
name="userInput"
disabled
placeholder="Reply to sunpeak..."
className="w-full rounded-xl px-4 py-2.5 text-sm"
<div
className="relative rounded-[20px] px-4 py-2.5"
style={{
backgroundColor: 'var(--sim-bg-reply-input, var(--color-background-secondary))',
border: '1px solid var(--color-border-primary)',
color: 'var(--color-text-primary)',
boxShadow:
'0 4px 20px rgba(0, 0, 0, 0.035), 0 0 0 0.5px var(--color-border-tertiary)',
}}
/>
>
<div
className="w-full text-base outline-none opacity-50"
style={{
lineHeight: '1.4',
color: 'var(--color-text-tertiary)',
}}
>
Reply to sunpeak...
</div>
</div>
</div>
</footer>
</div>
Expand Down Expand Up @@ -182,8 +190,12 @@ export function ClaudeConversation({
<div className="px-4 py-4">
<div className="max-w-[48rem] mx-auto flex justify-end">
<div
className="inline-block rounded-2xl px-4 py-2.5 text-sm max-w-[85%]"
className="inline-flex rounded-xl max-w-[85%] break-words"
style={{
padding: '10px 16px',
lineHeight: '22.4px',
fontSize: '16px',
fontWeight: 430,
backgroundColor:
'var(--sim-bg-user-bubble, var(--color-background-tertiary))',
}}
Expand Down Expand Up @@ -292,18 +304,23 @@ export function ClaudeConversation({
}}
>
<div className="max-w-[48rem] mx-auto px-4 py-4">
<div className="relative">
<input
type="text"
name="userInput"
disabled
placeholder="Reply to sunpeak..."
className="w-full rounded-xl px-4 py-2.5 text-sm"
<div
className="relative rounded-[20px] px-4 py-2.5"
style={{
backgroundColor: 'var(--sim-bg-reply-input, var(--color-background-secondary))',
boxShadow:
'0 4px 20px rgba(0, 0, 0, 0.035), 0 0 0 0.5px var(--color-border-tertiary)',
}}
>
<div
className="w-full text-base outline-none opacity-50"
style={{
backgroundColor: 'var(--sim-bg-reply-input, var(--color-background-secondary))',
border: '1px solid var(--color-border-primary)',
lineHeight: '1.4',
color: 'var(--color-text-tertiary)',
}}
/>
>
Reply to sunpeak...
</div>
</div>
</div>
</footer>
Expand Down
141 changes: 117 additions & 24 deletions packages/sunpeak/src/claude/claude-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,30 @@ import { registerHostShell } from '../inspector/hosts';
import { DEFAULT_STYLE_VARIABLES } from '../inspector/host-styles';
import { ClaudeConversation } from './claude-conversation';

/**
* Claude host version info — matches what Claude reports via the MCP protocol.
* Verified against production Claude on 2026-03-25.
*/
const CLAUDE_HOST_INFO = {
name: 'Claude',
version: '1.0.0',
};

/**
* Claude host capabilities — matches what Claude reports via the MCP protocol.
* Verified against production Claude on 2026-03-25.
*
* Notable: Claude supports downloadFile, updateModelContext.image, and
* message.text. serverTools and serverResources both report listChanged.
* No sandbox.permissions (no microphone etc.). No PiP display mode.
*/
const CLAUDE_HOST_CAPABILITIES: McpUiHostCapabilities = {
openLinks: {},
serverTools: {},
serverResources: {},
downloadFile: {},
serverTools: { listChanged: true },
serverResources: { listChanged: true },
logging: {},
updateModelContext: { text: {} },
updateModelContext: { text: {}, image: {} },
message: { text: {} },
sandbox: {},
};
Expand All @@ -29,25 +41,90 @@ function applyClaudeTheme(theme: 'light' | 'dark'): void {
}

/**
* Claude-specific style variable overrides (warm beige/cream palette).
* Inherits defaults from DEFAULT_STYLE_VARIABLES, overriding only colors.
* Claude style variable overrides — warm beige/cream palette with Anthropic Sans.
* Verified against production Claude on 2026-03-25.
*
* Only overrides values that differ from DEFAULT_STYLE_VARIABLES.
* Claude sends all variables via styles.variables using light-dark(rgba()) format.
*/
const CLAUDE_STYLE_VARIABLES = {
...DEFAULT_STYLE_VARIABLES,
// Background colors — warm beige/cream palette
'--color-background-primary': 'light-dark(#faf9f5, #262624)',
'--color-background-secondary': 'light-dark(#ffffff, #3a3935)',
'--color-background-tertiary': 'light-dark(#e8e4dc, #4a4843)',
'--color-background-inverse': 'light-dark(#2b2a27, #f3f0e8)',
// Text colors
'--color-text-primary': 'light-dark(#2d2b27, #e8e4dc)',
'--color-text-secondary': 'light-dark(#6b6560, #9b9690)',
'--color-text-tertiary': 'light-dark(#9b9690, #6b6560)',
'--color-text-inverse': 'light-dark(#e8e4dc, #2d2b27)',
// Border colors
'--color-border-primary': 'light-dark(#e0ddd5, #4a4843)',
'--color-border-secondary': 'light-dark(#d5d1c8, #5a5753)',
'--color-border-tertiary': 'light-dark(#f0ede5, #3a3935)',

// ── Background colors ──
'--color-background-primary': 'light-dark(rgba(255, 255, 255, 1), rgba(48, 48, 46, 1))',
'--color-background-secondary': 'light-dark(rgba(245, 244, 237, 1), rgba(38, 38, 36, 1))',
'--color-background-tertiary': 'light-dark(rgba(250, 249, 245, 1), rgba(20, 20, 19, 1))',
'--color-background-inverse': 'light-dark(rgba(20, 20, 19, 1), rgba(250, 249, 245, 1))',
'--color-background-ghost': 'light-dark(rgba(255, 255, 255, 0), rgba(48, 48, 46, 0))',
'--color-background-info': 'light-dark(rgba(214, 228, 246, 1), rgba(37, 62, 95, 1))',
'--color-background-danger': 'light-dark(rgba(247, 236, 236, 1), rgba(96, 42, 40, 1))',
'--color-background-success': 'light-dark(rgba(233, 241, 220, 1), rgba(27, 70, 20, 1))',
'--color-background-warning': 'light-dark(rgba(246, 238, 223, 1), rgba(72, 58, 15, 1))',
'--color-background-disabled': 'light-dark(rgba(255, 255, 255, 0.5), rgba(48, 48, 46, 0.5))',

// ── Text colors ──
'--color-text-primary': 'light-dark(rgba(20, 20, 19, 1), rgba(250, 249, 245, 1))',
'--color-text-secondary': 'light-dark(rgba(61, 61, 58, 1), rgba(194, 192, 182, 1))',
'--color-text-tertiary': 'light-dark(rgba(115, 114, 108, 1), rgba(156, 154, 146, 1))',
'--color-text-inverse': 'light-dark(rgba(255, 255, 255, 1), rgba(20, 20, 19, 1))',
'--color-text-ghost': 'light-dark(rgba(115, 114, 108, 0.5), rgba(156, 154, 146, 0.5))',
'--color-text-info': 'light-dark(rgba(50, 102, 173, 1), rgba(128, 170, 221, 1))',
'--color-text-danger': 'light-dark(rgba(127, 44, 40, 1), rgba(238, 136, 132, 1))',
'--color-text-success': 'light-dark(rgba(38, 91, 25, 1), rgba(122, 185, 72, 1))',
'--color-text-warning': 'light-dark(rgba(90, 72, 21, 1), rgba(209, 160, 65, 1))',
'--color-text-disabled': 'light-dark(rgba(20, 20, 19, 0.5), rgba(250, 249, 245, 0.5))',

// ── Border colors ──
'--color-border-primary': 'light-dark(rgba(31, 30, 29, 0.4), rgba(222, 220, 209, 0.4))',
'--color-border-secondary': 'light-dark(rgba(31, 30, 29, 0.3), rgba(222, 220, 209, 0.3))',
'--color-border-tertiary': 'light-dark(rgba(31, 30, 29, 0.15), rgba(222, 220, 209, 0.15))',
'--color-border-inverse': 'light-dark(rgba(255, 255, 255, 0.3), rgba(20, 20, 19, 0.15))',
'--color-border-ghost': 'light-dark(rgba(31, 30, 29, 0), rgba(222, 220, 209, 0))',
'--color-border-info': 'light-dark(rgba(70, 130, 213, 1), rgba(70, 130, 213, 1))',
'--color-border-danger': 'light-dark(rgba(167, 61, 57, 1), rgba(205, 92, 88, 1))',
'--color-border-success': 'light-dark(rgba(67, 116, 38, 1), rgba(89, 145, 48, 1))',
'--color-border-warning': 'light-dark(rgba(128, 92, 31, 1), rgba(168, 120, 41, 1))',
'--color-border-disabled': 'light-dark(rgba(31, 30, 29, 0.1), rgba(222, 220, 209, 0.1))',

// ── Ring colors ──
'--color-ring-primary': 'light-dark(rgba(20, 20, 19, 0.7), rgba(250, 249, 245, 0.7))',
'--color-ring-secondary': 'light-dark(rgba(61, 61, 58, 0.7), rgba(194, 192, 182, 0.7))',
'--color-ring-inverse': 'light-dark(rgba(255, 255, 255, 0.7), rgba(20, 20, 19, 0.7))',
'--color-ring-info': 'light-dark(rgba(50, 102, 173, 0.5), rgba(128, 170, 221, 0.5))',
'--color-ring-danger': 'light-dark(rgba(167, 61, 57, 0.5), rgba(205, 92, 88, 0.5))',
'--color-ring-success': 'light-dark(rgba(67, 116, 38, 0.5), rgba(89, 145, 48, 0.5))',
'--color-ring-warning': 'light-dark(rgba(128, 92, 31, 0.5), rgba(168, 120, 41, 0.5))',

// ── Typography ──
'--font-sans': '"Anthropic Sans", system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
'--font-mono': 'ui-monospace, monospace',
// Sizes (px, not rem — Claude uses fixed px values)
'--font-text-xs-size': '12px',
'--font-text-sm-size': '14px',
'--font-text-md-size': '16px',
'--font-text-lg-size': '20px',
'--font-heading-xs-size': '12px',
'--font-heading-sm-size': '14px',
'--font-heading-md-size': '16px',
'--font-heading-lg-size': '20px',
'--font-heading-xl-size': '24px',
'--font-heading-2xl-size': '28px',
'--font-heading-3xl-size': '36px',
// Line heights
'--font-text-md-line-height': '1.4',
'--font-text-lg-line-height': '1.25',
'--font-heading-lg-line-height': '1.25',
'--font-heading-2xl-line-height': '1.1',
'--font-heading-3xl-line-height': '1',

// ── Border radius (Claude uses slightly larger values) ──
'--border-radius-xs': '4px',
'--border-radius-sm': '6px',
'--border-radius-md': '8px',
'--border-radius-lg': '10px',

// ── Border width ──
'--border-width-regular': '0.5px',
};

registerHostShell({
Expand All @@ -57,12 +134,28 @@ registerHostShell({
applyTheme: applyClaudeTheme,
hostInfo: CLAUDE_HOST_INFO,
hostCapabilities: CLAUDE_HOST_CAPABILITIES,
userAgent: 'claude',
styleVariables: CLAUDE_STYLE_VARIABLES,
pageStyles: {
'--sim-bg-sidebar': 'light-dark(#f9f8f3, #252523)',
'--sim-bg-conversation': 'light-dark(#faf9f5, #262624)',
'--sim-bg-user-bubble': 'light-dark(#f1eee6, #141413)',
'--sim-bg-reply-input': 'light-dark(#ffffff, #30302e)',
'--sim-bg-sidebar': 'light-dark(rgb(250, 249, 245), rgb(38, 38, 36))',
'--sim-bg-conversation': 'light-dark(rgb(250, 249, 245), rgb(38, 38, 36))',
'--sim-bg-user-bubble': 'light-dark(rgb(240, 238, 230), rgb(20, 20, 19))',
'--sim-bg-reply-input': 'light-dark(rgb(255, 255, 255), rgb(48, 48, 46))',
},
availableDisplayModes: ['inline', 'fullscreen'],
fontCss: `@font-face {
font-family: "Anthropic Sans";
src: url("https://assets-proxy.anthropic.com/claude-ai/v2/assets/v1/cc27851ad-CFxw3nG7.woff2") format("woff2");
font-weight: 300 800;
font-style: normal;
font-display: swap;
font-feature-settings: "dlig" 0;
}
@font-face {
font-family: "Anthropic Sans";
src: url("https://assets-proxy.anthropic.com/claude-ai/v2/assets/v1/c9d3a3a49-BI1hrwN4.woff2") format("woff2");
font-weight: 300 800;
font-style: italic;
font-display: swap;
font-feature-settings: "dlig" 0;
}`,
});
Loading
Loading