git:20260711.9b4beab to git:20260715.27375c1

3 added, 1 removed. Audit A to A.

# web/src — AGENTS.md
React 19 SPA for Astonish Studio. Mixed **TypeScript (`.ts`/`.tsx`)** and **legacy JSX (`.jsx`)** — new files should be `.tsx`.
## Scope
- Entry: `main.tsx` (React root) and `App.tsx`.
- `components/` — React components (mix of `.tsx` and `.jsx`).
- `api/` — SPA fetch/SSE client for the Go `/api/*` surface.
- `sandbox-runtime.ts` — sandboxed iframe runtime for Apps (generative UI).
## Conventions
- **Language**: `.tsx` for new UI files unless mirroring an existing `.jsx` neighbor. `npm run build` runs `tsc --noEmit` — do not commit code that fails it.
- **Components**: functional + hooks, single per file, `export default` for the main component.
- **State**: React hooks only. **No Redux/Zustand/Jotai/etc.** Cross-cutting state uses React Context sparingly.
- **Styling**: Tailwind CSS v4 with `var(--variable-name)` theming (`index.css`).
- **Imports**: external first, local second. Named exports preferred for utilities.
- **Handlers**: CamelCase, prevent default on forms, cleanup in `useEffect`.
- **Lint**: ESLint in `web/eslint.config.js`. Separate blocks for `{js,jsx}` and `{ts,tsx}` — do not merge them.
- **Testing**: Vitest (`npm test`).
## Non-negotiable invariants
### StudioChat.tsx (report vs. artifact rendering)
`StudioChat.tsx` implements the client side of the **three-signal report gate**. A markdown artifact renders inline (`EmbeddedFileViewer`) **only** when all three signals hold:
1. Emitted in the last turn (after the most recent user message).
2. `fileType === 'Markdown'`.
3. `isReport === true` (set by the backend when an `` ```astonish-report `` fence's `path:` matched the artifact path).
- Anything failing any one of these renders as the compact `ArtifactCard`. **Do not widen the gate in the SPA.** If the backend regresses the marker, fix it in `pkg/api/chat_runner.go`, not by weakening the client check.
+ Anything failing any one of these renders as the compact `ArtifactCard`. **Do not widen the markdown report gate in the SPA.** If the backend regresses the marker, fix it in `pkg/api/chat_runner.go`, not by weakening the client check.
+
+ Last-turn **Video** artifacts (e.g. `browser_stop_recording`) also embed via `EmbeddedFileViewer` with a native player — separate from the markdown report contract. FilePanel / EmbeddedFileViewer must not fetch video as text; use `fetchArtifactBlob` + `<video>`.
Authoritative reference: `docs/architecture/chat-rendering-pipeline.md`, "The Report Pipeline" section.
### Generative UI (Apps) sandbox
The Apps runtime runs user-described React apps in a sandboxed iframe with an opaque origin, communicating with the parent via `postMessage` and a SSRF-protected server-side proxy. Do not remove the iframe boundary or the origin isolation.
Authoritative reference: `docs/architecture/generative-ui.md`.
### SSE consumer contract
Every SSE event type produced by `pkg/api/chat_runner.go` has a matching handler in `StudioChat.tsx` (and related components). If you add an event type on the backend, add its handler here and add a scenario fixture. If you rename an event type, update both sides in the same commit.
Fleet SSE (`fleet_*` events from `FleetSessionStreamHandler`) follows the same same-commit rule. New events such as `fleet_agent_started` / `fleet_agent_finished` / `fleet_task_*` / `fleet_mailbox_delivered` must land with their Studio handlers together.
### FleetExecutionPanel parallel lanes
When `maxParallelAgents ≤ 1` (or no `lane_index >= 0` events), keep the single-column Perplexity timeline. Multi-column lanes are only for parallel sessions — do not force a multi-column layout onto serial fleets.
## When editing
1. Adding a new component? `.tsx`, functional + hooks, keep it under 300 lines. Extract if it grows.
2. Adding a new SSE event? See the "SSE consumer contract" above.
3. Adding a new page? Register it in the router, wire up any needed API call in `api/`, and cover it with a Vitest test.