From 880589c17585ad062c653fef58e8ea672bf63131 Mon Sep 17 00:00:00 2001 From: Muhammad Ahmed Cheema Date: Mon, 13 Apr 2026 12:30:40 -0700 Subject: [PATCH 1/2] feat: Phantom UI vocabulary, base template, and name customization Promote the Project 2 design system end to end. Replace the 468-line public/_base.html with the 1086-line warm cream / warm deep dark token system, the 89-class phantom-* vocabulary, the dual-theme ECharts phantom theme, and the window.phantomChart helper. Add the living style guide at public/_components.html and the new landing at public/index.html. Rewrite src/ui/login-page.ts to emit the new login surface using the phantom-* tokens with Instrument Serif display, dropping 136 lines of hand-crafted SVG. Build ten reference example pages under public/_examples/ that the live agent can read at runtime when it wants to remember how a dashboard, a session table, an evolution timeline, a memory explorer, a skills editor, a cost report, or a chat surface composes from the vocabulary. Each page is self-contained so the agent does not need to also load _base.html. Validated in both phantom-light and phantom-dark with zero console errors and zero failed network requests. Plumb the PHANTOM_NAME env var through every user-facing surface. Add src/ui/name.ts with capitalizeAgentName and agentNameInitial helpers, plus 13 unit tests. Add three new placeholder substitutions to wrapInBaseTemplate ({{AGENT_NAME}}, {{AGENT_NAME_CAPITALIZED}}, {{AGENT_NAME_INITIAL}}) and thread the agent name through createWebUiToolServer from src/index.ts. Wire setLoginPageAgentName through the same call site so the login HTML interpolates the agent name without modifying serve.ts. For the static landing, components, and example pages, ship a small inline /health-fetch script that populates data-agent-name markers at runtime with localStorage caching. Replace the legacy 88-line UI guidance block in src/agent/prompt-assembler.ts with the verbatim 155-line block from the research implementation plan, extracted into src/agent/prompt-blocks/ui-guidance.ts. Also extract the security and instructions blocks so prompt-assembler.ts stays under 300 lines. Tests: 948 before, 973 after (+25). Lint and typecheck clean. Playwright validation captured for all ten example pages plus the style guide and a wrapped base template page in scratch/02-project2/builder- validation/. Measured baseline shows FCP improving from 244ms to 40ms, DOM nodes 50 to 46, HTML bytes 27911 to 44347, with two new Instrument Serif font requests and one expected SSE 404 on the local Python server. --- public/_base.html | 1084 ++++++++++++++---- public/_components.html | 402 +++++++ public/_examples/01-landing.html | 224 ++++ public/_examples/02-login.html | 147 +++ public/_examples/03-dashboard-overview.html | 228 ++++ public/_examples/04-session-history.html | 154 +++ public/_examples/05-evolution-timeline.html | 145 +++ public/_examples/06-memory-explorer.html | 242 ++++ public/_examples/07-skills-editor.html | 184 +++ public/_examples/08-cost-report.html | 215 ++++ public/_examples/09-chat-welcome.html | 122 ++ public/_examples/10-chat-active.html | 122 ++ public/index.html | 567 ++++----- scripts/install-phantom-ui-skill.sh | 21 + src/agent/__tests__/prompt-assembler.test.ts | 36 + src/agent/prompt-assembler.ts | 146 +-- src/agent/prompt-blocks/instructions.ts | 56 + src/agent/prompt-blocks/security.ts | 35 + src/agent/prompt-blocks/ui-guidance.ts | 155 +++ src/index.ts | 4 +- src/ui/__tests__/name.test.ts | 58 + src/ui/__tests__/serve.test.ts | 6 +- src/ui/__tests__/tools.test.ts | 56 +- src/ui/login-page.ts | 412 +++---- src/ui/name.ts | 25 + src/ui/tools.ts | 36 +- 26 files changed, 3858 insertions(+), 1024 deletions(-) create mode 100644 public/_components.html create mode 100644 public/_examples/01-landing.html create mode 100644 public/_examples/02-login.html create mode 100644 public/_examples/03-dashboard-overview.html create mode 100644 public/_examples/04-session-history.html create mode 100644 public/_examples/05-evolution-timeline.html create mode 100644 public/_examples/06-memory-explorer.html create mode 100644 public/_examples/07-skills-editor.html create mode 100644 public/_examples/08-cost-report.html create mode 100644 public/_examples/09-chat-welcome.html create mode 100644 public/_examples/10-chat-active.html create mode 100755 scripts/install-phantom-ui-skill.sh create mode 100644 src/agent/prompt-blocks/instructions.ts create mode 100644 src/agent/prompt-blocks/security.ts create mode 100644 src/agent/prompt-blocks/ui-guidance.ts create mode 100644 src/ui/__tests__/name.test.ts create mode 100644 src/ui/name.ts diff --git a/public/_base.html b/public/_base.html index 5cbc571..f161998 100644 --- a/public/_base.html +++ b/public/_base.html @@ -3,7 +3,7 @@ - {{TITLE}} - Phantom + {{TITLE}} - {{AGENT_NAME_CAPITALIZED}} - + - + - + @@ -28,8 +28,6 @@ - - - + - + - - - - - + - + + + diff --git a/public/_components.html b/public/_components.html new file mode 100644 index 0000000..2f1ed60 --- /dev/null +++ b/public/_components.html @@ -0,0 +1,402 @@ + + + + + +Components - Living Style Guide + + + + + + + + + + + + + +
+ +
+

  design system

+

The living style guide.

+

Every vocabulary pattern the live agent can reach for, rendered in both themes. This is reference material, not rails. The agent reads this to remember how a pattern looks, then freehands HTML that uses the pattern.

+
+ +
+

Typography

+ +
+

phantom-display

Serif display for hero headings. Instrument Serif, weight 400, tight line-height.

+

  works alongside you.

+
+ +
+

phantom-h1

Page title inside dashboards. Serif, 32px, weight 500.

+

Dashboard overview

+
+ +
+

phantom-h2

Section title. Serif, 22px, weight 500.

+

Recent sessions

+
+ +
+

phantom-h3

Card or subsection label. Inter, 14px, weight 600.

+

Cost breakdown

+
+ +
+

phantom-eyebrow

Uppercase tracked label above a title.

+

Scheduled jobs

+
+ +
+

phantom-lead / phantom-body / phantom-mono

Body text variants for paragraph, lead-in, and code.

+
+

This is a lead paragraph for an opening section. It sits between the display heading and the body text, carrying the voice.

+

Standard body text. Fourteen pixels, 1.55 line-height, subtle negative letter-spacing, tabular numerics inherited from the body default so numbers in prose like 12 or 345 do not jitter next to their siblings in a table.

+

phantom_session/0.18.2 - 14h32m - Gen 3

+
+
+
+ +
+

Cards and stats

+ +
+

phantom-card

Standard surface container. 1px border, 14px radius, no default shadow.

+
+
+

Agent status

+

Everything nominal. The scheduler has 3 active jobs, evolution is at generation 3, and episodic memory has 142 points.

+
+
+
+ +
+

phantom-grid-stats + phantom-stat

Four-column responsive stats grid with labels, values, and trend indicators.

+
+
+
+

Sessions

127

+22% 7d

+

Cost

$4.28

-8% 7d

+

Turns

1,439

+12%

+

Evolution

Gen 3

stable

+
+
+
+
+
+ +
+

Badges, chips, dots

+ +
+

phantom-badge

Status chip. Primary, success, warning, error, info, neutral.

+
+
+ primary + success + warning + error + info + neutral +
+
+
+ +
+

phantom-chip

Filter chip, toggleable via aria-pressed.

+
+
+ + + + + +
+
+
+ +
+

phantom-dot

Status dot. Live dot pulses.

+
+
+ online + healthy + degraded + down + info +
+
+
+
+ +
+

Tables

+
+

phantom-table

Tabular numerics, uppercase header labels, zero-border row hover.

+
+
+ + + + + + + + +
ChannelStartedTurnsCostStatus
slack14:32:0118$0.24done
slack12:18:437$0.09done
webhook11:40:2242$0.58long
slack09:05:113$0.04error
+
+
+
+
+ +
+

Timeline

+
+

phantom-timeline

Vertical timeline with 1.5px ring markers and muted timestamps.

+
+
+
+

2026-04-13 15:12

Gen 3 shipped

Added `prefer_ripgrep` strategy. 3 files changed.

+

2026-04-11 08:44

Gen 2 shipped

Learned user preference: warm commits, no emoji.

+

2026-04-09 21:03

Gen 1 shipped

Initial constitution accepted. 930 tests passing.

+
+
+
+
+
+ +
+

Forms and buttons

+ +
+

phantom-input

Rounded 10px field. 3px focus ring uses color-mix.

+
+ +
+
+ +
+

phantom-button

Pill shape, 150ms transition, four variants.

+
+
+ + + + + +
+
+
+
+ +
+

Alerts

+
+

phantom-alert

Four variants. Inline icon plus body copy.

+
+
Scheduled job daily-report ran successfully at 09:00:00.
+
Qdrant is responding slowly. Embedding writes are queued.
+
Cost tracker is reporting lies. Provider-aware pricing table not loaded.
+
Evolution gate 5 (safety) rejected a change. No action required.
+
+
+
+ +
+

Tabs

+
+

phantom-tabs

Segmented control for in-card tabbed views.

+
+
+ + + +
+
+
+
+ +
+

Chat vocabulary

+
+

phantom-chat-bubble-*

Project 4 seeds. User bubble on right, assistant on left, tool card inline.

+
+
+
+
What is the current cost of the anthropic session?
+
Let me check. +
runningphantom_query_sessions
+ Today so far: $2.18, across 34 turns. Mostly cached reads. +
+
+
+
+
+
+ +
+

Empty state

+
+

phantom-empty

Dashed border, centered content, muted copy, optional icon.

+
+
+ +

No sessions yet

+

Send your agent a message in Slack and it will show up here.

+
+
+
+
+ +
+ + + + + + diff --git a/public/_examples/01-landing.html b/public/_examples/01-landing.html new file mode 100644 index 0000000..965f1db --- /dev/null +++ b/public/_examples/01-landing.html @@ -0,0 +1,224 @@ + + + + + +  + + + + + + + + + + + + + + + + + +
+ +
+

The agent is awake

+

  works alongside you, not for you.

+

Your autonomous AI co-worker. Generates dashboards, schedules jobs, remembers everything, and iterates on itself between messages. This is the surface it creates for the pages it wants to show you.

+
+ +
+
+

Agent status

+ + + online + +
+
+
+

Agent

+

 

+

swe role

+
+
+

Version

+

0.18.2

+

stable

+
+
+

Uptime

+

14h 32m

+

+2h since restart

+
+
+

Evolution

+

Gen 3

+

+1 this week

+
+
+
+ +
+

Quick links

+
+ + + + + + + + + + + + + + + + + + +
+
+ +
+
+

What is this?

+

Pages the agent creates for you.

+

When   writes a dashboard, report, chart, or anything richer than Slack can display, it publishes the HTML here. Every page is cookie-auth protected. Ask your agent in Slack for a magic link, or run phantom_generate_login via MCP.

+
+
+ +
+ + + + + + diff --git a/public/_examples/02-login.html b/public/_examples/02-login.html new file mode 100644 index 0000000..f152974 --- /dev/null +++ b/public/_examples/02-login.html @@ -0,0 +1,147 @@ + + + + + +Sign in + + + + + + + + + + + + + + + +
+ + +   + + +
+ +
+ +
+ + + + + + diff --git a/public/_examples/03-dashboard-overview.html b/public/_examples/03-dashboard-overview.html new file mode 100644 index 0000000..732df77 --- /dev/null +++ b/public/_examples/03-dashboard-overview.html @@ -0,0 +1,228 @@ + + + + + +Dashboard overview + + + + + + + + + + + + + + +
+ +
+

Dashboard

+

  overview

+

Live status, daily cost, evolution generation, and the most recent agent sessions.

+
+ +
+
+
+

Active sessions

+

7

+

+2 vs yesterday

+
+
+
+
+

Daily cost

+

$12.84

+

-18% vs 7d avg

+
+
+
+
+

Evolution gen

+

Gen 14

+

+1 this week

+
+
+
+
+

Uptime

+

22h 41m

+

stable since restart

+
+
+
+ +
+
+
+

Spend

+

Daily cost, last 30 days

+
+ USD +
+
+
+ +
+
+
+

Recent sessions

+ +
+ + + + + + + + + + + + + + +
ChannelStartedTurnsCostStatus
slack #ops14:2211$0.84done
slack #infra13:484$0.31done
scheduler nightly-summary09:001$0.12done
slack DM Cheema08:4123$1.92done
slack #cli-tools07:556$0.41stuck
cli local06:302$0.08done
+
+ +
+
+

System health

+ healthy +
+
Qdrant200 / 12ms
+
Ollama200 / 47ms
+
Slackconnected
+
Scheduler3 active jobs
+
MCP /mcp17 tools
+
Memory turns1,284
+
+

Last evolution: 2 hours ago, +3 facts, +1 strategy

+
+
+
+ +
+ + + + + diff --git a/public/_examples/04-session-history.html b/public/_examples/04-session-history.html new file mode 100644 index 0000000..4d11639 --- /dev/null +++ b/public/_examples/04-session-history.html @@ -0,0 +1,154 @@ + + + + + +Session history + + + + + + + + + + + + + +
+ +
+

History

+

Session history

+

Every conversation, scheduled job run, and tool invocation, in chronological order.

+
+ +
+ + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
ChannelStartedTurnsDurationCostStatus
slack #ops14:22:11114m 02s$0.84done
slack #infra13:48:3741m 18s$0.31done
scheduled nightly-summary09:00:00122s$0.12done
slack DM Cheema08:41:552311m 47s$1.92done
slack #cli-tools07:55:0262m 11s$0.41stuck
cli local06:30:1820m 47s$0.08done
scheduled hourly-pulse06:00:00114s$0.04done
webhook deploy-hook05:42:3331m 02s$0.18failed
slack #design04:18:0993m 22s$0.66done
scheduled nightly-summary03:00:00119s$0.10done
slack #ops02:14:5151m 33s$0.27done
cli ssh-debug01:55:30146m 18s$1.04done
slack DM Murphy23:42:08179m 02s$1.41active
email inbound22:18:4420m 41s$0.07done
slack #infra21:55:1982m 47s$0.58done
+
+ +
+ 15 of 1,284 sessions +
+ + +
+
+ +
+ + + + diff --git a/public/_examples/05-evolution-timeline.html b/public/_examples/05-evolution-timeline.html new file mode 100644 index 0000000..b622013 --- /dev/null +++ b/public/_examples/05-evolution-timeline.html @@ -0,0 +1,145 @@ + + + + + +Evolution timeline + + + + + + + + + + + + + +
+ +
+

Evolution

+

Generation timeline

+

Each generation is a self-modification accepted through the 5-gate validator. Constitution gate, regression gate, size gate, drift gate, safety gate.

+
+ +
+
+ +
+

Generation 14, 2 hours ago

+

Added stripe customer cache routine

+

Reflection observed three back-to-back stripe billing tasks in the same week. Added a 30-minute cache for stripe customer lookups to procedural memory. Drift gate passed at 0.18.

+
+ +
+

Generation 13, yesterday 23:14

+

Updated user profile, prefer Bun over Node

+

User correction during ops session captured to userProfile.md. Now defaults to Bun for any new TypeScript project unless the operator explicitly asks for Node.

+
+ +
+

Generation 12, yesterday 16:02

+

Refined Slack reply pacing

+

Persona update. Replies now batch tool calls and emit a single status reaction instead of one reaction per tool. Reduces channel noise by an estimated 40 percent.

+
+ +
+

Generation 11, 2 days ago

+

Rolled back constitution edit

+

An evolution proposal weakened the secret-handling clause. Constitution gate veto by minority judge. Rolled back automatically. No persisted state from the rejected change.

+
+ +
+

Generation 10, 3 days ago

+

Added domain knowledge: Specter VM lifecycle

+

Captured the full Specter cloud-init flow, the Caddy TLS hooks, and the systemd unit pattern as a domain knowledge entry. Future deploys will not need to relearn these.

+
+ +
+

Generation 9, 4 days ago

+

New strategy: prefer rsync to scp for code-only updates

+

Two production deploys used scp and overwrote .env files. Strategy added: always use rsync with explicit excludes for env, config, data, and dotfiles. Drift gate passed.

+
+ +
+

Generation 8, 5 days ago

+

Initial role specialization, swe

+

Onboarding completed. Role template loaded from config/roles/swe.yaml. Persona, communication style, and tool preferences seeded from operator answers.

+
+ +
+
+ +
+ + + + diff --git a/public/_examples/06-memory-explorer.html b/public/_examples/06-memory-explorer.html new file mode 100644 index 0000000..3726ada --- /dev/null +++ b/public/_examples/06-memory-explorer.html @@ -0,0 +1,242 @@ + + + + + +Memory explorer + + + + + + + + + + + + + +
+ +
+

Three tier vector memory

+

Memory explorer

+

Episodic events, semantic facts, and procedural workflows. Stored in Qdrant with hybrid dense and sparse retrieval.

+
+ +
+ + + +
+ +
+
+ +
+
+ +
+
+
+

Helped Cheema debug a Specter VM cloud-init failure. Root cause was a missing tini binary in the container layer. Fixed by adding tini to the Dockerfile RUN apt-get install line.

+

slack #infra, 14h ago, 11 turns

+
+ resolved + +
+
+ +
+
+
+

Generated the weekly cost report dashboard. Operator preferred the daily breakdown over the per-channel split. Updated procedural memory to default to daily breakdowns for cost reports.

+

scheduled weekly-cost, 1d ago, 1 turn

+
+ delivered + +
+
+ +
+
+
+

Tried to deploy the new auth flow but the rsync overwrote the production .env. Operator caught it before restart. Created strategy entry to always pass --exclude env to rsync deploys.

+

cli local, 2d ago, 7 turns

+
+ corrected + +
+
+ +
+
+
+

Composed a refund email to a stripe customer. Operator approved on first draft. Email sent via phantom_send_email and confirmation logged.

+

slack DM Cheema, 3d ago, 4 turns

+
+ resolved + +
+
+ +
+
+
+

Attempted to clone a private repo but the gh CLI was not authenticated. Asked operator for a personal access token via phantom_collect_secrets. Cloned successfully on retry.

+

slack #ops, 4d ago, 9 turns

+
+ resolved + +
+
+ +
+
+ +
+
+ +
+

Operator preferences

+
+
Bun is the default TypeScript runtime. Use Node only if explicitly asked.
+
Reply pacing in Slack: batch tool calls into one status reaction per message.
+
Cost reports default to daily breakdown, never per-channel split.
+
No em dashes in any text the agent emits. Use commas, periods, or hyphens.
+
+
+ +
+

Repository conventions

+
+
Agent config files live under phantom-config/. Never commit secrets there.
+
Production deploys use rsync with explicit excludes for env, config, data, and dotfiles.
+
All TypeScript files under 300 lines. Split if approaching 250.
+
+
+ +
+

Infrastructure facts

+
+
Specter VMs use Caddy for TLS and systemd for the phantom unit.
+
Qdrant stores three named vectors per memory: episodic, semantic, procedural.
+
+
+ +
+
+ +
+
+ +
+

Stripe customer cache

+

Cache stripe.customers.retrieve results for 30 minutes keyed by customer id. Expire on subscription update events. Reduces stripe API calls by 80 percent on multi-customer days.

+
+ +
+

Production deploy

+

Run rsync with --exclude=env, --exclude=config, --exclude=data, --exclude=*.db. Then ssh to the host and restart the systemd unit. Wait 5 seconds, then curl /health and confirm 200.

+
+ +
+

Slack incident triage

+

When a #infra incident message comes in, immediately drop a status reaction, pull the most recent error logs from the affected service, and post a summary in thread within 2 minutes.

+
+ +
+
+ +
+ + + + diff --git a/public/_examples/07-skills-editor.html b/public/_examples/07-skills-editor.html new file mode 100644 index 0000000..00c5768 --- /dev/null +++ b/public/_examples/07-skills-editor.html @@ -0,0 +1,184 @@ + + + + + +Skills editor + + + + + + + + + + + + + +
+ +
+

Skills

+

Skills editor

+

Markdown skill files the agent loads on demand. Each skill has YAML frontmatter that controls when and how it activates.

+
+ +
+ + + +
+
+ + +
+ +
+ Lint warning + Frontmatter is missing the optional version field. Recommended for shareable skills. +
+ +
+ 198 lines, last edited 2 hours ago +
+ + +
+
+
+ +
+ +
+ + + + diff --git a/public/_examples/08-cost-report.html b/public/_examples/08-cost-report.html new file mode 100644 index 0000000..56b0611 --- /dev/null +++ b/public/_examples/08-cost-report.html @@ -0,0 +1,215 @@ + + + + + +Cost report + + + + + + + + + + + + + + +
+ +
+

Spend

+

Cost report, last 30 days

+

Per-provider breakdown, daily trend, and the most expensive sessions of the period.

+
+ +
+
+
+

Total

+

$324.18

+

-12% vs prior 30d

+
+
+
+
+

Average per day

+

$10.81

+

close to budget

+
+
+
+
+

Top provider

+

Anthropic

+

82% of total

+
+
+
+
+

Top model

+

opus-4.6

+

+8% week over week

+
+
+
+ +
+
+

Daily spend

+ USD, last 30 days +
+
+
+ +
+
+

Stacked by provider

+ last 14 days +
+
+
+ +
+

Top 10 most expensive sessions

+ + + + + + + + + + + + + + + + +
DateChannelModelTokensCost
2026-04-12slack DM Cheemaopus-4.6812,304$8.42
2026-04-11scheduled deep-researchopus-4.6694,221$7.18
2026-04-10slack #infraopus-4.6521,889$5.41
2026-04-09slack DM Murphysonnet-4.61,204,503$4.84
2026-04-08cli ssh-debugopus-4.6432,118$4.48
2026-04-07slack #opsopus-4.6388,764$4.02
2026-04-06scheduled weekly-costsonnet-4.6927,108$3.71
2026-04-05slack #designopus-4.6348,990$3.62
2026-04-04webhook deploy-hookopus-4.6298,447$3.09
2026-04-03slack DM Cheemaopus-4.6277,332$2.87
+
+ +
+ + + + + diff --git a/public/_examples/09-chat-welcome.html b/public/_examples/09-chat-welcome.html new file mode 100644 index 0000000..942cb64 --- /dev/null +++ b/public/_examples/09-chat-welcome.html @@ -0,0 +1,122 @@ + + + + + +Chat + + + + + + + + + + + + + +
+ +
+
 
+
+ +

Hi, I am  .

+

Ask me anything. I can build dashboards, schedule jobs, debug your infra, or just have a conversation. Pick a starting point or write your own.

+ +
+ +
+

Build

+

Make me a dashboard for our daily token spend.

+
+ +
+

Debug

+

Why did the deploy script fail this morning?

+
+ +
+

Schedule

+

Every weekday at 9am, summarize open PRs.

+
+ +
+

Search

+

What did we decide about the auth flow last week?

+
+ +
+

Plan

+

Help me think through the v2 evolution validator.

+
+ +
+

Compose

+

Draft an email to the design team about the new vocabulary.

+
+ +
+ +
+ +
+
+ + +
+
+ + + diff --git a/public/_examples/10-chat-active.html b/public/_examples/10-chat-active.html new file mode 100644 index 0000000..b879716 --- /dev/null +++ b/public/_examples/10-chat-active.html @@ -0,0 +1,122 @@ + + + + + +Chat + + + + + + + + + + + + + +
+
+ +
+ Can you build me a quick preview page that shows the new dashboard layout? I want to see how it looks before we ship. +
+ +
+ On it. I will compose a page using the phantom-* vocabulary, save it under /ui/preview-dashboard.html, then preview it in both themes before sharing the link. + +
+ + phantom_create_page + path: "preview-dashboard.html" +
+ +
+ + phantom_preview_page + path: "preview-dashboard.html" +
+
+ +
+ Use the warm cream theme as the default and make sure the cost chart uses real data from yesterday. +
+ +
+ Got it. Switching to the warm cream theme and pulling cost data from the metrics SQLite from the last 24 hours. The chart will use window.phantomChart() so it stays theme-aware on toggle. I will share the magic link as soon as the preview validates clean. + 2 tool calls, 1.4s elapsed +
+ +
+ +
+ +
+
+ +
+
+ + +
+
+ + + diff --git a/public/index.html b/public/index.html index 565a64c..965f1db 100644 --- a/public/index.html +++ b/public/index.html @@ -1,381 +1,224 @@ - - - Phantom - - - - - - - - - - - - - - - + + +  + + + + + + + + + + + + - - - - - -
- - -
-
- Phantom -

Phantom

- agent +
+
+

Agent

+

 

+

swe role

+
+
+

Version

+

0.18.2

+

stable

+
+
+

Uptime

+

14h 32m

+

+2h since restart

+
+
+

Evolution

+

Gen 3

+

+1 this week

-

- Your autonomous AI co-worker. Phantom creates pages here for dashboards, - reports, charts, and anything that Slack can't display. -

+ - -
-
-
-

Agent Status

- - checking... - -
+
+

Quick links

+
-
- - - - - - - - - - - - + + + + + + + diff --git a/scripts/install-phantom-ui-skill.sh b/scripts/install-phantom-ui-skill.sh new file mode 100755 index 0000000..c0043ed --- /dev/null +++ b/scripts/install-phantom-ui-skill.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Installs the phantom-ui skill seed into the user-level Claude Agent SDK +# skills directory. The seed is shipped inside the repo at +# local/2026-04-12-phantom-ui-chapter/scratch/02-project2/phantom-ui-skill.md +# and gets copied into ~/.claude/skills/phantom-ui/SKILL.md so the live agent +# discovers it once Project 3 wires settingSources to include 'user'. +set -euo pipefail + +SOURCE_FILE="local/2026-04-12-phantom-ui-chapter/scratch/02-project2/phantom-ui-skill.md" +TARGET_DIR="${HOME}/.claude/skills/phantom-ui" +TARGET_FILE="${TARGET_DIR}/SKILL.md" + +if [ ! -f "${SOURCE_FILE}" ]; then + echo "error: source file not found at ${SOURCE_FILE}" + echo " run this script from the repository root" + exit 1 +fi + +mkdir -p "${TARGET_DIR}" +cp "${SOURCE_FILE}" "${TARGET_FILE}" +echo "installed phantom-ui skill seed at ${TARGET_FILE}" diff --git a/src/agent/__tests__/prompt-assembler.test.ts b/src/agent/__tests__/prompt-assembler.test.ts index 0172fd6..9c33773 100644 --- a/src/agent/__tests__/prompt-assembler.test.ts +++ b/src/agent/__tests__/prompt-assembler.test.ts @@ -72,3 +72,39 @@ describe("assemblePrompt Docker awareness", () => { expect(prompt).toContain("Security Boundaries"); }); }); + +describe("assemblePrompt UI vocabulary guidance", () => { + test("includes phantom-* vocabulary references", () => { + const prompt = assemblePrompt(baseConfig); + expect(prompt).toContain("phantom-card"); + expect(prompt).toContain("phantom-stat"); + expect(prompt).toContain("phantom-table"); + expect(prompt).toContain("phantom-chat-bubble-user"); + }); + + test("includes Instrument Serif font reference", () => { + const prompt = assemblePrompt(baseConfig); + expect(prompt).toContain("Instrument Serif"); + }); + + test("includes the chart helper reference", () => { + const prompt = assemblePrompt(baseConfig); + expect(prompt).toContain("window.phantomChart"); + }); + + test("includes the self-validate phantom_preview_page guidance", () => { + const prompt = assemblePrompt(baseConfig); + expect(prompt).toContain("phantom_preview_page"); + }); + + test("references the living style guide and base template paths", () => { + const prompt = assemblePrompt(baseConfig); + expect(prompt).toContain("public/_base.html"); + expect(prompt).toContain("/ui/_components.html"); + }); + + test("references the eight reference example pages", () => { + const prompt = assemblePrompt(baseConfig); + expect(prompt).toContain("public/_examples/"); + }); +}); diff --git a/src/agent/prompt-assembler.ts b/src/agent/prompt-assembler.ts index 69ef5e8..c0e4fb4 100644 --- a/src/agent/prompt-assembler.ts +++ b/src/agent/prompt-assembler.ts @@ -3,6 +3,9 @@ import { join } from "node:path"; import type { PhantomConfig } from "../config/types.ts"; import type { EvolvedConfig } from "../evolution/types.ts"; import type { RoleTemplate } from "../roles/types.ts"; +import { buildInstructions } from "./prompt-blocks/instructions.ts"; +import { buildSecurity } from "./prompt-blocks/security.ts"; +import { buildUIGuidanceLines } from "./prompt-blocks/ui-guidance.ts"; export function assemblePrompt( config: PhantomConfig, @@ -124,51 +127,11 @@ function buildEnvironment(config: PhantomConfig): string { lines.push(""); lines.push("Schedule types: one-shot (at), interval (every N ms), cron (weekdays at 9am)."); lines.push(""); - lines.push("You can create web pages and serve them on your domain:"); - lines.push("- Write HTML files to the public/ directory (they're served at /ui/)"); - lines.push("- Use the base template at public/_base.html for consistent styling"); - lines.push("- The base template includes Tailwind v4, DaisyUI v5, Inter font, light/dark themes"); - lines.push("- Light mode is the default. Theme toggle is in the navbar. Users can switch."); - lines.push("- For charts, add ECharts CDN. A pre-configured Phantom chart theme is in the base template."); - lines.push(" Use echarts.registerTheme() with window.phantomChartTheme.light or .dark, or use"); - lines.push(" window.getPhantomChartTheme() to get the current theme name. For diagrams, add Mermaid CDN."); - lines.push("- To give the user access, use phantom_generate_login to create a magic link"); - lines.push("- Send the magic link to the user via Slack. They click it, get authenticated."); - lines.push( - "- IMPORTANT: Never wrap URLs in asterisks, bold, or any formatting. URLs must be plain text so Slack renders them as clickable links without corrupting the token.", - ); + lines.push("To give a user access to a /ui/ page, call phantom_generate_login to create a magic link"); + lines.push("and send the link to them via Slack. The link must be sent as plain text without any"); + lines.push("Markdown wrapping (no asterisks, no bold, no parentheses) so Slack renders it cleanly."); lines.push(""); - lines.push("When creating web pages, follow these design guidelines:"); - lines.push("1. PAGE MODE. For simple pages (no CDN libraries): use phantom_create_page with title+content."); - lines.push(" For pages with charts/diagrams (ECharts, Mermaid, D3): use phantom_create_page with the html"); - lines.push(" parameter for FULL page control. The content parameter injects inside
, which breaks"); - lines.push(" CDN script loading (race conditions, empty charts). Copy the base template structure from"); - lines.push(" public/_base.html and put CDN scripts in , app scripts at bottom of ."); - lines.push("2. SCRIPT PLACEMENT. CDN . +// Replaces [data-agent-name], [data-agent-name-initial], [data-agent-name-lower] +// nodes with the deployed agent name and substitutes {{AGENT_NAME_CAPITALIZED}} +// in any template. +// +// Mirrors the server-side capitalizeAgentName contract: empty/whitespace name +// falls back to "Phantom" so the brand never reads as blank. Paints an +// optimistic value from localStorage (or "Phantom") on load, then swaps when +// /health resolves so warm loads have no flash and cold loads see "Phantom" +// instead of a stray   until the fetch resolves. +(function () { + function cap(name) { + if (!name) return "Phantom"; + var trimmed = String(name).trim(); + if (!trimmed) return "Phantom"; + return trimmed + .split(/([-_])/) + .map(function (part) { + if (part === "-" || part === "_") return part; + if (!part.length) return part; + return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); + }) + .join(""); + } + + var titleEl = document.querySelector("title[data-agent-name-title]"); + var titleTemplate = titleEl ? titleEl.getAttribute("data-agent-name-title-template") : ""; + if (titleEl && !titleTemplate) { + var initial = titleEl.textContent || ""; + if (initial.indexOf("{{AGENT_NAME_CAPITALIZED}}") !== -1) { + titleTemplate = initial; + titleEl.setAttribute("data-agent-name-title-template", initial); + } + } + + function apply(name) { + var display = cap(name); + var initial = display.charAt(0).toUpperCase(); + var lower = display.toLowerCase(); + document.querySelectorAll("[data-agent-name]").forEach(function (el) { + el.textContent = display; + }); + document.querySelectorAll("[data-agent-name-initial]").forEach(function (el) { + el.textContent = initial; + }); + document.querySelectorAll("[data-agent-name-lower]").forEach(function (el) { + el.textContent = lower; + }); + if (titleEl && titleTemplate) { + titleEl.textContent = titleTemplate.split("{{AGENT_NAME_CAPITALIZED}}").join(display); + } + try { + if (name) { + localStorage.setItem("phantom-agent-name", name); + } + } catch (e) {} + } + + var cached = ""; + try { + cached = localStorage.getItem("phantom-agent-name") || ""; + } catch (e) {} + apply(cached || "Phantom"); + + fetch("/health", { credentials: "same-origin" }) + .then(function (r) { + return r.ok ? r.json() : null; + }) + .then(function (d) { + if (d && d.agent) apply(d.agent); + }) + .catch(function () {}); +})(); diff --git a/public/_base.html b/public/_base.html index f161998..8e38cc1 100644 --- a/public/_base.html +++ b/public/_base.html @@ -783,6 +783,14 @@ color: var(--color-primary); background: color-mix(in oklab, var(--color-primary) 8%, transparent); } + .phantom-nav-date { + display: none; + } + @media (min-width: 640px) { + .phantom-nav-date { + display: inline; + } + } .phantom-breadcrumb { display: inline-flex; align-items: center; @@ -1009,7 +1017,7 @@ <span class="phantom-breadcrumb">{{TITLE}}</span> <div style="margin-left:auto;display:flex;align-items:center;gap:12px;"> - <span class="phantom-mono phantom-muted" style="display:none;" class="sm:inline">{{DATE}}</span> + <span class="phantom-mono phantom-muted phantom-nav-date">{{DATE}}</span> <button id="theme-toggle" class="phantom-chip" aria-label="Toggle theme" style="padding:6px 10px;"> <svg id="icon-sun" style="width:14px;height:14px;display:none;" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"/></svg> <svg id="icon-moon" style="width:14px;height:14px;" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"/></svg> diff --git a/public/_components.html b/public/_components.html index 2f1ed60..14473e0 100644 --- a/public/_components.html +++ b/public/_components.html @@ -3,31 +3,10 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> -<title data-agent-name-title>Components - Living Style Guide +{{AGENT_NAME_CAPITALIZED}} components - Living Style Guide - + @@ -39,8 +18,8 @@ --motion-fast:100ms; --motion-base:150ms; --motion-slow:300ms; --ease-out:cubic-bezier(0.25,0.46,0.45,0.94); } -[data-theme="phantom-light"] { --color-base-100:#faf9f5; --color-base-200:#ffffff; --color-base-300:#ece9df; --color-base-content:#1c1917; --color-primary:#4850c4; --color-primary-content:#ffffff; --color-success:#16a34a; --color-error:#dc2626; --color-warning:#ca8a04; --color-info:#2563eb; color-scheme:light; } -[data-theme="phantom-dark"] { --color-base-100:#0b0a09; --color-base-200:#161412; --color-base-300:#26211d; --color-base-content:#f7f6f1; --color-primary:#7078e0; --color-primary-content:#0b0a09; --color-success:#4ade80; --color-error:#f87171; --color-warning:#fbbf24; --color-info:#60a5fa; color-scheme:dark; } +[data-theme="phantom-light"] { --color-base-100:#faf9f5; --color-base-200:#ffffff; --color-base-300:#ece9df; --color-base-content:#1c1917; --color-primary:#4850c4; --color-primary-content:#ffffff; --color-success:#16a34a; --color-error:#dc2626; --color-error-content:#ffffff; --color-warning:#ca8a04; --color-info:#2563eb; color-scheme:light; } +[data-theme="phantom-dark"] { --color-base-100:#0b0a09; --color-base-200:#161412; --color-base-300:#26211d; --color-base-content:#f7f6f1; --color-primary:#7078e0; --color-primary-content:#0b0a09; --color-success:#4ade80; --color-error:#f87171; --color-error-content:#0b0a09; --color-warning:#fbbf24; --color-info:#60a5fa; color-scheme:dark; } html { transition: background-color 150ms ease, color 150ms ease; } body { background:var(--color-base-100); color:var(--color-base-content); font-family:Inter,system-ui,sans-serif; margin:0; min-height:100vh; font-variant-numeric:tabular-nums; -webkit-font-smoothing:antialiased; } @@ -86,7 +65,7 @@ .phantom-badge-warning { background:color-mix(in oklab, var(--color-warning) 14%, transparent); color:var(--color-warning); } .phantom-badge-info { background:color-mix(in oklab, var(--color-info) 12%, transparent); color:var(--color-info); } -.phantom-chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; padding:4px 10px; border:1px solid var(--color-base-300); border-radius:var(--radius-pill); background:transparent; color:var(--color-base-content); cursor:pointer; transition:all 100ms; } +.phantom-chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; padding:4px 10px; border:1px solid var(--color-base-300); border-radius:var(--radius-pill); background:transparent; color:var(--color-base-content); cursor:pointer; transition:border-color var(--motion-fast) var(--ease-out), background-color var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out); } .phantom-chip:hover { border-color:color-mix(in oklab, var(--color-primary) 40%, var(--color-base-300)); background:color-mix(in oklab, var(--color-primary) 4%, transparent); } .phantom-chip[aria-pressed="true"] { background:color-mix(in oklab, var(--color-primary) 10%, transparent); border-color:color-mix(in oklab, var(--color-primary) 45%, var(--color-base-300)); color:var(--color-primary); } @@ -113,7 +92,7 @@ .phantom-button-primary { background:var(--color-primary); color:var(--color-primary-content); } .phantom-button-ghost { background:transparent; color:var(--color-base-content); border-color:var(--color-base-300); } .phantom-button-ghost:hover { background:color-mix(in oklab, var(--color-base-content) 5%, transparent); opacity:1; } -.phantom-button-danger { background:var(--color-error); color:#fff; } +.phantom-button-danger { background:var(--color-error); color:var(--color-error-content); } .phantom-button-sm { font-size:13px; padding:7px 14px; } .phantom-alert { display:flex; gap:var(--space-3); align-items:flex-start; padding:var(--space-3) var(--space-4); border:1px solid var(--color-base-300); border-radius:var(--radius-md); background:var(--color-base-200); font-size:13px; line-height:1.5; } @@ -123,7 +102,7 @@ .phantom-alert-info { border-color:color-mix(in oklab, var(--color-info) 40%, var(--color-base-300)); background:color-mix(in oklab, var(--color-info) 6%, transparent); } .phantom-tabs { display:inline-flex; gap:2px; padding:3px; background:color-mix(in oklab, var(--color-base-content) 5%, transparent); border-radius:var(--radius-md); } -.phantom-tab { font-size:13px; font-weight:500; padding:7px 14px; border-radius:var(--radius-sm); color:color-mix(in oklab, var(--color-base-content) 65%, transparent); background:transparent; border:0; cursor:pointer; transition:all 100ms; } +.phantom-tab { font-size:13px; font-weight:500; padding:7px 14px; border-radius:var(--radius-sm); color:color-mix(in oklab, var(--color-base-content) 65%, transparent); background:transparent; border:0; cursor:pointer; transition:background-color var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out), box-shadow var(--motion-fast) var(--ease-out); } .phantom-tab[aria-selected="true"] { background:var(--color-base-200); color:var(--color-base-content); box-shadow:0 1px 3px rgba(0,0,0,0.06); } .phantom-chat-bubble-user { align-self:flex-end; max-width:72%; padding:var(--space-3) var(--space-4); background:var(--color-primary); color:var(--color-primary-content); border-radius:var(--radius-lg); border-bottom-right-radius:4px; font-size:14px; line-height:1.55; } diff --git a/public/_examples/01-landing.html b/public/_examples/01-landing.html index 965f1db..2cfd9fd 100644 --- a/public/_examples/01-landing.html +++ b/public/_examples/01-landing.html @@ -3,31 +3,10 @@ -  +{{AGENT_NAME_CAPITALIZED}} - + diff --git a/public/_examples/02-login.html b/public/_examples/02-login.html index f152974..d4c3da0 100644 --- a/public/_examples/02-login.html +++ b/public/_examples/02-login.html @@ -3,27 +3,10 @@ -Sign in +Sign in - {{AGENT_NAME_CAPITALIZED}} - + diff --git a/public/_examples/03-dashboard-overview.html b/public/_examples/03-dashboard-overview.html index 732df77..3f5271c 100644 --- a/public/_examples/03-dashboard-overview.html +++ b/public/_examples/03-dashboard-overview.html @@ -3,18 +3,10 @@ -Dashboard overview +{{AGENT_NAME_CAPITALIZED}} overview - + @@ -201,8 +193,18 @@

System health

})(); - + @@ -47,7 +39,7 @@ .phantom-row { display:flex; align-items:center; gap:var(--space-3); flex-wrap:wrap; } -.phantom-chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; padding:5px 12px; border:1px solid var(--color-base-300); border-radius:var(--radius-pill); background:transparent; color:var(--color-base-content); cursor:pointer; transition:all 100ms; } +.phantom-chip { display:inline-flex; align-items:center; gap:6px; font-size:12px; padding:5px 12px; border:1px solid var(--color-base-300); border-radius:var(--radius-pill); background:transparent; color:var(--color-base-content); cursor:pointer; transition:border-color var(--motion-fast) var(--ease-out), background-color var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out); } .phantom-chip:hover { border-color:color-mix(in oklab, var(--color-primary) 40%, var(--color-base-300)); background:color-mix(in oklab, var(--color-primary) 4%, transparent); } .phantom-chip[aria-pressed="true"] { background:color-mix(in oklab, var(--color-primary) 10%, transparent); border-color:color-mix(in oklab, var(--color-primary) 45%, var(--color-base-300)); color:var(--color-primary); } diff --git a/public/_examples/05-evolution-timeline.html b/public/_examples/05-evolution-timeline.html index b622013..72a6d92 100644 --- a/public/_examples/05-evolution-timeline.html +++ b/public/_examples/05-evolution-timeline.html @@ -3,18 +3,10 @@ -Evolution timeline +{{AGENT_NAME_CAPITALIZED}} evolution - + diff --git a/public/_examples/06-memory-explorer.html b/public/_examples/06-memory-explorer.html index 3726ada..fe619a4 100644 --- a/public/_examples/06-memory-explorer.html +++ b/public/_examples/06-memory-explorer.html @@ -3,18 +3,10 @@ -Memory explorer +{{AGENT_NAME_CAPITALIZED}} memory - + @@ -49,7 +41,7 @@ .phantom-card-compact:hover { border-color:color-mix(in oklab, var(--color-primary) 28%, var(--color-base-300)); } .phantom-tabs { display:inline-flex; gap:2px; padding:3px; background:color-mix(in oklab, var(--color-base-content) 5%, transparent); border-radius:var(--radius-md); margin-bottom:var(--space-6); } -.phantom-tab { font-size:13px; font-weight:500; padding:7px 14px; border-radius:var(--radius-sm); color:color-mix(in oklab, var(--color-base-content) 65%, transparent); background:transparent; border:0; cursor:pointer; transition:all 100ms; font-family:Inter,sans-serif; } +.phantom-tab { font-size:13px; font-weight:500; padding:7px 14px; border-radius:var(--radius-sm); color:color-mix(in oklab, var(--color-base-content) 65%, transparent); background:transparent; border:0; cursor:pointer; transition:background-color var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out), box-shadow var(--motion-fast) var(--ease-out); font-family:Inter,sans-serif; } .phantom-tab[aria-selected="true"] { background:var(--color-base-200); color:var(--color-base-content); box-shadow:0 1px 3px rgba(0,0,0,0.06); } .phantom-input { width:100%; box-sizing:border-box; font-family:Inter,sans-serif; font-size:14px; line-height:1.4; background:var(--color-base-200); color:var(--color-base-content); border:1px solid var(--color-base-300); border-radius:var(--radius-md); padding:10px 14px; transition:border-color 150ms, box-shadow 150ms; } diff --git a/public/_examples/07-skills-editor.html b/public/_examples/07-skills-editor.html index 00c5768..723097c 100644 --- a/public/_examples/07-skills-editor.html +++ b/public/_examples/07-skills-editor.html @@ -3,18 +3,10 @@ -Skills editor +{{AGENT_NAME_CAPITALIZED}} skills - + diff --git a/public/_examples/08-cost-report.html b/public/_examples/08-cost-report.html index 56b0611..f1d05f1 100644 --- a/public/_examples/08-cost-report.html +++ b/public/_examples/08-cost-report.html @@ -3,18 +3,10 @@ -Cost report +{{AGENT_NAME_CAPITALIZED}} cost report - + @@ -170,8 +162,21 @@

Top 10 most expensive })(); - + diff --git a/public/_examples/10-chat-active.html b/public/_examples/10-chat-active.html index b879716..2a8bca1 100644 --- a/public/_examples/10-chat-active.html +++ b/public/_examples/10-chat-active.html @@ -3,18 +3,10 @@ -Chat +Chat with {{AGENT_NAME_CAPITALIZED}} - + diff --git a/public/index.html b/public/index.html index 965f1db..2cfd9fd 100644 --- a/public/index.html +++ b/public/index.html @@ -3,31 +3,10 @@ -  +{{AGENT_NAME_CAPITALIZED}} - + diff --git a/scripts/install-phantom-ui-skill.sh b/scripts/install-phantom-ui-skill.sh index c0043ed..dd4d679 100755 --- a/scripts/install-phantom-ui-skill.sh +++ b/scripts/install-phantom-ui-skill.sh @@ -1,12 +1,13 @@ #!/usr/bin/env bash # Installs the phantom-ui skill seed into the user-level Claude Agent SDK -# skills directory. The seed is shipped inside the repo at -# local/2026-04-12-phantom-ui-chapter/scratch/02-project2/phantom-ui-skill.md -# and gets copied into ~/.claude/skills/phantom-ui/SKILL.md so the live agent -# discovers it once Project 3 wires settingSources to include 'user'. +# skills directory. The seed is tracked in this repo at +# scripts/skills/phantom-ui.md and gets copied into +# ~/.claude/skills/phantom-ui/SKILL.md so the live agent discovers it once +# the runtime wires settingSources to include 'user'. set -euo pipefail -SOURCE_FILE="local/2026-04-12-phantom-ui-chapter/scratch/02-project2/phantom-ui-skill.md" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SOURCE_FILE="${SCRIPT_DIR}/skills/phantom-ui.md" TARGET_DIR="${HOME}/.claude/skills/phantom-ui" TARGET_FILE="${TARGET_DIR}/SKILL.md" diff --git a/scripts/skills/phantom-ui.md b/scripts/skills/phantom-ui.md new file mode 100644 index 0000000..08507ec --- /dev/null +++ b/scripts/skills/phantom-ui.md @@ -0,0 +1,198 @@ +--- +name: phantom-ui +description: Phantom visual design system and component vocabulary +when_to_use: Use when creating or updating a page served at /ui/. Examples. "make a dashboard", "create a report page", "design a landing", "show me a cost chart". +allowed-tools: Read, Glob +--- + +# Phantom UI skill + +You are authoring a page in Phantom's operator dashboard surface. The pages you create are served at `/ui/` behind magic-link cookie auth. This skill is your reference material for the design system. It is not rails. You are free to write any HTML you want. This skill tells you what the house style looks like when you feel like reaching for it. + +## Direction in one sentence + +Warm cream light theme paired with a warm deep dark theme, Instrument Serif display face over Inter Variable UI text, indigo accent at `#4850c4` light and `#7078e0` dark, 1px borders everywhere, zero default shadow chrome, tabular numerics on every number, pill-shaped primary buttons, 100 to 150 millisecond transitions, `color-mix(in oklab)` for every tint. + +The aesthetic is informed by the Anthropic console and API docs. Warm cream background, serif headings, restraint in chrome. Linear and Raycast for the dark theme near-black proportions. Stripe for tabular numerics discipline. Not a copy of any of them; a distinctive Phantom synthesis. + +## Use the base template + +`public/_base.html` declares all the tokens and all the vocabulary classes. When you call `phantom_create_page` with `title` and `content`, the content is wrapped in the base template and inherits everything for free. You do not need to redeclare any CSS in the content. Just use the class names. + +When you need a full custom `` (for page-specific CDN scripts like ECharts or Mermaid), use `phantom_create_page` with the `html` parameter and copy the structure from `_base.html` so you inherit the tokens and the vocabulary. + +## The reference style guide + +`/ui/_components.html` is the living style guide. Every vocabulary pattern renders there in both themes with a label. When you want to remember what a pattern looks like, open that file with Read. It is the closest thing to a Figma library file you have. + +## Vocabulary, grouped by category + +### Layout primitives + +- `phantom-page` - max-width 1240px container with 24px vertical padding, 32px horizontal padding +- `phantom-page-narrow` - max-width 760px variant for editorial and single-column pages +- `phantom-section` - adds 40px margin-bottom for vertical rhythm +- `phantom-row` - horizontal flex with 16px gap, centered items +- `phantom-col` - vertical flex with 16px gap +- `phantom-grid-stats` - 4-column responsive grid that collapses to 2 on mobile +- `phantom-grid-main-side` - 2/3 + 1/3 grid that collapses to 1 column on mobile +- `phantom-grid-cards` - auto-fill grid with 280px minimum card width +- `phantom-divider` - 1px horizontal rule in the base-300 color + +### Typography + +- `phantom-display` - serif hero heading, 44-60px clamp, weight 400, use `` for italic +- `phantom-h1` - serif 32px weight 500 for page titles +- `phantom-h2` - serif 22px weight 500 for section titles +- `phantom-h3` - sans 14px weight 600 for card labels +- `phantom-eyebrow` - uppercase tracked 11px label above titles +- `phantom-lead` - 16-17px body for opening paragraphs +- `phantom-body` - standard 14px body text +- `phantom-muted` - color modifier for secondary text +- `phantom-mono` - JetBrains Mono 12px with tabular numerics +- `phantom-meta` - 12px metadata like timestamps + +### Cards + +- `phantom-card` - 1px border, 14px radius, 20px padding, no default shadow +- `phantom-card-compact` - 10px radius, 12px padding for denser contexts +- `phantom-card-hover` - adds subtle hover shadow and cursor pointer + +### Stats + +- `phantom-stat` - vertical flex container +- `phantom-stat-label` - uppercase tracked 11px label +- `phantom-stat-value` - 28px weight 500 tabular-nums number +- `phantom-stat-value-serif` - optional serif variant for editorial stats +- `phantom-stat-trend-up` / `-down` / `-flat` - colored trend indicator below the value + +### Tables + +- `phantom-table` - uppercase header cells, tabular numerics in every body cell, row hover with indigo tint +- `phantom-table-compact` - denser variant with smaller padding and 12px body + +### Badges and chips + +- `phantom-badge` - pill chip, default neutral +- `phantom-badge-primary` / `-success` / `-warning` / `-error` / `-info` / `-neutral` - color variants +- `phantom-chip` - togglable filter chip with `aria-pressed` support + +### Status indicators + +- `phantom-dot` - 6px status dot +- `phantom-dot-success` / `-warning` / `-error` / `-info` - color variants +- `phantom-dot-live` - pulsing green dot for live indicators + +### Timeline + +- `phantom-timeline` - vertical timeline with left rule and ring markers +- `phantom-timeline-item` - single entry, includes time/title/body subelements + +### Empty states + +- `phantom-empty` - centered empty state with dashed border +- `phantom-empty-icon` / `-title` / `-body` - structured subelements + +### Forms + +- `phantom-form-row` - label + input pair with consistent spacing +- `phantom-label` - 12px weight 500 label text +- `phantom-input` / `phantom-textarea` / `phantom-select` - 10px radius fields with 3px focus ring +- `phantom-button` - pill primary button, dark on cream light, indigo dark +- `phantom-button-primary` - indigo accent variant +- `phantom-button-ghost` - outlined secondary variant +- `phantom-button-danger` - red destructive variant +- `phantom-button-sm` - compact variant + +### Alerts and toasts + +- `phantom-alert` - inline banner with 1px border and subtle tinted background +- `phantom-alert-success` / `-error` / `-warning` / `-info` - color variants +- `phantom-toast` - floating notification with shadow + +### Modals and sheets + +- `phantom-modal-backdrop` - fixed backdrop with blur +- `phantom-modal` - centered modal with 20px radius +- `phantom-sheet` - side-slide panel + +### Navigation + +- `phantom-nav` - top navigation bar with sticky and blur +- `phantom-nav-brand` - serif wordmark with logo +- `phantom-nav-item` - nav link with active state via `aria-current` +- `phantom-breadcrumb` - breadcrumb row with separator +- `phantom-tabs` / `phantom-tab` - segmented control tabs + +### Chat (Project 4 seeds) + +- `phantom-chat-bubble-user` - right-aligned user message in primary color +- `phantom-chat-bubble-assistant` - left-aligned assistant message +- `phantom-chat-tool-card` - monospace inline tool call indicator +- `phantom-chat-thinking` - animated typing indicator + +### Charts + +- `phantom-chart` - ECharts container, 320px default height +- `phantom-chart-sm` - 160px height variant +- `phantom-chart-lg` - 480px height variant +- Always use `window.phantomChart(el, option)` to init ECharts. The helper reads the current theme, registers the chart, adds a resize handler, and watches the theme attribute so the chart redraws on toggle. Reduces chart boilerplate from ~20 lines to 3. + +## Token reference + +Every semantic token is declared in `_base.html`. You reference by name. + +Colors (both themes): +- `--color-base-100` - page background (cream in light, warm near-black in dark) +- `--color-base-200` - card surface +- `--color-base-300` - border +- `--color-base-content` - primary text +- `--color-primary` - indigo accent +- `--color-success` / `--color-warning` / `--color-error` / `--color-info` - status accents + +Spacing: +- `--space-1` = 4px, `--space-2` = 8px, `--space-3` = 12px, `--space-4` = 16px, `--space-5` = 20px, `--space-6` = 24px, `--space-8` = 32px, `--space-10` = 40px, `--space-12` = 48px, `--space-16` = 64px + +Radii: +- `--radius-sm` = 8px (chips) +- `--radius-md` = 10px (inputs, buttons in rectangular mode) +- `--radius-lg` = 14px (cards) +- `--radius-xl` = 20px (modals) +- `--radius-pill` = 9999px (primary buttons, badges, chips) + +Motion: +- `--motion-fast` = 100ms (buttons, nav, hover) +- `--motion-base` = 150ms (card borders, inputs) +- `--motion-slow` = 300ms (layout transitions, modal reveal) +- `--ease-out` = `cubic-bezier(0.25, 0.46, 0.45, 0.94)` + +## Taste calibration + +Five things to hold in mind when you author a page: + +**One.** Make every dashboard as dense and crisp as Linear, as restrained as Anthropic console, as typographically clean as Notion, and as numerically tabular as Stripe. Never as cluttered as Jira, never as loud as Vercel marketing, never as branded as anyone trying to sell a product. You are building an operator tool for the person who owns the agent. The aesthetic is serious and quiet. + +**Two.** When you use a stat card, pair it with a trend indicator underneath. Bare numbers are prototype energy. `phantom-stat-value` plus `phantom-stat-trend-up` with "+22% 7d" is professional. + +**Three.** When you write a number anywhere, tabular numerics unless prose context demands otherwise. A table column of costs where `$2.34` and `$19.02` do not vertically align is immediately noticeable and immediately cheap-looking. `font-variant-numeric: tabular-nums` is already on the body default; keep it there. + +**Four.** When you use a chart, use the helper `window.phantomChart(el, option)` instead of raw `echarts.init` plus manual theme wiring. The helper gives you theme-aware redraw for free. Also, keep charts below 1-2 per page. Three charts on one page is a dashboard generator, not a considered surface. + +**Five.** When in doubt, open `/ui/_components.html` with Read and look at how the pattern you need is already rendered. The style guide is your memory. Do not freehand a chip when `phantom-chip` exists. Do not freehand a table when `phantom-table` exists. Do freehand a unique layout when the page calls for one; raise the floor, touch the ceiling. + +## Always validate + +After `phantom_create_page` returns, always call `phantom_preview_page` with the same path. Review the screenshot. Read the JSON metadata block. If `console.errors > 0` or `network.failedRequests.length > 0`, fix the HTML and re-preview until both are zero. Only then report the page to the user. + +The acceptance bar for any page you ship is: zero console errors, zero failed network requests, the theme toggle works in both directions, the layout does not collapse at 900px, and the surface reads at the quality of the reference examples in `/ui/_examples/` (once the builder publishes those). + +## Non-negotiables + +- No em dashes in body text. Use commas, periods, or regular hyphens. +- No emojis in body text. +- No hardcoded hex colors. Always `var(--color-*)`. +- No `bg-primary/10` slash opacity in `text/tailwindcss` blocks (Tailwind v4 Browser CDN parser bug). Use `color-mix(in oklab, var(--color-primary) 10%, transparent)` instead. +- No inline styles except for one-off micro-adjustments that would not make a good vocabulary class. +- No `"); + expect(out).not.toContain(""); + expect(out).toContain("<script>"); + }); + + test("does not re-scan substituted values for placeholder tokens", () => { + // Second-order injection check: if a substituted value happens to + // itself contain a placeholder token, the single-pass regex must not + // recurse and rewrite it on a second pass. A title value that looks + // like "{{AGENT_NAME_INITIAL}}" should land in the rendered title as + // that literal string (escapeHtml leaves braces alone), while the + // real initial slot in the navbar template still resolves to "C". + const out = wrapInBaseTemplate("{{AGENT_NAME_INITIAL}}", "

hi

", "cheeks"); + // The literal survives in the title position. + expect(out).toContain("{{AGENT_NAME_INITIAL}}"); + // The navbar agent name slot is still substituted cleanly. + expect(out).toContain("Cheeks"); + }); +}); diff --git a/src/ui/html.ts b/src/ui/html.ts new file mode 100644 index 0000000..b774921 --- /dev/null +++ b/src/ui/html.ts @@ -0,0 +1,11 @@ +// Minimal HTML entity escape for the five characters that matter in +// quoted attributes and element content. Used by server-side page +// generators to defend against operator-supplied brand strings. +export function escapeHtml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/src/ui/login-page.ts b/src/ui/login-page.ts index d86b232..e318656 100644 --- a/src/ui/login-page.ts +++ b/src/ui/login-page.ts @@ -5,7 +5,8 @@ // module-level setter wired from src/index.ts at startup so this file stays // callable from src/ui/serve.ts with no signature change. -import { capitalizeAgentName } from "./name.ts"; +import { escapeHtml } from "./html.ts"; +import { agentNameInitial, capitalizeAgentName } from "./name.ts"; let configuredAgentName = "Phantom"; @@ -15,12 +16,14 @@ export function setLoginPageAgentName(name: string): void { export function loginPageHtml(): string { const displayName = capitalizeAgentName(configuredAgentName); + const safeName = escapeHtml(displayName); + const safeInitial = escapeHtml(agentNameInitial(displayName)); return ` -Sign in - ${displayName} +Sign in - ${safeName}