Skill Tracer
Skills › gstack › browse › inspector.ts

inspector.ts

132 lines · browse/src/routes/inspector.ts at 3f79d81

1/**
2 * CSS inspector routes and the in-memory inspector state they share: pick,
3 * read, apply, reset, history and the inspector SSE stream.
4 *
5 * GET /inspector/events is root-bearer: it sat behind the if-chain's blanket
6 * root-bearer check, so the SSE cookie its handler also accepted never reached
7 * it. The declaration keeps that behavior.
8 */
9
10import { json, jsonError, type RouteEntry } from './table';
11import { inspectElement, modifyStyle, resetModifications, getModificationHistory, type InspectorResult } from '../cdp-inspector';
12import { createSseEndpoint } from '../sse-helpers';
14let inspectorData: InspectorResult | null = null;
15let inspectorTimestamp = 0;
17type InspectorSubscriber = (event: any) => void;
18const inspectorSubscribers = new Set<InspectorSubscriber>();
20/** Diagnostic accessor used by the $B memory snapshot. */
21export function getInspectorSubscriberCount(): number {
22 return inspectorSubscribers.size;
25/** Drops every inspector SSE subscriber (daemon shutdown). */
26export function clearInspectorSubscribers(): void {
27 inspectorSubscribers.clear();
30function emitInspectorEvent(event: any): void {
31 for (const notify of inspectorSubscribers) {
32 queueMicrotask(() => {
33 try { notify(event); } catch (err: any) {
34 console.error('[browse] Inspector event subscriber threw:', err.message);
35 }
36 });
37 }
40export const inspectorRoutes: RouteEntry[] = [
41 // POST /inspector/pick — receive element pick from extension, run CDP inspection
42 {
43 method: 'POST', path: '/inspector/pick', auth: 'root-bearer', surfaces: ['local'],
44 handler: async (req, _r, ctx) => {
45 const body = await req.json();
46 const { selector, activeTabUrl } = body;
47 if (!selector) return jsonError(400, 'Missing selector');
48 try {
49 const page = ctx.browserManager.getPage();
50 const result = await inspectElement(page, selector);
51 inspectorData = result;
52 inspectorTimestamp = Date.now();
53 // Also store on browserManager for CLI access
54 (ctx.browserManager as any)._inspectorData = result;
55 (ctx.browserManager as any)._inspectorTimestamp = inspectorTimestamp;
56 emitInspectorEvent({ type: 'pick', selector, timestamp: inspectorTimestamp });
57 return json(result);
58 } catch (err: any) {
59 return jsonError(500, err.message);
60 }
61 },
62 },
64 // GET /inspector — return latest inspector data
65 {
66 method: 'GET', path: '/inspector', auth: 'root-bearer', surfaces: ['local'],
67 handler: () => {
68 if (!inspectorData) return json({ data: null });
69 const stale = inspectorTimestamp > 0 && (Date.now() - inspectorTimestamp > 60000);
70 return json({ data: inspectorData, timestamp: inspectorTimestamp, stale });
71 },
72 },
74 // POST /inspector/apply — apply a CSS modification
75 {
76 method: 'POST', path: '/inspector/apply', auth: 'root-bearer', surfaces: ['local'],
77 handler: async (req, _r, ctx) => {
78 const body = await req.json();
79 const { selector, property, value } = body;
80 if (!selector || !property || value === undefined) return jsonError(400, 'Missing selector, property, or value');
81 try {
82 const page = ctx.browserManager.getPage();
83 const mod = await modifyStyle(page, selector, property, value);
84 emitInspectorEvent({ type: 'apply', modification: mod, timestamp: Date.now() });
85 return json(mod);
86 } catch (err: any) {
87 return jsonError(500, err.message);
88 }
89 },
90 },
92 // POST /inspector/reset — clear all modifications
93 {
94 method: 'POST', path: '/inspector/reset', auth: 'root-bearer', surfaces: ['local'],
95 handler: async (_req, _r, ctx) => {
96 try {
97 const page = ctx.browserManager.getPage();
98 await resetModifications(page);
99 emitInspectorEvent({ type: 'reset', timestamp: Date.now() });
100 return json({ ok: true });
101 } catch (err: any) {
102 return jsonError(500, err.message);
103 }
104 },
105 },
107 // GET /inspector/history — return modification list
108 {
109 method: 'GET', path: '/inspector/history', auth: 'root-bearer', surfaces: ['local'],
110 handler: () => json({ history: getModificationHistory() }),
111 },
113 // GET /inspector/events — SSE for inspector state changes
114 {
115 method: 'GET', path: '/inspector/events', auth: 'root-bearer', surfaces: ['local'],
116 handler: (req) => {
117 // Cleanup contract (abort + enqueue-fail + heartbeat-fail, idempotent)
118 // lives in createSseEndpoint; sanitizeReplacer is applied to every
119 // JSON.stringify inside the helper.
120 return createSseEndpoint(req, {
121 initialReplay: inspectorData
122 ? (send) => send('state', { data: inspectorData, timestamp: inspectorTimestamp })
123 : undefined,
124 subscribe: (notify) => {
125 inspectorSubscribers.add(notify);
126 return () => inspectorSubscribers.delete(notify);
127 },
128 liveEventName: 'inspector',
129 });
130 },
131 },
132];