@copilotkit/web-inspector 1.60.0 → 1.60.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/web-inspector",
3
- "version": "1.60.0",
3
+ "version": "1.60.2",
4
4
  "description": "Lit-based web component for the CopilotKit web inspector",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,11 +23,11 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@ag-ui/client": "0.0.56",
26
+ "@ag-ui/client": "0.0.57",
27
27
  "lit": "^3.2.0",
28
28
  "lucide": "^0.525.0",
29
29
  "marked": "^12.0.2",
30
- "@copilotkit/core": "1.60.0"
30
+ "@copilotkit/core": "1.60.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@tailwindcss/cli": "^4.1.11",
@@ -1,9 +1,7 @@
1
1
  import { WebInspectorElement, ɵCpkThreadDetails } from "../index";
2
- import {
3
- CopilotKitCore,
4
- CopilotKitCoreRuntimeConnectionStatus,
5
- type CopilotKitCoreSubscriber,
6
- } from "@copilotkit/core";
2
+ import type { CopilotKitCore } from "@copilotkit/core";
3
+ import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
4
+ import type { CopilotKitCoreSubscriber } from "@copilotkit/core";
7
5
  import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
8
6
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
9
7
 
@@ -462,3 +460,118 @@ describe("ɵCpkThreadDetails caching", () => {
462
460
  expect(internals.renderEvents()).not.toBe(eventsA);
463
461
  });
464
462
  });
463
+
464
+ // ─────────────────────────────────────────────────────────────────────────
465
+ // Announcement preview (popout) dismissal MUST persist
466
+ // ─────────────────────────────────────────────────────────────────────────
467
+ //
468
+ // The preview bubble that pops out of the floating button carries an X. Clicking
469
+ // it MUST persist the announcement timestamp to localStorage. Otherwise
470
+ // fetchAnnouncement() recomputes `showAnnouncementPreview` from the (still
471
+ // empty) stored timestamp on the next mount and the bubble pops straight back
472
+ // out — the regression these tests guard against. Persistence lives only in
473
+ // markAnnouncementSeen(); the body-click / open paths clear the flag in memory
474
+ // only and are intentionally NOT persistent.
475
+
476
+ const ANNOUNCEMENT_STORAGE_KEY = "cpk:inspector:announcements";
477
+
478
+ type AnnouncementInternals = {
479
+ hasUnseenAnnouncement: boolean;
480
+ showAnnouncementPreview: boolean;
481
+ announcementPreviewText: string | null;
482
+ announcementTimestamp: string | null;
483
+ isOpen: boolean;
484
+ };
485
+
486
+ describe("WebInspectorElement announcement preview dismissal", () => {
487
+ let store: Record<string, string>;
488
+
489
+ beforeEach(() => {
490
+ document.body.innerHTML = "";
491
+ store = {};
492
+ vi.stubGlobal("localStorage", {
493
+ getItem: (key: string) => store[key] ?? null,
494
+ setItem: (key: string, value: string) => {
495
+ store[key] = value;
496
+ },
497
+ removeItem: (key: string) => {
498
+ delete store[key];
499
+ },
500
+ clear: () => {
501
+ for (const key of Object.keys(store)) delete store[key];
502
+ },
503
+ get length() {
504
+ return Object.keys(store).length;
505
+ },
506
+ key: (index: number) => Object.keys(store)[index] ?? null,
507
+ });
508
+ });
509
+
510
+ /** Mount a closed inspector with an unseen announcement so the popout renders. */
511
+ async function mountWithUnseenAnnouncement(timestamp: string) {
512
+ const { core } = createMockCore();
513
+ const inspector = createInspectorWithCore(core);
514
+ const a = inspector as unknown as AnnouncementInternals;
515
+ a.announcementTimestamp = timestamp;
516
+ a.announcementPreviewText = "Slack early access is here!";
517
+ a.hasUnseenAnnouncement = true;
518
+ a.showAnnouncementPreview = true;
519
+ inspector.requestUpdate();
520
+ await inspector.updateComplete;
521
+ return { inspector, a };
522
+ }
523
+
524
+ it("persists the announcement timestamp when the popout X is clicked", async () => {
525
+ const timestamp = "2026-06-11T13:00:00.000Z";
526
+ const { inspector, a } = await mountWithUnseenAnnouncement(timestamp);
527
+
528
+ const dismiss = inspector.shadowRoot?.querySelector<HTMLElement>(
529
+ ".announcement-preview__dismiss",
530
+ );
531
+ expect(dismiss, "popout dismiss control should render").not.toBeNull();
532
+
533
+ dismiss?.click();
534
+ await inspector.updateComplete;
535
+
536
+ // The dismissal is persisted, so a remount would stay closed.
537
+ expect(store[ANNOUNCEMENT_STORAGE_KEY]).toBe(JSON.stringify({ timestamp }));
538
+ // In-memory flags cleared and the bubble is gone.
539
+ expect(a.hasUnseenAnnouncement).toBe(false);
540
+ expect(a.showAnnouncementPreview).toBe(false);
541
+ expect(
542
+ inspector.shadowRoot?.querySelector(".announcement-preview"),
543
+ ).toBeNull();
544
+ });
545
+
546
+ it("dismissing the popout X does not open the inspector", async () => {
547
+ const { inspector, a } = await mountWithUnseenAnnouncement(
548
+ "2026-06-11T13:00:00.000Z",
549
+ );
550
+ expect(a.isOpen).toBe(false);
551
+
552
+ inspector.shadowRoot
553
+ ?.querySelector<HTMLElement>(".announcement-preview__dismiss")
554
+ ?.click();
555
+ await inspector.updateComplete;
556
+
557
+ // X dismisses without opening (only a body click opens the inspector).
558
+ expect(a.isOpen).toBe(false);
559
+ });
560
+
561
+ it("clicking the popout body opens the inspector without persisting", async () => {
562
+ const { inspector, a } = await mountWithUnseenAnnouncement(
563
+ "2026-06-11T13:00:00.000Z",
564
+ );
565
+
566
+ inspector.shadowRoot
567
+ ?.querySelector<HTMLElement>(".announcement-preview")
568
+ ?.click();
569
+ await inspector.updateComplete;
570
+
571
+ // Body click is engagement, not dismissal: it opens but must NOT persist,
572
+ // so the in-window banner still shows the announcement.
573
+ expect(a.isOpen).toBe(true);
574
+ expect(store[ANNOUNCEMENT_STORAGE_KEY]).toBeUndefined();
575
+ expect(a.hasUnseenAnnouncement).toBe(true);
576
+ });
577
+ });
package/src/index.ts CHANGED
@@ -3463,6 +3463,11 @@ ${argsString}</pre
3463
3463
  transition: transform 300ms ease;
3464
3464
  }
3465
3465
 
3466
+ .console-button-wrapper {
3467
+ position: relative;
3468
+ display: inline-flex;
3469
+ }
3470
+
3466
3471
  .console-button {
3467
3472
  transition:
3468
3473
  transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
@@ -3590,6 +3595,36 @@ ${argsString}</pre
3590
3595
  box-shadow: -6px 6px 10px rgba(1, 5, 7, 0.08);
3591
3596
  }
3592
3597
 
3598
+ .announcement-preview__dismiss {
3599
+ flex: none;
3600
+ margin-top: -1px;
3601
+ width: 20px;
3602
+ height: 20px;
3603
+ padding: 0;
3604
+ appearance: none;
3605
+ background: none;
3606
+ border: 0;
3607
+ display: inline-flex;
3608
+ align-items: center;
3609
+ justify-content: center;
3610
+ border-radius: 6px;
3611
+ color: #838389;
3612
+ cursor: pointer;
3613
+ transition:
3614
+ background 120ms ease,
3615
+ color 120ms ease;
3616
+ }
3617
+
3618
+ .announcement-preview__dismiss:hover {
3619
+ background: rgba(0, 0, 0, 0.06);
3620
+ color: #010507;
3621
+ }
3622
+
3623
+ .announcement-preview__dismiss:focus-visible {
3624
+ outline: 2px solid #bec2ff;
3625
+ outline-offset: 1px;
3626
+ }
3627
+
3593
3628
  .announcement-dismiss {
3594
3629
  background: none;
3595
3630
  border: none;
@@ -4270,29 +4305,38 @@ ${argsString}</pre
4270
4305
  this.isDragging ? "cursor-grabbing" : "cursor-grab",
4271
4306
  ].join(" ");
4272
4307
 
4308
+ // The announcement preview renders as a SIBLING of the floating button (not
4309
+ // a child) so its dismiss affordance can be a real <button>. Nesting any
4310
+ // interactive/tabbable element inside the floating <button> violates the
4311
+ // HTML button content model. The wrapper is position: relative so the
4312
+ // absolutely-positioned preview still anchors to the button's edge.
4273
4313
  return html`
4274
- <button
4275
- class=${buttonClasses}
4276
- type="button"
4277
- aria-label="Web Inspector"
4278
- data-drag-context="button"
4279
- data-dragging=${
4280
- this.isDragging && this.pointerContext === "button" ? "true" : "false"
4281
- }
4282
- @pointerdown=${this.handlePointerDown}
4283
- @pointermove=${this.handlePointerMove}
4284
- @pointerup=${this.handlePointerUp}
4285
- @pointercancel=${this.handlePointerCancel}
4286
- @click=${this.handleButtonClick}
4287
- >
4314
+ <div class="console-button-wrapper">
4315
+ <button
4316
+ class=${buttonClasses}
4317
+ type="button"
4318
+ aria-label="Web Inspector"
4319
+ data-drag-context="button"
4320
+ data-dragging=${
4321
+ this.isDragging && this.pointerContext === "button"
4322
+ ? "true"
4323
+ : "false"
4324
+ }
4325
+ @pointerdown=${this.handlePointerDown}
4326
+ @pointermove=${this.handlePointerMove}
4327
+ @pointerup=${this.handlePointerUp}
4328
+ @pointercancel=${this.handlePointerCancel}
4329
+ @click=${this.handleButtonClick}
4330
+ >
4331
+ <img
4332
+ src=${inspectorLogoIconUrl}
4333
+ alt="Inspector logo"
4334
+ class="h-5 w-auto"
4335
+ loading="lazy"
4336
+ />
4337
+ </button>
4288
4338
  ${this.renderAnnouncementPreview()}
4289
- <img
4290
- src=${inspectorLogoIconUrl}
4291
- alt="Inspector logo"
4292
- class="h-5 w-auto"
4293
- loading="lazy"
4294
- />
4295
- </button>
4339
+ </div>
4296
4340
  `;
4297
4341
  }
4298
4342
 
@@ -7439,6 +7483,9 @@ ${prettyEvent}</pre
7439
7483
  const side =
7440
7484
  this.contextState.button.anchor.horizontal === "left" ? "right" : "left";
7441
7485
 
7486
+ // The preview is a sibling of the floating button (see renderButton), so the
7487
+ // dismiss control is a real <button>. stopPropagation keeps the X from
7488
+ // bubbling to the preview body, whose click opens the inspector.
7442
7489
  return html`<div
7443
7490
  class="announcement-preview"
7444
7491
  data-side=${side}
@@ -7446,6 +7493,14 @@ ${prettyEvent}</pre
7446
7493
  @click=${() => this.handleAnnouncementPreviewClick()}
7447
7494
  >
7448
7495
  <span>${this.announcementPreviewText}</span>
7496
+ <button
7497
+ type="button"
7498
+ class="announcement-preview__dismiss"
7499
+ aria-label="Dismiss announcement"
7500
+ @click=${this.handleDismissAnnouncementPreview}
7501
+ >
7502
+ ${this.renderIcon("X")}
7503
+ </button>
7449
7504
  <span class="announcement-preview__arrow"></span>
7450
7505
  </div>`;
7451
7506
  }
@@ -7455,6 +7510,19 @@ ${prettyEvent}</pre
7455
7510
  this.openInspector();
7456
7511
  }
7457
7512
 
7513
+ // Dismissing the preview bubble must PERSIST via markAnnouncementSeen(),
7514
+ // otherwise the bubble pops back out on the next mount because
7515
+ // fetchAnnouncement() recomputes showAnnouncementPreview from the stored
7516
+ // timestamp. Clearing only the in-memory flag (as handleAnnouncementPreviewClick
7517
+ // and openInspector do) is intentionally transient — it's the X that makes
7518
+ // the dismissal stick.
7519
+ private handleDismissAnnouncementPreview = (event: Event): void => {
7520
+ // Don't let the dismiss bubble to the preview body, whose click opens the
7521
+ // inspector.
7522
+ event.stopPropagation();
7523
+ this.handleDismissAnnouncement();
7524
+ };
7525
+
7458
7526
  private handleDismissAnnouncement = (): void => {
7459
7527
  this.trackBannerClickedOnce({ cta: "dismiss" });
7460
7528
  this.markAnnouncementSeen();