Two related bugs in the React <CopilotThreadsDrawer> surface.
Bug 1 (web-components): the delete-confirm dialog's backdrop is
`position:absolute; inset:0; z-index:10`, but `.root` was not a positioning
context, so on desktop it resolved against the viewport and its low z-index
lost to the chat composer (`position:relative; z-index:20`), painting the
dialog UNDER the input. Anchor `.root` with `position:relative` to confine the
modal to the drawer column. Framework-agnostic (Angular wraps the same element).
Bug 2 (react-core): the provider's immediate runtime-info catch-up read grabbed
`a2uiEnabled` but omitted `licenseStatus`. The core starts its `/info` fetch
during construction, so on a cold first load (incognito/hard refresh) the
`Connected` event can fire before the passive subscribe effect runs; the event
is missed and license status stays null forever, pinning the drawer to
"Loading threads…". Read all three values immediately, mirroring the subscriber.
Angular/Vue already read licenseStatus in their catch-up, so they are unaffected.
Adds a deterministic regression test for the provider race (red before, green after).
ENT-1046
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
The `attachments` prop supports images, audio, video, and documents —
but the JSDoc example in `Chat.tsx` only showed
`image/*,application/pdf`, and the docs configuration example used
`accept: image/*`, silently teaching users to restrict themselves to
images.
**Before (Chat.tsx JSDoc):**
```tsx
accept: image/*,application/pdf,
```
**After:**
```tsx
accept: image/*,audio/*,video/*,application/pdf,
```
The docs configuration example now also clarifies that omitting `accept`
defaults to `*/*` (all files), and the shown value includes all four
supported modalities.
## Changes
- `packages/react-ui/src/components/chat/Chat.tsx` — updated JSDoc
example to show all modalities; added note that default `accept` is
`*/*`
- `showcase/shell-docs/src/content/docs/multimodal-attachments.mdx` —
updated configuration example to show
`image/*,audio/*,video/*,application/pdf` and note that omitting
`accept` allows all types
## Summary
Adds a public **Vue `CopilotThreadsDrawer`** — a thin wrapper over the
shared `<copilotkit-threads-drawer>` Lit element — bringing the
Threads-v2 drawer to Vue at parity with the shipped React and Angular
verticals. This is the Vue fast-follow tracked in the [CopilotDrawer —
Vue wrapper](https://app.notion.com/p/3883aa38185281fc8a2cfb69cd1dd88e)
spec.
**SDK-only PR** (`packages/vue`). Like the Angular vertical, the example
de-fork / demo route is a **separate follow-up** — the Vue examples
can't consume the drawer until `@copilotkit/web-components` + a
`react-core` release ship *and* the examples support managed
Intelligence.
Recon during planning corrected the spec's original "cheapest/thinnest
wrapper" premise: Vue is thin on element interop (native
`isCustomElement` + `v-bind`/`v-on`) but — exactly like Angular — needed
an active-thread foundation built. So this ports the full vertical, not
just a binding layer.
## What changed (SDK layers)
- **`useThreads` augmentation** (`hooks/use-threads.ts`): `enabled`
gate, `listError` (genuine list errors, excludes dev/config errors),
`isMutating`, `unarchiveThread` / `refetchThreads` / `startNewThread`;
plus **`registerThreadStore` core-registry integration** and
**`threadEndpoints` list/mutation gating** (React parity).
- **Active-thread + drawer-awareness on
`CopilotChatConfigurationProvider`**: `setActiveThreadId` /
`startNewThread` with the non-explicit-seed override (so
thread-switching + "+ New" work under `<CopilotKit>`), and `drawerOpen`
/ `setDrawerOpen` / `drawerRegistered` / `registerDrawer` with
bidirectional mobile (`<768px`) mutual-exclusion.
- **Clear-on-fresh in `CopilotChat`**: clears the conversation on a
genuine new-thread switch, guarded against initial mount and agent-store
swaps.
- **Mobile launcher in `CopilotModalHeader`**: renders only when a
drawer is registered AND the viewport is mobile.
- **The `CopilotThreadsDrawer.vue` wrapper** + barrel export.
- **SSR safety:** the wrapper imports the `<copilotkit-threads-drawer>`
Lit element **lazily** (`await import(...)` inside `onMounted`,
client-only) rather than at module scope. The element evaluates `class …
extends HTMLElement` at import time, which crashes Nuxt/Vite SSR
(`HTMLElement is not defined`) — a static import would break SSR for
**every** `@copilotkit/vue` consumer, not just drawer users. (Found via
live Nuxt testing; see Testing.)
Load-bearing behaviors carried from the React/Angular rounds: license
gate never flashes the locked view (`licensed || pending`, `loading ||
pending`) and issues no `/threads` fetch while unlicensed; provider-less
`localDrawerOpen` fallback; id-keyed per-row slot reconciliation.
## How it was built & reviewed
Executed via the `micro-task-execution` three-tier model (curator →
decomposition-reviewer → 8 waves, each opus-reviewed) → integration
review → **`cr-loop`: 4 seven-agent review rounds + 7 fix batches,
converged to zero mandatory findings + a Procedure 3 promotion-audit
with zero promotions.** The loop caught and fixed real defects the
per-wave reviews missed: a `useThreads` stuck-loading bug on
`enabled:false`, a wrong chat-input focus `data-testid` (silent a11y
failure), a `CopilotChatToggleButton` fallback regression from the
provider change, net-new lint (`no-dupe-keys`, deprecated `:slot`), and
— surfaced by the promotion audit as load-bearing on the shipped
surfaces — the `registerThreadStore` (inspector visibility) and
`threadEndpoints` gating parity gaps.
## Testing
**Automated (all green in the worktree):**
- `nx run @copilotkit/vue:test` — **99 files / 1061 tests passed**. New
coverage: full `useThreads` augmented surface incl. store registration +
endpoint gating + mutation guards + `enabled` re-arm; the wrapper's
entire 10-event routing table; license pending-vs-resolved gating;
delete-active-thread reset; clear-on-fresh (with a mutation-check
proving it's non-vacuous); bidirectional mobile mutual-exclusion;
`isMobileViewport` guards; the mobile launcher; the
`setModalOpen`-undefined contract that `CopilotChatToggleButton` depends
on.
- `nx run @copilotkit/vue:build` — succeeds (this is the real type gate:
`vue-tsc --declaration`); the compiled `dist` exports
`CopilotThreadsDrawer`.
- `nx run @copilotkit/vue:check-types` — passes.
- `pnpm install --frozen-lockfile` — passes (lockfile in sync).
- Lint: all new/changed source files are clean. (The package-wide `nx
lint` has 173 pre-existing errors, red on `main` and unrelated to this
change.)
Red-green discipline was applied to every behavioral fix (test written
to fail against the bug, then confirmed green after the fix).
**Live-verified via a throwaway hacked example** (uncommitted; the
examples can't ship managed Intelligence yet, so the committed
demo/de-fork is a follow-up): wired the Vue Nuxt demo's runtime to a
managed-Intelligence runtime (`CopilotKitIntelligence` + `licenseToken`,
creds from a CLI scaffold) and loaded `/threads`. Observed: `/info`
reports `mode:"intelligence"`, `licenseStatus:"valid"`,
`threadEndpoints:{list,inspect,mutations,realtimeMetadata: true}`; the
drawer renders the **real thread list from the platform** (licensed —
not the locked or endpoints-unavailable gates), with the Active/All
filter, "New thread", per-row Archive/Delete, and the mobile launcher;
`CopilotChat` renders beside it; no console errors.
This live run is what surfaced the SSR bug above: before the lazy-import
fix, `/threads` (and `/`) 500'd with `HTMLElement is not defined`; after
it, `/threads` → 200 and the error is gone. (Note: the added node-env
regression test `CopilotThreadsDrawer.ssr.test.ts` is a forward-looking
smoke guard — current Lit ships a Node-guarded build so vitest-node
can't reproduce Nuxt's Vite-SSR resolution; the fix's proof is the live
Nuxt run.)
## Release gate
Like the React/Angular de-forks, the wrapper depends on
`@copilotkit/web-components` being published and a
`react-core`/`web-components` release containing the drawer.
`packages/vue` publishes alongside.
## Follow-up work (separate PRs, none blocking)
Tracked in **ENT-1037** (related to ENT-1035): the cross-framework
767/768 mobile-breakpoint reconciliation, `useThreads.startNewThread`
config-error dismissal parity, an identity-guarded
`unregisterThreadStore`, and CopilotChat send/connect error-UX parity.
The Vue examples are intentionally left as-is (no Vue de-fork).
Review feedback (@marthakelly) has been addressed in-branch — see the
review reply.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Type elRef as CopilotKitThreadsDrawerElement (drop the `& Record<string, unknown>`
escape hatch) so every el.<prop> write is checked against the real element type.
- Replace the 12-entry watch([...]) dependency array with watchEffect, which
auto-tracks its reads and removes the maintenance hazard of keeping the array
in sync.
- Extract MOBILE_MAX_WIDTH_QUERY in is-mobile-viewport.ts and reuse it in
CopilotModalHeader's matchMedia listener to remove the within-Vue duplication
of the breakpoint literal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CopilotThreadsDrawer.vue previously imported
`@copilotkit/web-components/threads-drawer` statically at module scope.
That module defines a Lit custom element, and eagerly loading it on every
`@copilotkit/vue` import risked an `HTMLElement is not defined`-style crash
under SSR (Node has no DOM) for any consumer that imports the package on
the server, e.g. Nuxt.
Fix: import the element module lazily, inside `onMounted`, so it is only
ever evaluated client-side. `elementTag`/`mounted` are set once the dynamic
import resolves, and the template gates rendering on `mounted`.
Consequence: the wrapper now mounts the custom element asynchronously.
Updated `CopilotThreadsDrawer.test.ts` so `mountDrawer()` awaits
`flushPromises()` (resolving the dynamic import) plus a trailing
`nextTick()` (flushing the render and the `flush: "post"` property-push
watcher) before returning, and centralized this settle in the shared
helper instead of repeating ad hoc `nextTick()` calls per test.
Added `CopilotThreadsDrawer.ssr.test.ts`, a `@vitest-environment node`
regression test asserting the package entry (which re-exports
CopilotThreadsDrawer) imports without throwing when there is no
`HTMLElement` global, guarding against reintroducing an eager DOM-dependent
import into the barrel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ports two React-parity gaps (audit-flagged as user-facing) into the Vue
`useThreads` composable, mirroring react-core `use-threads.tsx`.
Fix A — register the thread store with core's single-slot registry:
- Adds a `watch([resolvedEnabled, resolvedAgentId])` that calls
`copilotkit.value.registerThreadStore(agentId, store)` and, via the
watch's `onCleanup`, `unregisterThreadStore(agentId)` on
disable/agentId-change/unmount. Gated on `resolvedEnabled` so a disabled
(unlicensed) store never evicts a co-mounted live store for the same agent.
Fix B — gate list + mutations on `copilotkit.threadEndpoints`:
- Derives `threadListEndpointSupported`/`threadMutationsSupported` via
`!== false` (legacy runtimes advertise `undefined` => supported).
- Context-dispatch watcher skips dispatching (setContext(null)) when the
list endpoint is unsupported, so no `/threads` fetch fires.
- Folds `threadEndpointsError` ("Thread endpoints are not available on this
CopilotKit runtime") into `error` (NOT `listError`, which stays
storeError-only) and factors `!threadEndpointsUnavailable` into
`preConnectLoading` so the UI doesn't spin against an endpoint-less runtime.
- `guardMutation` wraps rename/archive/unarchive/delete to reject with
"Thread mutations are not available on this CopilotKit runtime" when
`threadEndpoints.mutations === false`, before touching the network.
Call sites:
- The shipped `CopilotThreadsDrawer` consumes `error` for its user-facing
error banner (now surfaces the endpoints-unavailable message instead of
spinning) and `listError` for genuine list-load failures (unchanged
contract: storeError only, no config/runtime-setup leakage). Its `enabled`
prop (unlicensed gate) already suppressed fetches; it now also correctly
suppresses core-registry registration so a co-mounted live chat store for
the same agent is not evicted. Mutation buttons (rename/archive/delete)
reject locally on runtimes that don't serve mutations.
Item 3 (startNewThread configErrorDismissed) intentionally left out of scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wave-4 introduced setModalOpenWithDrawerExclusion as an always-defined
function on configurationValue.setModalOpen. Under a bare
<CopilotChatConfigurationProvider> (no isModalDefaultOpen, no parent
providing modal state), this broke the pre-existing contract that
setModalOpen is undefined when the provider owns no backing modal
state — the function was defined but a no-op, since it delegated to
parentConfigValue?.setModalOpen, which was undefined.
CopilotChatToggleButton.vue depends on that undefined-ness: it checks
`config.value?.setModalOpen` and falls back to a local `fallbackOpen`
ref when absent. With the regression, clicks routed into the no-op
setter instead of the fallback, permanently stuck closed.
Fix: replace the standalone function with a `publicSetModalOpen`
computed that returns undefined when resolvedSetModalOpen (the real,
possibly-parent-inherited setter) is undefined, and otherwise wraps it
with the mobile drawer-exclusion behavior. This preserves the
undefined contract without touching resolvedIsModalOpen,
resolvedSetModalOpen, or the drawer-registration code.
Call-site enumeration for setModalOpen consumers (all read via
optional chaining, so all remain safe):
- CopilotChatToggleButton.vue: `config.value?.setModalOpen` — restored
fallback-open behavior when the provider is bare, confirmed via new
presence-contract tests plus the existing 5-test
CopilotChatToggleButton.test.ts suite (all pass).
- CopilotModalHeader.vue: `config.value?.setModalOpen?.(false)` — only
used inside modal-backed compositions (CopilotPopup/Sidebar), which
always pass isModalDefaultOpen, so setModalOpen is always defined
there; unaffected.
- CopilotSidebarViewInternal.vue / CopilotPopupViewInternal.vue: same
pattern, same modal-backed guarantee; unaffected.
React's CopilotChatConfigurationProvider.tsx intentionally keeps
setModalOpen always-defined and always backed by internal state — a
different resolution than Vue's. This fix takes the minimal Vue-local
path (preserve the undefined contract CopilotChatToggleButton relies
on) rather than reworking resolvedIsModalOpen's backing, to avoid
changing bare-provider modal-open semantics.
Tests: added a "modal-setter presence contract" describe block
verifying (1) a bare provider exposes setModalOpen as undefined
(red-green verified: fails on pre-fix code, passes after), and (2) a
provider with isModalDefaultOpen exposes a working setModalOpen that
toggles isModalOpen. All 21 tests in the provider suite pass,
including the existing mobile drawer<->modal mutual-exclusion tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
- Include `@copilotkit/web-components` in the shared `monorepo` release
scope.
- Remove the standalone `web-components` release scope from workflow
dispatch options and type definitions.
- Keep notification package counts aligned with the 16-package monorepo
scope.
## Why
`@copilotkit/web-components` should move with the rest of the shared
monorepo packages after its initial manual publish, not be released as a
separate scope.
## How
- Added `@copilotkit/web-components` to `release.config.json` under
`monorepo`.
- Removed standalone scope dropdown/options and npm URL handling.
- Verified dropdown parity, monorepo dry run, standalone-scope
rejection, and notification tests.
## What does this PR do?
This PR fixes build failures on Windows by replacing Unix-only shell
commands (`rm -rf`, `cp`, `mkdir -p`) in `package.json` scripts with
cross-platform Node.js `fs` built-in commands.
This follows the project's existing codebase pattern for cross-platform
operations, as seen in `packages/react-ui/package.json` (line 45).
### 🛠️ Changes:
- **`packages/runtime`**: Replaced `rm -rf` in `generate-graphql-schema`
with `fs.rmSync`.
- **`packages/vue`**: Replaced `cp` in `build:types` and `rm -rf` in
`clean` with `fs.cpSync` and `fs.rmSync`.
- **`packages/angular`**: Replaced `mkdir -p` and `cp` in `build:css`
with `fs.mkdirSync` and `fs.cpSync`.
- **`examples/v1/next-openai`, `next-pages-router`, `state-machine`**:
Replaced `rm -rf` clean commands with a single Node.js loop that deletes
`.turbo`, `node_modules`, `dist`, and `.next`.
All modified packages now build successfully on Windows.
## Related PRs and Issues
- Closes#5601
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [ ] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked (lets us help iterate on
your PR directly — faster turnaround for everyone)
CopilotThreadsDrawer.vue declared local handlers `onNewThread`/`onLicensed`
with the same names as the `onNewThread`/`onLicensed` input-callback props,
tripping vue/no-dupe-keys. Renamed the handlers to `handleNewThread`/
`handleLicensed` (props left untouched) and updated their template bindings
(`@new-thread`, `@licensed`) and the one internal call site in `onDelete`
that re-implemented the new-thread flow inline.
Rewrote the per-row `:slot="`row:${t.id}`"` binding as
`v-bind="{ slot: `row:${t.id}` }"` to satisfy vue/no-deprecated-slot-attribute
without changing behavior — it still compiles to a real DOM `slot` attribute
used to project light-DOM children into the custom element's named shadow
slots (not a Vue component slot).
Added a `typeof window.matchMedia !== "function"` guard to
isMobileViewport(), matching CopilotModalHeader.vue and the React reference,
so environments where `window` exists but `matchMedia` doesn't (some test
runners, embedded webviews) don't throw when the provider opens the
drawer/modal.
CopilotModalHeader.vue:40 `_className` unused-var lint error predates this
branch (introduced in 28d07ccaa, already on main) and is left as out of
scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
focusChatInput() queried `[data-testid="copilot-chat-textarea"]`, a testid
that exists nowhere in the Vue package (copied from React) — the `?.` made
the miss silent, so focus never returned to the composer after selecting a
thread. Corrected to `copilot-chat-input-textarea` (CopilotChatInput.vue)
and added React-parity scoping: `findChatInput` walks up from the drawer's
element via `closest('[data-testid="copilot-chat-view"]')` to scope the
input lookup to the enclosing chat, falling back to a document-global query
when no such ancestor exists. Sole call site: `onThreadSelected`.
Added a `localDrawerOpen` ref fallback for when there is no surrounding
`CopilotChatConfigurationProvider` (`config.value === null`): previously
`el.open` was pinned to `false` forever and `onOpenChange` was a no-op in
that case, since both routed through `config.value?.`. A `drawerOpen`
computed / `setDrawerOpen` function now pick the provider's state when
present and the local ref otherwise; both the property-push watcher and
`onOpenChange` route through them. Mirrors React's bare-drawer-starts-closed
fallback.
Documented (no behavior change) that the `row` scoped slot, unlike React's
`renderRow`, has no per-row escape hatch back to the element default — once
provided it projects for every thread.
Added tests: focus-return after thread-selected (red against the old
testid), delete-of-active-thread resets to a new thread (plus the
negative non-active case), and the provider-less drawerOpen fallback
reflecting open-change events.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Call sites: `preConnectLoading` only feeds `isLoading` (returned to consumers,
e.g. CopilotThreadsDrawer's licensed-gate loading state); `hasDispatchedContext`
is read only by `preConnectLoading` and written only in the context watcher.
Without gating on `resolvedEnabled`, useThreads({ enabled: false }) left
isLoading stuck true forever (hasDispatchedContext never set). Without
resetting the flag on the disabled/no-runtimeUrl branch, toggling
enabled false->true (or runtimeUrl removed+re-added) failed to re-arm the
pre-connect loading synthesis, regressing the empty-list flash it exists
to suppress. Brings the Vue hook back in parity with the React reference's
reset behavior for these two branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a mobile-only launcher button at the leading edge of
CopilotModalHeader that toggles the drawer. It renders only when a
CopilotThreadsDrawer has registered with the chat configuration AND
the viewport is mobile (<=767px, matched via matchMedia and mirrored
from the React CopilotModalHeader implementation). Also exports
IconPanelLeftOpen from the shared icons barrel for the button's icon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>