@copilotkit/web-inspector 1.60.1 → 1.61.0
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/dist/index.cjs +68 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +68 -19
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +68 -19
- package/dist/index.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/web-inspector.spec.ts +118 -5
- package/src/index.ts +89 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/web-inspector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.61.0",
|
|
4
4
|
"description": "Lit-based web component for the CopilotKit web inspector",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"lit": "^3.2.0",
|
|
28
28
|
"lucide": "^0.525.0",
|
|
29
29
|
"marked": "^12.0.2",
|
|
30
|
-
"@copilotkit/core": "1.
|
|
30
|
+
"@copilotkit/core": "1.61.0"
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
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
|
-
|
|
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();
|