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
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,23 @@ jobs:
- name: Compile (warnings as errors)
run: mix compile --warnings-as-errors

- name: Compile without optional dependencies
run: mix compile --force --warnings-as-errors --no-optional-deps

- name: Check for compile-time dependency cycles
run: mix xref graph --format cycles --label compile-connected --fail-above 0

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install npm dependencies
run: npm ci

- name: Build preview assets
run: npm run build

- name: Run tests
run: epmd -daemon && mix test

Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ phantom_mcp-*.tar
/.claude/*.local.*
.tool-versions
phantom_mcp
/bin/phantom-stdio

# Node/JS build artifacts
/node_modules/
/test/support/app/priv/static/mcp_app.js
/test/support/app/priv/static/mcp_preview.js
/priv/static/preview.css
/priv/static/preview.js
2 changes: 1 addition & 1 deletion .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"phantom-test-https": {
"type": "http",
"url": "http://localhost:4000/mcp"
"url": "http://localhost:4001/mcp"
},
"phantom-test-stdio": {
"command": "bin/phantom-stdio"
Expand Down
273 changes: 273 additions & 0 deletions assets/css/preview.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
@import "tailwindcss";
@source "../../lib/phantom/app/preview.ex";
@source "../../assets/js/preview.js";

@theme {
--color-phantom: oklch(0.65 0.15 280);
--color-phantom-glow: oklch(0.72 0.18 280);
--color-phantom-dim: oklch(0.45 0.12 280);
--color-phantom-surface: oklch(0.22 0.02 270);
--color-phantom-surface-hover: oklch(0.26 0.025 270);
}

/* Phantom dot-grid canvas for the preview artboard. The artboard reflects
the simulated host theme (data-host-theme on body), not the OS theme. */
@utility canvas-bg {
background-color: #f4f4f6;
background-image: radial-gradient(circle, #d0d0d8 0.75px, transparent 0.75px);
background-size: 16px 16px;
}

[data-host-theme="dark"] .canvas-bg,
.canvas-bg[data-host-theme="dark"] {
background-color: oklch(0.14 0.01 270);
background-image: radial-gradient(circle, oklch(0.24 0.015 270) 0.75px, transparent 0.75px);
}

body[data-host-theme="dark"] #mcp-app-container.canvas-bg {
background-color: oklch(0.14 0.01 270);
background-image: radial-gradient(circle, oklch(0.24 0.015 270) 0.75px, transparent 0.75px);
}

/* The iframe is sized by ui/notifications/size-changed in inline mode.
This min-height keeps it usable until the first size message arrives
(and acts as a floor while content loads). Border follows the host
theme so it blends with the canvas. */
.mcp-app-frame {
min-height: 320px;
border: 1px solid #d4d4d8;
}

body[data-host-theme="dark"] .mcp-app-frame {
border-color: oklch(0.30 0.015 270);
}

/* Resize handle — vertical drag bar */
.mcp-app-frame-handle::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 3px;
height: 32px;
border-radius: 3px;
transition: background 0.2s ease, height 0.2s ease, box-shadow 0.2s ease;
@apply bg-zinc-300 dark:bg-zinc-600;
}

.mcp-app-frame-handle:hover::before,
.mcp-app-frame-handle.is-dragging::before {
height: 48px;
background-color: var(--color-phantom);
box-shadow: 0 0 8px var(--color-phantom-glow);
}

/* Drag state */
body.mcp-resizing,
body.mcp-resizing * {
cursor: ew-resize !important;
}
body.mcp-resizing iframe {
pointer-events: none !important;
}

/* Host context controls bar */
.phantom-control-group {
display: flex;
align-items: center;
gap: 0.375rem;
}

.phantom-control-label {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: oklch(0.45 0.01 270);
white-space: nowrap;
}

.phantom-control-select {
appearance: none;
background-color: oklch(0.16 0.01 270);
border: 1px solid oklch(0.30 0.015 270);
border-radius: 0.375rem;
color: oklch(0.65 0.01 270);
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
font-size: 0.6875rem;
line-height: 1;
padding: 0.25rem 1.25rem 0.25rem 0.5rem;
cursor: pointer;
transition: border-color 0.15s ease, color 0.15s ease;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23717179' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.375rem center;
}

.phantom-control-select:hover {
border-color: var(--color-phantom-dim);
color: oklch(0.80 0.01 270);
}

.phantom-control-select:focus {
outline: none;
border-color: var(--color-phantom);
box-shadow: 0 0 0 1px var(--color-phantom-dim);
}

/* ------------------------------------------------------------------ */
/* Chat simulation */
/* ------------------------------------------------------------------ */

.mcp-chat-container {
display: flex;
flex-direction: column;
gap: 1rem;
flex: 1;
min-height: 0;
overflow-y: auto;
/* Top padding is inside the chat so the scrollbar runs the full
height of the canvas — putting it on the parent would short the
scrollbar by the padding amount. */
padding-top: 1rem;
max-width: 56rem;
width: 100%;
margin: 0 auto;
}

.mcp-chat-message {
display: flex;
flex-direction: column;
}

.mcp-chat-user {
align-items: flex-end;
}

.mcp-chat-assistant {
align-items: flex-start;
}

.mcp-chat-bubble {
max-width: 80%;
padding: 0.625rem 0.875rem;
font-size: 0.875rem;
line-height: 1.5;
}

.mcp-chat-bubble-user {
background-color: oklch(0.55 0.14 280);
color: #ffffff;
border-radius: 1rem 1rem 0.25rem 1rem;
}

.mcp-chat-bubble-assistant {
background-color: #ffffff;
color: #1f2937;
border-radius: 1rem 1rem 1rem 0.25rem;
border: 1px solid #e5e7eb;
}

body[data-host-theme="dark"] .mcp-chat-bubble-user {
background-color: oklch(0.42 0.10 280);
color: oklch(0.96 0.01 270);
}

body[data-host-theme="dark"] .mcp-chat-bubble-assistant {
background-color: oklch(0.22 0.015 270);
color: oklch(0.86 0.01 270);
border-color: oklch(0.30 0.015 270);
}

body[data-host-theme="dark"] .mcp-chat-tool-label {
color: oklch(0.62 0.01 270);
}

.mcp-chat-tool-result {
display: flex;
flex-direction: column;
gap: 0.375rem;
}

.mcp-chat-tool-label {
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6b7280;
padding-left: 0.25rem;
}

.mcp-chat-tool-body {
display: flex;
flex-direction: row;
align-items: stretch;
}

/* ------------------------------------------------------------------ */
/* PiP window */
/* ------------------------------------------------------------------ */

.mcp-pip-window {
position: absolute;
bottom: 1rem;
right: 1rem;
width: 380px;
height: 320px;
border-radius: 0.75rem;
border: 1px solid #d4d4d8;
background: #ffffff;
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.18), 0 0 0 1px rgba(15, 23, 42, 0.04);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 20;
resize: both;
min-width: 240px;
min-height: 180px;
}

.mcp-pip-header {
display: flex;
align-items: center;
padding: 0.375rem 0.75rem;
background: #f4f4f5;
border-bottom: 1px solid #e4e4e7;
cursor: grab;
flex-shrink: 0;
user-select: none;
}

.mcp-pip-header:active {
cursor: grabbing;
}

.mcp-pip-title {
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #52525b;
}

body[data-host-theme="dark"] .mcp-pip-window {
border-color: oklch(0.35 0.02 270);
background: oklch(0.16 0.01 270);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.04);
}

body[data-host-theme="dark"] .mcp-pip-header {
background: oklch(0.20 0.015 270);
border-bottom-color: oklch(0.30 0.015 270);
}

body[data-host-theme="dark"] .mcp-pip-title {
color: oklch(0.60 0.01 270);
}

.mcp-pip-body {
flex: 1;
min-height: 0;
overflow: hidden;
}
Loading
Loading