mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
feat(devtools): migrate devtools package into monorepo (#18936)
This commit is contained in:
85
packages/devtools/GEMINI.md
Normal file
85
packages/devtools/GEMINI.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Gemini CLI DevTools
|
||||
|
||||
Integrated Developer Tools for Gemini CLI, providing a Chrome DevTools-like
|
||||
interface for Network and Console inspection. Launched automatically when the
|
||||
`general.devtools` setting is enabled.
|
||||
|
||||
## Features
|
||||
|
||||
- **Network Inspector**: Real-time request/response logging with streaming
|
||||
chunks and duration tracking
|
||||
- **Console Inspector**: Real-time console log viewing
|
||||
(log/warn/error/debug/info)
|
||||
- **Session Management**: Multiple CLI session support with live connection
|
||||
status
|
||||
- **Import/Export**: Import JSONL log files, export current session logs
|
||||
|
||||
## How It Works
|
||||
|
||||
When `general.devtools` is enabled, the CLI's `devtoolsService` automatically:
|
||||
|
||||
1. Probes port 25417 for an existing DevTools instance
|
||||
2. If found, connects as a WebSocket client
|
||||
3. If not, starts a new DevTools server and connects to it
|
||||
4. If another instance races for the port, the loser connects to the winner
|
||||
|
||||
No environment variables needed for normal use.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
gemini.tsx / nonInteractiveCli.ts
|
||||
│ (dynamic import)
|
||||
▼
|
||||
devtoolsService.ts ← orchestration + DevTools lifecycle
|
||||
│ (imports)
|
||||
▼
|
||||
activityLogger.ts ← pure logging (capture, file, WebSocket transport)
|
||||
│ (events)
|
||||
▼
|
||||
DevTools server (:25417) ← this package (HTTP + WebSocket + SSE)
|
||||
│ (SSE /events)
|
||||
▼
|
||||
DevTools UI (React) ← client/ compiled by esbuild
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------------------------------- | --------------------------------------------- |
|
||||
| `GEMINI_CLI_ACTIVITY_LOG_TARGET` | File path for JSONL mode (optional, fallback) |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
| --------- | --------- | --------------------------------------------------------------------------- |
|
||||
| `/ws` | WebSocket | Log ingestion from CLI sessions (register, network, console) |
|
||||
| `/events` | SSE | Pushes snapshot on connect, then incremental network/console/session events |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Build everything (client + server)
|
||||
npm run build
|
||||
|
||||
# Rebuild client only after UI changes
|
||||
npm run build:client
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
packages/devtools/
|
||||
├── src/
|
||||
│ └── index.ts # DevTools server (HTTP, WebSocket, SSE)
|
||||
├── client/
|
||||
│ ├── index.html
|
||||
│ └── src/
|
||||
│ ├── main.tsx # React entry
|
||||
│ ├── App.tsx # DevTools UI
|
||||
│ └── hooks.ts # Data fetching hooks
|
||||
├── esbuild.client.js # Client build script
|
||||
└── dist/ # Build output
|
||||
├── src/index.js # Compiled server
|
||||
└── client/ # Bundled client assets
|
||||
```
|
||||
25
packages/devtools/client/index.html
Normal file
25
packages/devtools/client/index.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gemini CLI DevTools</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
|
||||
Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/assets/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1875
packages/devtools/client/src/App.tsx
Normal file
1875
packages/devtools/client/src/App.tsx
Normal file
File diff suppressed because it is too large
Load Diff
94
packages/devtools/client/src/hooks.ts
Normal file
94
packages/devtools/client/src/hooks.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import type {
|
||||
NetworkLog,
|
||||
InspectorConsoleLog as ConsoleLog,
|
||||
} from '../../src/types.js';
|
||||
|
||||
export type { NetworkLog };
|
||||
export type { InspectorConsoleLog as ConsoleLog } from '../../src/types.js';
|
||||
|
||||
export function useDevToolsData() {
|
||||
const [networkLogs, setNetworkLogs] = useState<NetworkLog[]>([]);
|
||||
const [consoleLogs, setConsoleLogs] = useState<ConsoleLog[]>([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [connectedSessions, setConnectedSessions] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const evtSource = new EventSource('/events');
|
||||
|
||||
evtSource.onopen = () => setIsConnected(true);
|
||||
evtSource.onerror = () => setIsConnected(false);
|
||||
|
||||
evtSource.addEventListener('snapshot', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
// Merge with existing data to preserve logs across server restarts
|
||||
setNetworkLogs((prev) => {
|
||||
if (data.networkLogs.length === 0) return prev;
|
||||
const merged = new Map(prev.map((l: NetworkLog) => [l.id, l]));
|
||||
for (const log of data.networkLogs) merged.set(log.id, log);
|
||||
return Array.from(merged.values());
|
||||
});
|
||||
setConsoleLogs((prev) => {
|
||||
if (data.consoleLogs.length === 0) return prev;
|
||||
const existingIds = new Set(prev.map((l: ConsoleLog) => l.id));
|
||||
const newLogs = data.consoleLogs.filter(
|
||||
(l: ConsoleLog) => !existingIds.has(l.id),
|
||||
);
|
||||
const merged = [...prev, ...newLogs];
|
||||
return merged.length > 5000 ? merged.slice(-5000) : merged;
|
||||
});
|
||||
setConnectedSessions(data.sessions);
|
||||
} catch {
|
||||
// Malformed snapshot — ignore
|
||||
}
|
||||
});
|
||||
|
||||
evtSource.addEventListener('network', (e) => {
|
||||
try {
|
||||
const log = JSON.parse(e.data) as NetworkLog;
|
||||
setNetworkLogs((prev) => {
|
||||
const idx = prev.findIndex((l) => l.id === log.id);
|
||||
if (idx > -1) {
|
||||
const next = [...prev];
|
||||
next[idx] = log;
|
||||
return next;
|
||||
}
|
||||
return [...prev, log];
|
||||
});
|
||||
} catch {
|
||||
// Malformed network event — ignore
|
||||
}
|
||||
});
|
||||
|
||||
evtSource.addEventListener('console', (e) => {
|
||||
try {
|
||||
const log = JSON.parse(e.data) as ConsoleLog;
|
||||
setConsoleLogs((prev) => {
|
||||
const next = [...prev, log];
|
||||
return next.length > 5000 ? next.slice(-5000) : next;
|
||||
});
|
||||
} catch {
|
||||
// Malformed console event — ignore
|
||||
}
|
||||
});
|
||||
|
||||
evtSource.addEventListener('session', (e) => {
|
||||
try {
|
||||
setConnectedSessions(JSON.parse(e.data));
|
||||
} catch {
|
||||
// Malformed session event — ignore
|
||||
}
|
||||
});
|
||||
|
||||
return () => evtSource.close();
|
||||
}, []);
|
||||
|
||||
return { networkLogs, consoleLogs, isConnected, connectedSessions };
|
||||
}
|
||||
15
packages/devtools/client/src/main.tsx
Normal file
15
packages/devtools/client/src/main.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
30
packages/devtools/esbuild.client.js
Normal file
30
packages/devtools/esbuild.client.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import esbuild from 'esbuild';
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
mkdirSync('dist/client', { recursive: true });
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: ['client/src/main.tsx'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
format: 'esm',
|
||||
target: 'es2020',
|
||||
jsx: 'automatic',
|
||||
outfile: 'dist/client/main.js',
|
||||
});
|
||||
|
||||
// Embed client assets as string constants so the devtools server can be
|
||||
// bundled into the CLI without needing readFileSync + __dirname at runtime.
|
||||
const indexHtml = readFileSync('client/index.html', 'utf-8');
|
||||
const clientJs = readFileSync('dist/client/main.js', 'utf-8');
|
||||
|
||||
writeFileSync(
|
||||
'src/_client-assets.ts',
|
||||
`/**\n * @license\n * Copyright 2025 Google LLC\n * SPDX-License-Identifier: Apache-2.0\n */\n\n// Auto-generated by esbuild.client.js — do not edit\nexport const INDEX_HTML = ${JSON.stringify(indexHtml)};\nexport const CLIENT_JS = ${JSON.stringify(clientJs)};\n`,
|
||||
);
|
||||
32
packages/devtools/package.json
Normal file
32
packages/devtools/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.30.0-nightly.20260210.a2174751d",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"default": "./dist/src/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:client && tsc -p tsconfig.build.json",
|
||||
"build:client": "node esbuild.client.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"client/index.html"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
}
|
||||
359
packages/devtools/src/index.ts
Normal file
359
packages/devtools/src/index.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import http from 'node:http';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { WebSocketServer, type WebSocket } from 'ws';
|
||||
import type {
|
||||
NetworkLog,
|
||||
ConsoleLogPayload,
|
||||
InspectorConsoleLog,
|
||||
} from './types.js';
|
||||
import { INDEX_HTML, CLIENT_JS } from './_client-assets.js';
|
||||
|
||||
export type {
|
||||
NetworkLog,
|
||||
ConsoleLogPayload,
|
||||
InspectorConsoleLog,
|
||||
} from './types.js';
|
||||
|
||||
interface IncomingNetworkPayload extends Partial<NetworkLog> {
|
||||
chunk?: {
|
||||
index: number;
|
||||
data: string;
|
||||
timestamp: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionInfo {
|
||||
sessionId: string;
|
||||
ws: WebSocket;
|
||||
lastPing: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* DevTools Viewer
|
||||
*
|
||||
* Receives logs via WebSocket from CLI sessions.
|
||||
*/
|
||||
export class DevTools extends EventEmitter {
|
||||
private static instance: DevTools | undefined;
|
||||
private logs: NetworkLog[] = [];
|
||||
private consoleLogs: InspectorConsoleLog[] = [];
|
||||
private server: http.Server | null = null;
|
||||
private wss: WebSocketServer | null = null;
|
||||
private sessions = new Map<string, SessionInfo>();
|
||||
private heartbeatTimer: NodeJS.Timeout | null = null;
|
||||
private port = 25417;
|
||||
private static readonly DEFAULT_PORT = 25417;
|
||||
private static readonly MAX_PORT_RETRIES = 10;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
// Each SSE client adds 3 listeners; raise the limit to avoid warnings
|
||||
this.setMaxListeners(50);
|
||||
}
|
||||
|
||||
static getInstance(): DevTools {
|
||||
if (!DevTools.instance) {
|
||||
DevTools.instance = new DevTools();
|
||||
}
|
||||
return DevTools.instance;
|
||||
}
|
||||
|
||||
addInternalConsoleLog(
|
||||
payload: ConsoleLogPayload,
|
||||
sessionId?: string,
|
||||
timestamp?: number,
|
||||
) {
|
||||
const entry: InspectorConsoleLog = {
|
||||
...payload,
|
||||
id: randomUUID(),
|
||||
sessionId,
|
||||
timestamp: timestamp || Date.now(),
|
||||
};
|
||||
this.consoleLogs.push(entry);
|
||||
if (this.consoleLogs.length > 5000) this.consoleLogs.shift();
|
||||
this.emit('console-update', entry);
|
||||
}
|
||||
|
||||
addInternalNetworkLog(
|
||||
payload: IncomingNetworkPayload,
|
||||
sessionId?: string,
|
||||
timestamp?: number,
|
||||
) {
|
||||
if (!payload.id) return;
|
||||
const existingIndex = this.logs.findIndex((l) => l.id === payload.id);
|
||||
if (existingIndex > -1) {
|
||||
const existing = this.logs[existingIndex];
|
||||
|
||||
// Handle chunk accumulation
|
||||
if (payload.chunk) {
|
||||
const chunks = existing.chunks || [];
|
||||
chunks.push(payload.chunk);
|
||||
this.logs[existingIndex] = {
|
||||
...existing,
|
||||
chunks,
|
||||
sessionId: sessionId || existing.sessionId,
|
||||
};
|
||||
} else {
|
||||
this.logs[existingIndex] = {
|
||||
...existing,
|
||||
...payload,
|
||||
sessionId: sessionId || existing.sessionId,
|
||||
// Drop chunks once we have the full response body — the data
|
||||
// is redundant and keeping both can blow past V8's string limit
|
||||
// when serializing the snapshot.
|
||||
chunks: payload.response?.body ? undefined : existing.chunks,
|
||||
response: payload.response
|
||||
? { ...existing.response, ...payload.response }
|
||||
: existing.response,
|
||||
} as NetworkLog;
|
||||
}
|
||||
this.emit('update', this.logs[existingIndex]);
|
||||
} else if (payload.url) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const entry = {
|
||||
...payload,
|
||||
sessionId,
|
||||
timestamp: timestamp || Date.now(),
|
||||
chunks: payload.chunk ? [payload.chunk] : undefined,
|
||||
} as NetworkLog;
|
||||
this.logs.push(entry);
|
||||
if (this.logs.length > 2000) this.logs.shift();
|
||||
this.emit('update', entry);
|
||||
}
|
||||
}
|
||||
|
||||
getUrl(): string {
|
||||
return `http://127.0.0.1:${this.port}`;
|
||||
}
|
||||
|
||||
getPort(): number {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
if (this.wss) {
|
||||
this.wss.close();
|
||||
this.wss = null;
|
||||
}
|
||||
if (this.server) {
|
||||
this.server.close(() => resolve());
|
||||
this.server = null;
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
// Reset singleton so a fresh start() is possible
|
||||
DevTools.instance = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
start(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.server) {
|
||||
resolve(this.getUrl());
|
||||
return;
|
||||
}
|
||||
this.server = http.createServer((req, res) => {
|
||||
// Only allow same-origin requests — the client is served from this
|
||||
// server so cross-origin access is unnecessary and would let arbitrary
|
||||
// websites exfiltrate logs (which may contain API keys/headers).
|
||||
const origin = req.headers.origin;
|
||||
if (origin) {
|
||||
const allowed = `http://127.0.0.1:${this.port}`;
|
||||
if (origin === allowed) {
|
||||
res.setHeader('Access-Control-Allow-Origin', allowed);
|
||||
}
|
||||
}
|
||||
|
||||
// API routes
|
||||
if (req.url === '/events') {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
});
|
||||
|
||||
// Send full snapshot on connect
|
||||
const snapshot = JSON.stringify({
|
||||
networkLogs: this.logs,
|
||||
consoleLogs: this.consoleLogs,
|
||||
sessions: Array.from(this.sessions.keys()),
|
||||
});
|
||||
res.write(`event: snapshot\ndata: ${snapshot}\n\n`);
|
||||
|
||||
// Incremental updates
|
||||
const onNetwork = (log: NetworkLog) => {
|
||||
res.write(`event: network\ndata: ${JSON.stringify(log)}\n\n`);
|
||||
};
|
||||
const onConsole = (log: InspectorConsoleLog) => {
|
||||
res.write(`event: console\ndata: ${JSON.stringify(log)}\n\n`);
|
||||
};
|
||||
const onSession = () => {
|
||||
const sessions = Array.from(this.sessions.keys());
|
||||
res.write(`event: session\ndata: ${JSON.stringify(sessions)}\n\n`);
|
||||
};
|
||||
this.on('update', onNetwork);
|
||||
this.on('console-update', onConsole);
|
||||
this.on('session-update', onSession);
|
||||
req.on('close', () => {
|
||||
this.off('update', onNetwork);
|
||||
this.off('console-update', onConsole);
|
||||
this.off('session-update', onSession);
|
||||
});
|
||||
} else if (req.url === '/' || req.url === '/index.html') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(INDEX_HTML);
|
||||
} else if (req.url === '/assets/main.js') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
||||
res.end(CLIENT_JS);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
}
|
||||
});
|
||||
this.server.on('error', (e: unknown) => {
|
||||
if (
|
||||
typeof e === 'object' &&
|
||||
e !== null &&
|
||||
'code' in e &&
|
||||
e.code === 'EADDRINUSE'
|
||||
) {
|
||||
if (this.port - DevTools.DEFAULT_PORT >= DevTools.MAX_PORT_RETRIES) {
|
||||
reject(
|
||||
new Error(
|
||||
`DevTools: all ports ${DevTools.DEFAULT_PORT}–${this.port} in use`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.port++;
|
||||
this.server?.listen(this.port, '127.0.0.1');
|
||||
} else {
|
||||
reject(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
});
|
||||
this.server.listen(this.port, '127.0.0.1', () => {
|
||||
this.setupWebSocketServer();
|
||||
resolve(this.getUrl());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setupWebSocketServer() {
|
||||
if (!this.server) return;
|
||||
|
||||
this.wss = new WebSocketServer({ server: this.server, path: '/ws' });
|
||||
|
||||
this.wss.on('connection', (ws: WebSocket) => {
|
||||
let sessionId: string | null = null;
|
||||
|
||||
ws.on('message', (data: Buffer) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
|
||||
// Handle registration first
|
||||
if (message.type === 'register') {
|
||||
sessionId = String(message.sessionId);
|
||||
if (!sessionId) return;
|
||||
|
||||
this.sessions.set(sessionId, {
|
||||
sessionId,
|
||||
ws,
|
||||
lastPing: Date.now(),
|
||||
});
|
||||
|
||||
// Notify session update
|
||||
this.emit('session-update');
|
||||
|
||||
// Send registration acknowledgement
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'registered',
|
||||
sessionId,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
);
|
||||
} else if (sessionId) {
|
||||
this.handleWebSocketMessage(sessionId, message);
|
||||
}
|
||||
} catch {
|
||||
// Invalid WebSocket message
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
if (sessionId) {
|
||||
this.sessions.delete(sessionId);
|
||||
this.emit('session-update');
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', () => {
|
||||
// WebSocket error — no action needed
|
||||
});
|
||||
});
|
||||
|
||||
// Heartbeat mechanism
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
const now = Date.now();
|
||||
this.sessions.forEach((session, sessionId) => {
|
||||
if (now - session.lastPing > 30000) {
|
||||
session.ws.close();
|
||||
this.sessions.delete(sessionId);
|
||||
} else {
|
||||
// Send ping
|
||||
session.ws.send(JSON.stringify({ type: 'ping', timestamp: now }));
|
||||
}
|
||||
});
|
||||
}, 10000);
|
||||
this.heartbeatTimer.unref();
|
||||
}
|
||||
|
||||
private handleWebSocketMessage(
|
||||
sessionId: string,
|
||||
message: Record<string, unknown>,
|
||||
) {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
switch (message['type']) {
|
||||
case 'pong':
|
||||
session.lastPing = Date.now();
|
||||
break;
|
||||
|
||||
case 'console':
|
||||
this.addInternalConsoleLog(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
message['payload'] as ConsoleLogPayload,
|
||||
sessionId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
message['timestamp'] as number,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'network':
|
||||
this.addInternalNetworkLog(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
message['payload'] as IncomingNetworkPayload,
|
||||
sessionId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
message['timestamp'] as number,
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
packages/devtools/src/types.ts
Normal file
39
packages/devtools/src/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export interface NetworkLog {
|
||||
id: string;
|
||||
sessionId?: string;
|
||||
timestamp: number;
|
||||
method: string;
|
||||
url: string;
|
||||
headers: Record<string, string | string[] | undefined>;
|
||||
body?: string;
|
||||
pending?: boolean;
|
||||
chunks?: Array<{
|
||||
index: number;
|
||||
data: string;
|
||||
timestamp: number;
|
||||
}>;
|
||||
response?: {
|
||||
status: number;
|
||||
headers: Record<string, string | string[] | undefined>;
|
||||
body?: string;
|
||||
durationMs: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ConsoleLogPayload {
|
||||
type: 'log' | 'warn' | 'error' | 'debug' | 'info';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface InspectorConsoleLog extends ConsoleLogPayload {
|
||||
id: string;
|
||||
sessionId?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
13
packages/devtools/tsconfig.build.json
Normal file
13
packages/devtools/tsconfig.build.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/src",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"noEmit": false,
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"verbatimModuleSyntax": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
10
packages/devtools/tsconfig.json
Normal file
10
packages/devtools/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
||||
"jsx": "react-jsx",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src", "client/src"]
|
||||
}
|
||||
Reference in New Issue
Block a user