Commit Graph

6618 Commits

Author SHA1 Message Date
Alem Tuzlak 0c6503e79b test(vscode-extension): end-to-end mount-and-capture with invoking render 2026-04-17 18:40:39 +02:00
Alem Tuzlak 7278bc888b test(vscode-extension): add StyledAction fixture exercising CSS collector plugin
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
2026-04-17 18:34:16 +02:00
Alem Tuzlak 2257892206 test(vscode-extension): hook explorer integration fixtures and scan+bundle test
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.
2026-04-17 18:26:35 +02:00
Alem Tuzlak 8a654a14dd Merge branch 'main' into worktree-lucky-popping-wren 2026-04-17 18:21:02 +02:00
Alem Tuzlak 43082ed602 refactor(vscode-extension): extract activateHookExplorer + use category filter in focusPanel
- 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.
2026-04-17 18:15:22 +02:00
Alem Tuzlak 40a0914150 feat(vscode-extension): wire hook explorer into activation 2026-04-17 18:11:39 +02:00
Alem Tuzlak 4cf604d6a8 feat(vscode-extension): sidebar tree data provider 2026-04-17 18:07:08 +02:00
Alem Tuzlak 321454b823 fix: pin langgraph-python to stable deps, fix mastra dev, restore Windows .bat fallback
- 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
2026-04-17 18:04:18 +02:00
Maxim a0c72f9a22 refactor: move throttle logic to CopilotKitCore.subscribeToAgent (#3734) 2026-04-17 16:50:34 +02:00
Maxim 073c0edefd fix: resolve merge conflict with main (pnpm-lock.yaml) 2026-04-17 16:36:21 +02:00
Maxim bd2ccbd0fa refactor(core): extract logAndEmitError helper to deduplicate error handling
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.
2026-04-17 16:33:09 +02:00
Maxim 3ffe9cb6bf fix(sdk-python): always emit assistant message for AIMessages with tool_calls (#3894)
## 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)
2026-04-17 15:34:04 +02:00
Alem Tuzlak e6045758a4 fix(vscode-extension): HookPreviewPanel dispose race + stale-push token + typed onMessage
- 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).
2026-04-17 15:30:47 +02:00
Alem Tuzlak ac8dc78812 feat(vscode-extension): hook preview panel host 2026-04-17 15:27:47 +02:00
Alem Tuzlak 8c8786c707 fix(vscode-extension): bundle error surfacing, ControlsDispatch extraction, mountError to host
- 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.
2026-04-17 15:24:37 +02:00
Alem Tuzlak 07f2423a48 build(vscode-extension): stub node builtins, CSS, and markdown chain for hook-preview bundle
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.
2026-04-17 15:20:52 +02:00
Alem Tuzlak 3d4c4fb4fc feat(vscode-extension): hook-preview webview App, bundle loader, and entry 2026-04-17 15:14:50 +02:00
Alem Tuzlak a796bbc84b fix(vscode-extension): RenderToolControls passes narrowed ActionValues, recombines toolCallId
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.
2026-04-17 15:07:34 +02:00
Alem Tuzlak 5839f3871f feat(vscode-extension): per-hook controls components 2026-04-17 15:04:03 +02:00
Maxim 45b28dd3b3 fix(sdk-python): update test to match always-emit assistant message behavior
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>
2026-04-17 15:02:31 +02:00
Alem Tuzlak c2e00fb1be refactor(vscode-extension): flatten CapturedRegistry + widen harness error boundary + waitFor test
- 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.
2026-04-17 14:59:20 +02:00
Alem Tuzlak b35413332f feat(vscode-extension): harness, registry reader, and capture flow 2026-04-17 14:54:34 +02:00
Alem Tuzlak 2cb31be9ab chore(vscode-extension): add @copilotkit/react-core as devDependency 2026-04-17 14:48:46 +02:00
Alem Tuzlak c9122b9101 feat(vscode-extension): fetch interceptor with disposer and double-install guard
- 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.
2026-04-17 14:43:23 +02:00
Maxim 85dbf25c74 Merge branch 'main' into fix/always-emit-assistant-message 2026-04-17 14:42:22 +02:00
Maxim cd3299b3a2 fix: inspector syncs state on direct agent.setState() (#3911)
## 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)
2026-04-17 14:42:03 +02:00
Alem Tuzlak 9f8c440363 feat(vscode-extension): webview fetch interceptor for dummy runtime URL 2026-04-17 14:41:04 +02:00
Alem Tuzlak c27a5bd71d refactor(vscode-extension): clean up ArrayField seed logic + BooleanField toggle-off test
- 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.
2026-04-17 14:37:38 +02:00
Alem Tuzlak dcd21e0ec1 refactor(vscode-extension): harden HookControlsStore.load + add robustness tests
- 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.
2026-04-17 14:37:33 +02:00
Alem Tuzlak abe27a5a45 refactor(vscode-extension): adapter cleanup and coverage
- 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).
2026-04-17 14:35:43 +02:00
Alem Tuzlak 705ccfa688 refactor(vscode-extension): schema converter cleanup
- 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.
2026-04-17 14:34:22 +02:00
Maxim 5b5ecde69a Merge branch 'main' into fix/inspector-state-on-direct-setState 2026-04-17 14:31:32 +02:00
Alem Tuzlak 07c46ebe53 feat(vscode-extension): Memento-backed controls persistence 2026-04-17 14:26:21 +02:00
Alem Tuzlak d7b4b0d649 feat(vscode-extension): hook adapters for every render-props kind 2026-04-17 14:22:45 +02:00
Alem Tuzlak 6048aa24ab fix(vscode-extension): FormRenderer drops undefined keys instead of leaving them as undefined
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.
2026-04-17 14:19:04 +02:00
Alem Tuzlak fcc6e7082d feat(vscode-extension): FormRenderer and full FieldRenderer dispatcher 2026-04-17 14:16:35 +02:00
Alem Tuzlak 8f4e34cf05 fix(vscode-extension): RawJsonField skips onChange on empty input; adds aria-invalid and error-clear test
- 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.
2026-04-17 14:13:23 +02:00
Alem Tuzlak 64eee33c8f feat(vscode-extension): ObjectField and RawJsonField 2026-04-17 14:10:09 +02:00
Alem Tuzlak f060998a2c feat(vscode-extension): ArrayField with partial FieldRenderer 2026-04-17 14:04:22 +02:00
Alem Tuzlak f055cddfe0 fix(vscode-extension): primitive field a11y + NumberField empty-emits-undefined
- 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.
2026-04-17 13:59:42 +02:00
Alem Tuzlak 9609f911bf feat(vscode-extension): primitive form fields (string, number, boolean) 2026-04-17 13:55:56 +02:00
Alem Tuzlak b2341078c9 feat(vscode-extension): schema value defaults and merge logic 2026-04-17 13:52:18 +02:00
Alem Tuzlak 0ec195bf44 refactor(vscode-extension): export JSONSchemaNode and drop as-any cast in standard-schema 2026-04-17 13:50:35 +02:00
Alem Tuzlak a0dedd201d feat(vscode-extension): Standard Schema to FormSchema via zod-to-json-schema 2026-04-17 13:47:57 +02:00
Alem Tuzlak 7f5e960a21 feat(vscode-extension): JSON Schema to FormSchema converter 2026-04-17 13:44:07 +02:00
Alem Tuzlak 95bcf855e9 feat(vscode-extension): v1 parameters to FormSchema converter 2026-04-17 13:40:32 +02:00
Alem Tuzlak 8d95caeadc refactor(vscode-extension): extract shared IIFE bundler helper
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.
2026-04-17 13:28:23 +02:00
Alem Tuzlak 785bcb64b4 refactor(vscode-extension): single-pass scanWorkspace with inherited gitignore stack
- 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.
2026-04-17 13:26:31 +02:00
Alem Tuzlak 484ef82dad test(vscode-extension): drop string-key fixture (formatter rewrites quoted keys, making test redundant) 2026-04-17 13:25:26 +02:00
Alem Tuzlak cfc2608dc1 refactor(vscode-extension): tighten hook-scanner AST visitor + pin loc.line + cover string-keyed name prop 2026-04-17 13:24:24 +02:00