Repository navigation
feat(web): postMessage agent status from /embed/thread route to iframe parent #3
Description
Goal
Extend the existing `/embed/thread/:environmentId/:threadId` route to broadcast agent status events to its iframe parent via `window.parent.postMessage`. Enables the host (t3-canvas) to render per-tile status chips so users can triage agents at a glance.
Context
t3-canvas embeds T3 Code threads as canvas tiles. Phase 4.5 over there needs each tile to show a live status indicator (running / awaiting input / error / etc). The cleanest source of truth is T3 Code itself — we know turn state, approvals, errors. The cheapest channel is `window.parent.postMessage` since the iframe and host are same-origin localhost.
Acceptance criteria
- EmbedThreadView (or wherever the route component lives) subscribes to the existing thread state in the store via the same hooks used by `_chat.$environmentId.$threadId.tsx`
- On every relevant state transition, post a message to `window.parent` with shape:
```ts
window.parent.postMessage({
source: "t3-code-embed",
type: "agent-status",
threadId: string,
environmentId: string,
status: "idle" | "streaming" | "awaiting-input" | "error" | "completed" | "disconnected",
timestamp: number,
// optional payload — leave room for future fields without breaking consumers
payload?: { errorMessage?: string; turnId?: string }
}, "*");
``` - Status mapping (start with these, refine if T3 Code's internal taxonomy differs):
- turn started / first delta arrives → `streaming`
- turn finished cleanly → `completed` (host will auto-fade to idle)
- approval/user-input request received → `awaiting-input`
- turn errored → `error` (include errorMessage in payload)
- no active turn AND last turn was not just completed → `idle`
- no thread or thread fetch failed → `disconnected`
- Initial post on mount with current state
- Cleanup on unmount: post a final `disconnected` so the parent knows the iframe went away
- Wrap the postMessage in a `source: "t3-code-embed"` discriminator so the host can ignore unrelated postMessage chatter
- Existing route behavior (rendering the conversation, no sidebar chrome) is unchanged
- No new dependencies — read state from existing stores/hooks only
Files (verify against current repo state)
- `apps/web/src/routes/embed.thread.$environmentId.$threadId.tsx` — add a useEffect that subscribes to thread state and emits postMessage on changes
- `apps/web/src/components/EmbedThreadView.tsx` (or wherever the conversation component is) — possibly the better location depending on how state is wired
Read `apps/web/src/routes/_chat.$environmentId.$threadId.tsx` to see how the main route subscribes to thread state. Reuse the same hooks (`useStore`, `createThreadSelectorByRef`, `selectEnvironmentState`).
Branch
`feat/embed-status-postmessage` (separate from any other in-flight work).
Non-goals
- Two-way postMessage (host → embed) — read only for now
- Authenticated postMessage origin checks — same-origin localhost only
- Wire status events for tools-use, model thinking deltas, etc. — keep the taxonomy minimal for v1
How to test
- `bun run dev` in t3code
- Build & run t3-canvas with the matching Phase 4.5 PR
- Create a thread in T3 Code, copy env+thread IDs, open via t3-canvas's New Agent dialog
- Send a turn → t3-canvas tile border should pulse blue (streaming) → settle green (completed) → fade gray (idle)
- Trigger an approval-required tool → border turns amber
Activity
rororowyourboat commentedon Apr 10, 2026
Implemented in feat/sidebar-project-groups — embed route at /embed/:threadId with postMessage status bridge.