@copilotkitnext/web-inspector 0.0.13
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/.turbo/turbo-build.log +29 -0
- package/LICENSE +11 -0
- package/dist/index.d.mts +108 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +1232 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1205 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +3 -0
- package/package.json +46 -0
- package/src/assets/logo-mark.svg +8 -0
- package/src/components.d.ts +24 -0
- package/src/index.ts +1300 -0
- package/src/lib/context-helpers.ts +125 -0
- package/src/lib/persistence.ts +88 -0
- package/src/lib/types.ts +17 -0
- package/src/styles/generated.css +2 -0
- package/src/styles/tailwind.css +23 -0
- package/src/types/css.d.ts +4 -0
- package/src/types/svg.d.ts +4 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +15 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
import { LitElement, css, html, nothing, unsafeCSS } from "lit";
|
|
2
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
3
|
+
import tailwindStyles from "./styles/generated.css";
|
|
4
|
+
import logoMarkUrl from "./assets/logo-mark.svg";
|
|
5
|
+
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
6
|
+
import { icons } from "lucide";
|
|
7
|
+
import type { CopilotKitCore, CopilotKitCoreSubscriber } from "@copilotkitnext/core";
|
|
8
|
+
import type { AbstractAgent, AgentSubscriber } from "@ag-ui/client";
|
|
9
|
+
import type { Anchor, ContextKey, ContextState, Position, Size } from "./lib/types";
|
|
10
|
+
import {
|
|
11
|
+
applyAnchorPosition as applyAnchorPositionHelper,
|
|
12
|
+
centerContext as centerContextHelper,
|
|
13
|
+
constrainToViewport,
|
|
14
|
+
keepPositionWithinViewport,
|
|
15
|
+
updateAnchorFromPosition as updateAnchorFromPositionHelper,
|
|
16
|
+
updateSizeFromElement,
|
|
17
|
+
clampSize as clampSizeToViewport,
|
|
18
|
+
} from "./lib/context-helpers";
|
|
19
|
+
import {
|
|
20
|
+
loadInspectorState,
|
|
21
|
+
saveInspectorState,
|
|
22
|
+
type PersistedState,
|
|
23
|
+
isValidAnchor,
|
|
24
|
+
isValidPosition,
|
|
25
|
+
isValidSize,
|
|
26
|
+
} from "./lib/persistence";
|
|
27
|
+
|
|
28
|
+
export const WEB_INSPECTOR_TAG = "web-inspector" as const;
|
|
29
|
+
|
|
30
|
+
type LucideIconName = keyof typeof icons;
|
|
31
|
+
|
|
32
|
+
type MenuKey = "ag-ui-events" | "agents" | "frontend-tools" | "agent-context";
|
|
33
|
+
|
|
34
|
+
type MenuItem = {
|
|
35
|
+
key: MenuKey;
|
|
36
|
+
label: string;
|
|
37
|
+
icon: LucideIconName;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const EDGE_MARGIN = 16;
|
|
41
|
+
const DRAG_THRESHOLD = 6;
|
|
42
|
+
const MIN_WINDOW_WIDTH = 260;
|
|
43
|
+
const MIN_WINDOW_HEIGHT = 200;
|
|
44
|
+
const COOKIE_NAME = "copilotkit_inspector_state";
|
|
45
|
+
const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; // 30 days
|
|
46
|
+
const DEFAULT_BUTTON_SIZE: Size = { width: 48, height: 48 };
|
|
47
|
+
const DEFAULT_WINDOW_SIZE: Size = { width: 320, height: 380 };
|
|
48
|
+
const MAX_AGENT_EVENTS = 200;
|
|
49
|
+
const MAX_TOTAL_EVENTS = 500;
|
|
50
|
+
|
|
51
|
+
type InspectorEvent = {
|
|
52
|
+
id: string;
|
|
53
|
+
agentId: string;
|
|
54
|
+
type: string;
|
|
55
|
+
timestamp: number;
|
|
56
|
+
payload: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export class WebInspectorElement extends LitElement {
|
|
60
|
+
static properties = {
|
|
61
|
+
core: { attribute: false },
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
private _core: CopilotKitCore | null = null;
|
|
65
|
+
private coreSubscriber: CopilotKitCoreSubscriber | null = null;
|
|
66
|
+
private coreUnsubscribe: (() => void) | null = null;
|
|
67
|
+
private agentSubscriptions: Map<string, () => void> = new Map();
|
|
68
|
+
private agentEvents: Map<string, InspectorEvent[]> = new Map();
|
|
69
|
+
private flattenedEvents: InspectorEvent[] = [];
|
|
70
|
+
private eventCounter = 0;
|
|
71
|
+
|
|
72
|
+
private pointerId: number | null = null;
|
|
73
|
+
private dragStart: Position | null = null;
|
|
74
|
+
private dragOffset: Position = { x: 0, y: 0 };
|
|
75
|
+
private isDragging = false;
|
|
76
|
+
private pointerContext: ContextKey | null = null;
|
|
77
|
+
private isOpen = false;
|
|
78
|
+
private draggedDuringInteraction = false;
|
|
79
|
+
private ignoreNextButtonClick = false;
|
|
80
|
+
private selectedMenu: MenuKey = "ag-ui-events";
|
|
81
|
+
private contextMenuOpen = false;
|
|
82
|
+
|
|
83
|
+
get core(): CopilotKitCore | null {
|
|
84
|
+
return this._core;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
set core(value: CopilotKitCore | null) {
|
|
88
|
+
const oldValue = this._core;
|
|
89
|
+
if (oldValue === value) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.detachFromCore();
|
|
94
|
+
|
|
95
|
+
this._core = value ?? null;
|
|
96
|
+
this.requestUpdate("core", oldValue);
|
|
97
|
+
|
|
98
|
+
if (this._core) {
|
|
99
|
+
this.attachToCore(this._core);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private readonly contextState: Record<ContextKey, ContextState> = {
|
|
104
|
+
button: {
|
|
105
|
+
position: { x: EDGE_MARGIN, y: EDGE_MARGIN },
|
|
106
|
+
size: { ...DEFAULT_BUTTON_SIZE },
|
|
107
|
+
anchor: { horizontal: "right", vertical: "bottom" },
|
|
108
|
+
anchorOffset: { x: EDGE_MARGIN, y: EDGE_MARGIN },
|
|
109
|
+
},
|
|
110
|
+
window: {
|
|
111
|
+
position: { x: EDGE_MARGIN, y: EDGE_MARGIN },
|
|
112
|
+
size: { ...DEFAULT_WINDOW_SIZE },
|
|
113
|
+
anchor: { horizontal: "right", vertical: "bottom" },
|
|
114
|
+
anchorOffset: { x: EDGE_MARGIN, y: EDGE_MARGIN },
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
private hasCustomPosition: Record<ContextKey, boolean> = {
|
|
119
|
+
button: false,
|
|
120
|
+
window: false,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
private resizePointerId: number | null = null;
|
|
124
|
+
private resizeStart: Position | null = null;
|
|
125
|
+
private resizeInitialSize: { width: number; height: number } | null = null;
|
|
126
|
+
private isResizing = false;
|
|
127
|
+
|
|
128
|
+
private readonly menuItems: MenuItem[] = [
|
|
129
|
+
{ key: "ag-ui-events", label: "AG-UI Events", icon: "Zap" },
|
|
130
|
+
{ key: "agents", label: "Agents", icon: "Bot" },
|
|
131
|
+
{ key: "frontend-tools", label: "Frontend Tools", icon: "Hammer" },
|
|
132
|
+
{ key: "agent-context", label: "Agent Context", icon: "FileText" },
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
private attachToCore(core: CopilotKitCore): void {
|
|
136
|
+
this.coreSubscriber = {
|
|
137
|
+
onAgentsChanged: ({ agents }) => {
|
|
138
|
+
this.processAgentsChanged(agents);
|
|
139
|
+
},
|
|
140
|
+
} satisfies CopilotKitCoreSubscriber;
|
|
141
|
+
|
|
142
|
+
this.coreUnsubscribe = core.subscribe(this.coreSubscriber);
|
|
143
|
+
this.processAgentsChanged(core.agents);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private detachFromCore(): void {
|
|
147
|
+
if (this.coreUnsubscribe) {
|
|
148
|
+
this.coreUnsubscribe();
|
|
149
|
+
this.coreUnsubscribe = null;
|
|
150
|
+
}
|
|
151
|
+
this.coreSubscriber = null;
|
|
152
|
+
this.teardownAgentSubscriptions();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private teardownAgentSubscriptions(): void {
|
|
156
|
+
for (const unsubscribe of this.agentSubscriptions.values()) {
|
|
157
|
+
unsubscribe();
|
|
158
|
+
}
|
|
159
|
+
this.agentSubscriptions.clear();
|
|
160
|
+
this.agentEvents.clear();
|
|
161
|
+
this.flattenedEvents = [];
|
|
162
|
+
this.eventCounter = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private processAgentsChanged(agents: Readonly<Record<string, AbstractAgent>>): void {
|
|
166
|
+
const seenAgentIds = new Set<string>();
|
|
167
|
+
|
|
168
|
+
for (const agent of Object.values(agents)) {
|
|
169
|
+
if (!agent?.agentId) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
seenAgentIds.add(agent.agentId);
|
|
173
|
+
this.subscribeToAgent(agent);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const agentId of Array.from(this.agentSubscriptions.keys())) {
|
|
177
|
+
if (!seenAgentIds.has(agentId)) {
|
|
178
|
+
this.unsubscribeFromAgent(agentId);
|
|
179
|
+
this.agentEvents.delete(agentId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.updateContextOptions(seenAgentIds);
|
|
184
|
+
this.requestUpdate();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private subscribeToAgent(agent: AbstractAgent): void {
|
|
188
|
+
if (!agent.agentId) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const agentId = agent.agentId;
|
|
193
|
+
|
|
194
|
+
this.unsubscribeFromAgent(agentId);
|
|
195
|
+
|
|
196
|
+
const subscriber: AgentSubscriber = {
|
|
197
|
+
onRunStartedEvent: ({ event }) => {
|
|
198
|
+
this.recordAgentEvent(agentId, "RUN_STARTED", event);
|
|
199
|
+
},
|
|
200
|
+
onRunFinishedEvent: ({ event, result }) => {
|
|
201
|
+
this.recordAgentEvent(agentId, "RUN_FINISHED", { event, result });
|
|
202
|
+
},
|
|
203
|
+
onRunErrorEvent: ({ event }) => {
|
|
204
|
+
this.recordAgentEvent(agentId, "RUN_ERROR", event);
|
|
205
|
+
},
|
|
206
|
+
onTextMessageStartEvent: ({ event }) => {
|
|
207
|
+
this.recordAgentEvent(agentId, "TEXT_MESSAGE_START", event);
|
|
208
|
+
},
|
|
209
|
+
onTextMessageContentEvent: ({ event, textMessageBuffer }) => {
|
|
210
|
+
this.recordAgentEvent(agentId, "TEXT_MESSAGE_CONTENT", { event, textMessageBuffer });
|
|
211
|
+
},
|
|
212
|
+
onTextMessageEndEvent: ({ event, textMessageBuffer }) => {
|
|
213
|
+
this.recordAgentEvent(agentId, "TEXT_MESSAGE_END", { event, textMessageBuffer });
|
|
214
|
+
},
|
|
215
|
+
onToolCallStartEvent: ({ event }) => {
|
|
216
|
+
this.recordAgentEvent(agentId, "TOOL_CALL_START", event);
|
|
217
|
+
},
|
|
218
|
+
onToolCallArgsEvent: ({ event, toolCallBuffer, toolCallName, partialToolCallArgs }) => {
|
|
219
|
+
this.recordAgentEvent(agentId, "TOOL_CALL_ARGS", { event, toolCallBuffer, toolCallName, partialToolCallArgs });
|
|
220
|
+
},
|
|
221
|
+
onToolCallEndEvent: ({ event, toolCallArgs, toolCallName }) => {
|
|
222
|
+
this.recordAgentEvent(agentId, "TOOL_CALL_END", { event, toolCallArgs, toolCallName });
|
|
223
|
+
},
|
|
224
|
+
onToolCallResultEvent: ({ event }) => {
|
|
225
|
+
this.recordAgentEvent(agentId, "TOOL_CALL_RESULT", event);
|
|
226
|
+
},
|
|
227
|
+
onStateSnapshotEvent: ({ event }) => {
|
|
228
|
+
this.recordAgentEvent(agentId, "STATE_SNAPSHOT", event);
|
|
229
|
+
},
|
|
230
|
+
onStateDeltaEvent: ({ event }) => {
|
|
231
|
+
this.recordAgentEvent(agentId, "STATE_DELTA", event);
|
|
232
|
+
},
|
|
233
|
+
onMessagesSnapshotEvent: ({ event }) => {
|
|
234
|
+
this.recordAgentEvent(agentId, "MESSAGES_SNAPSHOT", event);
|
|
235
|
+
},
|
|
236
|
+
onRawEvent: ({ event }) => {
|
|
237
|
+
this.recordAgentEvent(agentId, "RAW_EVENT", event);
|
|
238
|
+
},
|
|
239
|
+
onCustomEvent: ({ event }) => {
|
|
240
|
+
this.recordAgentEvent(agentId, "CUSTOM_EVENT", event);
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const { unsubscribe } = agent.subscribe(subscriber);
|
|
245
|
+
this.agentSubscriptions.set(agentId, unsubscribe);
|
|
246
|
+
|
|
247
|
+
if (!this.agentEvents.has(agentId)) {
|
|
248
|
+
this.agentEvents.set(agentId, []);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private unsubscribeFromAgent(agentId: string): void {
|
|
253
|
+
const unsubscribe = this.agentSubscriptions.get(agentId);
|
|
254
|
+
if (unsubscribe) {
|
|
255
|
+
unsubscribe();
|
|
256
|
+
this.agentSubscriptions.delete(agentId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private recordAgentEvent(agentId: string, type: string, payload: unknown): void {
|
|
261
|
+
const eventId = `${agentId}:${++this.eventCounter}`;
|
|
262
|
+
const event: InspectorEvent = {
|
|
263
|
+
id: eventId,
|
|
264
|
+
agentId,
|
|
265
|
+
type,
|
|
266
|
+
timestamp: Date.now(),
|
|
267
|
+
payload,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const currentAgentEvents = this.agentEvents.get(agentId) ?? [];
|
|
271
|
+
const nextAgentEvents = [event, ...currentAgentEvents].slice(0, MAX_AGENT_EVENTS);
|
|
272
|
+
this.agentEvents.set(agentId, nextAgentEvents);
|
|
273
|
+
|
|
274
|
+
this.flattenedEvents = [event, ...this.flattenedEvents].slice(0, MAX_TOTAL_EVENTS);
|
|
275
|
+
this.requestUpdate();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private updateContextOptions(agentIds: Set<string>): void {
|
|
279
|
+
const nextOptions: Array<{ key: string; label: string }> = [
|
|
280
|
+
{ key: "all-agents", label: "All Agents" },
|
|
281
|
+
...Array.from(agentIds)
|
|
282
|
+
.sort((a, b) => a.localeCompare(b))
|
|
283
|
+
.map((id) => ({ key: id, label: id })),
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
const optionsChanged =
|
|
287
|
+
this.contextOptions.length !== nextOptions.length ||
|
|
288
|
+
this.contextOptions.some((option, index) => option.key !== nextOptions[index]?.key);
|
|
289
|
+
|
|
290
|
+
if (optionsChanged) {
|
|
291
|
+
this.contextOptions = nextOptions;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (!nextOptions.some((option) => option.key === this.selectedContext)) {
|
|
295
|
+
this.selectedContext = "all-agents";
|
|
296
|
+
this.expandedRows.clear();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private getEventsForSelectedContext(): InspectorEvent[] {
|
|
301
|
+
if (this.selectedContext === "all-agents") {
|
|
302
|
+
return this.flattenedEvents;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return this.agentEvents.get(this.selectedContext) ?? [];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private getEventBadgeClasses(type: string): string {
|
|
309
|
+
const base = "font-mono text-[10px] font-medium inline-flex items-center rounded-sm px-1.5 py-0.5 border";
|
|
310
|
+
|
|
311
|
+
if (type.startsWith("RUN_")) {
|
|
312
|
+
return `${base} bg-blue-50 text-blue-700 border-blue-200`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (type.startsWith("TEXT_MESSAGE")) {
|
|
316
|
+
return `${base} bg-emerald-50 text-emerald-700 border-emerald-200`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (type.startsWith("TOOL_CALL")) {
|
|
320
|
+
return `${base} bg-amber-50 text-amber-700 border-amber-200`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (type.startsWith("STATE")) {
|
|
324
|
+
return `${base} bg-violet-50 text-violet-700 border-violet-200`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (type.startsWith("MESSAGES")) {
|
|
328
|
+
return `${base} bg-sky-50 text-sky-700 border-sky-200`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (type === "RUN_ERROR") {
|
|
332
|
+
return `${base} bg-rose-50 text-rose-700 border-rose-200`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return `${base} bg-gray-100 text-gray-600 border-gray-200`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private stringifyPayload(payload: unknown, pretty: boolean): string {
|
|
339
|
+
try {
|
|
340
|
+
if (payload === undefined) {
|
|
341
|
+
return pretty ? "undefined" : "undefined";
|
|
342
|
+
}
|
|
343
|
+
if (typeof payload === "string") {
|
|
344
|
+
return payload;
|
|
345
|
+
}
|
|
346
|
+
return JSON.stringify(payload, null, pretty ? 2 : 0) ?? "";
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.warn("Failed to stringify inspector payload", error);
|
|
349
|
+
return String(payload);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
static styles = [
|
|
354
|
+
unsafeCSS(tailwindStyles),
|
|
355
|
+
css`
|
|
356
|
+
:host {
|
|
357
|
+
position: fixed;
|
|
358
|
+
top: 0;
|
|
359
|
+
left: 0;
|
|
360
|
+
z-index: 2147483646;
|
|
361
|
+
display: block;
|
|
362
|
+
will-change: transform;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.console-button {
|
|
366
|
+
transition:
|
|
367
|
+
transform 160ms ease,
|
|
368
|
+
opacity 160ms ease;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.resize-handle {
|
|
372
|
+
touch-action: none;
|
|
373
|
+
user-select: none;
|
|
374
|
+
}
|
|
375
|
+
`,
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
connectedCallback(): void {
|
|
379
|
+
super.connectedCallback();
|
|
380
|
+
if (typeof window !== "undefined") {
|
|
381
|
+
window.addEventListener("resize", this.handleResize);
|
|
382
|
+
window.addEventListener("pointerdown", this.handleGlobalPointerDown as EventListener);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
disconnectedCallback(): void {
|
|
387
|
+
super.disconnectedCallback();
|
|
388
|
+
if (typeof window !== "undefined") {
|
|
389
|
+
window.removeEventListener("resize", this.handleResize);
|
|
390
|
+
window.removeEventListener("pointerdown", this.handleGlobalPointerDown as EventListener);
|
|
391
|
+
}
|
|
392
|
+
this.detachFromCore();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
firstUpdated(): void {
|
|
396
|
+
if (typeof window === "undefined") {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this.measureContext("button");
|
|
401
|
+
this.measureContext("window");
|
|
402
|
+
|
|
403
|
+
this.contextState.button.anchor = { horizontal: "right", vertical: "bottom" };
|
|
404
|
+
this.contextState.button.anchorOffset = { x: EDGE_MARGIN, y: EDGE_MARGIN };
|
|
405
|
+
|
|
406
|
+
this.contextState.window.anchor = { horizontal: "right", vertical: "bottom" };
|
|
407
|
+
this.contextState.window.anchorOffset = { x: EDGE_MARGIN, y: EDGE_MARGIN };
|
|
408
|
+
|
|
409
|
+
this.hydrateStateFromCookie();
|
|
410
|
+
|
|
411
|
+
this.applyAnchorPosition("button");
|
|
412
|
+
|
|
413
|
+
if (this.hasCustomPosition.window) {
|
|
414
|
+
this.applyAnchorPosition("window");
|
|
415
|
+
} else {
|
|
416
|
+
this.centerContext("window");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this.updateHostTransform("button");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
render() {
|
|
423
|
+
return this.isOpen ? this.renderWindow() : this.renderButton();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private renderButton() {
|
|
427
|
+
const buttonClasses = [
|
|
428
|
+
"console-button",
|
|
429
|
+
"group",
|
|
430
|
+
"pointer-events-auto",
|
|
431
|
+
"inline-flex",
|
|
432
|
+
"h-12",
|
|
433
|
+
"w-12",
|
|
434
|
+
"items-center",
|
|
435
|
+
"justify-center",
|
|
436
|
+
"rounded-full",
|
|
437
|
+
"border",
|
|
438
|
+
"border-white/20",
|
|
439
|
+
"bg-slate-950/95",
|
|
440
|
+
"text-xs",
|
|
441
|
+
"font-medium",
|
|
442
|
+
"text-white",
|
|
443
|
+
"ring-1",
|
|
444
|
+
"ring-white/10",
|
|
445
|
+
"backdrop-blur-md",
|
|
446
|
+
"transition",
|
|
447
|
+
"hover:border-white/30",
|
|
448
|
+
"hover:bg-slate-900/95",
|
|
449
|
+
"hover:scale-105",
|
|
450
|
+
"focus-visible:outline",
|
|
451
|
+
"focus-visible:outline-2",
|
|
452
|
+
"focus-visible:outline-offset-2",
|
|
453
|
+
"focus-visible:outline-rose-500",
|
|
454
|
+
"touch-none",
|
|
455
|
+
"select-none",
|
|
456
|
+
this.isDragging ? "cursor-grabbing" : "cursor-grab",
|
|
457
|
+
].join(" ");
|
|
458
|
+
|
|
459
|
+
return html`
|
|
460
|
+
<button
|
|
461
|
+
class=${buttonClasses}
|
|
462
|
+
type="button"
|
|
463
|
+
aria-label="Web Inspector"
|
|
464
|
+
data-drag-context="button"
|
|
465
|
+
@pointerdown=${this.handlePointerDown}
|
|
466
|
+
@pointermove=${this.handlePointerMove}
|
|
467
|
+
@pointerup=${this.handlePointerUp}
|
|
468
|
+
@pointercancel=${this.handlePointerCancel}
|
|
469
|
+
@click=${this.handleButtonClick}
|
|
470
|
+
>
|
|
471
|
+
<img src=${logoMarkUrl} alt="" class="h-7 w-7" loading="lazy" />
|
|
472
|
+
</button>
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private renderWindow() {
|
|
477
|
+
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
|
+
};
|
|
484
|
+
const contextDropdown = this.renderContextDropdown();
|
|
485
|
+
const hasContextDropdown = contextDropdown !== nothing;
|
|
486
|
+
|
|
487
|
+
return html`
|
|
488
|
+
<section
|
|
489
|
+
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
|
+
style=${styleMap(windowStyles)}
|
|
491
|
+
>
|
|
492
|
+
<div class="flex flex-1 overflow-hidden bg-white text-gray-800">
|
|
493
|
+
<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"
|
|
495
|
+
aria-label="Inspector sections"
|
|
496
|
+
>
|
|
497
|
+
<div class="flex flex-col gap-4 overflow-y-auto">
|
|
498
|
+
<div
|
|
499
|
+
class="flex items-center gap-2 pl-1 touch-none select-none ${this.isDragging && this.pointerContext === 'window' ? 'cursor-grabbing' : 'cursor-grab'}"
|
|
500
|
+
data-drag-context="window"
|
|
501
|
+
@pointerdown=${this.handlePointerDown}
|
|
502
|
+
@pointermove=${this.handlePointerMove}
|
|
503
|
+
@pointerup=${this.handlePointerUp}
|
|
504
|
+
@pointercancel=${this.handlePointerCancel}
|
|
505
|
+
>
|
|
506
|
+
<span
|
|
507
|
+
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gray-900 text-white pointer-events-none"
|
|
508
|
+
>
|
|
509
|
+
${this.renderIcon("Building2")}
|
|
510
|
+
</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>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
<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>
|
|
519
|
+
<div class="flex flex-col gap-0.5">
|
|
520
|
+
${this.menuItems.map(({ key, label, icon }) => {
|
|
521
|
+
const isSelected = this.selectedMenu === key;
|
|
522
|
+
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",
|
|
524
|
+
isSelected
|
|
525
|
+
? "bg-gray-900 text-white"
|
|
526
|
+
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900",
|
|
527
|
+
].join(" ");
|
|
528
|
+
|
|
529
|
+
const badgeClasses = isSelected
|
|
530
|
+
? "bg-gray-800 text-white"
|
|
531
|
+
: "bg-white border border-gray-200 text-gray-500 group-hover:border-gray-300 group-hover:text-gray-700";
|
|
532
|
+
|
|
533
|
+
return html`
|
|
534
|
+
<button
|
|
535
|
+
type="button"
|
|
536
|
+
class=${buttonClasses}
|
|
537
|
+
aria-pressed=${isSelected}
|
|
538
|
+
@click=${() => this.handleMenuSelect(key)}
|
|
539
|
+
>
|
|
540
|
+
<span
|
|
541
|
+
class="flex h-6 w-6 items-center justify-center rounded ${badgeClasses}"
|
|
542
|
+
aria-hidden="true"
|
|
543
|
+
>
|
|
544
|
+
${this.renderIcon(icon)}
|
|
545
|
+
</span>
|
|
546
|
+
<span class="flex-1">${label}</span>
|
|
547
|
+
<span class="text-gray-400 opacity-60">${this.renderIcon("ChevronRight")}</span>
|
|
548
|
+
</button>
|
|
549
|
+
`;
|
|
550
|
+
})}
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<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"
|
|
557
|
+
>
|
|
558
|
+
<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"
|
|
560
|
+
>
|
|
561
|
+
JS
|
|
562
|
+
</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>
|
|
568
|
+
</div>
|
|
569
|
+
</nav>
|
|
570
|
+
<div class="relative flex flex-1 flex-col overflow-hidden">
|
|
571
|
+
<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'}"
|
|
573
|
+
data-drag-context="window"
|
|
574
|
+
@pointerdown=${this.handlePointerDown}
|
|
575
|
+
@pointermove=${this.handlePointerMove}
|
|
576
|
+
@pointerup=${this.handlePointerUp}
|
|
577
|
+
@pointercancel=${this.handlePointerCancel}
|
|
578
|
+
>
|
|
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>
|
|
585
|
+
${hasContextDropdown
|
|
586
|
+
? html`
|
|
587
|
+
<span class="h-3 w-px bg-gray-200"></span>
|
|
588
|
+
<div class="pl-3">${contextDropdown}</div>
|
|
589
|
+
`
|
|
590
|
+
: nothing}
|
|
591
|
+
</div>
|
|
592
|
+
</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>
|
|
602
|
+
</div>
|
|
603
|
+
<div class="flex-1 overflow-auto">
|
|
604
|
+
${this.renderMainContent()}
|
|
605
|
+
<slot></slot>
|
|
606
|
+
</div>
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
<div
|
|
610
|
+
class="resize-handle pointer-events-auto absolute bottom-1 right-1 flex h-5 w-5 cursor-nwse-resize items-center justify-center text-gray-400 transition hover:text-gray-600"
|
|
611
|
+
role="presentation"
|
|
612
|
+
aria-hidden="true"
|
|
613
|
+
@pointerdown=${this.handleResizePointerDown}
|
|
614
|
+
@pointermove=${this.handleResizePointerMove}
|
|
615
|
+
@pointerup=${this.handleResizePointerUp}
|
|
616
|
+
@pointercancel=${this.handleResizePointerCancel}
|
|
617
|
+
>
|
|
618
|
+
<svg
|
|
619
|
+
class="h-3 w-3"
|
|
620
|
+
viewBox="0 0 16 16"
|
|
621
|
+
fill="none"
|
|
622
|
+
stroke="currentColor"
|
|
623
|
+
stroke-linecap="round"
|
|
624
|
+
stroke-width="1.5"
|
|
625
|
+
>
|
|
626
|
+
<path d="M5 15L15 5" />
|
|
627
|
+
<path d="M9 15L15 9" />
|
|
628
|
+
</svg>
|
|
629
|
+
</div>
|
|
630
|
+
</section>
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private hydrateStateFromCookie(): void {
|
|
635
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const persisted = loadInspectorState(COOKIE_NAME);
|
|
640
|
+
if (!persisted) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const persistedButton = persisted.button;
|
|
645
|
+
if (persistedButton) {
|
|
646
|
+
if (isValidAnchor(persistedButton.anchor)) {
|
|
647
|
+
this.contextState.button.anchor = persistedButton.anchor;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (isValidPosition(persistedButton.anchorOffset)) {
|
|
651
|
+
this.contextState.button.anchorOffset = persistedButton.anchorOffset;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (typeof persistedButton.hasCustomPosition === "boolean") {
|
|
655
|
+
this.hasCustomPosition.button = persistedButton.hasCustomPosition;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const persistedWindow = persisted.window;
|
|
660
|
+
if (persistedWindow) {
|
|
661
|
+
if (isValidAnchor(persistedWindow.anchor)) {
|
|
662
|
+
this.contextState.window.anchor = persistedWindow.anchor;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (isValidPosition(persistedWindow.anchorOffset)) {
|
|
666
|
+
this.contextState.window.anchorOffset = persistedWindow.anchorOffset;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (isValidSize(persistedWindow.size)) {
|
|
670
|
+
this.contextState.window.size = this.clampWindowSize(persistedWindow.size);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (typeof persistedWindow.hasCustomPosition === "boolean") {
|
|
674
|
+
this.hasCustomPosition.window = persistedWindow.hasCustomPosition;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private get activeContext(): ContextKey {
|
|
680
|
+
return this.isOpen ? "window" : "button";
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private handlePointerDown = (event: PointerEvent) => {
|
|
684
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
685
|
+
const contextAttr = target?.dataset.dragContext;
|
|
686
|
+
const context: ContextKey = contextAttr === "window" ? "window" : "button";
|
|
687
|
+
|
|
688
|
+
this.pointerContext = context;
|
|
689
|
+
this.measureContext(context);
|
|
690
|
+
|
|
691
|
+
event.preventDefault();
|
|
692
|
+
|
|
693
|
+
this.pointerId = event.pointerId;
|
|
694
|
+
this.dragStart = { x: event.clientX, y: event.clientY };
|
|
695
|
+
const state = this.contextState[context];
|
|
696
|
+
this.dragOffset = {
|
|
697
|
+
x: event.clientX - state.position.x,
|
|
698
|
+
y: event.clientY - state.position.y,
|
|
699
|
+
};
|
|
700
|
+
this.isDragging = false;
|
|
701
|
+
this.draggedDuringInteraction = false;
|
|
702
|
+
this.ignoreNextButtonClick = false;
|
|
703
|
+
|
|
704
|
+
target?.setPointerCapture?.(this.pointerId);
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
private handlePointerMove = (event: PointerEvent) => {
|
|
708
|
+
if (this.pointerId !== event.pointerId || !this.dragStart || !this.pointerContext) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const distance = Math.hypot(event.clientX - this.dragStart.x, event.clientY - this.dragStart.y);
|
|
713
|
+
if (!this.isDragging && distance < DRAG_THRESHOLD) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
this.setDragging(true);
|
|
719
|
+
this.draggedDuringInteraction = true;
|
|
720
|
+
|
|
721
|
+
const desired: Position = {
|
|
722
|
+
x: event.clientX - this.dragOffset.x,
|
|
723
|
+
y: event.clientY - this.dragOffset.y,
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const constrained = this.constrainToViewport(desired, this.pointerContext);
|
|
727
|
+
this.contextState[this.pointerContext].position = constrained;
|
|
728
|
+
this.updateHostTransform(this.pointerContext);
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
private handlePointerUp = (event: PointerEvent) => {
|
|
732
|
+
if (this.pointerId !== event.pointerId) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
737
|
+
if (target?.hasPointerCapture(this.pointerId)) {
|
|
738
|
+
target.releasePointerCapture(this.pointerId);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const context = this.pointerContext ?? this.activeContext;
|
|
742
|
+
|
|
743
|
+
if (this.isDragging && this.pointerContext) {
|
|
744
|
+
event.preventDefault();
|
|
745
|
+
this.setDragging(false);
|
|
746
|
+
this.updateAnchorFromPosition(this.pointerContext);
|
|
747
|
+
if (this.pointerContext === "window") {
|
|
748
|
+
this.hasCustomPosition.window = true;
|
|
749
|
+
} else if (this.pointerContext === "button") {
|
|
750
|
+
this.hasCustomPosition.button = true;
|
|
751
|
+
if (this.draggedDuringInteraction) {
|
|
752
|
+
this.ignoreNextButtonClick = true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
this.applyAnchorPosition(this.pointerContext);
|
|
756
|
+
} else if (context === "button" && !this.isOpen && !this.draggedDuringInteraction) {
|
|
757
|
+
this.openInspector();
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
this.resetPointerTracking();
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
private handlePointerCancel = (event: PointerEvent) => {
|
|
764
|
+
if (this.pointerId !== event.pointerId) {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
769
|
+
if (target?.hasPointerCapture(this.pointerId)) {
|
|
770
|
+
target.releasePointerCapture(this.pointerId);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
this.resetPointerTracking();
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
private handleButtonClick = (event: Event) => {
|
|
777
|
+
if (this.isDragging) {
|
|
778
|
+
event.preventDefault();
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (this.ignoreNextButtonClick) {
|
|
783
|
+
event.preventDefault();
|
|
784
|
+
this.ignoreNextButtonClick = false;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (!this.isOpen) {
|
|
789
|
+
event.preventDefault();
|
|
790
|
+
this.openInspector();
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
private handleClosePointerDown = (event: PointerEvent) => {
|
|
795
|
+
event.stopPropagation();
|
|
796
|
+
event.preventDefault();
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
private handleCloseClick = () => {
|
|
800
|
+
this.closeInspector();
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
private handleResizePointerDown = (event: PointerEvent) => {
|
|
804
|
+
event.stopPropagation();
|
|
805
|
+
event.preventDefault();
|
|
806
|
+
|
|
807
|
+
this.hasCustomPosition.window = true;
|
|
808
|
+
this.isResizing = true;
|
|
809
|
+
this.resizePointerId = event.pointerId;
|
|
810
|
+
this.resizeStart = { x: event.clientX, y: event.clientY };
|
|
811
|
+
this.resizeInitialSize = { ...this.contextState.window.size };
|
|
812
|
+
|
|
813
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
814
|
+
target?.setPointerCapture?.(event.pointerId);
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
private handleResizePointerMove = (event: PointerEvent) => {
|
|
818
|
+
if (!this.isResizing || this.resizePointerId !== event.pointerId || !this.resizeStart || !this.resizeInitialSize) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
event.preventDefault();
|
|
823
|
+
|
|
824
|
+
const deltaX = event.clientX - this.resizeStart.x;
|
|
825
|
+
const deltaY = event.clientY - this.resizeStart.y;
|
|
826
|
+
const state = this.contextState.window;
|
|
827
|
+
|
|
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");
|
|
834
|
+
this.requestUpdate();
|
|
835
|
+
this.updateHostTransform("window");
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
private handleResizePointerUp = (event: PointerEvent) => {
|
|
839
|
+
if (this.resizePointerId !== event.pointerId) {
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
844
|
+
if (target?.hasPointerCapture(this.resizePointerId)) {
|
|
845
|
+
target.releasePointerCapture(this.resizePointerId);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
this.updateAnchorFromPosition("window");
|
|
849
|
+
this.applyAnchorPosition("window");
|
|
850
|
+
this.resetResizeTracking();
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
private handleResizePointerCancel = (event: PointerEvent) => {
|
|
854
|
+
if (this.resizePointerId !== event.pointerId) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const target = event.currentTarget as HTMLElement | null;
|
|
859
|
+
if (target?.hasPointerCapture(this.resizePointerId)) {
|
|
860
|
+
target.releasePointerCapture(this.resizePointerId);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
this.updateAnchorFromPosition("window");
|
|
864
|
+
this.applyAnchorPosition("window");
|
|
865
|
+
this.resetResizeTracking();
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
private handleResize = () => {
|
|
869
|
+
this.measureContext("button");
|
|
870
|
+
this.applyAnchorPosition("button");
|
|
871
|
+
|
|
872
|
+
this.measureContext("window");
|
|
873
|
+
if (this.hasCustomPosition.window) {
|
|
874
|
+
this.applyAnchorPosition("window");
|
|
875
|
+
} else {
|
|
876
|
+
this.centerContext("window");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
this.updateHostTransform();
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
private measureContext(context: ContextKey): void {
|
|
883
|
+
const selector = context === "window" ? ".inspector-window" : ".console-button";
|
|
884
|
+
const element = this.renderRoot?.querySelector(selector) as HTMLElement | null;
|
|
885
|
+
if (!element) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const fallback = context === "window" ? DEFAULT_WINDOW_SIZE : DEFAULT_BUTTON_SIZE;
|
|
889
|
+
updateSizeFromElement(this.contextState[context], element, fallback);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
private centerContext(context: ContextKey): void {
|
|
893
|
+
if (typeof window === "undefined") {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const viewport = this.getViewportSize();
|
|
898
|
+
centerContextHelper(this.contextState[context], viewport, EDGE_MARGIN);
|
|
899
|
+
|
|
900
|
+
if (context === this.activeContext) {
|
|
901
|
+
this.updateHostTransform(context);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
this.hasCustomPosition[context] = false;
|
|
905
|
+
this.persistState();
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private ensureWindowPlacement(): void {
|
|
909
|
+
if (typeof window === "undefined") {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (!this.hasCustomPosition.window) {
|
|
914
|
+
this.centerContext("window");
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const viewport = this.getViewportSize();
|
|
919
|
+
keepPositionWithinViewport(this.contextState.window, viewport, EDGE_MARGIN);
|
|
920
|
+
updateAnchorFromPositionHelper(this.contextState.window, viewport, EDGE_MARGIN);
|
|
921
|
+
this.updateHostTransform("window");
|
|
922
|
+
this.persistState();
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private constrainToViewport(position: Position, context: ContextKey): Position {
|
|
926
|
+
if (typeof window === "undefined") {
|
|
927
|
+
return position;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const viewport = this.getViewportSize();
|
|
931
|
+
return constrainToViewport(this.contextState[context], position, viewport, EDGE_MARGIN);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private keepPositionWithinViewport(context: ContextKey): void {
|
|
935
|
+
if (typeof window === "undefined") {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const viewport = this.getViewportSize();
|
|
940
|
+
keepPositionWithinViewport(this.contextState[context], viewport, EDGE_MARGIN);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private getViewportSize(): Size {
|
|
944
|
+
if (typeof window === "undefined") {
|
|
945
|
+
return { ...DEFAULT_WINDOW_SIZE };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return { width: window.innerWidth, height: window.innerHeight };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private persistState(): void {
|
|
952
|
+
const state: PersistedState = {
|
|
953
|
+
button: {
|
|
954
|
+
anchor: this.contextState.button.anchor,
|
|
955
|
+
anchorOffset: this.contextState.button.anchorOffset,
|
|
956
|
+
hasCustomPosition: this.hasCustomPosition.button,
|
|
957
|
+
},
|
|
958
|
+
window: {
|
|
959
|
+
anchor: this.contextState.window.anchor,
|
|
960
|
+
anchorOffset: this.contextState.window.anchorOffset,
|
|
961
|
+
size: {
|
|
962
|
+
width: Math.round(this.contextState.window.size.width),
|
|
963
|
+
height: Math.round(this.contextState.window.size.height),
|
|
964
|
+
},
|
|
965
|
+
hasCustomPosition: this.hasCustomPosition.window,
|
|
966
|
+
},
|
|
967
|
+
};
|
|
968
|
+
saveInspectorState(COOKIE_NAME, state, COOKIE_MAX_AGE_SECONDS);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private clampWindowSize(size: Size): Size {
|
|
972
|
+
if (typeof window === "undefined") {
|
|
973
|
+
return {
|
|
974
|
+
width: Math.max(MIN_WINDOW_WIDTH, size.width),
|
|
975
|
+
height: Math.max(MIN_WINDOW_HEIGHT, size.height),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const viewport = this.getViewportSize();
|
|
980
|
+
return clampSizeToViewport(size, viewport, EDGE_MARGIN, MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private updateHostTransform(context: ContextKey = this.activeContext): void {
|
|
984
|
+
if (context !== this.activeContext) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const { position } = this.contextState[context];
|
|
989
|
+
this.style.transform = `translate3d(${position.x}px, ${position.y}px, 0)`;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private setDragging(value: boolean): void {
|
|
993
|
+
if (this.isDragging !== value) {
|
|
994
|
+
this.isDragging = value;
|
|
995
|
+
this.requestUpdate();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private updateAnchorFromPosition(context: ContextKey): void {
|
|
1000
|
+
if (typeof window === "undefined") {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const viewport = this.getViewportSize();
|
|
1004
|
+
updateAnchorFromPositionHelper(this.contextState[context], viewport, EDGE_MARGIN);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
private applyAnchorPosition(context: ContextKey): void {
|
|
1008
|
+
if (typeof window === "undefined") {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const viewport = this.getViewportSize();
|
|
1012
|
+
applyAnchorPositionHelper(this.contextState[context], viewport, EDGE_MARGIN);
|
|
1013
|
+
this.updateHostTransform(context);
|
|
1014
|
+
this.persistState();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private resetResizeTracking(): void {
|
|
1018
|
+
this.resizePointerId = null;
|
|
1019
|
+
this.resizeStart = null;
|
|
1020
|
+
this.resizeInitialSize = null;
|
|
1021
|
+
this.isResizing = false;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private resetPointerTracking(): void {
|
|
1025
|
+
this.pointerId = null;
|
|
1026
|
+
this.dragStart = null;
|
|
1027
|
+
this.pointerContext = null;
|
|
1028
|
+
this.setDragging(false);
|
|
1029
|
+
this.draggedDuringInteraction = false;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private openInspector(): void {
|
|
1033
|
+
if (this.isOpen) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
this.isOpen = true;
|
|
1038
|
+
this.ensureWindowPlacement();
|
|
1039
|
+
this.requestUpdate();
|
|
1040
|
+
void this.updateComplete.then(() => {
|
|
1041
|
+
this.measureContext("window");
|
|
1042
|
+
if (this.hasCustomPosition.window) {
|
|
1043
|
+
this.applyAnchorPosition("window");
|
|
1044
|
+
} else {
|
|
1045
|
+
this.centerContext("window");
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private closeInspector(): void {
|
|
1051
|
+
if (!this.isOpen) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
this.isOpen = false;
|
|
1056
|
+
this.updateHostTransform("button");
|
|
1057
|
+
this.requestUpdate();
|
|
1058
|
+
void this.updateComplete.then(() => {
|
|
1059
|
+
this.measureContext("button");
|
|
1060
|
+
this.applyAnchorPosition("button");
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
private renderIcon(name: LucideIconName) {
|
|
1065
|
+
const iconNode = icons[name];
|
|
1066
|
+
if (!iconNode) {
|
|
1067
|
+
return nothing;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const svgAttrs: Record<string, string | number> = {
|
|
1071
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1072
|
+
viewBox: "0 0 24 24",
|
|
1073
|
+
fill: "none",
|
|
1074
|
+
stroke: "currentColor",
|
|
1075
|
+
"stroke-width": "1.5",
|
|
1076
|
+
"stroke-linecap": "round",
|
|
1077
|
+
"stroke-linejoin": "round",
|
|
1078
|
+
class: "h-3.5 w-3.5",
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
const svgMarkup = `<svg ${this.serializeAttributes(svgAttrs)}>${iconNode
|
|
1082
|
+
.map(([tag, attrs]) => `<${tag} ${this.serializeAttributes(attrs)} />`)
|
|
1083
|
+
.join("")}</svg>`;
|
|
1084
|
+
|
|
1085
|
+
return unsafeHTML(svgMarkup);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private serializeAttributes(attributes: Record<string, string | number | undefined>): string {
|
|
1089
|
+
return Object.entries(attributes)
|
|
1090
|
+
.filter(([key, value]) => key !== "key" && value !== undefined && value !== null && value !== "")
|
|
1091
|
+
.map(([key, value]) => `${key}="${String(value).replace(/"/g, """)}"`)
|
|
1092
|
+
.join(" ");
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private contextOptions: Array<{ key: string; label: string }> = [
|
|
1096
|
+
{ key: "all-agents", label: "All Agents" },
|
|
1097
|
+
];
|
|
1098
|
+
|
|
1099
|
+
private selectedContext = "all-agents";
|
|
1100
|
+
private expandedRows: Set<string> = new Set();
|
|
1101
|
+
|
|
1102
|
+
private getSelectedMenu(): MenuItem {
|
|
1103
|
+
const found = this.menuItems.find((item) => item.key === this.selectedMenu);
|
|
1104
|
+
return found ?? this.menuItems[0]!;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
private renderMainContent() {
|
|
1108
|
+
if (this.selectedMenu === "ag-ui-events") {
|
|
1109
|
+
return this.renderEventsTable();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Default placeholder content for other sections
|
|
1113
|
+
return html`
|
|
1114
|
+
<div class="flex flex-col gap-3 p-4">
|
|
1115
|
+
<div class="h-24 rounded-lg bg-gray-50"></div>
|
|
1116
|
+
<div class="h-20 rounded-lg bg-gray-50"></div>
|
|
1117
|
+
</div>
|
|
1118
|
+
`;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
private renderEventsTable() {
|
|
1122
|
+
const events = this.getEventsForSelectedContext();
|
|
1123
|
+
|
|
1124
|
+
if (events.length === 0) {
|
|
1125
|
+
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.
|
|
1128
|
+
</div>
|
|
1129
|
+
`;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return html`
|
|
1133
|
+
<div class="overflow-hidden">
|
|
1134
|
+
<table class="w-full border-collapse text-xs">
|
|
1135
|
+
<thead>
|
|
1136
|
+
<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
|
|
1139
|
+
</th>
|
|
1140
|
+
<th class="border-r border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
|
|
1141
|
+
Time
|
|
1142
|
+
</th>
|
|
1143
|
+
<th class="border-b border-gray-200 bg-white px-3 py-2 text-left font-medium text-gray-900">
|
|
1144
|
+
Payload
|
|
1145
|
+
</th>
|
|
1146
|
+
</tr>
|
|
1147
|
+
</thead>
|
|
1148
|
+
<tbody>
|
|
1149
|
+
${events.map((event, index) => {
|
|
1150
|
+
const isLastRow = index === events.length - 1;
|
|
1151
|
+
const rowBg = index % 2 === 0 ? "bg-white" : "bg-gray-50/50";
|
|
1152
|
+
const badgeClasses = this.getEventBadgeClasses(event.type);
|
|
1153
|
+
const inlinePayload = this.stringifyPayload(event.payload, false) || "—";
|
|
1154
|
+
const prettyPayload = this.stringifyPayload(event.payload, true) || inlinePayload;
|
|
1155
|
+
const isExpanded = this.expandedRows.has(event.id);
|
|
1156
|
+
|
|
1157
|
+
return html`
|
|
1158
|
+
<tr
|
|
1159
|
+
class="${rowBg} transition hover:bg-blue-50/50"
|
|
1160
|
+
@click=${() => this.toggleRowExpansion(event.id)}
|
|
1161
|
+
>
|
|
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>
|
|
1167
|
+
</td>
|
|
1168
|
+
<td class="border-r ${!isLastRow ? 'border-b' : ''} border-gray-200 px-3 py-2 font-mono text-[11px] text-gray-600">
|
|
1169
|
+
<span title=${new Date(event.timestamp).toLocaleString()}>
|
|
1170
|
+
${new Date(event.timestamp).toLocaleTimeString()}
|
|
1171
|
+
</span>
|
|
1172
|
+
</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'}">
|
|
1174
|
+
${isExpanded
|
|
1175
|
+
? html`<pre class="m-0 whitespace-pre-wrap text-[10px] font-mono text-gray-600">${prettyPayload}</pre>`
|
|
1176
|
+
: inlinePayload}
|
|
1177
|
+
</td>
|
|
1178
|
+
</tr>
|
|
1179
|
+
`;
|
|
1180
|
+
})}
|
|
1181
|
+
</tbody>
|
|
1182
|
+
</table>
|
|
1183
|
+
</div>
|
|
1184
|
+
`;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private renderContextDropdown() {
|
|
1188
|
+
if (this.selectedMenu !== "ag-ui-events") {
|
|
1189
|
+
return nothing;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const selectedLabel = this.contextOptions.find((opt) => opt.key === this.selectedContext)?.label ?? "";
|
|
1193
|
+
|
|
1194
|
+
return html`
|
|
1195
|
+
<div class="relative" data-context-dropdown-root="true">
|
|
1196
|
+
<button
|
|
1197
|
+
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"
|
|
1199
|
+
@pointerdown=${this.handleContextDropdownToggle}
|
|
1200
|
+
>
|
|
1201
|
+
<span>${selectedLabel}</span>
|
|
1202
|
+
<span class="text-gray-400">${this.renderIcon("ChevronDown")}</span>
|
|
1203
|
+
</button>
|
|
1204
|
+
${this.contextMenuOpen
|
|
1205
|
+
? html`
|
|
1206
|
+
<div
|
|
1207
|
+
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
|
+
data-context-dropdown-root="true"
|
|
1209
|
+
>
|
|
1210
|
+
${this.contextOptions.map(
|
|
1211
|
+
(option) => html`
|
|
1212
|
+
<button
|
|
1213
|
+
type="button"
|
|
1214
|
+
class="flex w-full items-center justify-between px-3 py-1.5 text-left text-xs transition hover:bg-gray-50 focus:bg-gray-50 focus:outline-none"
|
|
1215
|
+
data-context-dropdown-root="true"
|
|
1216
|
+
@click=${() => this.handleContextOptionSelect(option.key)}
|
|
1217
|
+
>
|
|
1218
|
+
<span class="${option.key === this.selectedContext ? 'text-gray-900 font-medium' : 'text-gray-600'}">${option.label}</span>
|
|
1219
|
+
${option.key === this.selectedContext
|
|
1220
|
+
? html`<span class="text-gray-500">${this.renderIcon("Check")}</span>`
|
|
1221
|
+
: nothing}
|
|
1222
|
+
</button>
|
|
1223
|
+
`,
|
|
1224
|
+
)}
|
|
1225
|
+
</div>
|
|
1226
|
+
`
|
|
1227
|
+
: nothing}
|
|
1228
|
+
</div>
|
|
1229
|
+
`;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private handleMenuSelect(key: MenuKey): void {
|
|
1233
|
+
if (!this.menuItems.some((item) => item.key === key)) {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
this.selectedMenu = key;
|
|
1238
|
+
this.contextMenuOpen = false;
|
|
1239
|
+
this.requestUpdate();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
private handleContextDropdownToggle(event: PointerEvent): void {
|
|
1243
|
+
event.preventDefault();
|
|
1244
|
+
event.stopPropagation();
|
|
1245
|
+
this.contextMenuOpen = !this.contextMenuOpen;
|
|
1246
|
+
this.requestUpdate();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
private handleContextOptionSelect(key: string): void {
|
|
1250
|
+
if (!this.contextOptions.some((option) => option.key === key)) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (this.selectedContext !== key) {
|
|
1255
|
+
this.selectedContext = key;
|
|
1256
|
+
this.expandedRows.clear();
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
this.contextMenuOpen = false;
|
|
1260
|
+
this.requestUpdate();
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
private handleGlobalPointerDown = (event: PointerEvent): void => {
|
|
1264
|
+
if (!this.contextMenuOpen) {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
const clickedDropdown = event.composedPath().some((node) => {
|
|
1269
|
+
return node instanceof HTMLElement && node.dataset?.contextDropdownRoot === "true";
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
if (!clickedDropdown) {
|
|
1273
|
+
this.contextMenuOpen = false;
|
|
1274
|
+
this.requestUpdate();
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
private toggleRowExpansion(eventId: string): void {
|
|
1279
|
+
if (this.expandedRows.has(eventId)) {
|
|
1280
|
+
this.expandedRows.delete(eventId);
|
|
1281
|
+
} else {
|
|
1282
|
+
this.expandedRows.add(eventId);
|
|
1283
|
+
}
|
|
1284
|
+
this.requestUpdate();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
export function defineWebInspector(): void {
|
|
1289
|
+
if (!customElements.get(WEB_INSPECTOR_TAG)) {
|
|
1290
|
+
customElements.define(WEB_INSPECTOR_TAG, WebInspectorElement);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
defineWebInspector();
|
|
1295
|
+
|
|
1296
|
+
declare global {
|
|
1297
|
+
interface HTMLElementTagNameMap {
|
|
1298
|
+
"web-inspector": WebInspectorElement;
|
|
1299
|
+
}
|
|
1300
|
+
}
|