Addresses PR #5823 review (MikeRyanDev + marthakelly):
- core/threads.ts: a full-list refetch (filter-change / retry) now clears
fetchMoreError on both listRequested and listSucceeded, so the inline
'couldn't load more - retry' banner no longer survives onto a fresh list.
- web-components: Escape while the confirm-delete <dialog> is open is now
consumed by a confirm guard in the host keydown handler; previously the
bubbled keydown fell through to the mobile branch and closed the whole drawer
along with the confirmation.
- web-components: an open kebab popover now shields the rest of the list -
.list.menu-open .row:not(.menu-open) gets pointer-events:none so other rows
no longer reveal their kebab or paint a host ::part(row):hover background
around/behind the menu. Click-away dismissal is preserved via the existing
document pointerdown handler.
- README: drop the removed --cpk-drawer-rail-width from the documented tokens.
Tests: core clears-fetchMoreError-on-refetch; web-components Escape-while-
confirming, confirm-dialog backdrop-click, menu-open row shield; Angular
scoped-chat-input focus (ancestor copilot-chat-view over the global fallback).
Menu-shield verified live in the langgraph-js example (:3002).
The collapse toggle keeps the header bar visible on desktop, so the row's 12px
top margin doubled up with the header's padding (extra gap vs mobile). Make the
top margin conditional on the header being HIDDEN (collapsible=false + no header
slot) via .header[hidden] + .new-conversation; otherwise the header supplies the
top spacing on both breakpoints.
- Floating cluster/launcher default gutter → 24px on both top and left (was 12px).
- Selected row: drop the border-color; the background change alone marks it
(base row keeps its 1px transparent border for layout stability).
- Cap --_radius at 4px via min(theme, 4px) and lower the hardcoded 6px button
radii to 4px, so no bordered element exceeds a 4px corner radius.
Intersect the .root rect with the viewport before centering the confirm dialog.
A host grid that doesn't bound the drawer's row lets .root grow to content
height, so centering over the raw rect dropped the modal far down the page (seen
in the langgraph-js example, whose grid has no row bound). Clamping to the
on-screen band keeps it centered in the visible drawer regardless of host sizing.
Move the collapse toggle after the header slot so it right-aligns (the slot has
flex:1 and pushes it over), matching the mobile close button — it was reading as
left-aligned above New Conversation, which the designer flagged.
Design iteration (Ben's designer):
- RESTORE desktop collapse. Re-add collapsed/collapsible + collapse-change
(element + all three wrappers, lockstep), the desktop header collapse toggle,
and the CollapseChangeDetail type/re-export. Default is EXPANDED.
- UNIFY the closed affordance into one floating cluster (Figma 'closed' mockup):
a sidebar-glyph toggle + a New Conversation (+) icon button, shown in BOTH the
mobile-closed state (adds New Conversation to the old single launcher) and the
desktop-collapsed state. Parts: launcher-cluster, launcher, launcher-new-thread.
- COLUMN RECLAIM (no empty reserved gap): on desktop-collapse the element sets
--cpk-drawer-reserved-width: 0px on the document root (reaches the grid past
the wrapper host via :root inheritance); hosts read it in grid-template-columns.
Default expanded never sets it, so no hydration flicker.
- DELETE MODAL centered over the DRAWER PANEL, not the viewport: keeps the
top-layer showModal() robustness (never clipped) but drives --confirm-cx/cy
from the visible .root rect and caps width to the drawer band.
- Vue fix: default collapsible to true in the wrapper. Vue coerces an omitted
Boolean prop to false, which was silently forcing collapsible=false (collapse
toggle vanished) — React/Angular pass undefined and keep the element default.
Validated live in the Nuxt (Vue) and Angular demos against managed Intelligence:
collapse/expand, cluster + New Conversation, column reclaim, drawer-centered
top-layer modal, mobile cluster. Tests: web-components 89, react-core 1419,
vue 32, angular 34 — all green.
The confirm-delete overlay was a CSS-positioned div trapped in the drawer
host's stacking context, so it could paint under other UI (e.g. the chat's
welcome view). Render it as a native <dialog> opened with showModal(), which
lives in the browser top layer and can never be occluded. The <dialog> is
always present so updated() can drive showModal()/close() from
_confirmingDeleteId; jsdom implements neither method, so a feature-detect falls
back to toggling the `open` attribute (which HTMLDialogElement.open reflects)
to keep unit tests observing open/closed state. Native Escape is handled via
the dialog's `cancel` event (the manual confirm-Escape branch in _onKeyDown is
removed); backdrop clicks dismiss via a target===currentTarget check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The thread panel is a persistent always-visible sidebar on desktop; the Figma
"closed" mockup is the MOBILE state, already covered by off-canvas behavior.
- web-components: remove the `collapsed`/`collapsible` properties,
`_toggleCollapsed`, the header collapse-toggle button, and the
collapsed-cluster render branch; render() always paints the full panel body.
Gate the now control-less header on a `_hasHeader` slotchange flag so no empty
bar renders. Drop the unused `iconSidebar`, the `CollapseChangeDetail` type +
`collapse-change` event-map entry, the index re-export, and the
`.root.collapsed`/`.collapsed-cluster` styles.
- react: drop the `collapsible` prop + property assignment, the
`onCollapseChange` prop + `collapse-change` listener/handler, and the local
`CollapseChangeDetail` type.
- vue: drop the `collapsible` prop + element binding, the `collapse-change`
emit + `@collapse-change` handler, and the local `CollapseChangeDetail` type.
- angular: drop the `collapsible` input + property push, the `collapseChange`
@Output + event wiring (and now-unused EventEmitter/Output imports), and the
local `CollapseChangeDetail` type.
- tests: remove all collapse tests across the four packages; add an element
header-gating test. Mobile off-canvas (open-driven) behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Element-only; downstream wrappers' search removal lands in the paired commit.
Verified in isolation: @copilotkit/web-components test (86) + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Search: the magnifier now expands an inline search input in the header icon
row and focuses it; it auto-collapses back to the icon on blur when the query
is empty/whitespace, but stays open while a non-empty query is active.
Toggle-click and Escape still close + clear + emit search{query:''}.
- Remove all clipped-name tooltip machinery (data-tooltip attr,
_syncNameClipping + its updated() call, name-clipped toggling, the tooltip
::after/::before CSS, and the name-clipped:hover z-index lift). Ellipsis
truncation is kept.
- Add reactive collapsible property (default true, mirrors licensed): when
false the header omits the collapse toggle and the collapsed cluster never
renders (drawer stays expanded). Mobile off-canvas is unaffected.
- Add collapse-change event + CollapseChangeDetail (types + index re-export);
the collapse toggle and cluster-expand route through _toggleCollapsed(), which
flips collapsed and emits collapse-change with the new value (user-driven only).
Bucket A — mobile open-flash:
- Default the drawer element's `open` property to `false`. On a mobile viewport
the previous `open = true` default made the first render satisfy
_isMobileModalOpen(), painting the modal + body scroll-lock + focus steal for
one frame before any wrapper effect could close it. Desktop is unaffected
(only .root.mobile.open / _isMobileModalOpen() consume `open`).
- Element tests: added an `open` option to setup(); updated the 6 mobile/desktop
tests that relied on the old open=true default to opt in explicitly; added a
regression test asserting a fresh mobile element defaults open=false and paints
no backdrop / applies no scroll-lock until opened.
Bucket B — dead CSS + inert `confirming` machinery:
- styles.ts: removed the `.row-action[data-tooltip]` hover/focus tooltip rules
and the `.root.confirming .row-action[data-tooltip]…` suppression rules — no
rendered .row-action carries data-tooltip anymore (row actions moved into the
kebab menu as labeled .row-menu-items). Kept the .row-action base rules (still
used by the confirm-dialog Cancel button + fetch-more retry).
- element: removed the now-inert `confirming` root class (it gated only the
dead CSS above) and its stale comment; deleted the test asserting the no-op
tooltip suppression. Refreshed two comments that referenced the removed
row-action tooltip lineage.
Bucket B — comment/test hygiene:
- react-core CopilotThreadsDrawer.tsx: reworded the imprecise event-rebind
comment to describe the actual [mounted] deps.
- angular spec: added a beforeEach resetting the module-level threadsState
signals + clearing mock fns to remove order-coupling (parity with react/vue).
- vue use-threads.test.ts: aligned MockThreadStore.unarchiveThread + its
assertion to the real core contract (PATCH /threads/{id} { archived:false },
not POST /threads/{id}/unarchive).
Verified: web-components (89), vue (1068), angular (176), react-core suites all
green; web-components + react-core builds green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Search toggle-close clears the query and emits search{query:""} via a shared
_closeSearch() helper, so it no longer leaves a stale, invisible list filter.
- Escape-close routes through _closeSearch() too, so a consumer's onSearch is
never left holding a stale query.
- Composed document pointerdown listener (bound/removed in connected/
disconnectedCallback) dismisses the funnel popover and row kebab menu on
outside clicks; selecting a thread row also closes an open menu.
- Kebab popover no longer clipped by the list overflow for bottom rows:
lower-half rows open the menu upward (menu-up -> bottom-anchored popover).
- Locked/unlicensed view no longer renders the search toggle, search input, or
"New Conversation" row — only the Upgrade panel (collapse toggle kept).
- Row-action aria-labels use the shared hasName?name:"New thread" fallback so an
empty-string name never announces a trailing blank.
- Hardened CSS-contract tests: archived-italic and :host height:100% match
selector+declaration as a unit; muted-not-struck checks color + no line-through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the 'a filter is applied' dot (part=filter-indicator, --cpk-drawer-indicator,
default #5b94e4) shown on the funnel when the active filter is not the default,
matching the Figma archived view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Also re-exports SearchDetail from @copilotkit/web-components/threads-drawer
so the React wrapper's type import resolves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the optional header projection slot back to the redesigned icon-row
header (flex:1 so empty keeps icons right-aligned, filled fills the left),
keep `label` driving the region + listbox accessible names only, drop the
retired visible-title expectations from the two label tests, and remove the
orphaned --_rail-width token and .row-action.danger rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Add a `licenseUrl` property to the shared element (default
https://docs.copilotkit.ai/intelligence). The locked view's Upgrade CTA now
dispatches a cancelable `licensed` event carrying the url and, unless the
host calls preventDefault(), opens it in a new tab; a blank url suppresses
navigation. React and Angular wrappers expose an optional `licenseUrl` prop.
License-gate the Angular drawer like React: surface `licenseStatus` as a
signal on the CopilotKit service and gate the wrapper on
status valid|expiring && checkFeature("threads"), skipping the thread fetch
while unlicensed and showing the loading state (not the locked view) until
the status resolves. Reverses the earlier always-licensed Angular call.
Closes ENT-1027.
Final naming decision. Renames the public component + element across the
board: React/Angular CopilotDrawer -> CopilotThreadsDrawer, the Lit element
copilotkit-drawer -> copilotkit-threads-drawer (tag, CopilotKitThreadsDrawer
class, COPILOTKIT_THREADS_DRAWER_TAG, defineCopilotKitThreadsDrawer), the
@copilotkit/web-components/drawer subpath -> /threads-drawer (+ src dir),
the CopilotThreadsDrawerRow directive / copilotThreadsDrawerRow input, and
all prose/test references. Generic types (DrawerThread, DrawerFilter),
--cpk-drawer-* tokens, and ::part names are unchanged. Behavior unchanged.
Per review, settle the locked-view affordance on a neutral 'license'
name. Renames the event (unlicensed -> licensed), React prop
(onUnlicensed -> onLicensed), slot/part/class (licensed, licensed-cta),
LicensedDetail type, data-testid, render method, and identifier-referencing
comments/test names. The existing 'licensed' boolean gate is unchanged;
prose describing the not-licensed state still reads 'unlicensed'/'locked
view'. Behavior unchanged; Angular unaffected.
Per review, drop the 'upsell' monetization jargon. Renames the event
(upsell -> unlicensed), the React prop (onUpsell -> onUnlicensed), the
slot/part/class (upsell -> unlicensed, upsell-cta -> unlicensed-cta),
the UnlicensedDetail type, the data-testid, and all comments/test names.
Behavior unchanged. Angular is unaffected (always-licensed, no gate).
Wires up the previously-dormant pagination plumbing (ENT-1016):
- Element: render a 'Load more' button at the list bottom when hasMore
(and not fetching / not errored), emitting a new load-more event
(LoadMoreDetail). Distinct from retry{scope:'fetch-more'} (error
recovery); both advance pagination.
- React CopilotDrawer: add a limit prop (forwarded to useThreads) and
route load-more to fetchMoreThreads.
- Angular CopilotDrawer: add a limit input (forwarded to injectThreads)
and route load-more to fetchMoreThreads.
Tests: element load-more render + emit + precedence; React limit
forwarding + load-more routing; Angular load-more routing.
_syncNameClipping toggled name-clipped on .row-name, but the stacking fix in
styles.ts targets .row.name-clipped:hover — a different element — so the z-index
lift never matched and clipped-name tooltips still painted under later rows
(each row is its own transform stacking context). Stamp the flag on the owning
.row too; the tooltip bubble stays scoped to .row-name:hover so a row-action
hover never surfaces it. Adds a red-green contract test asserting the class
lands on both the row-name and the row.
Addresses MikeRyanDev review on #5707.
A long thread name is clipped with an ellipsis; expose the full text via the
same instant primary-bubble tooltip as the row actions (not the native `title`),
shown only when the name is actually truncated. The name text moved to an inner
span so the outer `.row-name` can host the un-clipped bubble, and the hovered
row is z-lifted so the bubble isn't painted under later rows (each row is its
own stacking context from the entry-animation transform).
Row-action tooltips used a surface background + border, which read as a button.
Restyle to the standard CopilotKit tooltip: primary bubble, primary-foreground
text, an arrow, no border. Also suppress the tooltip while the delete-confirm
dialog is open — the clicked trash button keeps :focus-visible, which otherwise
left its "Delete" tooltip floating over the dialog.
Polish on top of the shipped <copilotkit-drawer> (#5701), surfaced by the first
real de-fork pilot (langgraph-js):
- Theming follows the host app's light/dark theme: each token resolves
override -> host theme var (--background/--card/...) -> built-in light default,
instead of baking light-only values (drawer was light on a dark app).
- Row actions are icon buttons (inline lucide-style SVG, currentColor) with
instant tooltips. Native title is replaced by a CSS tooltip (matches the
react components' delayDuration:0; title's show-delay is browser-fixed) and is
positioned to the side so the list's scroll-overflow never clips it.
- Archived rows are muted, no longer struck through.
- A refetch (e.g. Active<->All filter toggle) keeps the known list visible;
the full loading state shows only on the initial empty fetch.
- The reserved footer region hides when nothing is slotted into it (it rendered
as an empty box at the bottom); mirrors the memories region.
- Mobile gets a self-owned floating launcher so the drawer is always openable
with no host header wiring. Its icon is swappable via a launcher-icon slot and
its position via --cpk-drawer-launcher-top/left (host can center it on its own
header controls); exposes part="launcher".
Adds element tests for icons+tooltips, archived styling, refetch-preserves-list,
footer hide-when-empty, and the mobile launcher (render + open + desktop-absent).
The mobile drawer is a modal dialog, but the Tab trap was bound to .root only,
while the backdrop button renders as a sibling OUTSIDE .root — so Tab from the
backdrop (or any out-of-root node) could escape the modal. Move the trap to the
host keydown listener (keydown is composed, so it catches the backdrop too) and
include the backdrop in the focusable cycle; keep initial focus on the first
real control. Adds a regression test for backdrop/outside-root Tab containment.
The UMD bundle inlines lit for a self-contained CDN <script>; inlining lit also
pulls in its runtime deps (@lit/reactive-element, lit-html, lit-element,
@lit-labs/ssr-dom-shim). tsdown flags that transitive bundling as 'unintended'
and promotes the warning to a fatal error under CI, breaking
@copilotkit/web-components:build (and every dependent). Declare all lit packages
in noExternal and set inlineOnly: false to mark the bundling intentional.
Shadow-DOM Lit element that renders the threads drawer: self-contained styles
with build-time token sync from react-core's theme, slot projection for custom
rows, license/upsell gating, filtering, and a mobile modal. View-state lives on
the element; domain state stays with the host. Multi-format build (ESM/CJS
externalize lit as a peer dep; UMD inlines lit so the CDN <script> path is
self-contained).