UI chrome
- Replace the unstyled 'hook-preview' classes with Tailwind utilities layered
over the theme-aware VS Code CSS variables; Tailwind CDN is loaded from
panel.ts so user render props can use utilities too.
- Render section is now visually unmistakable: 'Rendered output' label,
sky-blue framed card with a 'render() →' chip, and a caption explaining
the frame. Caption + sidebar row follow the same visual language.
- Controls + form fields converted to Tailwind (ActionControls,
CoAgentStateControls, InterruptControls, RenderToolControls,
CustomMessageControls, RawJsonField, StringField, NumberField,
BooleanField, FormRenderer).
- JSON textarea styled to match regular inputs: same bg/border/padding,
minHeight floor, tab-size: 2, ligatures off, spellcheck off, 'JSON' pill.
Sidebar UX swap
- Clicking a render-row now previews the hook (the primary action); the
hover-button opens source (`</>` glyph). Data-row click still opens
source because they have no preview.
Load flow robustness
- Ready-handshake in panel.ts: buffer sends in `pendingMessages` until the
webview posts `{type:'ready'}`, then flush. Fixes 'Waiting for scan…'
that was caused by postMessage racing the webview's module load.
- Check fs.existsSync before bundleHookSite so a deleted-fixture path
surfaces a clean in-webview error instead of a rolldown UNRESOLVED_ENTRY
toast.
- Assign `window.__copilotkit_hookSite = undefined` between bundle loads
(rolldown's `var` declaration is non-configurable so `delete` throws).
- Gate Harness mount on a real hostRoot: render a 'Loading bundle…'
placeholder until the bundle finishes, otherwise the placeholder HostRoot
publishes an empty registry past the gate.
- Strip function values (onRespond / onResolve) before postMessage —
structured-clone can't serialize callbacks.
- Reset controls/respondValue/resolveValue in the load handler so a cross-
kind switch can't leave the previous hook's shape under a new kind's
control component.
- Top-level PreviewErrorBoundary with a resetKey-driven
componentDidUpdate recovery: any crash from the render prop, controls,
or descendants shows a readable message and auto-clears when the user
picks a different hook. Boundary does not remount descendants, so
AppInner's message listener stays attached and incoming 'load's aren't
lost on reset.
- FormRenderer filters undefined fields defensively and falls back to
an empty values object on first render tick.
Capture-only CopilotKit stubs
- Externalize @copilotkit/react-core and /v2, resolve at runtime to a
Proxy-backed stubs object that captures every hook call into
window.__copilotkit_captured and renders providers as Fragment
pass-throughs. Avoids dragging the chat/runtime-client/markdown graph
through rolldown IIFE (which was the __commonJSMin TDZ root cause).
- RegistryReader fans hook captures into the right slot (renderToolCalls,
tools, coAgentStateRenders) and keeps a `byHook` bucket so findConfig
can look up anonymous/nameless hook variants.
Hook registry coverage
- useComponent and useDefaultTool retagged from category:'data' to
category:'render' with correct renderProps. Both have render props in
real react-core; they're just thin wrappers over useFrontendTool and
useCopilotAction respectively.
- useCopilotAuthenticatedAction_c, useHumanInTheLoop, useDefaultRenderTool,
useComponent, useDefaultTool added to RENDER_TOOL_CALL_HOOKS in the
registry-reader so their captures route correctly.
Weather-themed fixtures (14 hook instances covering every V1 + V2 render hook)
- useCopilotAction: addLocation + removeLocation (WeatherActions), severeAlert
with imported CSS (SevereWeatherAlert + severe-alert.css)
- useCopilotAuthenticatedAction_c: publishAlert (AdminIssueAlert)
- useCoAgentStateRender: forecast_agent (ForecastAgent)
- useLangGraphInterrupt: LocationPermissionInterrupt
- useRenderTool: getWeather (WeatherTool)
- useRenderToolCall: viewRadar (WeatherRadar)
- useDefaultRenderTool: defaultWeatherFallback (DefaultWeatherRender)
- useLazyToolRenderer: historicalTemperatures (LazyHistoricalChart)
- useRenderCustomMessages: WeatherCustomMessage
- useRenderActivityMessage: WeatherActivityMessage
- useHumanInTheLoop: confirmEvacuation (ConfirmEvacuation)
- useInterrupt: UnitsPreferenceInterrupt
- useFrontendTool: precipitationGauge (PrecipGaugeFrontendTool)
- useComponent: sunTimes (WeatherComponent)
- useDefaultTool: DefaultWeatherCatchAll
Each fixture uses a distinct visual (gradient cards, severity-palette
alerts, forecast strip, radar grid, conic-gradient gauge, permission
dialogs) so the preview exercises a broad range of layouts.
Tests
- Integration test enumerates all 17 expected hook sites and bundles every
fixture file; capture test uses the new synthetic interrupt name.
ControlsDispatch (and its adapters — ActionControls, etc.) read fields
off `values` like `values.status`. `controls` is seeded via useEffect
after `config` lands, so the first render post-capture had
`controls === null` → 'Cannot read properties of null (reading status)'.
Add a short 'Preparing controls…' placeholder between the config arrival
and the next render tick when the seeding effect has run.
Reload race: after a 'load' message App called setHostRoot(null) + setPayload(new).
The first render showed Harness with a placeholder HostRoot (() => null).
RegistryReader's useEffect fired against an empty capture array, published
an empty registry, and the !hostRoot || !registry gate let the next render
past it before the real component ever ran — so the user saw 'was not
captured' even when the bundle was valid.
Split the gate: render a 'Loading bundle…' placeholder while hostRoot is
null, mount Harness only once it's set, then wait on registry. The real
component now always gets at least one render pass, populating
window.__copilotkit_captured before RegistryReader reads it.
Also remove the temporary diagnostic console.logs and drop the stray
excalidraw.log I accidentally staged earlier.
Temporary diagnostics to narrow down why the capture registry appears
empty in the real webview even though jsdom E2E tests (stub + Harness +
React render) pass. Will be removed once the flow is confirmed.
Rolldown's IIFE emits `var __copilotkit_hookSite = …` at the top level,
which binds a non-configurable property on `window`. The reload-cleanup
path tried to `delete` it, which throws in strict mode. Assigning
`undefined` has the same semantics for our read-below check (which
already treats `undefined` as "not set") without the throw.
The previous harness imported `@copilotkit/react-core` and mounted a real
`<CopilotKit>` provider inside the webview. That dragged the full
chat/runtime-client/markdown dep graph through rolldown's IIFE output,
which wraps CJS modules in `__commonJSMin`. Under the circular-import
graph this triangle produces, the wrappers hit a TDZ at runtime:
Cannot access 'require_clipboard' before initialization
Cannot access 'require_graphql' before initialization
Cannot access 'require_context_helpers' before initialization
(and every other chunk whose factory assignment lost its race with its
first caller). Every targeted fix only moved the failure to the next
chunk.
Pivot: the preview never needed a live CopilotKit runtime — we only ever
wanted the `render` prop the user passed to their hook, driven by a
form over its declared parameters. So:
- Externalize `@copilotkit/react-core` and `@copilotkit/react-core/v2`
in the hook bundler; map both globals to
`__copilotkit_deps.copilotkitStubs`.
- Add `copilotkit-stubs.ts`: a Proxy-backed object that captures every
hook call into `window.__copilotkit_captured`, renders CopilotKit/
CopilotKitProvider as Fragment pass-throughs, and returns null
components / noop functions for anything else — so the bundle never
fails on unknown exports.
- Harness drops the `<CopilotKit>` wrapper and the fetch interceptor;
RegistryReader reads the capture array in a useEffect and shapes it
into the existing `CapturedRegistry` layout.
- Tests rewritten against the stub path (no real react-core import).
Result: TodoActions.tsx bundle is 1.3KB with zero `__commonJSMin`
wrappers, down from 24MB with 1000+ `require_" identifiers. The
tradeoff is explicit in the stub file: hook-level state that needs a
real runtime (e.g. useCopilotChat returning live messages) isn't
available here — add a specific stub return value if a preview needs it
rather than leaning on the Proxy fallback.
The runtime IIFE bundler (used when previewing a user's hook site) had no
workspace-source aliasing — so @copilotkit/react-core → @copilotkit/shared
and → @copilotkit/runtime-client-gql both landed on the CJS dist, which
rolldown wraps in __commonJSMin. Under the circular-import graph that flows
through the chat/runtime-client/shared triangle that wrapping trips a TDZ:
Cannot access 'require_clipboard' before initialization
Cannot access 'require_graphql' before initialization
Mirrors the workspaceSourceAliases fix already applied in tsdown.config.ts
for the extension's own webview bundles: pre-compute a spec → src/index.ts
map by resolving each workspace package's package.json via extensionRequire,
then short-circuit resolveId before Node fallback. Only applied when the
src file exists on disk (monorepo checkout); for marketplace installs where
users pull these packages from npm the CJS dist is the only thing on disk,
so the alias silently falls through — that production path is a separate
concern tracked outside this change.
Bundled CJS libs (lucide-react, some radix internals) keep verbatim
require("react") calls inside __commonJSMin wrappers. Rolldown's
globals config rewrites ESM-style external refs but leaves those
require() calls alone, and the browser has no global require — so
the IIFE threw 'require is not defined' at first execution.
Install a window.require shim before appending the script that maps
the React externals to the already-imported modules and warns + returns
{} for unknown specifiers, so a stray transitive require doesn't take
the whole preview down.
Two-part fix that together make 'node_path is not defined' go away.
Problem 1: hook-bundler passed skipSpecifierPrefixes: ["node:", "vscode"].
The iife-bundler resolver's skipPrefixes check ran BEFORE the isBuiltin
stub, so "node:path" short-circuited to null. Rolldown then externalized
node:path as an IIFE parameter — the bundle opened with
(function(exports, react, ..., node_path, node_process, node_url) { ... })
and the webview call site only passed exports+react globals, leaving
node_path undefined. ReferenceError at first access.
Fix A: move the isBuiltin stub BEFORE the skipPrefixes check so both
"path" and "node:path" resolve to the virtual empty/shim module. Clean
up hook-bundler's skipSpecifierPrefixes to just ["vscode"] since "node:"
is no longer needed as a skip hint.
Problem 2: with Node builtins stubbed, rolldown's package-condition
resolver picks the BROWSER condition for subpath exports like
vfile's '#minurl'. The browser variant only exports { isUrl }, but
vfile/lib/index.js imports { urlToPath, isUrl }. MISSING_EXPORT errors
block the build — previously hidden because externalizing Node builtins
kept a node-ish resolution mode.
Fix B: stub the unreachable markdown chain (vfile, stringify-entities,
parse-entities, character-entities*, decode-named-character-reference,
hast-util-to-html) with a Record<string, string[]> that enumerates each
dep's named exports so rolldown's named-export analysis succeeds. Same
pattern as tsdown.config.ts's HOOK_PREVIEW_STUBBED_DEPS for the static
webview bundle.
Adds a regression test that bundles TodoActions.tsx and asserts no
`node_<builtin>` identifier is referenced without a declaration.
Prior behaviour marked transitive Node builtins ("path", "fs", etc.) as
external during per-hook source bundling. In IIFE format without a globals
map entry, rolldown emits:
var node_path = node_path;
…which throws 'node_path is not defined' the moment the IIFE initialises
inside the hook-preview webview. The user sees it as a Mount error.
Route builtins to virtual stub modules instead (same pattern as the
tsdown.config.ts stub for the static webview bundle):
- crypto → WebCrypto-backed shim (randomFillSync/randomBytes/randomUUID)
- anything else → empty module (export default {})
Browser-reachable code paths either never call the missing API or hit the
crypto shim and work. Code that does try to call e.g. path.join now fails
at the call site with a clearer error rather than an opaque ReferenceError
at module init.
Prior stub resolved 'crypto' to an empty module; that broke the uuid npm
package's v4 path (used by react-core's ThreadsProvider on first render):
Uncaught TypeError: (0, crypto_1.randomFillSync) is not a function
at rng → v4 → randomUUID → ThreadsProvider
Replace the empty stub with a tiny shim that forwards the three APIs we
actually see called at module-init / first-render time (randomFillSync,
randomBytes, randomUUID) to globalThis.crypto. The webview always has
WebCrypto available. Other crypto APIs (hashes, ciphers, key derivation)
still default to the empty stub since none of the transitive deps we
bundle hit them; extend the shim if that changes.
Second __commonJSMin TDZ hit:
Uncaught ReferenceError: Cannot access 'require_graphql' before initialization
Same shape as the clipboard fix in d551a2e77. runtime-client-gql's CJS
dist wraps the `graphql` npm lib (which is itself CJS) — with circular
imports through react-core's runtime client, the outer `require_graphql =
__commonJSMin(...)` declaration is accessed before its initializer runs.
TS source uses direct ESM imports that rolldown orders cleanly, so the
forward reference TDZ window doesn't open.
The button copied a made-up composite key (hook::name) that wasn't useful
for any realistic workflow — bug reports, codebase search, and Slack shares
all work better with the file path + line number already shown on the row.
Dropping it trims the hover area to just the ▷ preview button on render
hooks. Removes the copyIdentity case from the bridge type, view-provider
handler, and callback map. The copilotkit.hooks.copyIdentity palette
command stays registered (the helper still exists) — zero UI surface, so
it doesn't contribute to the clutter the user flagged.
Default interaction on a hook row was surprising — clicking a render hook
spawned the preview panel unprompted, while clicking a data hook opened
the source. Align both categories on the same default (open source on
click) and keep the preview action behind the dedicated ▷ button that
appears on hover for render hooks. Also drops the redundant hover
'open source' button since the row click already does that — removes
visual clutter the user flagged in the prior feedback.
The new HookListViewProvider replaces the TreeDataProvider entirely,
and the old view-provider.ts had no remaining imports after activate-
hook-explorer was migrated. Its tests covered buildTreeData /
findLeaf / statusKeyForSite, all of which now live in tree-model.test.ts
alongside the new groupSitesByHook coverage.
Replaces the native TreeView for copilotkit.hooks with a React webview
that mirrors the AG-UI Inspector sidebar. Registered hooks are shown
prominently as collapsible groups; HOOK_REGISTRY entries with zero call
sites are tucked behind a "Show available hooks" toggle so the main
view stays quiet. Uses VS Code CSS variables and Tailwind-via-CDN.
- HookListViewProvider implements vscode.WebviewViewProvider with a
typed bridge contract in hook-list-bridge-types.ts, replays sites on
the webview's `ready` signal, and forwards preview/openSource/
copyIdentity/refresh back to injected callbacks.
- New pure groupSitesByHook helper (in tree-model.ts) splits registry
entries into registered / available, covered by tree-model.test.ts.
- activate-hook-explorer keeps the five copilotkit.hooks.* commands for
the command palette; each now accepts an optional HookCallSite and
falls back to a QuickPick picker when invoked without args.
- tsdown.config.ts emits dist/webview/hook-list.js (no stubbing needed:
the bundle doesn't pull in @copilotkit/react-core).
@copilotkit/shared ships a CJS dist (utils/clipboard.cjs among others)
that rolldown wraps in __commonJSMin lazy-init closures. With circular
imports through react-core's markdown/chat chain, those closures get
accessed before their `let require_clipboard = ...` declaration has run,
producing a TDZ error at runtime:
Uncaught ReferenceError: Cannot access 'require_clipboard' before initialization
Symptom: the hook-preview webview never renders anything — the module
graph fails to initialize before React can mount.
Fix: add @copilotkit/shared to workspaceSourceAliases so rolldown reads the
TS source directly and skips the CJS wrapper, matching the existing
workaround for @copilotkit/a2ui-renderer.
Root cause: HostRoot was computed via useMemo([payload]) that read
window.__copilotkit_hookSite. That global is set by executeBundle inside a
separate useEffect([payload]). React runs useMemo during render and
useEffect after commit, so on the first render with a new payload:
1. setPayload(X) triggers re-render
2. useMemo runs, reads the stale global → returns null
3. Render commits showing 'Mounting host…'
4. useEffect fires, executeBundle writes the global — no state change
5. Harness mounts with () => null; RegistryReader fires setRegistry
6. Re-render: useMemo deps unchanged → returns CACHED null
7. UI stuck forever
mount-capture.test.tsx didn't catch this because it imports the fixture
directly and bypasses the bundle-then-resolve flow.
Fix:
- Extract the export-picking logic into resolve-host-root.ts (pure, unit
tested) so future readers see the precedence order (default → first
function) clearly.
- In App.tsx replace HostRoot useMemo with hostRoot useState populated
inside the executeBundle useEffect. The state write after executeBundle
triggers the re-render that was missing before. Clear window.__copilotkit_hookSite
before each execute so a failed bundle can't serve a stale module.
- Wrap the setter's argument in () => fn because useState treats a
function argument as an updater; we want hostRoot to BE the function,
not the result of calling it.
Root cause: HARDCODED_EXCLUDES omitted the standard Jest/Vitest directory
conventions. When running the extension against its own worktree (or any
repo that colocates test fixtures with source), we scanned fixture files
under src/**/__tests__/fixtures/*.tsx and listed them as real hook sites,
polluting the sidebar tree with entries the user perceives as 'outside' the
workspace.
Add __tests__, __fixtures__, __mocks__ to the excluded directory set so
the conventional boundary between test fixtures and shipped code is
respected.
Secondary fix: onDidSaveTextDocument fires for any .ts/.tsx the user saves,
including files opened via File > Open from outside the active workspace.
Those saves would push new sites into the tree via updateSitesForFile.
Gate the save handler on isInsideWorkspace(filePath, workspaceRoot) so a
stray editor tab doesn't leak entries.
Previously schemaHint in HookBundlePayload was always {kind: 'none'} — the
webview received empty form fields and users couldn't edit args. The captured
config object in the registry already carries the real parameters (V1 array
or V2 Zod schema), so we infer the FormSchema from it directly in the webview.
Changes:
- New form/schema/infer-from-config.ts: inferFormSchemaFromConfig(config)
inspects config.parameters, branches to v1ParametersToFormSchema for
arrays, standardSchemaToFormSchema for objects with ~standard vendor, or
returns empty fields. Same detection rules as extractSchemaHint in the
extension host but applied to live runtime data.
- App.tsx: resolve captured config in a useMemo, derive schema from that
config, seed controls once the config is available. Dropped the unused
buildSchema() + schemaHint-based derivation. Removed the duplicate
findConfig call later in the component (now reads from the memo).
- 4 new tests cover V1 array, Zod, missing parameters, and garbage inputs.
Net effect: clicking a useCopilotAction or useRenderTool now renders the
real args form (string/number/boolean/enum inputs) instead of an empty
controls column.
- registry.ts: expose CapturedRenderFn, CapturedRenderToolCall, CapturedTool,
and CapturedCoAgentStateRender with explicit render/parameters/name fields
instead of ad-hoc { [key: string]: unknown } wildcards. Consumers (App.tsx,
the mount-capture test) can now read .render, .parameters, .name typed.
- mount-capture test: drop the duplicate CapturedAction type, use the new
CapturedRenderToolCall directly. Also add a removeTodo lookup assertion —
locks in that the reader captures both actions from the same component's
effect (catches regressions where only the first registration is seen).
The node-resolve-fallback plugin short-circuited all Node builtins to external,
overriding any override the caller might have wanted. Callers who want to
polyfill a builtin at runtime (e.g. pass 'buffer' through a browser shim via
the existing external list) now have the final say: if opts.external matches
the specifier, the plugin returns null and lets rolldown route the resolution
through the caller's external rule set.
The prior save handler triggered scanWorkspace() on every .ts/.tsx save, which
is O(workspace) per save — painful in large monorepos. Replace with:
- HookTreeDataProvider.updateSitesForFile(filePath): re-scan the saved file
only and splice its sites into the cached allSites list. Full rebuild of
the displayed tree happens from the flat list (cheap compared to walking
the filesystem).
- Per-file debounce map with a 250ms window: batches rapid-fire saves
(format-on-save followed by manual save, or auto-save streams) into a
single rescan per file. Cleared on extension deactivation.
No change to semantics beyond timing — the tree converges to the same state
as the prior full rescan, just without touching unrelated files.
- Extract HookNode / HookTreeStatus / buildTreeData / statusKeyForSite /
findLeaf into a new tree-model.ts that doesn't import vscode. Tests can
now exercise the pure model without vi.mock('vscode').
- view-provider.ts becomes the VS Code adapter layer (TreeDataProvider,
getTreeItem, event emitter). It re-exports the model types for source
compatibility with existing consumers.
- setStatus now fires _changeEmitter with the affected leaf instead of a
full refresh. VS Code re-reads only that leaf's getTreeItem, preserving
the user's expand/collapse state elsewhere in the tree.
- Tests: drop vi.mock, add coverage for findLeaf (hit + miss) and the key
format of statusKeyForSite (named vs line fallback). 163 tests total.
Previously the test mutated an outer-scope variable inside waitFor and
relied on the non-null assertion downstream. Refactor to return the
resolved value from waitFor — the testing-library idiom — so there's no
time-dependent outer state and no '!' assertion after the await.
Introduces a minimal CapturedAction type so the test stops carrying a
duplicate '{ render?: unknown; [k: string]: unknown }' cast inline.
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.
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.
- 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.
- 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.
- 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.