@copilotkitnext/web-inspector 0.0.13 → 0.0.14

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/src/index.ts CHANGED
@@ -6,7 +6,7 @@ import { unsafeHTML } from "lit/directives/unsafe-html.js";
6
6
  import { icons } from "lucide";
7
7
  import type { CopilotKitCore, CopilotKitCoreSubscriber } from "@copilotkitnext/core";
8
8
  import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
9
- import type { Anchor, ContextKey, ContextState, Position, Size } from "./lib/types";
9
+ import type { Anchor, ContextKey, ContextState, DockMode, Position, Size } from "./lib/types";
10
10
  import {
11
11
  applyAnchorPosition as applyAnchorPositionHelper,
12
12
  centerContext as centerContextHelper,
@@ -23,6 +23,7 @@ import {
23
23
  isValidAnchor,
24
24
  isValidPosition,
25
25
  isValidSize,
26
+ isValidDockMode,
26
27
  } from "./lib/persistence";
27
28
 
28
29
  export const WEB_INSPECTOR_TAG = "web-inspector" as const;
@@ -39,12 +40,14 @@ type MenuItem = {
39
40
 
40
41
  const EDGE_MARGIN = 16;
41
42
  const DRAG_THRESHOLD = 6;
42
- const MIN_WINDOW_WIDTH = 260;
43
+ const MIN_WINDOW_WIDTH = 600;
44
+ const MIN_WINDOW_WIDTH_DOCKED_LEFT = 420;
43
45
  const MIN_WINDOW_HEIGHT = 200;
44
46
  const COOKIE_NAME = "copilotkit_inspector_state";
45
47
  const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; // 30 days
46
48
  const DEFAULT_BUTTON_SIZE: Size = { width: 48, height: 48 };
47
- const DEFAULT_WINDOW_SIZE: Size = { width: 320, height: 380 };
49
+ const DEFAULT_WINDOW_SIZE: Size = { width: 840, height: 560 };
50
+ const DOCKED_LEFT_WIDTH = 500; // Sensible width for left dock with collapsed sidebar
48
51
  const MAX_AGENT_EVENTS = 200;
49
52
  const MAX_TOTAL_EVENTS = 500;
50
53
 
@@ -66,8 +69,11 @@ export class WebInspectorElement extends LitElement {
66
69
  private coreUnsubscribe: (() => void) | null = null;
67
70
  private agentSubscriptions: Map<string, () => void> = new Map();
68
71
  private agentEvents: Map<string, InspectorEvent[]> = new Map();
72
+ private agentMessages: Map<string, unknown[]> = new Map();
73
+ private agentStates: Map<string, unknown> = new Map();
69
74
  private flattenedEvents: InspectorEvent[] = [];
70
75
  private eventCounter = 0;
76
+ private contextStore: Record<string, { description?: string; value: unknown }> = {};
71
77
 
72
78
  private pointerId: number | null = null;
73
79
  private dragStart: Position | null = null;
@@ -79,6 +85,10 @@ export class WebInspectorElement extends LitElement {
79
85
  private ignoreNextButtonClick = false;
80
86
  private selectedMenu: MenuKey = "ag-ui-events";
81
87
  private contextMenuOpen = false;
88
+ private dockMode: DockMode = "floating";
89
+ private previousBodyMargins: { left: string; bottom: string } | null = null;
90
+ private transitionTimeoutId: ReturnType<typeof setTimeout> | null = null;
91
+ private pendingSelectedContext: string | null = null;
82
92
 
83
93
  get core(): CopilotKitCore | null {
84
94
  return this._core;
@@ -137,10 +147,19 @@ export class WebInspectorElement extends LitElement {
137
147
  onAgentsChanged: ({ agents }) => {
138
148
  this.processAgentsChanged(agents);
139
149
  },
150
+ onContextChanged: ({ context }) => {
151
+ this.contextStore = { ...context };
152
+ this.requestUpdate();
153
+ },
140
154
  } satisfies CopilotKitCoreSubscriber;
141
155
 
142
156
  this.coreUnsubscribe = core.subscribe(this.coreSubscriber);
143
157
  this.processAgentsChanged(core.agents);
158
+
159
+ // Initialize context from core
160
+ if (core.context) {
161
+ this.contextStore = { ...core.context };
162
+ }
144
163
  }
145
164
 
146
165
  private detachFromCore(): void {
@@ -158,6 +177,8 @@ export class WebInspectorElement extends LitElement {
158
177
  }
159
178
  this.agentSubscriptions.clear();
160
179
  this.agentEvents.clear();
180
+ this.agentMessages.clear();
181
+ this.agentStates.clear();
161
182
  this.flattenedEvents = [];
162
183
  this.eventCounter = 0;
163
184
  }
@@ -177,6 +198,8 @@ export class WebInspectorElement extends LitElement {
177
198
  if (!seenAgentIds.has(agentId)) {
178
199
  this.unsubscribeFromAgent(agentId);
179
200
  this.agentEvents.delete(agentId);
201
+ this.agentMessages.delete(agentId);
202
+ this.agentStates.delete(agentId);
180
203
  }
181
204
  }
182
205
 
@@ -226,12 +249,18 @@ export class WebInspectorElement extends LitElement {
226
249
  },
227
250
  onStateSnapshotEvent: ({ event }) => {
228
251
  this.recordAgentEvent(agentId, "STATE_SNAPSHOT", event);
252
+ this.syncAgentState(agent);
229
253
  },
230
254
  onStateDeltaEvent: ({ event }) => {
231
255
  this.recordAgentEvent(agentId, "STATE_DELTA", event);
256
+ this.syncAgentState(agent);
232
257
  },
233
258
  onMessagesSnapshotEvent: ({ event }) => {
234
259
  this.recordAgentEvent(agentId, "MESSAGES_SNAPSHOT", event);
260
+ this.syncAgentMessages(agent);
261
+ },
262
+ onMessagesChanged: () => {
263
+ this.syncAgentMessages(agent);
235
264
  },
236
265
  onRawEvent: ({ event }) => {
237
266
  this.recordAgentEvent(agentId, "RAW_EVENT", event);
@@ -243,6 +272,8 @@ export class WebInspectorElement extends LitElement {
243
272
 
244
273
  const { unsubscribe } = agent.subscribe(subscriber);
245
274
  this.agentSubscriptions.set(agentId, unsubscribe);
275
+ this.syncAgentMessages(agent);
276
+ this.syncAgentState(agent);
246
277
 
247
278
  if (!this.agentEvents.has(agentId)) {
248
279
  this.agentEvents.set(agentId, []);
@@ -275,6 +306,38 @@ export class WebInspectorElement extends LitElement {
275
306
  this.requestUpdate();
276
307
  }
277
308
 
309
+ private syncAgentMessages(agent: AbstractAgent): void {
310
+ if (!agent?.agentId) {
311
+ return;
312
+ }
313
+
314
+ const messages = (agent as { messages?: unknown }).messages;
315
+
316
+ if (Array.isArray(messages)) {
317
+ this.agentMessages.set(agent.agentId, messages);
318
+ } else {
319
+ this.agentMessages.delete(agent.agentId);
320
+ }
321
+
322
+ this.requestUpdate();
323
+ }
324
+
325
+ private syncAgentState(agent: AbstractAgent): void {
326
+ if (!agent?.agentId) {
327
+ return;
328
+ }
329
+
330
+ const state = (agent as { state?: unknown }).state;
331
+
332
+ if (state === undefined || state === null) {
333
+ this.agentStates.delete(agent.agentId);
334
+ } else {
335
+ this.agentStates.set(agent.agentId, state);
336
+ }
337
+
338
+ this.requestUpdate();
339
+ }
340
+
278
341
  private updateContextOptions(agentIds: Set<string>): void {
279
342
  const nextOptions: Array<{ key: string; label: string }> = [
280
343
  { key: "all-agents", label: "All Agents" },
@@ -291,9 +354,38 @@ export class WebInspectorElement extends LitElement {
291
354
  this.contextOptions = nextOptions;
292
355
  }
293
356
 
294
- if (!nextOptions.some((option) => option.key === this.selectedContext)) {
295
- this.selectedContext = "all-agents";
296
- this.expandedRows.clear();
357
+ const pendingContext = this.pendingSelectedContext;
358
+ if (pendingContext) {
359
+ const isPendingAvailable = pendingContext === "all-agents" || agentIds.has(pendingContext);
360
+ if (isPendingAvailable) {
361
+ if (this.selectedContext !== pendingContext) {
362
+ this.selectedContext = pendingContext;
363
+ this.expandedRows.clear();
364
+ }
365
+ this.pendingSelectedContext = null;
366
+ } else if (agentIds.size > 0) {
367
+ // Agents are loaded but the pending selection no longer exists
368
+ this.pendingSelectedContext = null;
369
+ }
370
+ }
371
+
372
+ const hasSelectedContext = nextOptions.some((option) => option.key === this.selectedContext);
373
+
374
+ if (!hasSelectedContext && this.pendingSelectedContext === null) {
375
+ // Auto-select "default" agent if it exists, otherwise first agent, otherwise "all-agents"
376
+ let nextSelected: string = "all-agents";
377
+
378
+ if (agentIds.has("default")) {
379
+ nextSelected = "default";
380
+ } else if (agentIds.size > 0) {
381
+ nextSelected = Array.from(agentIds).sort((a, b) => a.localeCompare(b))[0]!;
382
+ }
383
+
384
+ if (this.selectedContext !== nextSelected) {
385
+ this.selectedContext = nextSelected;
386
+ this.expandedRows.clear();
387
+ this.persistState();
388
+ }
297
389
  }
298
390
  }
299
391
 
@@ -305,6 +397,183 @@ export class WebInspectorElement extends LitElement {
305
397
  return this.agentEvents.get(this.selectedContext) ?? [];
306
398
  }
307
399
 
400
+ private getLatestStateForAgent(agentId: string): unknown | null {
401
+ if (this.agentStates.has(agentId)) {
402
+ return this.agentStates.get(agentId);
403
+ }
404
+
405
+ const events = this.agentEvents.get(agentId) ?? [];
406
+ const stateEvent = events.find((e) => e.type === "STATE_SNAPSHOT");
407
+ return stateEvent?.payload ?? null;
408
+ }
409
+
410
+ private getLatestMessagesForAgent(agentId: string): unknown[] | null {
411
+ const messages = this.agentMessages.get(agentId);
412
+ return messages ?? null;
413
+ }
414
+
415
+ private getAgentStatus(agentId: string): "running" | "idle" | "error" {
416
+ const events = this.agentEvents.get(agentId) ?? [];
417
+ if (events.length === 0) {
418
+ return "idle";
419
+ }
420
+
421
+ // Check most recent run-related event
422
+ const runEvent = events.find((e) => e.type === "RUN_STARTED" || e.type === "RUN_FINISHED" || e.type === "RUN_ERROR");
423
+
424
+ if (!runEvent) {
425
+ return "idle";
426
+ }
427
+
428
+ if (runEvent.type === "RUN_ERROR") {
429
+ return "error";
430
+ }
431
+
432
+ if (runEvent.type === "RUN_STARTED") {
433
+ // Check if there's a RUN_FINISHED after this
434
+ const finishedAfter = events.find(
435
+ (e) => e.type === "RUN_FINISHED" && e.timestamp > runEvent.timestamp
436
+ );
437
+ return finishedAfter ? "idle" : "running";
438
+ }
439
+
440
+ return "idle";
441
+ }
442
+
443
+ private getAgentStats(agentId: string): { totalEvents: number; lastActivity: number | null; messages: number; toolCalls: number; errors: number } {
444
+ const events = this.agentEvents.get(agentId) ?? [];
445
+
446
+ const messages = this.agentMessages.get(agentId);
447
+
448
+ const toolCallCount = Array.isArray(messages)
449
+ ? (messages as unknown[]).reduce<number>((count, rawMessage) => {
450
+ if (!rawMessage || typeof rawMessage !== 'object') {
451
+ return count;
452
+ }
453
+
454
+ const toolCalls = (rawMessage as { toolCalls?: unknown }).toolCalls;
455
+ if (!Array.isArray(toolCalls)) {
456
+ return count;
457
+ }
458
+
459
+ return count + toolCalls.length;
460
+ }, 0)
461
+ : events.filter((e) => e.type === "TOOL_CALL_END").length;
462
+
463
+ const messageCount = Array.isArray(messages) ? messages.length : 0;
464
+
465
+ return {
466
+ totalEvents: events.length,
467
+ lastActivity: events[0]?.timestamp ?? null,
468
+ messages: messageCount,
469
+ toolCalls: toolCallCount,
470
+ errors: events.filter((e) => e.type === "RUN_ERROR").length,
471
+ };
472
+ }
473
+
474
+ private renderToolCallDetails(toolCalls: unknown[]) {
475
+ if (!Array.isArray(toolCalls) || toolCalls.length === 0) {
476
+ return nothing;
477
+ }
478
+
479
+ return html`
480
+ <div class="mt-2 space-y-2">
481
+ ${toolCalls.map((call, index) => {
482
+ const toolCall = call as any;
483
+ const functionName = typeof toolCall?.function?.name === 'string' ? toolCall.function.name : 'Unknown function';
484
+ const callId = typeof toolCall?.id === 'string' ? toolCall.id : `tool-call-${index + 1}`;
485
+ const argsString = this.formatToolCallArguments(toolCall?.function?.arguments);
486
+ return html`
487
+ <div class="rounded-md border border-gray-200 bg-gray-50 p-3 text-xs text-gray-700">
488
+ <div class="flex flex-wrap items-center justify-between gap-1 font-medium text-gray-900">
489
+ <span>${functionName}</span>
490
+ <span class="text-[10px] text-gray-500">ID: ${callId}</span>
491
+ </div>
492
+ ${argsString
493
+ ? html`<pre class="mt-2 overflow-auto rounded bg-white p-2 text-[11px] leading-relaxed text-gray-800">${argsString}</pre>`
494
+ : nothing}
495
+ </div>
496
+ `;
497
+ })}
498
+ </div>
499
+ `;
500
+ }
501
+
502
+ private formatToolCallArguments(args: unknown): string | null {
503
+ if (args === undefined || args === null || args === '') {
504
+ return null;
505
+ }
506
+
507
+ if (typeof args === 'string') {
508
+ try {
509
+ const parsed = JSON.parse(args);
510
+ return JSON.stringify(parsed, null, 2);
511
+ } catch (error) {
512
+ return args;
513
+ }
514
+ }
515
+
516
+ if (typeof args === 'object') {
517
+ try {
518
+ return JSON.stringify(args, null, 2);
519
+ } catch (error) {
520
+ return String(args);
521
+ }
522
+ }
523
+
524
+ return String(args);
525
+ }
526
+
527
+ private hasRenderableState(state: unknown): boolean {
528
+ if (state === null || state === undefined) {
529
+ return false;
530
+ }
531
+
532
+ if (Array.isArray(state)) {
533
+ return state.length > 0;
534
+ }
535
+
536
+ if (typeof state === 'object') {
537
+ return Object.keys(state as Record<string, unknown>).length > 0;
538
+ }
539
+
540
+ if (typeof state === 'string') {
541
+ const trimmed = state.trim();
542
+ return trimmed.length > 0 && trimmed !== '{}';
543
+ }
544
+
545
+ return true;
546
+ }
547
+
548
+ private formatStateForDisplay(state: unknown): string {
549
+ if (state === null || state === undefined) {
550
+ return '';
551
+ }
552
+
553
+ if (typeof state === 'string') {
554
+ const trimmed = state.trim();
555
+ if (trimmed.length === 0) {
556
+ return '';
557
+ }
558
+ try {
559
+ const parsed = JSON.parse(trimmed);
560
+ return JSON.stringify(parsed, null, 2);
561
+ } catch {
562
+ return state;
563
+ }
564
+ }
565
+
566
+ if (typeof state === 'object') {
567
+ try {
568
+ return JSON.stringify(state, null, 2);
569
+ } catch {
570
+ return String(state);
571
+ }
572
+ }
573
+
574
+ return String(state);
575
+ }
576
+
308
577
  private getEventBadgeClasses(type: string): string {
309
578
  const base = "font-mono text-[10px] font-medium inline-flex items-center rounded-sm px-1.5 py-0.5 border";
310
579
 
@@ -350,6 +619,31 @@ export class WebInspectorElement extends LitElement {
350
619
  }
351
620
  }
352
621
 
622
+ private extractEventFromPayload(payload: unknown): unknown {
623
+ // If payload is an object with an 'event' field, extract it
624
+ if (payload && typeof payload === "object" && "event" in payload) {
625
+ return (payload as any).event;
626
+ }
627
+ // Otherwise, assume the payload itself is the event
628
+ return payload;
629
+ }
630
+
631
+ private async copyToClipboard(text: string, eventId: string): Promise<void> {
632
+ try {
633
+ await navigator.clipboard.writeText(text);
634
+ this.copiedEvents.add(eventId);
635
+ this.requestUpdate();
636
+
637
+ // Clear the "copied" state after 2 seconds
638
+ setTimeout(() => {
639
+ this.copiedEvents.delete(eventId);
640
+ this.requestUpdate();
641
+ }, 2000);
642
+ } catch (err) {
643
+ console.error("Failed to copy to clipboard:", err);
644
+ }
645
+ }
646
+
353
647
  static styles = [
354
648
  unsafeCSS(tailwindStyles),
355
649
  css`
@@ -362,16 +656,45 @@ export class WebInspectorElement extends LitElement {
362
656
  will-change: transform;
363
657
  }
364
658
 
659
+ :host([data-transitioning="true"]) {
660
+ transition: transform 300ms ease;
661
+ }
662
+
365
663
  .console-button {
366
664
  transition:
367
- transform 160ms ease,
665
+ transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1),
368
666
  opacity 160ms ease;
369
667
  }
370
668
 
669
+ .console-button[data-dragging="true"] {
670
+ transition: opacity 160ms ease;
671
+ }
672
+
673
+ .inspector-window[data-transitioning="true"] {
674
+ transition: width 300ms ease, height 300ms ease;
675
+ }
676
+
677
+ .inspector-window[data-docked="true"] {
678
+ border-radius: 0 !important;
679
+ box-shadow: none !important;
680
+ }
681
+
371
682
  .resize-handle {
372
683
  touch-action: none;
373
684
  user-select: none;
374
685
  }
686
+
687
+ .dock-resize-handle {
688
+ position: absolute;
689
+ top: 0;
690
+ right: 0;
691
+ width: 10px;
692
+ height: 100%;
693
+ cursor: ew-resize;
694
+ touch-action: none;
695
+ z-index: 50;
696
+ background: transparent;
697
+ }
375
698
  `,
376
699
  ];
377
700
 
@@ -380,6 +703,9 @@ export class WebInspectorElement extends LitElement {
380
703
  if (typeof window !== "undefined") {
381
704
  window.addEventListener("resize", this.handleResize);
382
705
  window.addEventListener("pointerdown", this.handleGlobalPointerDown as EventListener);
706
+
707
+ // Load state early (before first render) so menu selection is correct
708
+ this.hydrateStateFromCookieEarly();
383
709
  }
384
710
  }
385
711
 
@@ -389,6 +715,7 @@ export class WebInspectorElement extends LitElement {
389
715
  window.removeEventListener("resize", this.handleResize);
390
716
  window.removeEventListener("pointerdown", this.handleGlobalPointerDown as EventListener);
391
717
  }
718
+ this.removeDockStyles(); // Clean up any docking styles
392
719
  this.detachFromCore();
393
720
  }
394
721
 
@@ -408,15 +735,22 @@ export class WebInspectorElement extends LitElement {
408
735
 
409
736
  this.hydrateStateFromCookie();
410
737
 
738
+ // Apply docking styles if open and docked (skip transition on initial load)
739
+ if (this.isOpen && this.dockMode !== 'floating') {
740
+ this.applyDockStyles(true);
741
+ }
742
+
411
743
  this.applyAnchorPosition("button");
412
744
 
413
- if (this.hasCustomPosition.window) {
414
- this.applyAnchorPosition("window");
415
- } else {
416
- this.centerContext("window");
745
+ if (this.dockMode === 'floating') {
746
+ if (this.hasCustomPosition.window) {
747
+ this.applyAnchorPosition("window");
748
+ } else {
749
+ this.centerContext("window");
750
+ }
417
751
  }
418
752
 
419
- this.updateHostTransform("button");
753
+ this.updateHostTransform(this.isOpen ? "window" : "button");
420
754
  }
421
755
 
422
756
  render() {
@@ -462,6 +796,7 @@ export class WebInspectorElement extends LitElement {
462
796
  type="button"
463
797
  aria-label="Web Inspector"
464
798
  data-drag-context="button"
799
+ data-dragging=${this.isDragging && this.pointerContext === "button" ? "true" : "false"}
465
800
  @pointerdown=${this.handlePointerDown}
466
801
  @pointermove=${this.handlePointerMove}
467
802
  @pointerup=${this.handlePointerUp}
@@ -475,12 +810,19 @@ export class WebInspectorElement extends LitElement {
475
810
 
476
811
  private renderWindow() {
477
812
  const windowState = this.contextState.window;
478
- const windowStyles = {
479
- width: `${Math.round(windowState.size.width)}px`,
480
- height: `${Math.round(windowState.size.height)}px`,
481
- minWidth: `${MIN_WINDOW_WIDTH}px`,
482
- minHeight: `${MIN_WINDOW_HEIGHT}px`,
483
- };
813
+ const isDocked = this.dockMode !== 'floating';
814
+ const isTransitioning = this.hasAttribute('data-transitioning');
815
+ const isCollapsed = this.dockMode === 'docked-left';
816
+
817
+ const windowStyles = isDocked
818
+ ? this.getDockedWindowStyles()
819
+ : {
820
+ width: `${Math.round(windowState.size.width)}px`,
821
+ height: `${Math.round(windowState.size.height)}px`,
822
+ minWidth: `${MIN_WINDOW_WIDTH}px`,
823
+ minHeight: `${MIN_WINDOW_HEIGHT}px`,
824
+ };
825
+
484
826
  const contextDropdown = this.renderContextDropdown();
485
827
  const hasContextDropdown = contextDropdown !== nothing;
486
828
 
@@ -488,39 +830,63 @@ export class WebInspectorElement extends LitElement {
488
830
  <section
489
831
  class="inspector-window pointer-events-auto relative flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white text-gray-900 shadow-lg"
490
832
  style=${styleMap(windowStyles)}
833
+ data-docked=${isDocked}
834
+ data-transitioning=${isTransitioning}
491
835
  >
836
+ ${isDocked
837
+ ? html`
838
+ <div
839
+ class="dock-resize-handle pointer-events-auto"
840
+ role="presentation"
841
+ aria-hidden="true"
842
+ @pointerdown=${this.handleResizePointerDown}
843
+ @pointermove=${this.handleResizePointerMove}
844
+ @pointerup=${this.handleResizePointerUp}
845
+ @pointercancel=${this.handleResizePointerCancel}
846
+ ></div>
847
+ `
848
+ : nothing}
492
849
  <div class="flex flex-1 overflow-hidden bg-white text-gray-800">
493
850
  <nav
494
- class="flex w-56 shrink-0 flex-col justify-between border-r border-gray-200 bg-gray-50/50 px-3 pb-3 pt-3 text-xs"
851
+ class="flex ${isCollapsed ? 'w-16' : 'w-56'} shrink-0 flex-col justify-between border-r border-gray-200 bg-gray-50/50 px-3 pb-3 pt-3 text-xs transition-all duration-300"
495
852
  aria-label="Inspector sections"
496
853
  >
497
854
  <div class="flex flex-col gap-4 overflow-y-auto">
498
855
  <div
499
- class="flex items-center gap-2 pl-1 touch-none select-none ${this.isDragging && this.pointerContext === 'window' ? 'cursor-grabbing' : 'cursor-grab'}"
856
+ class="flex items-center ${isCollapsed ? 'justify-center' : 'gap-2 pl-1'} touch-none select-none ${this.isDragging && this.pointerContext === 'window' ? 'cursor-grabbing' : 'cursor-grab'}"
500
857
  data-drag-context="window"
501
858
  @pointerdown=${this.handlePointerDown}
502
859
  @pointermove=${this.handlePointerMove}
503
860
  @pointerup=${this.handlePointerUp}
504
861
  @pointercancel=${this.handlePointerCancel}
862
+ title="${isCollapsed ? 'Acme Inc - Enterprise' : ''}"
505
863
  >
506
864
  <span
507
865
  class="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-900 text-white pointer-events-none"
508
866
  >
509
867
  ${this.renderIcon("Building2")}
510
868
  </span>
511
- <div class="flex flex-1 flex-col leading-tight pointer-events-none">
512
- <span class="text-sm font-semibold text-gray-900">Acme Inc</span>
513
- <span class="text-[10px] text-gray-500">Enterprise</span>
514
- </div>
869
+ ${!isCollapsed
870
+ ? html`
871
+ <div class="flex flex-1 flex-col leading-tight pointer-events-none">
872
+ <span class="text-sm font-semibold text-gray-900">Acme Inc</span>
873
+ <span class="text-[10px] text-gray-500">Enterprise</span>
874
+ </div>
875
+ `
876
+ : nothing}
515
877
  </div>
516
878
 
517
879
  <div class="flex flex-col gap-2 pt-2">
518
- <div class="px-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">Platform</div>
880
+ ${!isCollapsed
881
+ ? html`<div class="px-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">Platform</div>`
882
+ : nothing}
519
883
  <div class="flex flex-col gap-0.5">
520
884
  ${this.menuItems.map(({ key, label, icon }) => {
521
885
  const isSelected = this.selectedMenu === key;
522
886
  const buttonClasses = [
523
- "group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300",
887
+ "group flex w-full items-center",
888
+ isCollapsed ? "justify-center p-2" : "gap-2 px-2 py-1.5",
889
+ "rounded-md text-left text-xs transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300",
524
890
  isSelected
525
891
  ? "bg-gray-900 text-white"
526
892
  : "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
@@ -535,16 +901,21 @@ export class WebInspectorElement extends LitElement {
535
901
  type="button"
536
902
  class=${buttonClasses}
537
903
  aria-pressed=${isSelected}
904
+ title="${isCollapsed ? label : ''}"
538
905
  @click=${() => this.handleMenuSelect(key)}
539
906
  >
540
907
  <span
541
- class="flex h-6 w-6 items-center justify-center rounded ${badgeClasses}"
908
+ class="flex h-6 w-6 items-center justify-center rounded ${isCollapsed && isSelected ? 'text-white' : isCollapsed ? 'text-gray-600' : badgeClasses}"
542
909
  aria-hidden="true"
543
910
  >
544
911
  ${this.renderIcon(icon)}
545
912
  </span>
546
- <span class="flex-1">${label}</span>
547
- <span class="text-gray-400 opacity-60">${this.renderIcon("ChevronRight")}</span>
913
+ ${!isCollapsed
914
+ ? html`
915
+ <span class="flex-1">${label}</span>
916
+ <span class="text-gray-400 opacity-60">${this.renderIcon("ChevronRight")}</span>
917
+ `
918
+ : nothing}
548
919
  </button>
549
920
  `;
550
921
  })}
@@ -553,52 +924,65 @@ export class WebInspectorElement extends LitElement {
553
924
  </div>
554
925
 
555
926
  <div
556
- class="relative flex items-center rounded-lg border border-gray-200 bg-white px-2 py-2 text-left text-xs text-gray-700 cursor-pointer hover:bg-gray-50 transition"
927
+ class="relative flex items-center ${isCollapsed ? 'justify-center p-1' : ''} rounded-lg border border-gray-200 bg-white ${isCollapsed ? '' : 'px-2 py-2'} text-left text-xs text-gray-700 cursor-pointer hover:bg-gray-50 transition"
928
+ title="${isCollapsed ? 'John Snow - john@snow.com' : ''}"
557
929
  >
558
930
  <span
559
- class="w-6 h-6 flex items-center justify-center overflow-hidden rounded bg-gray-100 text-[10px] font-semibold text-gray-700"
931
+ class="${isCollapsed ? 'w-8 h-8 shrink-0' : 'w-6 h-6'} flex items-center justify-center overflow-hidden rounded bg-gray-100 text-[10px] font-semibold text-gray-700"
560
932
  >
561
933
  JS
562
934
  </span>
563
- <div class="pl-2 flex flex-1 flex-col leading-tight">
564
- <span class="font-medium text-gray-900">John Snow</span>
565
- <span class="text-[10px] text-gray-500">john@snow.com</span>
566
- </div>
567
- <span class="text-gray-300">${this.renderIcon("ChevronRight")}</span>
935
+ ${!isCollapsed
936
+ ? html`
937
+ <div class="pl-2 flex flex-1 flex-col leading-tight">
938
+ <span class="font-medium text-gray-900">John Snow</span>
939
+ <span class="text-[10px] text-gray-500">john@snow.com</span>
940
+ </div>
941
+ <span class="text-gray-300">${this.renderIcon("ChevronRight")}</span>
942
+ `
943
+ : nothing}
568
944
  </div>
569
945
  </nav>
570
946
  <div class="relative flex flex-1 flex-col overflow-hidden">
571
947
  <div
572
- class="drag-handle flex items-center justify-between border-b border-gray-200 px-4 py-3 touch-none select-none ${this.isDragging && this.pointerContext === 'window' ? 'cursor-grabbing' : 'cursor-grab'}"
948
+ class="drag-handle flex items-center justify-between border-b border-gray-200 px-4 py-3 touch-none select-none ${isDocked ? '' : (this.isDragging && this.pointerContext === 'window' ? 'cursor-grabbing' : 'cursor-grab')}"
573
949
  data-drag-context="window"
574
- @pointerdown=${this.handlePointerDown}
575
- @pointermove=${this.handlePointerMove}
576
- @pointerup=${this.handlePointerUp}
577
- @pointercancel=${this.handlePointerCancel}
950
+ @pointerdown=${isDocked ? undefined : this.handlePointerDown}
951
+ @pointermove=${isDocked ? undefined : this.handlePointerMove}
952
+ @pointerup=${isDocked ? undefined : this.handlePointerUp}
953
+ @pointercancel=${isDocked ? undefined : this.handlePointerCancel}
578
954
  >
579
- <div class="flex items-center gap-2 text-xs text-gray-500">
580
- <span class="text-gray-400">
581
- ${this.renderIcon(this.getSelectedMenu().icon)}
582
- </span>
583
- <div class="flex items-center text-xs text-gray-600">
584
- <span class="pr-3">${this.getSelectedMenu().label}</span>
955
+ <div class="flex min-w-0 flex-1 items-center gap-2 text-xs text-gray-500">
956
+ <div class="flex min-w-0 flex-1 items-center text-xs text-gray-600">
957
+ <span class="flex shrink-0 items-center gap-1">
958
+ <span>🪁</span>
959
+ <span class="font-medium whitespace-nowrap">CopilotKit Inspector</span>
960
+ </span>
961
+ <span class="mx-3 h-3 w-px shrink-0 bg-gray-200"></span>
962
+ <span class="shrink-0 text-gray-400">
963
+ ${this.renderIcon(this.getSelectedMenu().icon)}
964
+ </span>
965
+ <span class="ml-2 truncate">${this.getSelectedMenu().label}</span>
585
966
  ${hasContextDropdown
586
967
  ? html`
587
- <span class="h-3 w-px bg-gray-200"></span>
588
- <div class="pl-3">${contextDropdown}</div>
968
+ <span class="mx-3 h-3 w-px shrink-0 bg-gray-200"></span>
969
+ <div class="min-w-0">${contextDropdown}</div>
589
970
  `
590
971
  : nothing}
591
972
  </div>
592
973
  </div>
593
- <button
594
- class="flex h-6 w-6 items-center justify-center rounded text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400"
595
- type="button"
596
- aria-label="Close Web Inspector"
597
- @pointerdown=${this.handleClosePointerDown}
598
- @click=${this.handleCloseClick}
599
- >
600
- ${this.renderIcon("X")}
601
- </button>
974
+ <div class="flex items-center gap-1">
975
+ ${this.renderDockControls()}
976
+ <button
977
+ class="flex h-6 w-6 items-center justify-center rounded text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400"
978
+ type="button"
979
+ aria-label="Close Web Inspector"
980
+ @pointerdown=${this.handleClosePointerDown}
981
+ @click=${this.handleCloseClick}
982
+ >
983
+ ${this.renderIcon("X")}
984
+ </button>
985
+ </div>
602
986
  </div>
603
987
  <div class="flex-1 overflow-auto">
604
988
  ${this.renderMainContent()}
@@ -631,6 +1015,41 @@ export class WebInspectorElement extends LitElement {
631
1015
  `;
632
1016
  }
633
1017
 
1018
+ private hydrateStateFromCookieEarly(): void {
1019
+ if (typeof document === "undefined" || typeof window === "undefined") {
1020
+ return;
1021
+ }
1022
+
1023
+ const persisted = loadInspectorState(COOKIE_NAME);
1024
+ if (!persisted) {
1025
+ return;
1026
+ }
1027
+
1028
+ // Restore the open/closed state
1029
+ if (typeof persisted.isOpen === "boolean") {
1030
+ this.isOpen = persisted.isOpen;
1031
+ }
1032
+
1033
+ // Restore the dock mode
1034
+ if (isValidDockMode(persisted.dockMode)) {
1035
+ this.dockMode = persisted.dockMode;
1036
+ }
1037
+
1038
+ // Restore selected menu
1039
+ if (typeof persisted.selectedMenu === "string") {
1040
+ const validMenu = this.menuItems.find((item) => item.key === persisted.selectedMenu);
1041
+ if (validMenu) {
1042
+ this.selectedMenu = validMenu.key;
1043
+ }
1044
+ }
1045
+
1046
+ // Restore selected context (agent), will be validated later against available agents
1047
+ if (typeof persisted.selectedContext === "string") {
1048
+ this.selectedContext = persisted.selectedContext;
1049
+ this.pendingSelectedContext = persisted.selectedContext;
1050
+ }
1051
+ }
1052
+
634
1053
  private hydrateStateFromCookie(): void {
635
1054
  if (typeof document === "undefined" || typeof window === "undefined") {
636
1055
  return;
@@ -667,6 +1086,7 @@ export class WebInspectorElement extends LitElement {
667
1086
  }
668
1087
 
669
1088
  if (isValidSize(persistedWindow.size)) {
1089
+ // Now clampWindowSize will use the correct minimum based on dockMode
670
1090
  this.contextState.window.size = this.clampWindowSize(persistedWindow.size);
671
1091
  }
672
1092
 
@@ -674,6 +1094,11 @@ export class WebInspectorElement extends LitElement {
674
1094
  this.hasCustomPosition.window = persistedWindow.hasCustomPosition;
675
1095
  }
676
1096
  }
1097
+
1098
+ if (typeof persisted.selectedContext === "string") {
1099
+ this.selectedContext = persisted.selectedContext;
1100
+ this.pendingSelectedContext = persisted.selectedContext;
1101
+ }
677
1102
  }
678
1103
 
679
1104
  private get activeContext(): ContextKey {
@@ -681,6 +1106,11 @@ export class WebInspectorElement extends LitElement {
681
1106
  }
682
1107
 
683
1108
  private handlePointerDown = (event: PointerEvent) => {
1109
+ // Don't allow dragging when docked
1110
+ if (this.dockMode !== 'floating' && this.isOpen) {
1111
+ return;
1112
+ }
1113
+
684
1114
  const target = event.currentTarget as HTMLElement | null;
685
1115
  const contextAttr = target?.dataset.dragContext;
686
1116
  const context: ContextKey = contextAttr === "window" ? "window" : "button";
@@ -743,16 +1173,18 @@ export class WebInspectorElement extends LitElement {
743
1173
  if (this.isDragging && this.pointerContext) {
744
1174
  event.preventDefault();
745
1175
  this.setDragging(false);
746
- this.updateAnchorFromPosition(this.pointerContext);
747
1176
  if (this.pointerContext === "window") {
1177
+ this.updateAnchorFromPosition(this.pointerContext);
748
1178
  this.hasCustomPosition.window = true;
1179
+ this.applyAnchorPosition(this.pointerContext);
749
1180
  } else if (this.pointerContext === "button") {
1181
+ // Snap button to nearest corner
1182
+ this.snapButtonToCorner();
750
1183
  this.hasCustomPosition.button = true;
751
1184
  if (this.draggedDuringInteraction) {
752
1185
  this.ignoreNextButtonClick = true;
753
1186
  }
754
1187
  }
755
- this.applyAnchorPosition(this.pointerContext);
756
1188
  } else if (context === "button" && !this.isOpen && !this.draggedDuringInteraction) {
757
1189
  this.openInspector();
758
1190
  }
@@ -810,6 +1242,11 @@ export class WebInspectorElement extends LitElement {
810
1242
  this.resizeStart = { x: event.clientX, y: event.clientY };
811
1243
  this.resizeInitialSize = { ...this.contextState.window.size };
812
1244
 
1245
+ // Remove transition from body during resize to prevent lag
1246
+ if (document.body && this.dockMode !== 'floating') {
1247
+ document.body.style.transition = '';
1248
+ }
1249
+
813
1250
  const target = event.currentTarget as HTMLElement | null;
814
1251
  target?.setPointerCapture?.(event.pointerId);
815
1252
  };
@@ -825,12 +1262,27 @@ export class WebInspectorElement extends LitElement {
825
1262
  const deltaY = event.clientY - this.resizeStart.y;
826
1263
  const state = this.contextState.window;
827
1264
 
828
- state.size = this.clampWindowSize({
829
- width: this.resizeInitialSize.width + deltaX,
830
- height: this.resizeInitialSize.height + deltaY,
831
- });
832
- this.keepPositionWithinViewport("window");
833
- this.updateAnchorFromPosition("window");
1265
+ // For docked states, only resize in the appropriate dimension
1266
+ if (this.dockMode === 'docked-left') {
1267
+ // Only resize width for left dock
1268
+ state.size = this.clampWindowSize({
1269
+ width: this.resizeInitialSize.width + deltaX,
1270
+ height: state.size.height,
1271
+ });
1272
+ // Update the body margin
1273
+ if (document.body) {
1274
+ document.body.style.marginLeft = `${state.size.width}px`;
1275
+ }
1276
+ } else {
1277
+ // Full resize for floating mode
1278
+ state.size = this.clampWindowSize({
1279
+ width: this.resizeInitialSize.width + deltaX,
1280
+ height: this.resizeInitialSize.height + deltaY,
1281
+ });
1282
+ this.keepPositionWithinViewport("window");
1283
+ this.updateAnchorFromPosition("window");
1284
+ }
1285
+
834
1286
  this.requestUpdate();
835
1287
  this.updateHostTransform("window");
836
1288
  };
@@ -845,8 +1297,14 @@ export class WebInspectorElement extends LitElement {
845
1297
  target.releasePointerCapture(this.resizePointerId);
846
1298
  }
847
1299
 
848
- this.updateAnchorFromPosition("window");
849
- this.applyAnchorPosition("window");
1300
+ // Only update anchor position for floating mode
1301
+ if (this.dockMode === 'floating') {
1302
+ this.updateAnchorFromPosition("window");
1303
+ this.applyAnchorPosition("window");
1304
+ }
1305
+
1306
+ // Persist the new size after resize completes
1307
+ this.persistState();
850
1308
  this.resetResizeTracking();
851
1309
  };
852
1310
 
@@ -860,8 +1318,14 @@ export class WebInspectorElement extends LitElement {
860
1318
  target.releasePointerCapture(this.resizePointerId);
861
1319
  }
862
1320
 
863
- this.updateAnchorFromPosition("window");
864
- this.applyAnchorPosition("window");
1321
+ // Only update anchor position for floating mode
1322
+ if (this.dockMode === 'floating') {
1323
+ this.updateAnchorFromPosition("window");
1324
+ this.applyAnchorPosition("window");
1325
+ }
1326
+
1327
+ // Persist the new size after resize completes
1328
+ this.persistState();
865
1329
  this.resetResizeTracking();
866
1330
  };
867
1331
 
@@ -964,20 +1428,135 @@ export class WebInspectorElement extends LitElement {
964
1428
  },
965
1429
  hasCustomPosition: this.hasCustomPosition.window,
966
1430
  },
1431
+ isOpen: this.isOpen,
1432
+ dockMode: this.dockMode,
1433
+ selectedMenu: this.selectedMenu,
1434
+ selectedContext: this.selectedContext,
967
1435
  };
968
1436
  saveInspectorState(COOKIE_NAME, state, COOKIE_MAX_AGE_SECONDS);
1437
+ this.pendingSelectedContext = state.selectedContext ?? null;
969
1438
  }
970
1439
 
971
1440
  private clampWindowSize(size: Size): Size {
1441
+ // Use smaller minimum width when docked left
1442
+ const minWidth = this.dockMode === 'docked-left' ? MIN_WINDOW_WIDTH_DOCKED_LEFT : MIN_WINDOW_WIDTH;
1443
+
972
1444
  if (typeof window === "undefined") {
973
1445
  return {
974
- width: Math.max(MIN_WINDOW_WIDTH, size.width),
1446
+ width: Math.max(minWidth, size.width),
975
1447
  height: Math.max(MIN_WINDOW_HEIGHT, size.height),
976
1448
  };
977
1449
  }
978
1450
 
979
1451
  const viewport = this.getViewportSize();
980
- return clampSizeToViewport(size, viewport, EDGE_MARGIN, MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
1452
+ return clampSizeToViewport(size, viewport, EDGE_MARGIN, minWidth, MIN_WINDOW_HEIGHT);
1453
+ }
1454
+
1455
+ private setDockMode(mode: DockMode): void {
1456
+ if (this.dockMode === mode) {
1457
+ return;
1458
+ }
1459
+
1460
+ // Add transition class for smooth dock mode changes
1461
+ this.startHostTransition();
1462
+
1463
+ // Clean up previous dock state
1464
+ this.removeDockStyles();
1465
+
1466
+ const previousMode = this.dockMode;
1467
+ this.dockMode = mode;
1468
+
1469
+ if (mode !== 'floating') {
1470
+ // For docking, set the target size immediately so body margins are correct
1471
+ if (mode === 'docked-left') {
1472
+ this.contextState.window.size.width = DOCKED_LEFT_WIDTH;
1473
+ }
1474
+
1475
+ // Then apply dock styles with correct sizes
1476
+ this.applyDockStyles();
1477
+ } else {
1478
+ // When floating, set size first then center
1479
+ this.contextState.window.size = { ...DEFAULT_WINDOW_SIZE };
1480
+ this.centerContext('window');
1481
+ }
1482
+
1483
+ this.persistState();
1484
+ this.requestUpdate();
1485
+ this.updateHostTransform('window');
1486
+ }
1487
+
1488
+ private startHostTransition(duration = 300): void {
1489
+ this.setAttribute('data-transitioning', 'true');
1490
+
1491
+ if (this.transitionTimeoutId !== null) {
1492
+ clearTimeout(this.transitionTimeoutId);
1493
+ }
1494
+
1495
+ this.transitionTimeoutId = setTimeout(() => {
1496
+ this.removeAttribute('data-transitioning');
1497
+ this.transitionTimeoutId = null;
1498
+ }, duration);
1499
+ }
1500
+
1501
+ private applyDockStyles(skipTransition = false): void {
1502
+ if (typeof document === 'undefined' || !document.body) {
1503
+ return;
1504
+ }
1505
+
1506
+ // Save original body margins
1507
+ const computedStyle = window.getComputedStyle(document.body);
1508
+ this.previousBodyMargins = {
1509
+ left: computedStyle.marginLeft,
1510
+ bottom: computedStyle.marginBottom,
1511
+ };
1512
+
1513
+ // Apply transition to body for smooth animation (only when docking, not during resize or initial load)
1514
+ if (!this.isResizing && !skipTransition) {
1515
+ document.body.style.transition = 'margin 300ms ease';
1516
+ }
1517
+
1518
+ // Apply body margins with the actual window sizes
1519
+ if (this.dockMode === 'docked-left') {
1520
+ document.body.style.marginLeft = `${this.contextState.window.size.width}px`;
1521
+ }
1522
+
1523
+ // Remove transition after animation completes
1524
+ if (!this.isResizing && !skipTransition) {
1525
+ setTimeout(() => {
1526
+ if (document.body) {
1527
+ document.body.style.transition = '';
1528
+ }
1529
+ }, 300);
1530
+ }
1531
+ }
1532
+
1533
+ private removeDockStyles(): void {
1534
+ if (typeof document === 'undefined' || !document.body) {
1535
+ return;
1536
+ }
1537
+
1538
+ // Only add transition if not resizing
1539
+ if (!this.isResizing) {
1540
+ document.body.style.transition = 'margin 300ms ease';
1541
+ }
1542
+
1543
+ // Restore original margins if saved
1544
+ if (this.previousBodyMargins) {
1545
+ document.body.style.marginLeft = this.previousBodyMargins.left;
1546
+ document.body.style.marginBottom = this.previousBodyMargins.bottom;
1547
+ this.previousBodyMargins = null;
1548
+ } else {
1549
+ // Reset to default if no previous values
1550
+ document.body.style.marginLeft = '';
1551
+ document.body.style.marginBottom = '';
1552
+ }
1553
+
1554
+ // Clean up transition after animation completes
1555
+ setTimeout(() => {
1556
+ if (document.body) {
1557
+ document.body.style.transition = '';
1558
+ }
1559
+ }, 300);
981
1560
  }
982
1561
 
983
1562
  private updateHostTransform(context: ContextKey = this.activeContext): void {
@@ -985,8 +1564,13 @@ export class WebInspectorElement extends LitElement {
985
1564
  return;
986
1565
  }
987
1566
 
988
- const { position } = this.contextState[context];
989
- this.style.transform = `translate3d(${position.x}px, ${position.y}px, 0)`;
1567
+ // For docked states, CSS handles positioning with fixed positioning
1568
+ if (this.isOpen && this.dockMode === 'docked-left') {
1569
+ this.style.transform = `translate3d(0, 0, 0)`;
1570
+ } else {
1571
+ const { position } = this.contextState[context];
1572
+ this.style.transform = `translate3d(${position.x}px, ${position.y}px, 0)`;
1573
+ }
990
1574
  }
991
1575
 
992
1576
  private setDragging(value: boolean): void {
@@ -1004,6 +1588,32 @@ export class WebInspectorElement extends LitElement {
1004
1588
  updateAnchorFromPositionHelper(this.contextState[context], viewport, EDGE_MARGIN);
1005
1589
  }
1006
1590
 
1591
+ private snapButtonToCorner(): void {
1592
+ if (typeof window === "undefined") {
1593
+ return;
1594
+ }
1595
+
1596
+ const viewport = this.getViewportSize();
1597
+ const state = this.contextState.button;
1598
+
1599
+ // Determine which corner is closest based on center of button
1600
+ const centerX = state.position.x + state.size.width / 2;
1601
+ const centerY = state.position.y + state.size.height / 2;
1602
+
1603
+ const horizontal: Anchor['horizontal'] = centerX < viewport.width / 2 ? 'left' : 'right';
1604
+ const vertical: Anchor['vertical'] = centerY < viewport.height / 2 ? 'top' : 'bottom';
1605
+
1606
+ // Set anchor to nearest corner
1607
+ state.anchor = { horizontal, vertical };
1608
+
1609
+ // Always use EDGE_MARGIN as offset (pinned to corner)
1610
+ state.anchorOffset = { x: EDGE_MARGIN, y: EDGE_MARGIN };
1611
+
1612
+ // Apply the anchor position to snap to corner
1613
+ this.startHostTransition();
1614
+ this.applyAnchorPosition('button');
1615
+ }
1616
+
1007
1617
  private applyAnchorPosition(context: ContextKey): void {
1008
1618
  if (typeof window === "undefined") {
1009
1619
  return;
@@ -1035,14 +1645,26 @@ export class WebInspectorElement extends LitElement {
1035
1645
  }
1036
1646
 
1037
1647
  this.isOpen = true;
1648
+ this.persistState(); // Save the open state
1649
+
1650
+ // Apply docking styles if in docked mode
1651
+ if (this.dockMode !== 'floating') {
1652
+ this.applyDockStyles();
1653
+ }
1654
+
1038
1655
  this.ensureWindowPlacement();
1039
1656
  this.requestUpdate();
1040
1657
  void this.updateComplete.then(() => {
1041
1658
  this.measureContext("window");
1042
- if (this.hasCustomPosition.window) {
1043
- this.applyAnchorPosition("window");
1659
+ if (this.dockMode === 'floating') {
1660
+ if (this.hasCustomPosition.window) {
1661
+ this.applyAnchorPosition("window");
1662
+ } else {
1663
+ this.centerContext("window");
1664
+ }
1044
1665
  } else {
1045
- this.centerContext("window");
1666
+ // Update transform for docked position
1667
+ this.updateHostTransform("window");
1046
1668
  }
1047
1669
  });
1048
1670
  }
@@ -1053,6 +1675,13 @@ export class WebInspectorElement extends LitElement {
1053
1675
  }
1054
1676
 
1055
1677
  this.isOpen = false;
1678
+
1679
+ // Remove docking styles when closing
1680
+ if (this.dockMode !== 'floating') {
1681
+ this.removeDockStyles();
1682
+ }
1683
+
1684
+ this.persistState(); // Save the closed state
1056
1685
  this.updateHostTransform("button");
1057
1686
  this.requestUpdate();
1058
1687
  void this.updateComplete.then(() => {
@@ -1085,6 +1714,62 @@ export class WebInspectorElement extends LitElement {
1085
1714
  return unsafeHTML(svgMarkup);
1086
1715
  }
1087
1716
 
1717
+ private renderDockControls() {
1718
+ if (this.dockMode === 'floating') {
1719
+ // Show dock left button
1720
+ return html`
1721
+ <button
1722
+ class="flex h-6 w-6 items-center justify-center rounded text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400"
1723
+ type="button"
1724
+ aria-label="Dock to left"
1725
+ title="Dock Left"
1726
+ @click=${() => this.handleDockClick('docked-left')}
1727
+ >
1728
+ ${this.renderIcon("PanelLeft")}
1729
+ </button>
1730
+ `;
1731
+ } else {
1732
+ // Show float button
1733
+ return html`
1734
+ <button
1735
+ class="flex h-6 w-6 items-center justify-center rounded text-gray-400 transition hover:bg-gray-100 hover:text-gray-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-400"
1736
+ type="button"
1737
+ aria-label="Float window"
1738
+ title="Float"
1739
+ @click=${() => this.handleDockClick('floating')}
1740
+ >
1741
+ ${this.renderIcon("Maximize2")}
1742
+ </button>
1743
+ `;
1744
+ }
1745
+ }
1746
+
1747
+ private getDockedWindowStyles(): Record<string, string> {
1748
+ if (this.dockMode === 'docked-left') {
1749
+ return {
1750
+ position: 'fixed',
1751
+ top: '0',
1752
+ left: '0',
1753
+ bottom: '0',
1754
+ width: `${Math.round(this.contextState.window.size.width)}px`,
1755
+ height: '100vh',
1756
+ minWidth: `${MIN_WINDOW_WIDTH_DOCKED_LEFT}px`,
1757
+ borderRadius: '0',
1758
+ };
1759
+ }
1760
+ // Default to floating styles
1761
+ return {
1762
+ width: `${Math.round(this.contextState.window.size.width)}px`,
1763
+ height: `${Math.round(this.contextState.window.size.height)}px`,
1764
+ minWidth: `${MIN_WINDOW_WIDTH}px`,
1765
+ minHeight: `${MIN_WINDOW_HEIGHT}px`,
1766
+ };
1767
+ }
1768
+
1769
+ private handleDockClick(mode: DockMode): void {
1770
+ this.setDockMode(mode);
1771
+ }
1772
+
1088
1773
  private serializeAttributes(attributes: Record<string, string | number | undefined>): string {
1089
1774
  return Object.entries(attributes)
1090
1775
  .filter(([key, value]) => key !== "key" && value !== undefined && value !== null && value !== "")
@@ -1098,6 +1783,9 @@ export class WebInspectorElement extends LitElement {
1098
1783
 
1099
1784
  private selectedContext = "all-agents";
1100
1785
  private expandedRows: Set<string> = new Set();
1786
+ private copiedEvents: Set<string> = new Set();
1787
+ private expandedTools: Set<string> = new Set();
1788
+ private expandedContextItems: Set<string> = new Set();
1101
1789
 
1102
1790
  private getSelectedMenu(): MenuItem {
1103
1791
  const found = this.menuItems.find((item) => item.key === this.selectedMenu);
@@ -1109,6 +1797,18 @@ export class WebInspectorElement extends LitElement {
1109
1797
  return this.renderEventsTable();
1110
1798
  }
1111
1799
 
1800
+ if (this.selectedMenu === "agents") {
1801
+ return this.renderAgentsView();
1802
+ }
1803
+
1804
+ if (this.selectedMenu === "frontend-tools") {
1805
+ return this.renderToolsView();
1806
+ }
1807
+
1808
+ if (this.selectedMenu === "agent-context") {
1809
+ return this.renderContextView();
1810
+ }
1811
+
1112
1812
  // Default placeholder content for other sections
1113
1813
  return html`
1114
1814
  <div class="flex flex-col gap-3 p-4">
@@ -1123,57 +1823,85 @@ export class WebInspectorElement extends LitElement {
1123
1823
 
1124
1824
  if (events.length === 0) {
1125
1825
  return html`
1126
- <div class="flex h-full items-center justify-center px-4 py-8 text-xs text-gray-500">
1127
- No events yet. Trigger an agent run to see live activity.
1826
+ <div class="flex h-full items-center justify-center px-4 py-8 text-center">
1827
+ <div class="max-w-md">
1828
+ <div class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8">
1829
+ ${this.renderIcon("Zap")}
1830
+ </div>
1831
+ <p class="text-sm text-gray-600">No events yet</p>
1832
+ <p class="mt-2 text-xs text-gray-500">Trigger an agent run to see live activity.</p>
1833
+ </div>
1128
1834
  </div>
1129
1835
  `;
1130
1836
  }
1131
1837
 
1132
1838
  return html`
1133
- <div class="overflow-hidden">
1134
- <table class="w-full border-collapse text-xs">
1135
- <thead>
1839
+ <div class="relative h-full overflow-auto">
1840
+ <table class="w-full border-separate border-spacing-0 text-xs">
1841
+ <thead class="sticky top-0 z-10">
1136
1842
  <tr class="bg-white">
1137
- <th class="border-r border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1138
- Type
1843
+ <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1844
+ Agent
1139
1845
  </th>
1140
- <th class="border-r border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1846
+ <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1141
1847
  Time
1142
1848
  </th>
1143
1849
  <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1144
- Payload
1850
+ Event Type
1851
+ </th>
1852
+ <th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
1853
+ AG-UI Event
1145
1854
  </th>
1146
1855
  </tr>
1147
1856
  </thead>
1148
1857
  <tbody>
1149
1858
  ${events.map((event, index) => {
1150
- const isLastRow = index === events.length - 1;
1151
1859
  const rowBg = index % 2 === 0 ? "bg-white" : "bg-gray-50/50";
1152
1860
  const badgeClasses = this.getEventBadgeClasses(event.type);
1153
- const inlinePayload = this.stringifyPayload(event.payload, false) || "—";
1154
- const prettyPayload = this.stringifyPayload(event.payload, true) || inlinePayload;
1861
+ const extractedEvent = this.extractEventFromPayload(event.payload);
1862
+ const inlineEvent = this.stringifyPayload(extractedEvent, false) || "—";
1863
+ const prettyEvent = this.stringifyPayload(extractedEvent, true) || inlineEvent;
1155
1864
  const isExpanded = this.expandedRows.has(event.id);
1156
1865
 
1157
1866
  return html`
1158
1867
  <tr
1159
- class="${rowBg} transition hover:bg-blue-50/50"
1868
+ class="${rowBg} cursor-pointer transition hover:bg-blue-50/50"
1160
1869
  @click=${() => this.toggleRowExpansion(event.id)}
1161
1870
  >
1162
- <td class="border-r ${!isLastRow ? 'border-b' : ''} border-gray-200 px-3 py-2">
1163
- <div class="flex flex-col gap-1">
1164
- <span class=${badgeClasses}>${event.type}</span>
1165
- <span class="font-mono text-[10px] text-gray-400">${event.agentId}</span>
1166
- </div>
1871
+ <td class="border-l border-r border-b border-gray-200 px-3 py-2">
1872
+ <span class="font-mono text-[11px] text-gray-600">${event.agentId}</span>
1167
1873
  </td>
1168
- <td class="border-r ${!isLastRow ? 'border-b' : ''} border-gray-200 px-3 py-2 font-mono text-[11px] text-gray-600">
1874
+ <td class="border-r border-b border-gray-200 px-3 py-2 font-mono text-[11px] text-gray-600">
1169
1875
  <span title=${new Date(event.timestamp).toLocaleString()}>
1170
1876
  ${new Date(event.timestamp).toLocaleTimeString()}
1171
1877
  </span>
1172
1878
  </td>
1173
- <td class="${!isLastRow ? 'border-b' : ''} border-gray-200 px-3 py-2 font-mono text-[10px] text-gray-600 ${isExpanded ? '' : 'truncate max-w-xs'}">
1879
+ <td class="border-r border-b border-gray-200 px-3 py-2">
1880
+ <span class=${badgeClasses}>${event.type}</span>
1881
+ </td>
1882
+ <td class="border-r border-b border-gray-200 px-3 py-2 font-mono text-[10px] text-gray-600 ${isExpanded ? '' : 'truncate max-w-xs'}">
1174
1883
  ${isExpanded
1175
- ? html`<pre class="m-0 whitespace-pre-wrap text-[10px] font-mono text-gray-600">${prettyPayload}</pre>`
1176
- : inlinePayload}
1884
+ ? html`
1885
+ <div class="group relative">
1886
+ <pre class="m-0 whitespace-pre-wrap text-[10px] font-mono text-gray-600">${prettyEvent}</pre>
1887
+ <button
1888
+ class="absolute right-0 top-0 cursor-pointer rounded px-2 py-1 text-[10px] opacity-0 transition group-hover:opacity-100 ${
1889
+ this.copiedEvents.has(event.id)
1890
+ ? 'bg-green-100 text-green-700'
1891
+ : 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900'
1892
+ }"
1893
+ @click=${(e: Event) => {
1894
+ e.stopPropagation();
1895
+ this.copyToClipboard(prettyEvent, event.id);
1896
+ }}
1897
+ >
1898
+ ${this.copiedEvents.has(event.id)
1899
+ ? html`<span>✓ Copied</span>`
1900
+ : html`<span>Copy</span>`}
1901
+ </button>
1902
+ </div>
1903
+ `
1904
+ : inlineEvent}
1177
1905
  </td>
1178
1906
  </tr>
1179
1907
  `;
@@ -1184,22 +1912,199 @@ export class WebInspectorElement extends LitElement {
1184
1912
  `;
1185
1913
  }
1186
1914
 
1915
+ private renderAgentsView() {
1916
+ // Show message if "all-agents" is selected or no agents available
1917
+ if (this.selectedContext === "all-agents") {
1918
+ return html`
1919
+ <div class="flex h-full items-center justify-center px-4 py-8 text-center">
1920
+ <div class="max-w-md">
1921
+ <div class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8">
1922
+ ${this.renderIcon("Bot")}
1923
+ </div>
1924
+ <p class="text-sm text-gray-600">No agent selected</p>
1925
+ <p class="mt-2 text-xs text-gray-500">Select an agent from the dropdown above to view details.</p>
1926
+ </div>
1927
+ </div>
1928
+ `;
1929
+ }
1930
+
1931
+ const agentId = this.selectedContext;
1932
+ const status = this.getAgentStatus(agentId);
1933
+ const stats = this.getAgentStats(agentId);
1934
+ const state = this.getLatestStateForAgent(agentId);
1935
+ const messages = this.getLatestMessagesForAgent(agentId);
1936
+
1937
+ const statusColors = {
1938
+ running: "bg-emerald-50 text-emerald-700",
1939
+ idle: "bg-gray-100 text-gray-600",
1940
+ error: "bg-rose-50 text-rose-700",
1941
+ };
1942
+
1943
+ return html`
1944
+ <div class="flex flex-col gap-4 p-4 overflow-auto">
1945
+ <!-- Agent Overview Card -->
1946
+ <div class="rounded-lg border border-gray-200 bg-white p-4">
1947
+ <div class="flex items-start justify-between mb-4">
1948
+ <div class="flex items-center gap-3">
1949
+ <div class="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100 text-blue-600">
1950
+ ${this.renderIcon("Bot")}
1951
+ </div>
1952
+ <div>
1953
+ <h3 class="font-semibold text-sm text-gray-900">${agentId}</h3>
1954
+ <span class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium ${statusColors[status]} relative -translate-y-[2px]">
1955
+ <span class="h-1.5 w-1.5 rounded-full ${status === 'running' ? 'bg-emerald-500 animate-pulse' : status === 'error' ? 'bg-rose-500' : 'bg-gray-400'}"></span>
1956
+ ${status.charAt(0).toUpperCase() + status.slice(1)}
1957
+ </span>
1958
+ </div>
1959
+ </div>
1960
+ ${stats.lastActivity
1961
+ ? html`<span class="text-xs text-gray-500">Last activity: ${new Date(stats.lastActivity).toLocaleTimeString()}</span>`
1962
+ : nothing}
1963
+ </div>
1964
+ <div class="grid grid-cols-2 gap-4 md:grid-cols-4">
1965
+ <button
1966
+ type="button"
1967
+ class="rounded-md bg-gray-50 px-3 py-2 text-left transition hover:bg-gray-100 cursor-pointer overflow-hidden"
1968
+ @click=${() => this.handleMenuSelect("ag-ui-events")}
1969
+ title="View all events in AG-UI Events"
1970
+ >
1971
+ <div class="truncate whitespace-nowrap text-xs text-gray-600">Total Events</div>
1972
+ <div class="text-lg font-semibold text-gray-900">${stats.totalEvents}</div>
1973
+ </button>
1974
+ <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
1975
+ <div class="truncate whitespace-nowrap text-xs text-gray-600">Messages</div>
1976
+ <div class="text-lg font-semibold text-gray-900">${stats.messages}</div>
1977
+ </div>
1978
+ <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
1979
+ <div class="truncate whitespace-nowrap text-xs text-gray-600">Tool Calls</div>
1980
+ <div class="text-lg font-semibold text-gray-900">${stats.toolCalls}</div>
1981
+ </div>
1982
+ <div class="rounded-md bg-gray-50 px-3 py-2 overflow-hidden">
1983
+ <div class="truncate whitespace-nowrap text-xs text-gray-600">Errors</div>
1984
+ <div class="text-lg font-semibold text-gray-900">${stats.errors}</div>
1985
+ </div>
1986
+ </div>
1987
+ </div>
1988
+
1989
+ <!-- Current State Section -->
1990
+ <div class="rounded-lg border border-gray-200 bg-white">
1991
+ <div class="border-b border-gray-200 px-4 py-3">
1992
+ <h4 class="text-sm font-semibold text-gray-900">Current State</h4>
1993
+ </div>
1994
+ <div class="overflow-auto p-4">
1995
+ ${this.hasRenderableState(state)
1996
+ ? html`
1997
+ <pre class="overflow-auto rounded-md bg-gray-50 p-3 text-xs text-gray-800 max-h-64"><code>${this.formatStateForDisplay(state)}</code></pre>
1998
+ `
1999
+ : html`
2000
+ <div class="flex h-40 items-center justify-center text-xs text-gray-500">
2001
+ <div class="flex items-center gap-2 text-gray-500">
2002
+ <span class="text-lg text-gray-400">${this.renderIcon("Database")}</span>
2003
+ <span>State is empty</span>
2004
+ </div>
2005
+ </div>
2006
+ `}
2007
+ </div>
2008
+ </div>
2009
+
2010
+ <!-- Current Messages Section -->
2011
+ <div class="rounded-lg border border-gray-200 bg-white">
2012
+ <div class="border-b border-gray-200 px-4 py-3">
2013
+ <h4 class="text-sm font-semibold text-gray-900">Current Messages</h4>
2014
+ </div>
2015
+ <div class="overflow-auto">
2016
+ ${messages && Array.isArray(messages) && messages.length > 0
2017
+ ? html`
2018
+ <table class="w-full text-xs">
2019
+ <thead class="bg-gray-50">
2020
+ <tr>
2021
+ <th class="px-4 py-2 text-left font-medium text-gray-700">Role</th>
2022
+ <th class="px-4 py-2 text-left font-medium text-gray-700">Content</th>
2023
+ </tr>
2024
+ </thead>
2025
+ <tbody class="divide-y divide-gray-200">
2026
+ ${messages.map((msg: any, idx: number) => {
2027
+ const role = msg?.role ?? "unknown";
2028
+ const roleColors: Record<string, string> = {
2029
+ user: "bg-blue-100 text-blue-800",
2030
+ assistant: "bg-green-100 text-green-800",
2031
+ system: "bg-gray-100 text-gray-800",
2032
+ unknown: "bg-gray-100 text-gray-600",
2033
+ };
2034
+
2035
+ const rawContent = typeof msg?.content === "string"
2036
+ ? msg.content
2037
+ : msg?.content != null
2038
+ ? JSON.stringify(msg.content)
2039
+ : "";
2040
+
2041
+ const toolCalls = Array.isArray(msg?.toolCalls) ? msg.toolCalls : [];
2042
+ const hasContent = rawContent.trim().length > 0;
2043
+ const contentFallback = toolCalls.length > 0
2044
+ ? "Invoked tool call"
2045
+ : "—";
2046
+
2047
+ return html`
2048
+ <tr>
2049
+ <td class="px-4 py-2 align-top">
2050
+ <span class="inline-flex rounded px-2 py-0.5 text-[10px] font-medium ${roleColors[role] || roleColors.unknown}">
2051
+ ${role}
2052
+ </span>
2053
+ </td>
2054
+ <td class="px-4 py-2">
2055
+ ${hasContent
2056
+ ? html`<div class="max-w-2xl whitespace-pre-wrap break-words text-gray-700">${rawContent}</div>`
2057
+ : html`<div class="text-xs italic text-gray-400">${contentFallback}</div>`}
2058
+ ${role === 'assistant' && toolCalls.length > 0
2059
+ ? this.renderToolCallDetails(toolCalls)
2060
+ : nothing}
2061
+ </td>
2062
+ </tr>
2063
+ `;
2064
+ })}
2065
+ </tbody>
2066
+ </table>
2067
+ `
2068
+ : html`
2069
+ <div class="flex h-40 items-center justify-center text-xs text-gray-500">
2070
+ <div class="flex items-center gap-2 text-gray-500">
2071
+ <span class="text-lg text-gray-400">${this.renderIcon("MessageSquare")}</span>
2072
+ <span>No messages available</span>
2073
+ </div>
2074
+ </div>
2075
+ `}
2076
+ </div>
2077
+ </div>
2078
+ </div>
2079
+ `;
2080
+ }
2081
+
1187
2082
  private renderContextDropdown() {
1188
- if (this.selectedMenu !== "ag-ui-events") {
2083
+ // Agent Context doesn't use the dropdown - it's global
2084
+ if (this.selectedMenu === "agent-context") {
2085
+ return nothing;
2086
+ }
2087
+
2088
+ if (this.selectedMenu !== "ag-ui-events" && this.selectedMenu !== "agents" && this.selectedMenu !== "frontend-tools") {
1189
2089
  return nothing;
1190
2090
  }
1191
2091
 
1192
- const selectedLabel = this.contextOptions.find((opt) => opt.key === this.selectedContext)?.label ?? "";
2092
+ // Filter out "all-agents" when in agents view
2093
+ const filteredOptions = this.selectedMenu === "agents"
2094
+ ? this.contextOptions.filter((opt) => opt.key !== "all-agents")
2095
+ : this.contextOptions;
2096
+
2097
+ const selectedLabel = filteredOptions.find((opt) => opt.key === this.selectedContext)?.label ?? "";
1193
2098
 
1194
2099
  return html`
1195
- <div class="relative" data-context-dropdown-root="true">
2100
+ <div class="relative min-w-0 flex-1" data-context-dropdown-root="true">
1196
2101
  <button
1197
2102
  type="button"
1198
- class="flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900"
2103
+ class="flex w-full min-w-0 max-w-[150px] items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900"
1199
2104
  @pointerdown=${this.handleContextDropdownToggle}
1200
2105
  >
1201
- <span>${selectedLabel}</span>
1202
- <span class="text-gray-400">${this.renderIcon("ChevronDown")}</span>
2106
+ <span class="truncate flex-1 text-left">${selectedLabel}</span>
2107
+ <span class="shrink-0 text-gray-400">${this.renderIcon("ChevronDown")}</span>
1203
2108
  </button>
1204
2109
  ${this.contextMenuOpen
1205
2110
  ? html`
@@ -1207,7 +2112,7 @@ export class WebInspectorElement extends LitElement {
1207
2112
  class="absolute left-0 z-50 mt-1.5 w-40 rounded-md border border-gray-200 bg-white py-1 shadow-md ring-1 ring-black/5"
1208
2113
  data-context-dropdown-root="true"
1209
2114
  >
1210
- ${this.contextOptions.map(
2115
+ ${filteredOptions.map(
1211
2116
  (option) => html`
1212
2117
  <button
1213
2118
  type="button"
@@ -1215,7 +2120,7 @@ export class WebInspectorElement extends LitElement {
1215
2120
  data-context-dropdown-root="true"
1216
2121
  @click=${() => this.handleContextOptionSelect(option.key)}
1217
2122
  >
1218
- <span class="${option.key === this.selectedContext ? 'text-gray-900 font-medium' : 'text-gray-600'}">${option.label}</span>
2123
+ <span class="truncate ${option.key === this.selectedContext ? 'text-gray-900 font-medium' : 'text-gray-600'}">${option.label}</span>
1219
2124
  ${option.key === this.selectedContext
1220
2125
  ? html`<span class="text-gray-500">${this.renderIcon("Check")}</span>`
1221
2126
  : nothing}
@@ -1235,7 +2140,19 @@ export class WebInspectorElement extends LitElement {
1235
2140
  }
1236
2141
 
1237
2142
  this.selectedMenu = key;
2143
+
2144
+ // If switching to agents view and "all-agents" is selected, switch to default or first agent
2145
+ if (key === "agents" && this.selectedContext === "all-agents") {
2146
+ const agentOptions = this.contextOptions.filter((opt) => opt.key !== "all-agents");
2147
+ if (agentOptions.length > 0) {
2148
+ // Try to find "default" agent first
2149
+ const defaultAgent = agentOptions.find((opt) => opt.key === "default");
2150
+ this.selectedContext = defaultAgent ? defaultAgent.key : agentOptions[0]!.key;
2151
+ }
2152
+ }
2153
+
1238
2154
  this.contextMenuOpen = false;
2155
+ this.persistState();
1239
2156
  this.requestUpdate();
1240
2157
  }
1241
2158
 
@@ -1257,6 +2174,531 @@ export class WebInspectorElement extends LitElement {
1257
2174
  }
1258
2175
 
1259
2176
  this.contextMenuOpen = false;
2177
+ this.persistState();
2178
+ this.requestUpdate();
2179
+ }
2180
+
2181
+ private renderToolsView() {
2182
+ if (!this._core) {
2183
+ return html`
2184
+ <div class="flex h-full items-center justify-center px-4 py-8 text-xs text-gray-500">
2185
+ No core instance available
2186
+ </div>
2187
+ `;
2188
+ }
2189
+
2190
+ const allTools = this.extractToolsFromAgents();
2191
+
2192
+ if (allTools.length === 0) {
2193
+ return html`
2194
+ <div class="flex h-full items-center justify-center px-4 py-8 text-center">
2195
+ <div class="max-w-md">
2196
+ <div class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8">
2197
+ ${this.renderIcon("Hammer")}
2198
+ </div>
2199
+ <p class="text-sm text-gray-600">No tools available</p>
2200
+ <p class="mt-2 text-xs text-gray-500">Tools will appear here once agents are configured with tool handlers or renderers.</p>
2201
+ </div>
2202
+ </div>
2203
+ `;
2204
+ }
2205
+
2206
+ // Filter tools by selected agent
2207
+ const filteredTools = this.selectedContext === "all-agents"
2208
+ ? allTools
2209
+ : allTools.filter(tool => tool.agentId === this.selectedContext);
2210
+
2211
+ return html`
2212
+ <div class="flex h-full flex-col overflow-hidden">
2213
+ <div class="overflow-auto p-4">
2214
+ <div class="space-y-3">
2215
+ ${filteredTools.map(tool => this.renderToolCard(tool))}
2216
+ </div>
2217
+ </div>
2218
+ </div>
2219
+ `;
2220
+ }
2221
+
2222
+ private extractToolsFromAgents(): Array<{
2223
+ agentId: string;
2224
+ name: string;
2225
+ description?: string;
2226
+ parameters?: unknown;
2227
+ type: 'handler' | 'renderer';
2228
+ }> {
2229
+ if (!this._core) {
2230
+ return [];
2231
+ }
2232
+
2233
+ const tools: Array<{
2234
+ agentId: string;
2235
+ name: string;
2236
+ description?: string;
2237
+ parameters?: unknown;
2238
+ type: 'handler' | 'renderer';
2239
+ }> = [];
2240
+
2241
+ for (const [agentId, agent] of Object.entries(this._core.agents)) {
2242
+ if (!agent) continue;
2243
+
2244
+ // Try to extract tool handlers
2245
+ const handlers = (agent as any).toolHandlers;
2246
+ if (handlers && typeof handlers === 'object') {
2247
+ for (const [toolName, handler] of Object.entries(handlers)) {
2248
+ if (handler && typeof handler === 'object') {
2249
+ const handlerObj = handler as any;
2250
+ tools.push({
2251
+ agentId,
2252
+ name: toolName,
2253
+ description: handlerObj.description || handlerObj.tool?.description,
2254
+ parameters: handlerObj.parameters || handlerObj.tool?.parameters,
2255
+ type: 'handler',
2256
+ });
2257
+ }
2258
+ }
2259
+ }
2260
+
2261
+ // Try to extract tool renderers
2262
+ const renderers = (agent as any).toolRenderers;
2263
+ if (renderers && typeof renderers === 'object') {
2264
+ for (const [toolName, renderer] of Object.entries(renderers)) {
2265
+ // Don't duplicate if we already have it as a handler
2266
+ if (!tools.some(t => t.agentId === agentId && t.name === toolName)) {
2267
+ if (renderer && typeof renderer === 'object') {
2268
+ const rendererObj = renderer as any;
2269
+ tools.push({
2270
+ agentId,
2271
+ name: toolName,
2272
+ description: rendererObj.description || rendererObj.tool?.description,
2273
+ parameters: rendererObj.parameters || rendererObj.tool?.parameters,
2274
+ type: 'renderer',
2275
+ });
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+ }
2281
+
2282
+ return tools.sort((a, b) => {
2283
+ const agentCompare = a.agentId.localeCompare(b.agentId);
2284
+ if (agentCompare !== 0) return agentCompare;
2285
+ return a.name.localeCompare(b.name);
2286
+ });
2287
+ }
2288
+
2289
+ private renderToolCard(tool: {
2290
+ agentId: string;
2291
+ name: string;
2292
+ description?: string;
2293
+ parameters?: unknown;
2294
+ type: 'handler' | 'renderer';
2295
+ }) {
2296
+ const isExpanded = this.expandedTools.has(`${tool.agentId}:${tool.name}`);
2297
+ const schema = this.extractSchemaInfo(tool.parameters);
2298
+
2299
+ const typeColors = {
2300
+ handler: "bg-blue-50 text-blue-700 border-blue-200",
2301
+ renderer: "bg-purple-50 text-purple-700 border-purple-200",
2302
+ };
2303
+
2304
+ return html`
2305
+ <div class="rounded-lg border border-gray-200 bg-white overflow-hidden">
2306
+ <button
2307
+ type="button"
2308
+ class="w-full px-4 py-3 text-left transition hover:bg-gray-50"
2309
+ @click=${() => this.toggleToolExpansion(`${tool.agentId}:${tool.name}`)}
2310
+ >
2311
+ <div class="flex items-start justify-between gap-3">
2312
+ <div class="flex-1 min-w-0">
2313
+ <div class="flex items-center gap-2 mb-1">
2314
+ <span class="font-mono text-sm font-semibold text-gray-900">${tool.name}</span>
2315
+ <span class="inline-flex items-center rounded-sm border px-1.5 py-0.5 text-[10px] font-medium ${typeColors[tool.type]}">
2316
+ ${tool.type}
2317
+ </span>
2318
+ </div>
2319
+ <div class="flex items-center gap-2 text-xs text-gray-500">
2320
+ <span class="flex items-center gap-1">
2321
+ ${this.renderIcon("Bot")}
2322
+ <span class="font-mono">${tool.agentId}</span>
2323
+ </span>
2324
+ ${schema.properties.length > 0
2325
+ ? html`
2326
+ <span class="text-gray-300">•</span>
2327
+ <span>${schema.properties.length} parameter${schema.properties.length !== 1 ? 's' : ''}</span>
2328
+ `
2329
+ : nothing}
2330
+ </div>
2331
+ ${tool.description
2332
+ ? html`<p class="mt-2 text-xs text-gray-600">${tool.description}</p>`
2333
+ : nothing}
2334
+ </div>
2335
+ <span class="shrink-0 text-gray-400 transition ${isExpanded ? 'rotate-180' : ''}">
2336
+ ${this.renderIcon("ChevronDown")}
2337
+ </span>
2338
+ </div>
2339
+ </button>
2340
+
2341
+ ${isExpanded
2342
+ ? html`
2343
+ <div class="border-t border-gray-200 bg-gray-50/50 px-4 py-3">
2344
+ ${schema.properties.length > 0
2345
+ ? html`
2346
+ <h5 class="mb-3 text-xs font-semibold text-gray-700">Parameters</h5>
2347
+ <div class="space-y-3">
2348
+ ${schema.properties.map(prop => html`
2349
+ <div class="rounded-md border border-gray-200 bg-white p-3">
2350
+ <div class="flex items-start justify-between gap-2 mb-1">
2351
+ <span class="font-mono text-xs font-medium text-gray-900">${prop.name}</span>
2352
+ <div class="flex items-center gap-1.5 shrink-0">
2353
+ ${prop.required
2354
+ ? html`<span class="text-[9px] rounded border border-rose-200 bg-rose-50 px-1 py-0.5 font-medium text-rose-700">required</span>`
2355
+ : html`<span class="text-[9px] rounded border border-gray-200 bg-gray-50 px-1 py-0.5 font-medium text-gray-600">optional</span>`}
2356
+ ${prop.type
2357
+ ? html`<span class="text-[9px] rounded border border-gray-200 bg-gray-50 px-1 py-0.5 font-mono text-gray-600">${prop.type}</span>`
2358
+ : nothing}
2359
+ </div>
2360
+ </div>
2361
+ ${prop.description
2362
+ ? html`<p class="mt-1 text-xs text-gray-600">${prop.description}</p>`
2363
+ : nothing}
2364
+ ${prop.defaultValue !== undefined
2365
+ ? html`
2366
+ <div class="mt-2 flex items-center gap-1.5 text-[10px] text-gray-500">
2367
+ <span>Default:</span>
2368
+ <code class="rounded bg-gray-100 px-1 py-0.5 font-mono">${JSON.stringify(prop.defaultValue)}</code>
2369
+ </div>
2370
+ `
2371
+ : nothing}
2372
+ ${prop.enum && prop.enum.length > 0
2373
+ ? html`
2374
+ <div class="mt-2">
2375
+ <span class="text-[10px] text-gray-500">Allowed values:</span>
2376
+ <div class="mt-1 flex flex-wrap gap-1">
2377
+ ${prop.enum.map(val => html`
2378
+ <code class="rounded border border-gray-200 bg-gray-50 px-1.5 py-0.5 text-[10px] font-mono text-gray-700">${JSON.stringify(val)}</code>
2379
+ `)}
2380
+ </div>
2381
+ </div>
2382
+ `
2383
+ : nothing}
2384
+ </div>
2385
+ `)}
2386
+ </div>
2387
+ `
2388
+ : html`
2389
+ <div class="flex items-center justify-center py-4 text-xs text-gray-500">
2390
+ <span>No parameters defined</span>
2391
+ </div>
2392
+ `}
2393
+ </div>
2394
+ `
2395
+ : nothing}
2396
+ </div>
2397
+ `;
2398
+ }
2399
+
2400
+ private extractSchemaInfo(parameters: unknown): {
2401
+ properties: Array<{
2402
+ name: string;
2403
+ type?: string;
2404
+ description?: string;
2405
+ required: boolean;
2406
+ defaultValue?: unknown;
2407
+ enum?: unknown[];
2408
+ }>;
2409
+ } {
2410
+ const result: {
2411
+ properties: Array<{
2412
+ name: string;
2413
+ type?: string;
2414
+ description?: string;
2415
+ required: boolean;
2416
+ defaultValue?: unknown;
2417
+ enum?: unknown[];
2418
+ }>;
2419
+ } = { properties: [] };
2420
+
2421
+ if (!parameters || typeof parameters !== 'object') {
2422
+ return result;
2423
+ }
2424
+
2425
+ // Try Zod schema introspection
2426
+ const zodDef = (parameters as any)._def;
2427
+ if (zodDef) {
2428
+ // Handle Zod object schema
2429
+ if (zodDef.typeName === 'ZodObject') {
2430
+ const shape = zodDef.shape?.() || zodDef.shape;
2431
+ const requiredKeys = new Set<string>();
2432
+
2433
+ // Get required fields
2434
+ if (zodDef.unknownKeys === 'strict' || !zodDef.catchall) {
2435
+ Object.keys(shape || {}).forEach(key => {
2436
+ const fieldDef = shape[key]?._def;
2437
+ if (fieldDef && !this.isZodOptional(shape[key])) {
2438
+ requiredKeys.add(key);
2439
+ }
2440
+ });
2441
+ }
2442
+
2443
+ // Extract properties
2444
+ for (const [key, value] of Object.entries(shape || {})) {
2445
+ const fieldInfo = this.extractZodFieldInfo(value);
2446
+ result.properties.push({
2447
+ name: key,
2448
+ type: fieldInfo.type,
2449
+ description: fieldInfo.description,
2450
+ required: requiredKeys.has(key),
2451
+ defaultValue: fieldInfo.defaultValue,
2452
+ enum: fieldInfo.enum,
2453
+ });
2454
+ }
2455
+ }
2456
+ } else if ((parameters as any).type === 'object' && (parameters as any).properties) {
2457
+ // Handle JSON Schema format
2458
+ const props = (parameters as any).properties;
2459
+ const required = new Set((parameters as any).required || []);
2460
+
2461
+ for (const [key, value] of Object.entries(props)) {
2462
+ const prop = value as any;
2463
+ result.properties.push({
2464
+ name: key,
2465
+ type: prop.type,
2466
+ description: prop.description,
2467
+ required: required.has(key),
2468
+ defaultValue: prop.default,
2469
+ enum: prop.enum,
2470
+ });
2471
+ }
2472
+ }
2473
+
2474
+ return result;
2475
+ }
2476
+
2477
+ private isZodOptional(zodSchema: any): boolean {
2478
+ if (!zodSchema?._def) return false;
2479
+
2480
+ const def = zodSchema._def;
2481
+
2482
+ // Check if it's explicitly optional or nullable
2483
+ if (def.typeName === 'ZodOptional' || def.typeName === 'ZodNullable') {
2484
+ return true;
2485
+ }
2486
+
2487
+ // Check if it has a default value
2488
+ if (def.defaultValue !== undefined) {
2489
+ return true;
2490
+ }
2491
+
2492
+ return false;
2493
+ }
2494
+
2495
+ private extractZodFieldInfo(zodSchema: any): {
2496
+ type?: string;
2497
+ description?: string;
2498
+ defaultValue?: unknown;
2499
+ enum?: unknown[];
2500
+ } {
2501
+ const info: {
2502
+ type?: string;
2503
+ description?: string;
2504
+ defaultValue?: unknown;
2505
+ enum?: unknown[];
2506
+ } = {};
2507
+
2508
+ if (!zodSchema?._def) return info;
2509
+
2510
+ let currentSchema = zodSchema;
2511
+ let def = currentSchema._def;
2512
+
2513
+ // Unwrap optional/nullable
2514
+ while (def.typeName === 'ZodOptional' || def.typeName === 'ZodNullable' || def.typeName === 'ZodDefault') {
2515
+ if (def.typeName === 'ZodDefault' && def.defaultValue !== undefined) {
2516
+ info.defaultValue = typeof def.defaultValue === 'function' ? def.defaultValue() : def.defaultValue;
2517
+ }
2518
+ currentSchema = def.innerType;
2519
+ if (!currentSchema?._def) break;
2520
+ def = currentSchema._def;
2521
+ }
2522
+
2523
+ // Extract description
2524
+ info.description = def.description;
2525
+
2526
+ // Extract type
2527
+ const typeMap: Record<string, string> = {
2528
+ ZodString: 'string',
2529
+ ZodNumber: 'number',
2530
+ ZodBoolean: 'boolean',
2531
+ ZodArray: 'array',
2532
+ ZodObject: 'object',
2533
+ ZodEnum: 'enum',
2534
+ ZodLiteral: 'literal',
2535
+ ZodUnion: 'union',
2536
+ ZodAny: 'any',
2537
+ ZodUnknown: 'unknown',
2538
+ };
2539
+ info.type = typeMap[def.typeName] || def.typeName?.replace('Zod', '').toLowerCase();
2540
+
2541
+ // Extract enum values
2542
+ if (def.typeName === 'ZodEnum' && Array.isArray(def.values)) {
2543
+ info.enum = def.values;
2544
+ } else if (def.typeName === 'ZodLiteral' && def.value !== undefined) {
2545
+ info.enum = [def.value];
2546
+ }
2547
+
2548
+ return info;
2549
+ }
2550
+
2551
+ private toggleToolExpansion(toolId: string): void {
2552
+ if (this.expandedTools.has(toolId)) {
2553
+ this.expandedTools.delete(toolId);
2554
+ } else {
2555
+ this.expandedTools.add(toolId);
2556
+ }
2557
+ this.requestUpdate();
2558
+ }
2559
+
2560
+ private renderContextView() {
2561
+ const contextEntries = Object.entries(this.contextStore);
2562
+
2563
+ if (contextEntries.length === 0) {
2564
+ return html`
2565
+ <div class="flex h-full items-center justify-center px-4 py-8 text-center">
2566
+ <div class="max-w-md">
2567
+ <div class="mb-3 flex justify-center text-gray-300 [&>svg]:!h-8 [&>svg]:!w-8">
2568
+ ${this.renderIcon("FileText")}
2569
+ </div>
2570
+ <p class="text-sm text-gray-600">No context available</p>
2571
+ <p class="mt-2 text-xs text-gray-500">Context will appear here once added to CopilotKit.</p>
2572
+ </div>
2573
+ </div>
2574
+ `;
2575
+ }
2576
+
2577
+ return html`
2578
+ <div class="flex h-full flex-col overflow-hidden">
2579
+ <div class="overflow-auto p-4">
2580
+ <div class="space-y-3">
2581
+ ${contextEntries.map(([id, context]) => this.renderContextCard(id, context))}
2582
+ </div>
2583
+ </div>
2584
+ </div>
2585
+ `;
2586
+ }
2587
+
2588
+ private renderContextCard(id: string, context: { description?: string; value: unknown }) {
2589
+ const isExpanded = this.expandedContextItems.has(id);
2590
+ const valuePreview = this.getContextValuePreview(context.value);
2591
+ const hasValue = context.value !== undefined && context.value !== null;
2592
+
2593
+ return html`
2594
+ <div class="rounded-lg border border-gray-200 bg-white overflow-hidden">
2595
+ <button
2596
+ type="button"
2597
+ class="w-full px-4 py-3 text-left transition hover:bg-gray-50"
2598
+ @click=${() => this.toggleContextExpansion(id)}
2599
+ >
2600
+ <div class="flex items-start justify-between gap-3">
2601
+ <div class="flex-1 min-w-0">
2602
+ ${context.description
2603
+ ? html`<p class="text-sm font-medium text-gray-900 mb-1">${context.description}</p>`
2604
+ : html`<p class="text-sm font-medium text-gray-500 italic mb-1">No description</p>`}
2605
+ <div class="flex items-center gap-2 text-xs text-gray-500">
2606
+ <span class="font-mono">${id.substring(0, 8)}...</span>
2607
+ ${hasValue
2608
+ ? html`
2609
+ <span class="text-gray-300">•</span>
2610
+ <span class="truncate">${valuePreview}</span>
2611
+ `
2612
+ : nothing}
2613
+ </div>
2614
+ </div>
2615
+ <span class="shrink-0 text-gray-400 transition ${isExpanded ? 'rotate-180' : ''}">
2616
+ ${this.renderIcon("ChevronDown")}
2617
+ </span>
2618
+ </div>
2619
+ </button>
2620
+
2621
+ ${isExpanded
2622
+ ? html`
2623
+ <div class="border-t border-gray-200 bg-gray-50/50 px-4 py-3">
2624
+ <div class="mb-3">
2625
+ <h5 class="mb-1 text-xs font-semibold text-gray-700">ID</h5>
2626
+ <code class="block rounded bg-white border border-gray-200 px-2 py-1 text-[10px] font-mono text-gray-600">${id}</code>
2627
+ </div>
2628
+ ${hasValue
2629
+ ? html`
2630
+ <h5 class="mb-2 text-xs font-semibold text-gray-700">Value</h5>
2631
+ <div class="rounded-md border border-gray-200 bg-white p-3">
2632
+ <pre class="overflow-auto text-xs text-gray-800 max-h-96"><code>${this.formatContextValue(context.value)}</code></pre>
2633
+ </div>
2634
+ `
2635
+ : html`
2636
+ <div class="flex items-center justify-center py-4 text-xs text-gray-500">
2637
+ <span>No value available</span>
2638
+ </div>
2639
+ `}
2640
+ </div>
2641
+ `
2642
+ : nothing}
2643
+ </div>
2644
+ `;
2645
+ }
2646
+
2647
+ private getContextValuePreview(value: unknown): string {
2648
+ if (value === undefined || value === null) {
2649
+ return '—';
2650
+ }
2651
+
2652
+ if (typeof value === 'string') {
2653
+ return value.length > 50 ? `${value.substring(0, 50)}...` : value;
2654
+ }
2655
+
2656
+ if (typeof value === 'number' || typeof value === 'boolean') {
2657
+ return String(value);
2658
+ }
2659
+
2660
+ if (Array.isArray(value)) {
2661
+ return `Array(${value.length})`;
2662
+ }
2663
+
2664
+ if (typeof value === 'object') {
2665
+ const keys = Object.keys(value);
2666
+ return `Object with ${keys.length} key${keys.length !== 1 ? 's' : ''}`;
2667
+ }
2668
+
2669
+ if (typeof value === 'function') {
2670
+ return 'Function';
2671
+ }
2672
+
2673
+ return String(value);
2674
+ }
2675
+
2676
+ private formatContextValue(value: unknown): string {
2677
+ if (value === undefined) {
2678
+ return 'undefined';
2679
+ }
2680
+
2681
+ if (value === null) {
2682
+ return 'null';
2683
+ }
2684
+
2685
+ if (typeof value === 'function') {
2686
+ return value.toString();
2687
+ }
2688
+
2689
+ try {
2690
+ return JSON.stringify(value, null, 2);
2691
+ } catch (error) {
2692
+ return String(value);
2693
+ }
2694
+ }
2695
+
2696
+ private toggleContextExpansion(contextId: string): void {
2697
+ if (this.expandedContextItems.has(contextId)) {
2698
+ this.expandedContextItems.delete(contextId);
2699
+ } else {
2700
+ this.expandedContextItems.add(contextId);
2701
+ }
1260
2702
  this.requestUpdate();
1261
2703
  }
1262
2704
 
@@ -1276,6 +2718,12 @@ export class WebInspectorElement extends LitElement {
1276
2718
  };
1277
2719
 
1278
2720
  private toggleRowExpansion(eventId: string): void {
2721
+ // Don't toggle if user is selecting text
2722
+ const selection = window.getSelection();
2723
+ if (selection && selection.toString().length > 0) {
2724
+ return;
2725
+ }
2726
+
1279
2727
  if (this.expandedRows.has(eventId)) {
1280
2728
  this.expandedRows.delete(eventId);
1281
2729
  } else {