Agent controls the whole look: validated theme spec, presets, layout, copy, chat, scoped CSS, protected fee/banner #4

Merged
leora merged 6 commits from feat/agent-theming into main 2026-10-08 19:41:37 +08:00
Owner

Feedback on the live demo: asked to change the background, the agent said it couldn't and showed the bike listing again. With this PR, the agent controls the whole look of the page.

What's in it

  • A new set_theme UI action, defined as a JSON theme spec of design tokens. It covers:
    • background: solid, gradient, pattern, or an image from an allowlist
    • palette and light/dark mode
    • fonts (19 curated) and font scale
    • corner radius, spacing density, card style
    • grid or list layout, and column count
    • copy: logo, headline, subhead, chips, bullets, footer
    • section order and visibility
    • chat position, size and colours
    • animations on/off
    • presets: retro, minimal, neon, pastel, high contrast, sunset, ocean
    • reset
  • A sanitised, scoped customCss escape hatch:
    • drops at-rules, !important, custom properties, positioning, transforms and content, script, expression() and HTML
    • neutralises url() to any host outside the allowlist
    • the server checks the CSS and the browser checks it again
  • Guardrails via CSS cascade layers (base < user < protected):
    • The TEST DEMO banner, the agent badge, and the checkout, cash and receipt panels (fees, totals, test-card notice, card inputs) can't be hidden or restyled.
    • Protected panels always use black or white ink, whichever contrasts more with a solid fill (at least 4.5:1).
    • Page text gets a readability scrim on busy gradients.
  • Per-visitor: the theme is stored in localStorage and sent with each chat turn as compact context. The server never stores it. There's a Reset look button in the header, and "reset the look" works in chat.
  • Prompt:
    • The model is told it controls the whole look. It must never say it can't change something visual, and it should offer the closest option when a request is out of bounds.
    • For a purely visual request it returns only set_theme.
    • Internal ids must never appear in the reply. The server also strips them as a backstop.
  • Free-tier resilience:
    • On the OpenRouter free-models-per-day 429, the whole chain is parked until the reset time from the header, instead of retrying every request.
    • The built-in agent now understands the common look commands, so restyling still works when the model's quota is used up.
    • The badge says honestly when the built-in agent answered.

Tests on staging (:8795)

  • test_theme.py: 19/19 passed.
  • llm_proxy_check.sh: 10/10 passed. This includes a mock model emitting a malicious set_theme, which gets cleaned, and the daily-quota path.
  • theme_check.js, chat-driven, run against the built-in agent: 19/19 passed. It covers the nine requested commands, guardrail contrast under sunset, light+green and retro, a fresh session seeing the default look, and reset.
  • e2e_http.py: 23/23 passed.
  • browser_e2e.js: 11/11 passed.

Still to do: the real model couldn't be exercised end to end. The free key hit OpenRouter's 50/day cap before this ran, and it resets at 00:00 UTC. The model path for set_theme is covered by the mock-model test.

Feedback on the live demo: asked to change the background, the agent said it couldn't and showed the bike listing again. With this PR, the agent controls the whole look of the page. **What's in it** - A new `set_theme` UI action, defined as a JSON theme spec of design tokens. It covers: - background: solid, gradient, pattern, or an image from an allowlist - palette and light/dark mode - fonts (19 curated) and font scale - corner radius, spacing density, card style - grid or list layout, and column count - copy: logo, headline, subhead, chips, bullets, footer - section order and visibility - chat position, size and colours - animations on/off - presets: retro, minimal, neon, pastel, high contrast, sunset, ocean - `reset` - A sanitised, scoped `customCss` escape hatch: - drops at-rules, `!important`, custom properties, positioning, transforms and `content`, script, expression() and HTML - neutralises `url()` to any host outside the allowlist - the server checks the CSS and the browser checks it again - **Guardrails** via CSS cascade layers (`base < user < protected`): - The TEST DEMO banner, the agent badge, and the checkout, cash and receipt panels (fees, totals, test-card notice, card inputs) can't be hidden or restyled. - Protected panels always use black or white ink, whichever contrasts more with a solid fill (at least 4.5:1). - Page text gets a readability scrim on busy gradients. - **Per-visitor:** the theme is stored in `localStorage` and sent with each chat turn as compact context. The server never stores it. There's a **Reset look** button in the header, and "reset the look" works in chat. - **Prompt:** - The model is told it controls the whole look. It must never say it can't change something visual, and it should offer the closest option when a request is out of bounds. - For a purely visual request it returns only `set_theme`. - Internal ids must never appear in the reply. The server also strips them as a backstop. - **Free-tier resilience:** - On the OpenRouter `free-models-per-day` 429, the whole chain is parked until the reset time from the header, instead of retrying every request. - The built-in agent now understands the common look commands, so restyling still works when the model's quota is used up. - The badge says honestly when the built-in agent answered. **Tests on staging (:8795)** - `test_theme.py`: 19/19 passed. - `llm_proxy_check.sh`: 10/10 passed. This includes a mock model emitting a malicious set_theme, which gets cleaned, and the daily-quota path. - `theme_check.js`, chat-driven, run against the built-in agent: 19/19 passed. It covers the nine requested commands, guardrail contrast under sunset, light+green and retro, a fresh session seeing the default look, and reset. - `e2e_http.py`: 23/23 passed. - `browser_e2e.js`: 11/11 passed. **Still to do:** the real model couldn't be exercised end to end. The free key hit OpenRouter's 50/day cap before this ran, and it resets at 00:00 UTC. The model path for `set_theme` is covered by the mock-model test.
leora added 6 commits 2026-10-08 19:41:33 +08:00
leora added 1 commit 2026-10-08 19:41:34 +08:00
leora merged commit c3d6871407 into main 2026-10-08 19:41:37 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: leora/agent-market#4
No description provided.