feat(chat): chat-first layout with in-chat draft cards

Closed
RA
7h ago
+708-1236

What is this contribution about?

Studio becomes chat-first. The chat is the one surface that is always there, and every other screen (home, a project, the Site Editor) opens beside it. This PR ships the layout, the cleanup of the empty chat, a first in-chat card for drafts, and a message restyle. It also writes down the model the chat follows from here on, including how it should pick up a sandbox. The second half of that model is not implemented yet; see "Not in this PR".

Premises

  1. The chat is always open. It stays the same across refreshes, page changes and project changes. The main panel can expand over it, which is the only way it hides.
  2. The chat is not a project. You don't "enter a project and talk to it". There is one assistant, and what you're looking at is context for it, so it knows what "this" refers to.
  3. Work shows up where it happened. When the chat changes something, such as a site draft, that work appears in the conversation as a card, not in a header or a side list.
  4. People manage changes, not machines. Sandboxes are plumbing. Nobody starts, stops or "removes" one. People decide what happens to the changes: publish, discard or keep as a draft.

Sandbox heuristics (the model this PR documents)

  • Attach on the first change, not on the first message. Reading (analytics, files, context) never needs a sandbox. The first time the chat edits a project, it opens a fresh draft branch there.
  • Don't ask first, but say so. A draft is isolated and changes nothing live, so a confirmation would only be friction. Instead, the conversation gets a line ("Working on Project in draft name") and the draft card appears.
  • One draft per project per chat. A chat can touch several projects, each in its own draft. Later edits to the same project reuse that draft and update its card; they don't create a new one.
  • The sandbox lifecycle is invisible. It starts when the chat needs it, sleeps when idle, and wakes when the chat or the editor needs it again. Stopping it is never a user action.
  • "Remove" means deciding about the changes:
    • Publish (or open a PR): the changes go live and the card collapses to one line.
    • Discard: the draft is deleted and the card collapses.
    • Keep as draft (detach): the chat lets go, and the draft stays in the project like any other draft.
  • When the target is unclear, ask. If the chat has been working on one project and you are looking at another, "change the title" gets a question back, not a guess. The page you're on is a hint, not an order.

What ships here

Layout

  • The chat sits on the right and never closes.
  • The main panel header has ⤢ Expand (hides the chat) and ✕ Close (closes the main panel). Once expanded, the button reads ⤡ Show chat.
  • With the main panel closed, an Open panel button appears at the left of the chat header. A collapsed sidebar can also be reopened from there.
  • Expanding is a per-person preference that persists across pages and visits.
  • Dragging the divider stops at 320px; it never closes a panel.
  • The project setting "open the chat when opening this project" works again, and unset now means open. Projects imported from deco.cx are created with it on.

Empty chat

  • The agent identity screen (avatar, title, description) and the ice-breaker prompts are removed. An empty chat is just the composer.

Draft card (first version)

  • When the chat's thread has a hosted (Blocks v8) draft with at least one change against main, the conversation shows a card with:
    • the project
    • the draft name
    • Editing / Ready to publish
    • the change count
    • Open, which goes to the project's Site Editor on that draft
  • Where it goes: after the first answer that used a tool, or above the composer in an empty chat.
  • A strip above the composer (● Draft pending · Project) scrolls back to the card once it's out of view.
  • The changes are read straight from the hosted draft every 15s, so the card doesn't depend on the editor's runtime.

Messages

  • User messages are right-aligned bubbles, 85% max width and not sticky.
  • Queued messages are dashed bubbles with their time, Send now (first in queue only) and ✕.
  • The reply's time sits on the same line as its cost.
  • User and agent text use the same size, and all times share one size and color.
  • The chat header has less left padding, and the chat icon is gone from the conversation picker.

Home

  • "Connect your agent to Studio" moves from the header to a banner at the top of the home. It leaves once the organization has a key created from Settings › Connect.

Not in this PR

  • One chat across projects (premises 1 and 2). Today, opening a project still selects or creates that project's thread, and threads still run as the project's agent. Making the open chat global and turning the project into a context chip is the next step.
  • Sandbox on first change. The heuristics above are the spec. Today a project thread still owns its branch and sandbox from the start.
  • Publish / Discard / Keep as draft on the card. These still live in the Site Editor header, which Open leads to.
  • Card placement and git drafts. The card sits after the first answer that used a tool, which only approximates "the first change". It also only counts hosted (v8) drafts; sandbox/git drafts don't show a card yet.
  • Banner detection. It can't detect a connection made through OAuth, the default connect mode, so it keeps showing until a key exists.

How did you verify your code works?

  • Format, lint, knip: bun run fmt, bun run lint (oxlint) and bun run knip pass with no findings. tsc --noEmit passes for apps/web.
  • Web unit tests: bun test --parallel over apps/web/src (the verify test command scoped to web) gives 4968 pass and 356 fail.
    • Base comparison: the same command on the base commit gives 4980 pass and 356 fail, and the two failing lists are identical. In this environment the failures come from tests leaking globals into each other (window and storage). For example, seen-flag, mcp-oauth and host-styles pass when run on their own.
    • Count difference: the 12 fewer tests come from the rewritten layout test file.
  • Not run in full: bun run verify as a whole. Its test step also runs API integration tests against local services, which failed with migration and connection errors unrelated to this change, so I stopped it.
  • Updated tests: use-chat-layout-state.test.ts (17 pass). The test that asserted the project setting was ignored now asserts it is honored, including ?sidepanel=true overriding it. A new test checks that an unset setting opens the chat.
  • Manual checks, against a local Studio with a public demo storefront linked to a project:
    • Expand ↔ Show chat, Close ↔ Open panel, and reopening a collapsed sidebar with the panel closed.
    • The draft card shows the real change count after one block was changed on the draft.
    • The strip appears when there is a pending draft.
    • User and agent text both measure 14px in the browser.

Screenshots/Demonstration

Layout and the draft card. Expand and close are on the main panel; the chat stays on the right with the draft it changed.
Main panel with expand and close; chat with a draft card

Main panel expanded. The chat comes back from the same corner.
Expanded main panel with Show chat

Main panel closed. The chat reopens it from its header.
Chat alone with the open panel button

Messages
User bubble on the right; reply with cost and time on one line

Home banner
Connect banner at the top of the home

How to Test

  1. Open the home. The chat is on the right. Click ⤢ on the main panel: the chat hides and the button reads Show chat. Click it and the chat returns. Reload: the choice is kept.
  2. Click ✕ on the main panel. The chat fills the space and shows Open panel at the left of its header; click it to bring the panel back. Collapse the sidebar first and its button also appears there.
  3. Open a project with "open the chat" turned off in its settings: it opens with the chat hidden. Turn it on, or leave it unset, and the chat opens.
  4. Open a new chat. Only the composer shows, with no agent identity and no prompts.
  5. On a Blocks v8 project, change something in the Site Editor on the chat's draft, then return to the chat. A card shows the project, the draft and its change count, and Open goes back to that draft. With messages above it, a Draft pending strip sits over the composer.

Migration Notes

  • No migrations.
  • Projects that already saved "open the chat" as off now really open with the chat hidden; before, the setting was ignored.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed): the product guide for the chat should follow once one chat spans projects
  • No breaking changes: see Migration Notes; about 12 E2E specs still assume the old left-side chat toggle and need updating

🤖 Generated with Claude Code


Summary by cubic

Studio becomes chat-first: the chat stays open beside every page and only hides when the main panel expands over it. Expanding is a per-person preference that persists, and the project setting "open the chat" is honored again, with unset now meaning open.

  • The empty chat is just the composer — the agent identity screen and ice-breaker prompts are gone.
  • A chat that changed a hosted (Blocks v8) draft shows it as a card in the conversation with its change count and an Open button; a strip above the composer scrolls back to it once it's out of view. The change count is read from the hosted draft every 15s.
  • User messages are right-aligned bubbles, queued messages are dashed bubbles, and the reply's timestamp sits on the same line as its cost.
  • "Connect your agent" moves from the header to a home banner that leaves once a key exists.

For later: the sandbox heuristics (attach on first change, one draft per project per chat, publish/discard/keep as draft) are documented in this PR but not implemented yet. Opening a project still selects that project's thread, the card only approximates "the first change", and git drafts don't show a card. The banner can't detect OAuth connections.