The prior 4 integration fixtures didn't import any CSS, so the iife-bundler
cssCollectorPlugin code path added in 225789220 was untested end-to-end.
Adds a 5th fixture (StyledAction.tsx) that imports a local .css file and an
assertion that the bundle result's css field contains the rule bodies.
Covers:
- CSS URL rewrite (.css → \0copilotkit-css virtual JS module)
- CSS file read via fs.readFileSync in the load handler
- Accumulation into the cssChunks array
- Join into the final css string returned alongside the JS bundle
Adds four hook fixtures (TodoActions, BasicAgent, InterruptDemo,
RenderToolDemo) under test-workspace/hooks and an integration test that
scans them and bundles each one end-to-end through the real iife bundler.
Also fixes two bundler blockers the test surfaced:
- Node builtins (os, crypto, stream, tty, ...) are now marked external so
transitive deps like supports-color and node-fetch no longer fail to
resolve inside the IIFE build.
- CSS imports are now collected via a virtual-JS plugin instead of relying
on rolldown's removed CSS bundling pipeline; the existing css return
field still surfaces the concatenated CSS for webview injection.
- Move the Hook Explorer wiring (output channel, tree, panel, scan, 5 commands)
out of activate.ts into hooks/activate-hook-explorer.ts. activate.ts drops
from 633 to 526 lines and goes back to being a thin composition root.
Matches the pattern of the other self-contained feature modules.
- focusPanel now filters leaves by leaf.category === 'render' instead of
string-matching g.label === 'Render hooks'. The label coupling was fragile
— if view-provider.ts changes the copy the command silently stops working.
Follow-up noted in JSDoc: onDidSaveTextDocument still triggers a full
scanWorkspace per save (O(workspace)). For large monorepos this will need
incremental scanFile + merge + debounce. Not blocking for MVP.
- langgraph-python: replace ephemeral pkg.pr.new URLs with stable @copilotkit/*@1.56.2
- mastra: wire dev to run ui + agent via concurrently (was only starting UI)
- Restore `|| scripts\setup-agent.bat` / `|| scripts\run-agent.bat` fallback across
12 starter templates so npm install + npm run dev work on Windows without Git Bash
Consolidate the repeated console.error + emitError + .catch pattern
into a single private logAndEmitError method on CopilotKitCore. All 4
call sites (setDefaultThrottleMs, subscribeToAgentWithOptions validation,
safeCall reportError, unsupported-keys warning) now go through the helper.
## Summary
- Reverts the `if content:` guard from c5ec0f8512 that skipped emitting
the assistant message when content was empty
- OpenAI models commonly send empty content for tool-call-only
responses, but the assistant message is structurally required as the
anchor for tool call grouping via `parentMessageId`
- Without it, tool calls become orphaned and the frontend can't
reconstruct tool call rendering on thread reconnect/page refresh
## Context
Commit `3b684d914b` correctly fixed the if/else bug by always emitting
the assistant message. The follow-up commit `c5ec0f8512` added an `if
content:` guard to avoid "spurious empty assistant messages" for
OpenAI-style tool calls, but this broke state reconstruction because
tool call entries reference the assistant message via `parentMessageId`.
When content is empty (standard OpenAI behavior for tool-call-only
responses), the assistant message was not created, leaving tool calls
with an orphaned `parentMessageId`.
If empty assistant speech bubbles are a concern, that should be handled
at the rendering layer rather than the data layer.
## Test plan
- [ ] Send a message that triggers tool calls with an OpenAI model
(where assistant content is empty)
- [ ] Verify tool calls render correctly in the chat
- [ ] Disconnect and reconnect to the same thread
- [ ] Verify tool call rendering is preserved after reconnect
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- pushBundle now increments a pushToken before awaiting bundleHookSite and
re-checks it after the await; a second show() or handleFileChange() that
races past (or a dispose during the await) silently drops the stale
result instead of posting to a disposed webview. Deterministic
last-writer-wins even when 'load' and 'reload' interleave.
- onMessage replaces the Record<string, unknown> cast with a discriminated
union + isWebviewMsg guard. Eliminates four unchecked casts.
- Drop dead currentNonce field; the nonce is only needed when generating
HTML and lives locally in show().
- Add CSP inline comment explaining the 'style-src unsafe-inline' tradeoff
(React runtime style injection + CopilotKit transitive CSS-in-JS).
- bundle-loader: inline scripts swallow exceptions thrown during execution;
they fire as window 'error' events instead. Capture that event during the
insert/remove window and re-throw so executeBundle's caller's try/catch
actually sees bundle-time failures. Also assert __copilotkit_hookSite is
set after execution — a malformed bundle now throws 'did not set hookSite'
instead of stalling at 'Mounting host…'.
- Extract ControlsDispatch: a dedicated component containing the single
kind-switch over render-props. App.tsx's 6-branch switch with 13 'as never'
casts collapses to one dispatcher call with two typed casts per branch
(localized and narrow).
- mountError now reaches the extension host via postMessage instead of only
setting local state. Panel host (Task 20) can log these.
Bundling @copilotkit/react-core for the hook-preview webview transitively
pulls in:
- node-fetch + its builtin deps (crypto, stream, string_decoder, zlib, http, ...)
- tailwind v4 CSS via the chat component styles
- streamdown / hast-util-to-html / decode-named-character-reference which
statically name-import from JSON modules rolldown can't re-export
Rolldown 1.0.0-rc.16 removed experimental CSS bundling and refuses to load
Node builtins in a browser target. None of these paths are exercised on the
hook-preview runtime (we render user JSX directly; there's no chat UI,
markdown, or server fetch in scope), so we stub them to empty modules at
bundle time via a rolldown plugin.
The stub emits the specific named exports dependents statically import,
keeping rolldown's named-export analysis happy. Extend HOOK_PREVIEW_STUBBED_DEPS
if additional markdown-chain deps surface.
Previously RenderToolControls passed the full {...values, toolCallId} down to
ActionControls and spread the onChange result back over values. If ActionControls
ever added a toolCallId field, the spreads would collide. Explicitly separate
the action-shape values from toolCallId and recombine only at the nested
onChange boundary.
The test_empty_list_returns_empty_content test expected empty-content
AIMessages to be filtered out, but the fix now always emits assistant
messages so tool calls can reference them via parentMessageId.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CapturedRegistry: drop the misleading v1.actions field. Both V1
useCopilotAction and V2 useRenderTool register into the same internal V2
registry; expose it directly as renderToolCalls. Keep coAgentStateRenders
and chatComponents (those are genuine V1 state) at the top level too.
Downstream consumers (Task 19+) will read renderToolCalls for any
action-shaped hook.
- Harness: widen HarnessBoundary to also cover RegistryReader so a reader
crash routes through onMountError instead of bubbling to React's default
handler. A reader crash should block capture publishing, not leak.
- capture.test.tsx: replace the 20ms fixed setTimeout wait with testing-
library waitFor. Polls for the final capture result until the action is
present; robust on slow CI.
- installFetchInterceptor now returns an uninstall function, so callers
(tests, harness teardown) have an explicit restore path rather than
racing against module-scoped mutation.
- Guard against stacked wrappers on double-install (relevant under React
StrictMode and HMR remounts): uses a Symbol tag so the second call is
a no-op and its disposer doesn't unwind the first.
- 4 tests: noop prefix match, passthrough, disposer restores original,
double-install is a no-op.
## Summary
- **Bug:** When using `selfManagedAgents`, calling `agent.setState()`
directly (from frontend UI interactions or `useFrontendTool` handlers)
never updates the CopilotKit inspector's "Current State" panel — it
stays permanently at the initial state.
- **Root cause:** `agent.setState()` fires `onStateChanged` on all
subscribers, but the WebInspector only subscribed to
`onStateSnapshotEvent` (which requires AG-UI events flowing through the
`runAgent()` pipeline). The inspector already subscribed to
`onMessagesChanged` to catch direct `addMessage()` calls — the same
pattern was missing for state.
- **Fix:** Add `onStateChanged` handler to the WebInspector's agent
subscriber, calling `syncAgentState(agent)` — 3 lines, mirroring the
existing `onMessagesChanged → syncAgentMessages` pattern.
- **Hardening:** Wrap `syncAgentMessages` and `syncAgentState` in
try/catch so a malformed agent payload can't crash the inspector.
## Context
Reported by
[@praveen.sivadasan](https://copilotkit.slack.com/archives/C0AC78NNBTR/p1776107637432809).
The user's workaround was to call `copilotkit.runAgent({ agent })` after
every `setState()` to force a `RUN_STARTED → STATE_SNAPSHOT →
RUN_FINISHED` cycle through the pipeline — functional but heavyweight
(creates run history, detaches active runs, etc.). With this fix, direct
`setState()` propagates to the inspector automatically.
## Test hygiene
Refactored the test file to reduce duplication and centralize
unavoidable casts:
- **`createInspectorWithCore(core)`** — replaces 3× inline inspector
setup (create → append → cast-assign core)
- **`getInternals(inspector)` / `getContextInternals(inspector)`** —
centralizes `as unknown as` casts for LitElement-private reactive
properties
- **`controller.simulateSetState(state)`** — encapsulates state mutation
+ `onStateChanged` emission, eliminating double-casts in the new test
- **`mockClipboard` promoted to describe scope** — removes navigator
clipboard double-cast from assertion
## Test plan
- [x] Added test: "syncs agent state on direct setState (onStateChanged
without pipeline events)" — creates a mock selfManagedAgent, calls
`controller.simulateSetState()`, asserts inspector `agentStates` map
reflects the update
- [x] All existing tests pass (`nx run web-inspector:test`)
- [x] Pre-commit hooks pass (full monorepo test suite + lint)
- [x] Manual: open inspector with a `selfManagedAgents` setup, call
`agent.setState()` from UI, verify "Current State" panel updates without
needing `copilotkit.runAgent()`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- ArrayField.add(): drop dead '?? null' fallback — defaultForField is total
for required fields, and we force required:true on the seed clone. Inline
comment explains the two separate uses of field.items (seed vs. render).
- primitive-fields.test.tsx: cover BooleanField checked→unchecked transition.
- load() now rejects non-object / array / null values from the Memento
(protects against hand-edited workspaceState containing junk) and returns
undefined in those cases.
- JSDoc on the class documents the caller's path-normalization contract.
- Adds 4 tests: reset on a non-existent key (no-op), workspace-root scoping
so two roots don't collide, save overwrites (does not merge), and the new
load-rejects-scalar-junk defensive branch.
- human-in-the-loop: inline the body (8 lines) instead of the double-cast
alias. Self-contained and avoids the 'as unknown as Adapter<K>' gymnastics.
- invokeRender: add JSDoc explaining why the ADAPTERS[kind] as Adapter<K>
cast is runtime-sound despite being unsound at the type level.
- Tests: add coverage for render-tool (args → parameters rename, toolCallId),
human-in-the-loop (matches action shape, status-gated result), custom-messages,
activity-message, and the no-render no-op path (9 tests total).
- v1-params: omit description field when undefined (cleaner output object);
recursion on [] suffix now naturally handles nested arrays like string[][];
add test for both.
- json-schema: JSDoc on JSONSchemaNode documenting MVP scope (no $ref, tuple
items, allOf/anyOf/oneOf); add tests for integer type and array without items.
- normalize: treat empty-enum as unconstrained instead of rejecting every
string (prevents a required field with enum:[] getting stuck); add comments
on the shallow array/object match; add tests for array/object/raw-json
defaults and the empty-enum path.
When a field emits undefined (e.g. NumberField with an optional field cleared),
previously the merged payload would contain { count: undefined } which
downstream render callbacks would see as a present-but-null property.
Now the key is removed entirely, matching the runtime contract for
'optional field not provided'.
Also adds three tests: nested object change, array add, and the
undefined-drop behavior.
- Empty textarea no longer clobbers prior value with {}. Fields representing
arrays, strings, or null would previously be silently overwritten; now empty
input means 'no change' and clears any prior parse error.
- Adds aria-invalid={!!error} on the textarea.
- Adds two tests: empty-input path (no onChange, no alert) and error-recovery
path (alert disappears + onChange fires with the new valid JSON).
- Documents the mount-only local-state contract so future callers aren't
surprised that external value resets don't propagate.
- NumberField: emit undefined for empty / non-finite input instead of silently
coercing to 0, matching the value: number | undefined type. Widens onChange
to (v: number | undefined) => void. Adds a test for the empty-input path.
- All three fields: drop redundant aria-label (wrapping <label> + htmlFor/id
provides the accessible name; the aria-label was double-announcing in
screen readers).
- Set native required attribute on input/select/checkbox so assistive tech
exposes the constraint alongside the (optional) suffix.
Previously bundler.ts (catalog) and hook-bundler.ts duplicated ~80 lines of
rolldown + nodeResolveFallback wiring. Extracts the common shell into
extension/shared/iife-bundler.ts exposing bundleIife() and IifeBundleResult.
bundler.ts and hook-bundler.ts are now thin wrappers supplying their specific
iifeName, externals, globals, and skip prefixes. Catalog and hook bundler
tests still pass unchanged; type aliases (BundleResult, HookBundleResult)
preserved for call-site source compatibility.
- Replaces the separate loadGitignore() walk + per-file isIgnored() loop with a
single directory walk that inherits gitignore scopes from parent to child.
For a workspace with G gitignore files and F source files, cuts syscalls
from O(2*(F+G)) to O(F+G) and ignore-checks from O(F*G) to O(F*depth).
- Drops unused ScanWorkspaceOptions.roots and unused rootDir param.
- HARDCODED_EXCLUDES is now a case-insensitive Set.