Skip to content

v0.20 PR 8: Landing page redesign (hero avatar, starter tiles, live pages list)#79

Merged
mcheemaa merged 7 commits intomainfrom
v0.20-pr-08-landing-redesign
Apr 17, 2026
Merged

v0.20 PR 8: Landing page redesign (hero avatar, starter tiles, live pages list)#79
mcheemaa merged 7 commits intomainfrom
v0.20-pr-08-landing-redesign

Conversation

@mcheemaa
Copy link
Copy Markdown
Member

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.

  1. Hero (2-column): 120x120 avatar tile (letter fallback at 72px Instrument Serif italic in primary), eyebrow + display title + lead, primary "Talk to <name>" -> /chat, ghost "Open dashboard" -> /ui/dashboard/.
  2. Agent status card (kept) with a small "Details ->" link to /health.
  3. What can <name> do? - 4 to 6 starter tiles fetched from GET /ui/api/starter-prompts. Each tile deep-links to /chat?prefill=<urlencoded>. Defaults ship in src/ui/starter-prompts.ts; operators override via phantom-config/starter-prompts.yaml (Zod-validated, strict, up to 6 tiles; falls back to defaults with a warning on any error).
  4. Pages <name> has created for you - live list from GET /ui/api/pages that walks public/ depth 3, excludes boilerplate (index.html, _base.html, _components.html, _agent-name.js, phantom-logo.svg, favicon.svg, robots.txt) plus dashboard/, _examples/, chat/. Top 10 by mtime desc. Empty state deep-links to /chat with a prefilled "build me a dashboard" prompt.
  5. Quick links slimmed from 3 to 2 (Dashboard + MCP). Health removed (now inline on the status card).

Also adds ?prefill=<urlencoded> handling to the chat SPA: on /chat mount we decode, cap at 2000 chars (ellipsis truncation with console warn if longer), seed the composer via a new ChatInput.initialText prop, 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 via textContent and encodeURIComponent to 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)
  • New: src/ui/api/__tests__/starter-prompts.test.ts (10 cases)
  • New: src/ui/api/__tests__/pages-api.test.ts (14 cases)

Manual (local visual harness against the new endpoints, verified):

  • Hero renders avatar letter at 120x120 in primary indigo italic
  • "Talk to <name>" -> /chat; "Open dashboard" -> /ui/dashboard/
  • Status card "Details ->" points to /health
  • Six starter tiles populate from /ui/api/starter-prompts
  • Click tile -> /chat?prefill=...; stub chat decodes and shows the prompt in the composer
  • Pages list renders 3 seeded HTML files sorted by mtime desc with <title> extracted and entities (&amp;, &middot;) decoded
  • Empty state appears when no agent-created pages exist, with prefilled deep-link
  • Quick links are two tiles (Dashboard + MCP); Health tile removed
  • Light theme + dark theme both render correctly
  • Mobile (380px): hero stacks avatar-above-copy, CTAs stack under 500px, tiles stack, pages rows stack
  • XSS check: malicious <title> payload stops at the first < in the regex (regex is [^<]), and titles flow into textContent with zero child nodes
  • No console errors on the landing page (a 404 on /ui/avatar is expected when no avatar is uploaded; the IIFE falls back to the letter)

Files

File Change
src/ui/starter-prompts.ts NEW: defaults + Zod schema + YAML loader
src/ui/api/starter-prompts.ts NEW: GET /ui/api/starter-prompts
src/ui/api/pages.ts NEW: GET /ui/api/pages walker
src/ui/api/__tests__/starter-prompts.test.ts NEW: 10 cases
src/ui/api/__tests__/pages-api.test.ts NEW: 14 cases
src/ui/serve.ts dispatcher wiring for both public endpoints
public/index.html five-section restructure
chat-ui/src/routes/chat-route.tsx ?prefill= handler
chat-ui/src/components/chat-input.tsx initialText prop
docs/landing.md NEW: schema + customization doc

LOC: +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.

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).
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread chat-ui/src/routes/chat-route.tsx Outdated
if (raw === null) return null;
let decoded: string;
try {
decoded = decodeURIComponent(raw);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@mcheemaa mcheemaa merged commit c439163 into main Apr 17, 2026
1 check passed
mcheemaa added a commit that referenced this pull request Apr 17, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant