v0.20 PR 8: Landing page redesign (hero avatar, starter tiles, live pages list)#79
v0.20 PR 8: Landing page redesign (hero avatar, starter tiles, live pages list)#79
Conversation
Adds GET /ui/api/starter-prompts (public) for the landing-page "What can <name> do?" section. Defaults ship in-process; operators override via phantom-config/starter-prompts.yaml with a Zod-validated schema (icon/title/description/prompt, max 6 tiles). Cardinal Rule preserved: the loader copies bytes through. No content classification, no intent branching. Tiles are invitations; the agent decides once the prompt lands in the composer. Any YAML or schema failure warns and falls back to defaults so the landing page never renders blank.
Walks public/ up to depth 3, excludes boilerplate (index.html, _base.html, _components.html, _agent-name.js, phantom-logo.svg, favicon.svg, robots.txt) and the dashboard/, _examples/, chat/ directories. Extracts <title> from the first 8 KiB with an entity decoder covering the common named and numeric refs, caps at 120 chars, falls back to the filename when missing. Sorts by mtime desc, returns top 10 with Cache-Control: private, max-age=30. Public endpoint (no cookie gate): content is filenames the agent itself chose to publish.
Both endpoints are public (no cookie gate) so they render on the landing page pre-authentication. Added next to the /ui/avatar GET which has the same access profile.
Reads ?prefill= from window.location on the /chat route entry, decodes it, caps at 2000 chars (ellipsis truncation + console warn if longer), seeds the composer via a new ChatInput initialText prop, and clears the query from the URL with history.replaceState so a refresh does not re-prefill. Does NOT auto-submit: the operator reviews the pre-filled prompt and hits Send. This is a consent surface. The seed only runs once (seededRef) so later re-renders never stomp user edits. Wire contract with the landing page: /chat?prefill=<urlencoded>. Cardinal Rule: the prefill string is bytes, not intent. Agent decides what to do on submit.
…, slimmed quick links
Five sections, same design vocabulary:
1. Nav (unchanged) with the existing avatar slot.
2. Hero 2-column: 120x120 avatar tile (letter fallback at 72px
Instrument Serif italic in primary color) + eyebrow, display
title, lead, primary "Talk to <name>" -> /chat, ghost
"Open dashboard" -> /ui/dashboard/.
3. Agent status card (kept) with a small "Details ->" link to
/health in the header.
4. "What can <name> do?" - fetches /ui/api/starter-prompts,
renders 4 to 6 tiles with inline phosphor-style icons,
skeleton placeholder while loading, section hidden on fetch
error. Each tile deep-links to /chat?prefill=<urlencoded>.
5. "Pages <name> has created for you" - fetches /ui/api/pages,
renders rows with path, title, relative time; empty state
deep-links to /chat with a "build me a dashboard" prefill.
6. Quick links slimmed from 3 to 2 (Dashboard + MCP). Health
removed in favor of the status-card "Details" link.
XSS posture: every operator- or agent-controlled string flows into
the DOM via textContent or createElement. Icon SVGs are our own
assets keyed by name. Query strings are encodeURIComponent'd.
Responsive: hero stacks below 720px, CTAs stack below 500px, tiles
grid is auto-fit minmax(260px,1fr), pages rows stack below 640px.
Status-badge re-render hardened to textContent too (removes the
prior innerHTML template, which is belt-and-suspenders since the
values came from our own health payload).
Documents the five landing sections, the starter-prompts.yaml schema and its strict-schema fallback behavior, the supported icon keys, the public /ui/api/starter-prompts and /ui/api/pages endpoints, and the Cardinal Rule preservation note (tiles are invitations; the agent decides at submit time).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69483694bf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (raw === null) return null; | ||
| let decoded: string; | ||
| try { | ||
| decoded = decodeURIComponent(raw); |
There was a problem hiding this comment.
Remove redundant URI decode when reading prefill
URLSearchParams.get("prefill") already returns a decoded value, so applying decodeURIComponent again corrupts valid literal %xx text in prompts. For example, a starter prompt containing "%2F" is transformed to "/" before it reaches the composer, which silently changes user-authored content. This occurs whenever the prefill text contains percent-encoded-looking substrings without an invalid % sequence to trigger the catch path.
Useful? React with 👍 / 👎.
P2 (Codex): URLSearchParams.get() already percent-decodes, so layering decodeURIComponent on top was a double-decode that silently corrupted literal %xx sequences in operator-authored prompts. A tile prompt 'Fetch a %20 file' would surface in the composer as 'Fetch a file'. Use the URLSearchParams value directly. Keep the PREFILL_MAX cap and the console.warn on truncation.
Bumps the version to 0.20.0 in every place it's referenced: - package.json (1) - src/core/server.ts VERSION constant - src/mcp/server.ts MCP server identity - src/cli/index.ts phantom --version output - README.md version + tests badges - CLAUDE.md tagline + bun test count - CONTRIBUTING.md test count Tests: 1,799 pass / 10 skip / 0 fail. Typecheck and lint clean. No 0.19.1 or 1,584-tests references remain in source, docs, or badges. v0.20 shipped eight PRs on top of v0.19.1: #71 entrypoint dashboard sync + / redirect + /health HTML #72 Sessions dashboard tab #73 Cost dashboard tab #74 Scheduler tab + create-job + Sonnet describe-assist #75 Evolution Phase A + Memory explorer tabs #76 Settings page restructure (phantom.yaml, 6 sections) #77 Agent avatar upload across 14 identity surfaces #79 Landing page redesign (hero, starter tiles, live pages list)
Summary
The final feature PR of v0.20. Restructures
/ui/into five sections so a first-visit operator sees their agent, a clear "Talk to it" CTA, what it can do, and what it has already made.<name>" ->/chat, ghost "Open dashboard" ->/ui/dashboard/./health.<name>do? - 4 to 6 starter tiles fetched fromGET /ui/api/starter-prompts. Each tile deep-links to/chat?prefill=<urlencoded>. Defaults ship insrc/ui/starter-prompts.ts; operators override viaphantom-config/starter-prompts.yaml(Zod-validated, strict, up to 6 tiles; falls back to defaults with a warning on any error).<name>has created for you - live list fromGET /ui/api/pagesthat walkspublic/depth 3, excludes boilerplate (index.html, _base.html, _components.html, _agent-name.js, phantom-logo.svg, favicon.svg, robots.txt) plusdashboard/,_examples/,chat/. Top 10 by mtime desc. Empty state deep-links to/chatwith a prefilled "build me a dashboard" prompt.Also adds
?prefill=<urlencoded>handling to the chat SPA: on/chatmount we decode, cap at 2000 chars (ellipsis truncation with console warn if longer), seed the composer via a newChatInput.initialTextprop, and clear the query from the URL. The operator still has to hit Send. No auto-submit.Cardinal Rule preservation
Starter tile titles, descriptions, and prompts are static strings. The "Ask now" button opens
/chat?prefill=<encoded prompt>and the agent at run time decides what to do. No server-side classification. No client-side branching on content. The loader copies YAML bytes to the response; the frontend copies them viatextContentandencodeURIComponentto the chat composer.Test plan
Automated (green):
bun run lint(Biome, 341 files)bun run typecheck(tsc --noEmit)bun test(1,799 pass, 10 skip, 0 fail, 3,969 expect calls)cd chat-ui && bun run typecheck && bun run build(clean)src/ui/api/__tests__/starter-prompts.test.ts(10 cases)src/ui/api/__tests__/pages-api.test.ts(14 cases)Manual (local visual harness against the new endpoints, verified):
<name>" ->/chat; "Open dashboard" ->/ui/dashboard//health/ui/api/starter-prompts/chat?prefill=...; stub chat decodes and shows the prompt in the composer<title>extracted and entities (&,·) decoded<title>payload stops at the first<in the regex (regex is[^<]), and titles flow intotextContentwith zero child nodes/ui/avataris expected when no avatar is uploaded; the IIFE falls back to the letter)Files
src/ui/starter-prompts.tssrc/ui/api/starter-prompts.tsGET /ui/api/starter-promptssrc/ui/api/pages.tsGET /ui/api/pageswalkersrc/ui/api/__tests__/starter-prompts.test.tssrc/ui/api/__tests__/pages-api.test.tssrc/ui/serve.tspublic/index.htmlchat-ui/src/routes/chat-route.tsx?prefill=handlerchat-ui/src/components/chat-input.tsxinitialTextpropdocs/landing.mdLOC: +1,117 / -45 (ceiling 1,170, under).
Note
This is the final feature PR of the v0.20 chapter. The v0.20.0 tag + deploy is a follow-up, not part of this PR.