CoworkkitWatch is a drop-in developer panel you mount during a build to see what the agent perceives and did. It reads the same internal session state the on-screen button does — no new wire, no instrumentation on your side — and renders it as a floating, resizable panel over your app. Because it reads the Provider's context, mount it anywhere inside CoworkkitProvider; where it sits in the tree doesn't matter.
It's the fastest way to answer the two questions every integration raises: does the agent actually see the surfaces and elements I declared, and did my actions fire — and gate — the way I intended?
Drop it in
It ships as a deep import so it stays tree-shakeable — the panel and its code never reach a bundle that doesn't import it. Add one line to the Provider you already have:
"use client";
import { CoworkkitProvider } from "@coworkkit/react";
import { CoworkkitWatch } from "@coworkkit/react/watch";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<CoworkkitProvider
getToken={async () => {
const res = await fetch("/api/session", { method: "POST" });
if (!res.ok) {
// Check before parsing, and guard the parse: an error page (a proxy 404,
// a framework 500) is often HTML, and a throw here would lose both fields.
const body = await res.json().catch(() => ({}));
// Pass the reason AND the status through — that is what lets the FAB say
// "Setup needed" for a bad key instead of a generic "Can't connect":
throw Object.assign(new Error(body.error ?? "session mint failed"), {
reason: body.reason,
status: res.status,
});
}
return res.json();
}}
>
{children}
{/* Mounts only in development; ships nothing to a production build. */}
<CoworkkitWatch />
</CoworkkitProvider>
);
}Watch renders only in development, and it doesn't open on its own — it's a panel you toggle. Open it from the watch icon in the on-screen button's control row, or press ctrl+shift+k. If you've mounted it and don't see the watch icon, you're almost certainly on a production build, where Watch hides itself by default — that's expected, not a missing feature; It's a dev surface (below) shows how to force it on when you need it.
What it shows
A pinned State strip across the top — session phase, mic, Hand-mode arm state, the active surface, session duration and idle time — plus four tabs:
- Timeline — the live event stream, turn by turn: each action call, how it was gated (allowed, Hand-blocked, or held for a confirmation), and its outcome. This is where you watch an action actually fire, or catch the
needs_hand_modethat explains why it didn't. - Catalogue — the live declared-exposure snapshot: the current surface, every mounted element (including see-only ones), and the full action catalogue with each entry's
kindandcontrol. This is what proves the agent sees what you declared — and it marks the declared-vs-live distinction, so a snapshot taken before the agent joins isn't mistaken for "the agent already knows". - Transcript — the conversation, both sides, as the agent heard and spoke it.
- Metrics — per-turn latency, a turn table, and a session summary, for spotting a slow leg without reaching for a profiler.
It's a dev surface
Watch is gated to NODE_ENV === "development" by default — in a production build it mounts nothing, so leaving <CoworkkitWatch /> in your tree is safe. Two escapes exist for when you need it past dev: enableInProduction for a QA build, and visibleTo — a predicate over your own auth that must return true before anything renders (pair it with the redact policy to mask transcripts and tool I/O).
The header's copy and download buttons emit the whole session — state, timeline, transcript, metrics — as a single JSON blob. That's how you hand a teammate the exact observable state you're looking at: send them the export instead of narrating what you saw.