The aimock-parity paragraph and the Rollback section still described
contributors hand-editing both package.json and CHANGELOG.md. That was
stale — the vscode-extension-changelog-sync.yml workflow auto-syncs
package.json.version (and optionally package.json.description) from
the CHANGELOG entry (and README first paragraph) when the PR is opened
and commits 'chore: release vX.Y.Z' on the PR branch.
Rewrite both passages so the documented flow matches reality: contributor
edits CHANGELOG.md only (optionally README.md), sync workflow handles the
package.json bump automatically, publish workflow self-gates via vsce show
and ships on merge to main.
Rewrites the 'Cutting a release' section to match the new
changelog-sync automation: maintainers prepend a CHANGELOG entry,
commit with any message, open a PR, and the sync workflow auto-bumps
package.json with 'chore: release vX.Y.Z'. Adds a manual escape hatch
for when the sync workflow is unavailable.
Other sections (Prerequisites, Open VSX setup, CI publish flow,
Rollback, Notes) are unchanged.
Rewrite the 'Cutting a release' + 'CI publish flow' sections to describe
the aimock-pattern flow (hand-bump + PR-merge + self-gated CI publish)
and the new multi-bullet --summary/--type CLI. Retain the Open VSX
one-time-setup block, the transient-failure handling notes, verification
steps, rollback guidance, and ADO PAT retirement note.
Rewrite vscode-extension-release.sh to cut an aimock-style release commit
(bump package.json + prepend CHANGELOG.md section) without tagging or
pushing — CI now owns tag/release creation on main-push. Takes one or
more --summary bullets grouped by --type (Added/Changed/Fixed/Removed/
Deprecated/Security, Keep-a-Changelog style). Supports --dry-run.
Seed packages/vscode-extension/CHANGELOG.md with a retroactive v0.1.0
entry covering the three launch surfaces (A2UI Preview, Hook Explorer,
AG-UI Inspector) so the first real release has prior history to anchor
against and the GH Release step has notes to pull from.
Wrap both registry publish steps in a bash retry helper that retries up
to 5 times with staggered backoff (10s/20s/40s/60s/90s) on transient
conditions (5xx, timeouts, connection resets, DNS). Auth and validation
errors still fail fast with no retry.
Critically, 'version already exists' is treated as idempotent success:
if attempt N-1 landed on the registry but its response was lost to a
502 after commit, attempt N sees the already-published version and
short-circuits rather than failing the job.
Motivated by Open VSX /publish returning intermittent 502 Bad Gateway
errors from Eclipse Foundation infra. Each attempt is wrapped in
::group:: markers so per-attempt logs are collapsible in the Actions
UI. Reconciliation step updated to reflect retry semantics and to
tell the operator to rerun the job (not bump the version) on exhausted
retries. RELEASING.md gets a 'Transient registry failures' section
documenting the behavior for both CI and manual publish paths.
Covers prerequisites (VSCE_PAT + OVSX_PAT as production env secrets),
the one-time Open VSX namespace claim, how to cut a release via the
local helper script, the CI publish flow, verification URLs, rollback
guidance (registries are immutable, ship forward), and a note on the
2026-12-01 Azure DevOps PAT sunset.
- package.json: remove the view/title binding that put
'Open AG-UI Inspector' on the A2UI Catalog view — the inspector is
its own sidebar tab now, so adding that action to the catalog title
bar was redundant noise. Inspector view keeps its own binding.
- Both sidebar webviews (hook-list + catalog-list) had an in-webview
refresh button that duplicated the native VS Code title-bar refresh
icon already registered under view/title. Drop the React buttons
and rely on the native icons; single source of truth and cleaner
header.
Review-loop fixes on the VS Code extension surface:
Scanner / lens
- hook-scanner.ts: extract scanContent(filePath, content) so callers
with the live editor buffer can parse that instead of re-reading the
last-saved on-disk version. scanFile keeps the same on-disk semantics
via delegation.
- hook-scanner.ts: scanWorkspace now returns {sites, capped, filesScanned}
so MAX_FILES_SCANNED truncation is observable instead of silent.
- activate-hook-explorer.ts: when the cap trips, log the detail to the
Hook Explorer output channel and surface a user-facing warning toast
pointing at it. Tighten the save-path catch to log unexpected throws
instead of swallowing them.
- hook-lens-provider.ts: consume document.getText() via scanContent so
lenses follow unsaved edits. Output channel is now required and the
scanContent-throw path logs to it.
Panels (bounded queues + durable error trails)
- preview-panel.ts + hooks/panel.ts: pendingMessages now capped
(1000 and 32 respectively). A wedged webview (CSP violation, crashed
bundle, never-ready) used to grow these buffers unbounded on every
save. Latched pendingCapLogged logs one breadcrumb per wedged
episode, resets on 'ready' (and on dispose for hooks/panel) so a
recovered session can re-arm the warning.
- preview-panel.ts: removed the misleading type:'error' message on
successful .fixture.ts[x] bundles — a valid fixture is not an error.
- activate.ts: catalog onOpenSource now wraps openTextDocument in a
try/catch that appends the full stack to a dedicated
'CopilotKit A2UI Catalog' output channel in addition to the toast,
so open-source failures leave a durable trail instead of a 5-second
flash.
- activate.ts: revert the dead startOffset param added to
findValuePosition — the plumbing through validateFixtureMessages
wasn't done in the same PR, leaving unused API surface. Kept the
limitation comment as documentation.
Catalog wire protocol
- catalog-list-view-provider.ts: normalize fixtureName (?? undefined)
at the wire boundary so a JSON null can't slip through into callback
code that declares 'string | undefined'. Exhaustive 'never' guard
also logs an unknown-variant warning for version-skew scenarios
(older host receives a newer webview's message).
Review-loop fixes on the runtime surface:
- sse-response.ts: wrap DebugEventBus.broadcast in try/catch inside the
observer's next — a buggy subscriber used to propagate an exception
that RxJS routed to `error`, killing the SSE stream for an unrelated
reason. Logged via the existing logError helper instead.
- sse-response.ts: non-abort write failures no longer fall through
silently. Previously streamClosed stayed false and the next event
re-attempted a broken writer. Now logs + sets streamClosed.
- sse-connect-agent-id.test.ts: new unit test that feeds a synthetic
observable into handleSseConnect and asserts the emitted envelope
carries the route-resolved agentId ('weather-agent') rather than
the literal 'connect'. Regression guard for the fix in 7f3a8c27c
that the /run-based integration tests didn't cover.
- debug-events.suite.ts: replaced a version of this assertion that
couldn't reliably drive /connect in the integration runtime with a
pointer to the unit test above.
- fetch-router.ts: clarify the cpk-debug-events comment — the prefix
is what guards against agent-name collision, not the ordering of
router branches.
- debug-stream.ts (extension client): include the underlying JSON parse
error message in the emitted error so a truncated SSE frame can be
distinguished from a runtime/protocol version mismatch.
Puts the two most-used views up top and demotes the inspector to last.
No functional change; just the "views.copilotkit" ordering in the
extension manifest.
Dropping 'blob:' from script-src in the prior review-response commit
broke the A2UI catalog preview: webview/App.tsx intentionally
URL.createObjectURLs the bundled IIFE and loads it via
'<script src=blob:…>', so the browser blocked every catalog load with
'Loading the script blob:vscode-webview://… violates Content Security
Policy directive'.
Re-add 'blob:' to preview-panel's script-src with a documented
rationale: the blob content originates from the trusted extension host
over postMessage, not from external input, so the general concern that
'blob: enables arbitrary-string JS' doesn't apply to this threat
model. The hook-preview panel still excludes 'blob:' because it uses
an inline nonce-gated <script> instead.
Per PR review suggestion (14) — a cpk- prefix makes collision with a
user-named agent essentially impossible. The earlier change only
documented the route as reserved; renaming it removes the hazard.
Rename scope:
- URL path segment (/debug-events → /cpk-debug-events) in the router,
handler switch arms, method-not-allowed check, and integration suite.
- RouteInfo union discriminator ('debug-events' → 'cpk-debug-events')
in hooks.ts and its two consumers in fetch-handler.ts.
- VS Code DebugStream client URL.
- Docs page (event-inspector.mdx) — both the Callout and the Steps copy.
Left unchanged (internal names that describe the feature but aren't the
public route):
- packages/runtime/src/v2/runtime/handlers/handle-debug-events.ts
- packages/runtime/src/v2/runtime/core/debug-event-bus.ts
- packages/shared/src/debug-event-envelope.ts (and the DebugEventEnvelope
type)
- 'debug-event' (singular) message type in the VS Code webview bridge —
that's the extension host ↔ webview protocol, not the HTTP route.
Critical
- scripts/hooks/check-binaries.sh: restore showcase data-file exclusions
(demo-content / search-index / starter-content >1 MB) that the inline
refactor dropped; add 'set -eu' so silent shell failures don't hide
policy violations.
- packages/vscode-extension/src/extension/preview-panel.ts: drop 'blob:'
from CSP script-src — it lets arbitrary-string JS execute via Blob URL
and defeats most of CSP's XSS protection.
- packages/vscode-extension/package.json: set private: true. The extension
ships as a .vsix via vsce, not npm, and workspace:* devDependencies
would break an accidental 'npm publish'.
Important
- runtime: forward the real agentId from handleConnectAgent into
handleSseConnect / createSseEventResponse so DebugEventBus envelopes
on /connect carry the actual agent name instead of the literal
'connect'. Updates handle-connect.ts, sse/connect.ts.
- hooks/panel.ts CSP: narrow connect-src from 'https:' to just the
Tailwind CDN. The preview path never drives a real CopilotKit runtime
— all hook calls route through the stub — so there's no legitimate
https: fetch to allow from inside bundled user code.
- extension/utils.ts getNonce(): switch to crypto.randomBytes. Math.random()
is not acceptable for a value that gates inline-script execution.
- .github/workflows/vscode-extension.yml:
* Build step uses 'nx run copilotkit-vscode-extension:build' instead
of 'pnpm run build' (targeted build with Nx caching, not full
monorepo rebuild).
* Added explicit 'Type check' step (tsc --noEmit).
* Added Lint step gated with continue-on-error until the Nx target
exists, so a missing target doesn't break the pipeline.
* Publish job now queries the Marketplace for the current published
version and skips 'vsce publish' when the local package.json
version matches — stops every docs/CI-only push to main from
failing on duplicate-version errors.
- hooks/hook-scanner.ts: bound the synchronous walk at 20 000 files so a
pathologically large workspace can't freeze the extension host; flag
kept in module-level constant with a rationale comment.
- sse-response.ts: document that debugEventBus.broadcast intentionally
runs before the stream-closed gate so debug subscribers see trailing
events even after the SSE client disconnects.
- inspector-panel.ts: subscribe to DebugStream lazily on show() rather
than in the constructor; unsubscribe on panel dispose. Avoids firing
the event callback on every envelope when no panel is open.
Suggestions
- fetch-router.ts: document 'debug-events' as a reserved route so it
can't be shadowed by an agent literally named 'debug-events'.
- activate.ts findValuePosition: add optional startOffset parameter,
document the first-occurrence limitation + the follow-up path for
per-fixture precision.
- activate-hook-explorer.ts isInsideWorkspace: fix JSDoc to reflect the
code (root itself is excluded).
- Remove the '⟵ render() →' chip on the preview card border; the blue
frame + 'Rendered output' label above already carry that meaning
clearly enough.
- Caption now says 'The controls on the left/top drive its props live.'
so it still makes sense at narrow widths where the split collapses
and the controls stack above the render column.
Label
- CodeLens now reads just '▶️ Preview Component' regardless of hook
kind / identity — the sidebar already carries the hook name and the
tooltip still has the full detail when you hover.
Fixtures
- ImportedAirQuality.tsx (V1, useCopilotAction) — render is an imported
reference to AirQualityBadge, defined in
test-workspace/hooks/shared/AirQualityBadge.tsx. Confirms the preview
bundler walks the import when the render prop is a component symbol
rather than an inline arrow.
- ImportedPollenReport.tsx (V2, useRenderTool) — render is an imported
PollenReport, which itself imports from ./shared/pollen-copy.ts.
Exercises a two-hop import graph rooted at the hook's render prop.
Scanner's prefilter skips shared/*.tsx automatically because neither
shared file calls a hook or references @copilotkit/*, so the sidebar
stays tidy — only the two new hook fixtures show up.
Integration test expectations updated: showAirQuality + pollenReport
added to the scan list, and both new files included in the bundle-smoke
loop.
Renders a '▶️ Preview <hook>(<name>)' CodeLens above every render-hook
call-site in .ts / .tsx files. Clicking the lens fires the existing
copilotkit.hooks.preview command with the resolved site — same flow as
the sidebar, just available inline where the hook is called.
- Only render-category hooks (useCopilotAction, useRenderTool,
useLangGraphInterrupt, useHumanInTheLoop, ...) get a lens; data hooks
have nothing to preview and are skipped.
- Provider parses on demand via the same oxc scanner as the sidebar, so
lens positions stay in sync without a separate cache.
- Debounced per-file save handler already rescans sites; we now also
call lensProvider.refresh() from there so lenses update on save.
The catalog sidebar was the last view still rendered via a native
TreeDataProvider while inspector + hooks had already moved to webviews.
Replace it with a matching webview so all three sidebars share the same
Tailwind-driven look and interaction model, and add a 'go to source' action
on every row (component and fixture).
Changes
- New CatalogListViewProvider mirrors HookListViewProvider: buffer
messages until the webview posts 'ready', then flush init +
components. Refresh / preview / openSource actions forwarded to
injected callbacks.
- New React webview at src/webview/catalog-list:
- Sticky 'A2UI Catalog' header with a refresh button
- One row per discovered component (name + relative path + 'auto'
badge when no fixture file exists)
- Expandable fixtures list (click the chevron / row to toggle)
- Hover-reveal actions: ▷ preview (component-only) + </> go-to-source
- Fixture rows preview via click, with a </> button that opens the
fixture file and reveals the named fixture key
- 'Go to source' for components opens the catalog file; for fixtures
opens the fixture file and selects the "<name>": key so the user
lands right on the edit point.
- New bridge types + new tsdown entry for the catalog-list bundle.
- activate.ts: drop the tree provider, own the scan results locally,
hand them to the webview, keep component registry seeding in sync on
file-change rescans.
- package.json: componentPreview view is now type: "webview", renamed
"A2UI Catalog". Removed the TreeView-only view/item/context binding.
- Deleted sidebar/view-provider.ts — no longer referenced.
All three sidebars (Inspector, Hooks, Catalog) now use the same webview
pattern, same go-to-source UX, and the same Tailwind styling.
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.