GadgetLogTransportSocket and the drone-to-IDE log
This commit is contained in:
parent
632caf11ed
commit
d7924a9d6f
@ -1,32 +1,131 @@
|
|||||||
# Gadget Code: GadgetLogTransportSocket
|
# Gadget Code: GadgetLogTransportSocket
|
||||||
|
|
||||||
We recently finished a large refactor of GadgetLog into the @gadget/api package, and all components are now consumers of this one unified logging interface. [GadgetLog](../packages/api/src/lib/log.ts) makes use of [GadgetLogTransport](../packages/api/src/lib/log-transport.ts) to provide an abstract log transport. The logging system is working as intended. Console logging works as expected. File logging works as expected.
|
GadgetLog now ships four transports out of the box: **Console**, **File**, **Socket**, and the abstract base. The socket transport (`GadgetLogTransportSocket`) transmits every log entry from `gadget-drone` → `gadget-code:backend` → `gadget-code:frontend` over Socket.IO, rendering live in the IDE's LogPanel.
|
||||||
|
|
||||||
We are now going to implement `GadgetLogTransportSocket`, which will transmit a Socket.IO message `log` from `gadget-drone` to `gadget-code:backend`, which will forward the log message directly to [ChatSessionView](../gadget-code/frontend/src/pages/ChatSessionView.tsx) for display in [LogPanel](../gadget-code/frontend/src/components/LogPanel.tsx).
|
## Message Flow
|
||||||
|
|
||||||
I have already added the `LogMessage` type to the drone message definitions, and added definitions to the appropriate interfaces we use for extending Socket.IO. It is a fire-n-forget message (no callback, no blocking).
|
```
|
||||||
|
gadget-drone
|
||||||
|
│ GadgetLogTransportSocket.writeLog()
|
||||||
|
│ socket.emit("log", timestamp, component, level, message, metadata?)
|
||||||
|
▼
|
||||||
|
gadget-code:backend (DroneSession.onLog)
|
||||||
|
│ SocketService.getCodeSessionByChatSessionId(chatSessionId)
|
||||||
|
│ codeSession.onLog(...)
|
||||||
|
▼
|
||||||
|
gadget-code:backend (CodeSession.onLog)
|
||||||
|
│ socket.emit("log", timestamp, component, level, message, metadata?)
|
||||||
|
▼
|
||||||
|
gadget-code:frontend (SocketClient)
|
||||||
|
│ raw socket "log" → internal "log:entry" event
|
||||||
|
│ { level, component, message, metadata, timestamp }
|
||||||
|
▼
|
||||||
|
ChatSessionView.handleLogEntry
|
||||||
|
│ setLogs(...)
|
||||||
|
▼
|
||||||
|
LogPanel — terminal-styled live log viewer
|
||||||
|
```
|
||||||
|
|
||||||
## How It Works
|
## Transport: `GadgetLogTransportSocket`
|
||||||
|
|
||||||
You will simply pass the parameters passed to the GadgetLog API as a message over Socket.IO to the Log panel dislay in the IDE.
|
**File:** `packages/api/src/lib/log-transport-socket.ts`
|
||||||
|
|
||||||
## Instructions
|
Accepts a generic emit function (no Socket.IO dependency in `@gadget/api`):
|
||||||
|
|
||||||
In this session, you will:
|
```typescript
|
||||||
|
export type LogEmitFunction = (
|
||||||
|
event: string,
|
||||||
|
timestamp: Date,
|
||||||
|
component: GadgetComponent,
|
||||||
|
level: GadgetLogLevel,
|
||||||
|
message: string,
|
||||||
|
metadata?: unknown,
|
||||||
|
) => void;
|
||||||
|
```
|
||||||
|
|
||||||
1. Implement [GadgetLogTransportSocket](../packages/api/src/lib/log-transport-socket.ts)
|
`writeLog()` calls `this.emit("log", ...)` with the same parameters `GadgetLog.writeLog()` receives.
|
||||||
2. Register it for use as a default transport on the gadget-drone logger instance
|
|
||||||
3. Implement session message routing in gadget-code:backend to forward the message to gadget-code:frontend (the IDE)
|
|
||||||
4. Deliver log messages to the Chat Session view's Log panel for display
|
|
||||||
|
|
||||||
Log messages should render as closely to the Console Transport's output as possible, matching the colors used, and style.
|
## Registration in gadget-drone
|
||||||
|
|
||||||
When you are done, you will re-write this document in the style
|
Registered in `gadget-drone.ts` `connectSocket()`, after the Socket.IO connection succeeds:
|
||||||
|
|
||||||
## References
|
```typescript
|
||||||
|
const socketTransport = new GadgetLogTransportSocket((event, ...args) => {
|
||||||
|
(this.socket as any)?.emit(event, ...args);
|
||||||
|
});
|
||||||
|
GadgetLog.addDefaultTransport(socketTransport);
|
||||||
|
this.log.transports.push(socketTransport);
|
||||||
|
AgentService.log.transports.push(socketTransport);
|
||||||
|
AiService.log.transports.push(socketTransport);
|
||||||
|
PlatformService.log.transports.push(socketTransport);
|
||||||
|
```
|
||||||
|
|
||||||
Always search first in the project's `docs` directories for information and knowledge. Here are some starting points for this session:
|
The transport is injected into all existing loggers (main process + 3 services) and set as a default for future `GadgetLog` instances.
|
||||||
|
|
||||||
- [UI Design and Style Guide](../gadget-code/docs/ui-design-guide.md)]
|
## Backend Routing
|
||||||
- [System Architecture](./architecture.md)
|
|
||||||
- [Socket Protocol](./socket-protocol.md)
|
### DroneSession (`gadget-code/src/lib/drone-session.ts`)
|
||||||
|
|
||||||
|
- Registers `this.socket.on("log", this.onLog.bind(this))`
|
||||||
|
- `onLog()` guards on `this.chatSessionId`, looks up the `CodeSession` via `SocketService.getCodeSessionByChatSessionId()`, forwards the call.
|
||||||
|
|
||||||
|
### CodeSession (`gadget-code/src/lib/code-session.ts`)
|
||||||
|
|
||||||
|
- `onLog()` calls `this.socket.emit("log", timestamp, component, level, message, metadata)` forwards to the IDE.
|
||||||
|
|
||||||
|
## Frontend Bridging
|
||||||
|
|
||||||
|
### SocketClient (`gadget-code/frontend/src/lib/socket.ts`)
|
||||||
|
|
||||||
|
The raw Socket.IO `log` event is forwarded to the internal `log:entry` event system:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
this.socket.on("log", (timestamp, component, level, message, metadata) => {
|
||||||
|
this.emit("log:entry", {
|
||||||
|
level,
|
||||||
|
component: component.name,
|
||||||
|
message,
|
||||||
|
metadata,
|
||||||
|
timestamp: new Date(timestamp).getTime(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### ChatSessionView
|
||||||
|
|
||||||
|
`handleLogEntry` callback builds a `LogEntry` with all fields and appends to the `logs` state array.
|
||||||
|
|
||||||
|
## LogPanel Display
|
||||||
|
|
||||||
|
**File:** `gadget-code/frontend/src/components/LogPanel.tsx`
|
||||||
|
|
||||||
|
Styled to match the Console Transport:
|
||||||
|
|
||||||
|
| Element | Console Color | Tailwind Class |
|
||||||
|
|---------|---------------|----------------|
|
||||||
|
| Timestamp | `darkGray` | `text-gray-600` |
|
||||||
|
| Level: debug | `darkGray` | `text-gray-500` |
|
||||||
|
| Level: info | `green` | `text-green-400` |
|
||||||
|
| Level: warn | `yellow` | `text-yellow-400` |
|
||||||
|
| Level: alert | `red` | `text-red-400` |
|
||||||
|
| Level: error | `bgRed` + `white` | `bg-red-800 text-white` |
|
||||||
|
| Level: crit | `bgRed` + `yellow` | `bg-red-800 text-yellow-300` |
|
||||||
|
| Level: fatal | `bgRed` + `darkGray` | `bg-red-800 text-gray-400` |
|
||||||
|
| Component | `cyan` | `text-cyan-400` |
|
||||||
|
| Message | `darkGray` | `text-text-secondary` |
|
||||||
|
|
||||||
|
Metadata is displayed as expandable JSON below the log line (toggled with `⊞`/`⊟`).
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Action |
|
||||||
|
|------|--------|
|
||||||
|
| `packages/api/src/lib/log-transport-socket.ts` | **NEW** — socket transport |
|
||||||
|
| `packages/api/src/index.ts` | **MODIFY** — export new transport |
|
||||||
|
| `gadget-drone/src/gadget-drone.ts` | **MODIFY** — register transport on connect |
|
||||||
|
| `gadget-drone/src/lib/service.ts` | **MODIFY** — `log` public for transport injection |
|
||||||
|
| `gadget-code/src/lib/drone-session.ts` | **MODIFY** — `onLog` handler |
|
||||||
|
| `gadget-code/src/lib/code-session.ts` | **MODIFY** — `onLog` forwarding |
|
||||||
|
| `gadget-code/frontend/src/lib/socket.ts` | **MODIFY** — bridge `log` → `log:entry` |
|
||||||
|
| `gadget-code/frontend/src/pages/ChatSessionView.tsx` | **MODIFY** — enriched `LogEntry` |
|
||||||
|
| `gadget-code/frontend/src/components/LogPanel.tsx` | **REWRITE** — console-styled display |
|
||||||
|
|||||||
@ -1,22 +1,44 @@
|
|||||||
import { useRef, useEffect } from 'react';
|
import { useRef, useEffect, useState } from 'react';
|
||||||
import { WorkspaceMode } from '../lib/types';
|
|
||||||
|
|
||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
id: string;
|
id: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
level: string;
|
level: string;
|
||||||
|
component: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
metadata?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LogPanelProps {
|
interface LogPanelProps {
|
||||||
logs: LogEntry[];
|
logs: LogEntry[];
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
workspaceMode: WorkspaceMode;
|
|
||||||
onToggleExpand: () => void;
|
onToggleExpand: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand }: LogPanelProps) {
|
const levelColors: Record<string, string> = {
|
||||||
|
debug: 'text-gray-500',
|
||||||
|
info: 'text-green-400',
|
||||||
|
warn: 'text-yellow-400',
|
||||||
|
alert: 'text-red-400',
|
||||||
|
error: 'bg-red-800 text-white',
|
||||||
|
crit: 'bg-red-800 text-yellow-300',
|
||||||
|
fatal: 'bg-red-800 text-gray-400',
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatTimestamp(date: Date): string {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hh = String(date.getHours()).padStart(2, '0');
|
||||||
|
const mm = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const ss = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||||
|
return `${y}-${m}-${d} ${hh}:${mm}:${ss}.${ms}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LogPanel({ logs, expanded, onToggleExpand }: LogPanelProps) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [expandedMetadata, setExpandedMetadata] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
@ -24,6 +46,18 @@ export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand
|
|||||||
}
|
}
|
||||||
}, [logs]);
|
}, [logs]);
|
||||||
|
|
||||||
|
const toggleMetadata = (id: string) => {
|
||||||
|
setExpandedMetadata(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) {
|
||||||
|
next.delete(id);
|
||||||
|
} else {
|
||||||
|
next.add(id);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`border-t border-border-subtle bg-bg-secondary flex flex-col ${
|
className={`border-t border-border-subtle bg-bg-secondary flex flex-col ${
|
||||||
@ -44,32 +78,46 @@ export default function LogPanel({ logs, expanded, workspaceMode, onToggleExpand
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="flex-1 overflow-y-auto p-2 font-mono text-xs"
|
className="flex-1 overflow-y-auto p-2 font-mono text-xs leading-relaxed"
|
||||||
>
|
>
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<div className="text-text-muted">No log entries</div>
|
<div className="text-text-muted">No log entries</div>
|
||||||
) : (
|
) : (
|
||||||
logs.map((entry) => (
|
logs.map((entry) => {
|
||||||
<div key={entry.id} className="flex gap-2 py-0.5">
|
const levelClass = levelColors[entry.level] || 'text-text-secondary';
|
||||||
<span className="text-text-muted shrink-0">
|
return (
|
||||||
{entry.timestamp.toLocaleTimeString()}
|
<div key={entry.id} className="flex gap-2 py-0.5 hover:bg-bg-elevated/50">
|
||||||
|
<span className="text-gray-600 shrink-0 select-none">
|
||||||
|
{formatTimestamp(entry.timestamp)}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span className={`shrink-0 font-bold ${levelClass}`}>
|
||||||
className={`shrink-0 ${
|
{entry.level.toUpperCase().padEnd(5)}
|
||||||
entry.level === 'error'
|
</span>
|
||||||
? 'text-red-500'
|
<span className="text-cyan-400 shrink-0">
|
||||||
: entry.level === 'warn'
|
{entry.component}
|
||||||
? 'text-yellow-500'
|
</span>
|
||||||
: entry.level === 'info'
|
<span className="text-text-secondary break-all min-w-0">
|
||||||
? 'text-blue-400'
|
{entry.message}
|
||||||
: 'text-text-secondary'
|
</span>
|
||||||
}`}
|
{entry.metadata && (
|
||||||
|
<button
|
||||||
|
onClick={() => toggleMetadata(entry.id)}
|
||||||
|
className="shrink-0 text-text-muted hover:text-text-secondary transition-colors"
|
||||||
|
title={expandedMetadata.has(entry.id) ? 'Hide metadata' : 'Show metadata'}
|
||||||
>
|
>
|
||||||
[{entry.level.toUpperCase()}]
|
{expandedMetadata.has(entry.id) ? '⊟' : '⊞'}
|
||||||
</span>
|
</button>
|
||||||
<span className="text-text-primary">{entry.message}</span>
|
)}
|
||||||
|
{entry.metadata && expandedMetadata.has(entry.id) && (
|
||||||
|
<div className="w-full text-text-muted pl-[1em] border-l border-border-subtle mt-0.5 mb-1">
|
||||||
|
<pre className="whitespace-pre-wrap break-all">
|
||||||
|
{JSON.stringify(entry.metadata, null, 2)}
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
))
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,6 +17,13 @@ export interface ServerToClientEvents {
|
|||||||
success: boolean,
|
success: boolean,
|
||||||
message?: string,
|
message?: string,
|
||||||
) => void;
|
) => void;
|
||||||
|
log: (
|
||||||
|
timestamp: string,
|
||||||
|
component: { name: string; slug: string },
|
||||||
|
level: string,
|
||||||
|
message: string,
|
||||||
|
metadata?: unknown,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientToServerEvents {
|
export interface ClientToServerEvents {
|
||||||
@ -72,7 +79,9 @@ export interface SocketEvents {
|
|||||||
"agent:complete": (data: { agentId: string }) => void;
|
"agent:complete": (data: { agentId: string }) => void;
|
||||||
"log:entry": (data: {
|
"log:entry": (data: {
|
||||||
level: string;
|
level: string;
|
||||||
|
component: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
metadata?: unknown;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}) => void;
|
}) => void;
|
||||||
"chat:message": (data: {
|
"chat:message": (data: {
|
||||||
@ -152,6 +161,25 @@ class SocketClient {
|
|||||||
this.emit("workspaceModeChanged", mode);
|
this.emit("workspaceModeChanged", mode);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.socket.on(
|
||||||
|
"log",
|
||||||
|
(
|
||||||
|
timestamp: string,
|
||||||
|
component: { name: string; slug: string },
|
||||||
|
level: string,
|
||||||
|
message: string,
|
||||||
|
metadata?: unknown,
|
||||||
|
) => {
|
||||||
|
this.emit("log:entry", {
|
||||||
|
level,
|
||||||
|
component: component.name,
|
||||||
|
message,
|
||||||
|
metadata,
|
||||||
|
timestamp: new Date(timestamp).getTime(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this._socket.on("connect", () => {
|
this._socket.on("connect", () => {
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.emit("connect");
|
this.emit("connect");
|
||||||
@ -297,7 +325,7 @@ class SocketClient {
|
|||||||
startSessionHeartbeat(): void {
|
startSessionHeartbeat(): void {
|
||||||
if (this.heartbeatInterval) return;
|
if (this.heartbeatInterval) return;
|
||||||
this.heartbeatInterval = setInterval(() => {
|
this.heartbeatInterval = setInterval(() => {
|
||||||
if (this._socket?.connected) {
|
if (this._socket) {
|
||||||
this._socket.emit("sessionHeartbeat", (ack: boolean) => {
|
this._socket.emit("sessionHeartbeat", (ack: boolean) => {
|
||||||
if (!ack) {
|
if (!ack) {
|
||||||
console.warn("sessionHeartbeat: drone did not acknowledge");
|
console.warn("sessionHeartbeat: drone did not acknowledge");
|
||||||
|
|||||||
@ -13,7 +13,9 @@ interface LogEntry {
|
|||||||
id: string;
|
id: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
level: string;
|
level: string;
|
||||||
|
component: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
metadata?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StreamingState {
|
interface StreamingState {
|
||||||
@ -107,6 +109,33 @@ export default function ChatSessionView() {
|
|||||||
};
|
};
|
||||||
}, [session, project]);
|
}, [session, project]);
|
||||||
|
|
||||||
|
// Re-lock on socket reconnect to restore lock on a new CodeSession
|
||||||
|
const handleSocketReconnect = useCallback(async () => {
|
||||||
|
if (!sessionRef.current || !projectRef.current) return;
|
||||||
|
const droneJson = localStorage.getItem('dtp_drone_registration');
|
||||||
|
if (!droneJson) return;
|
||||||
|
try {
|
||||||
|
const registration = JSON.parse(droneJson);
|
||||||
|
const success = await socketClient.requestSessionLock(
|
||||||
|
registration,
|
||||||
|
projectRef.current,
|
||||||
|
sessionRef.current,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
console.warn('Failed to re-lock drone after socket reconnect');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to re-lock drone after socket reconnect', err);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socketClient.on('connect', handleSocketReconnect);
|
||||||
|
return () => {
|
||||||
|
socketClient.off('connect', handleSocketReconnect);
|
||||||
|
};
|
||||||
|
}, [handleSocketReconnect]);
|
||||||
|
|
||||||
// Release session lock on unmount only
|
// Release session lock on unmount only
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -409,14 +438,16 @@ export default function ChatSessionView() {
|
|||||||
setWorkspaceMode(mode as WorkspaceMode);
|
setWorkspaceMode(mode as WorkspaceMode);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogEntry = useCallback((data: { level: string; message: string; timestamp: number }) => {
|
const handleLogEntry = useCallback((data: { level: string; component: string; message: string; metadata?: unknown; timestamp: number }) => {
|
||||||
setLogs(prev => [
|
setLogs(prev => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
id: `log-${Date.now()}-${Math.random()}`,
|
id: `log-${Date.now()}-${Math.random()}`,
|
||||||
timestamp: new Date(data.timestamp),
|
timestamp: new Date(data.timestamp),
|
||||||
level: data.level,
|
level: data.level,
|
||||||
|
component: data.component,
|
||||||
message: data.message,
|
message: data.message,
|
||||||
|
metadata: data.metadata,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}, []);
|
}, []);
|
||||||
@ -756,7 +787,6 @@ export default function ChatSessionView() {
|
|||||||
<LogPanel
|
<LogPanel
|
||||||
logs={logs}
|
logs={logs}
|
||||||
expanded={logExpanded}
|
expanded={logExpanded}
|
||||||
workspaceMode={workspaceMode}
|
|
||||||
onToggleExpand={() => setLogExpanded(!logExpanded)}
|
onToggleExpand={() => setLogExpanded(!logExpanded)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
SocketSessionType,
|
SocketSessionType,
|
||||||
} from "./socket-session.js";
|
} from "./socket-session.js";
|
||||||
import {
|
import {
|
||||||
|
GadgetComponent,
|
||||||
|
GadgetLogLevel,
|
||||||
IChatSession,
|
IChatSession,
|
||||||
IDroneRegistration,
|
IDroneRegistration,
|
||||||
IProject,
|
IProject,
|
||||||
@ -43,10 +45,7 @@ export class CodeSession extends SocketSession {
|
|||||||
this.onRequestWorkspaceMode.bind(this),
|
this.onRequestWorkspaceMode.bind(this),
|
||||||
);
|
);
|
||||||
this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this));
|
this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this));
|
||||||
this.socket.on(
|
this.socket.on("releaseSessionLock", this.onReleaseSessionLock.bind(this));
|
||||||
"releaseSessionLock",
|
|
||||||
this.onReleaseSessionLock.bind(this),
|
|
||||||
);
|
|
||||||
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this));
|
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +95,7 @@ export class CodeSession extends SocketSession {
|
|||||||
chatSession: IChatSession,
|
chatSession: IChatSession,
|
||||||
cb: (success: boolean, chatSessionId: string) => void,
|
cb: (success: boolean, chatSessionId: string) => void,
|
||||||
) {
|
) {
|
||||||
|
try {
|
||||||
const droneSession = SocketService.getDroneSession(registration);
|
const droneSession = SocketService.getDroneSession(registration);
|
||||||
droneSession.socket.emit(
|
droneSession.socket.emit(
|
||||||
"requestSessionLock",
|
"requestSessionLock",
|
||||||
@ -113,6 +113,9 @@ export class CodeSession extends SocketSession {
|
|||||||
cb(success, chatSessionId);
|
cb(success, chatSessionId);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
cb(false, "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -136,6 +139,7 @@ export class CodeSession extends SocketSession {
|
|||||||
return cb(false, this.workspaceMode);
|
return cb(false, this.workspaceMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
||||||
droneSession.socket.emit(
|
droneSession.socket.emit(
|
||||||
"requestWorkspaceMode",
|
"requestWorkspaceMode",
|
||||||
@ -150,6 +154,9 @@ export class CodeSession extends SocketSession {
|
|||||||
cb(success, currentMode);
|
cb(success, currentMode);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
cb(false, this.workspaceMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -173,6 +180,7 @@ export class CodeSession extends SocketSession {
|
|||||||
return cb(false, { message: "No project selected" });
|
return cb(false, { message: "No project selected" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
||||||
|
|
||||||
let turn: ChatTurnDocument = await ChatSessionService.createTurn(
|
let turn: ChatTurnDocument = await ChatSessionService.createTurn(
|
||||||
@ -210,6 +218,9 @@ export class CodeSession extends SocketSession {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
cb(false, {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -222,6 +233,7 @@ export class CodeSession extends SocketSession {
|
|||||||
chatSession: IChatSession,
|
chatSession: IChatSession,
|
||||||
cb: (success: boolean) => void,
|
cb: (success: boolean) => void,
|
||||||
) {
|
) {
|
||||||
|
try {
|
||||||
const droneSession = SocketService.getDroneSession(registration);
|
const droneSession = SocketService.getDroneSession(registration);
|
||||||
droneSession.socket.emit(
|
droneSession.socket.emit(
|
||||||
"releaseSessionLock",
|
"releaseSessionLock",
|
||||||
@ -239,6 +251,9 @@ export class CodeSession extends SocketSession {
|
|||||||
cb(success);
|
cb(success);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
cb(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -249,10 +264,17 @@ export class CodeSession extends SocketSession {
|
|||||||
if (!this.selectedDrone) {
|
if (!this.selectedDrone) {
|
||||||
return cb(false);
|
return cb(false);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
||||||
droneSession.socket.emit("sessionHeartbeat", (ack: boolean) => {
|
droneSession.socket.emit("sessionHeartbeat", (ack: boolean) => {
|
||||||
cb(ack);
|
cb(ack);
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error("failed to forward session heartbeat to drone", {
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
cb(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -268,6 +290,16 @@ export class CodeSession extends SocketSession {
|
|||||||
this.socket.emit("status", content);
|
this.socket.emit("status", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLog(
|
||||||
|
timestamp: Date,
|
||||||
|
component: GadgetComponent,
|
||||||
|
level: GadgetLogLevel,
|
||||||
|
message: string,
|
||||||
|
metadata?: unknown,
|
||||||
|
): void {
|
||||||
|
this.socket.emit("log", timestamp, component, level, message, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
onThinking(content: string): void {
|
onThinking(content: string): void {
|
||||||
this.socket.emit("thinking", content);
|
this.socket.emit("thinking", content);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
// All Rights Reserved
|
// All Rights Reserved
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
GadgetComponent,
|
||||||
|
GadgetLogLevel,
|
||||||
IUser,
|
IUser,
|
||||||
IDroneRegistration,
|
IDroneRegistration,
|
||||||
ChatTurnStatus,
|
ChatTurnStatus,
|
||||||
@ -58,6 +60,28 @@ export class DroneSession extends SocketSession {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
|
this.socket.on("requestTermination", this.onRequestTermination.bind(this));
|
||||||
|
|
||||||
|
this.socket.on("log", this.onLog.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLog(
|
||||||
|
timestamp: Date,
|
||||||
|
component: GadgetComponent,
|
||||||
|
level: GadgetLogLevel,
|
||||||
|
message: string,
|
||||||
|
metadata?: unknown,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.chatSessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const codeSession = SocketService.getCodeSessionByChatSessionId(
|
||||||
|
this.chatSessionId,
|
||||||
|
);
|
||||||
|
codeSession.onLog(timestamp, component, level, message, metadata);
|
||||||
|
} catch (error) {
|
||||||
|
this.log.error("failed to route log message", { error });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onStatus(message: string): Promise<void> {
|
async onStatus(message: string): Promise<void> {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import AgentService, { IAgentWorkOrder } from "./services/agent.ts";
|
|||||||
import AiService from "./services/ai.ts";
|
import AiService from "./services/ai.ts";
|
||||||
import PlatformService from "./services/platform.ts";
|
import PlatformService from "./services/platform.ts";
|
||||||
import WorkspaceService from "./services/workspace.ts";
|
import WorkspaceService from "./services/workspace.ts";
|
||||||
import { DroneStatus, IUser } from "@gadget/api";
|
import { DroneStatus, GadgetLog, GadgetLogTransportSocket, IUser } from "@gadget/api";
|
||||||
|
|
||||||
import { GadgetProcess } from "./lib/process.ts";
|
import { GadgetProcess } from "./lib/process.ts";
|
||||||
import {
|
import {
|
||||||
@ -207,6 +207,18 @@ class GadgetDrone extends GadgetProcess {
|
|||||||
});
|
});
|
||||||
this.socket.on("connect", () => {
|
this.socket.on("connect", () => {
|
||||||
this.log.info("connected to Gadget Code platform.");
|
this.log.info("connected to Gadget Code platform.");
|
||||||
|
|
||||||
|
const socketTransport = new GadgetLogTransportSocket(
|
||||||
|
(event, timestamp, component, level, message, metadata): void => {
|
||||||
|
(this.socket as any)?.emit(event, timestamp, component, level, message, metadata);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
GadgetLog.addDefaultTransport(socketTransport);
|
||||||
|
this.log.transports.push(socketTransport);
|
||||||
|
AgentService.log.transports.push(socketTransport);
|
||||||
|
AiService.log.transports.push(socketTransport);
|
||||||
|
PlatformService.log.transports.push(socketTransport);
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
import { GadgetComponent, GadgetLog } from "@gadget/api";
|
import { GadgetComponent, GadgetLog } from "@gadget/api";
|
||||||
|
|
||||||
export abstract class GadgetService implements GadgetComponent {
|
export abstract class GadgetService implements GadgetComponent {
|
||||||
protected log: GadgetLog;
|
public log: GadgetLog;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.log = new GadgetLog(this);
|
this.log = new GadgetLog(this);
|
||||||
|
|||||||
@ -90,7 +90,7 @@ class AgentService extends GadgetService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onStreamChunk = async (chunk: IAiStreamChunk): Promise<void> => {
|
const onStreamChunk = async (chunk: IAiStreamChunk): Promise<void> => {
|
||||||
this.log.debug("stream chunk received", { chunk });
|
// this.log.debug("stream chunk received", { chunk });
|
||||||
|
|
||||||
switch (chunk.type) {
|
switch (chunk.type) {
|
||||||
case "thinking":
|
case "thinking":
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export * from "./lib/log.ts";
|
|||||||
export * from "./lib/log-transport.ts";
|
export * from "./lib/log-transport.ts";
|
||||||
export * from "./lib/log-transport-console.ts";
|
export * from "./lib/log-transport-console.ts";
|
||||||
export * from "./lib/log-transport-file.ts";
|
export * from "./lib/log-transport-file.ts";
|
||||||
|
export * from "./lib/log-transport-socket.ts";
|
||||||
export * from "./lib/log-file.ts";
|
export * from "./lib/log-file.ts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
30
packages/api/src/lib/log-transport-socket.ts
Normal file
30
packages/api/src/lib/log-transport-socket.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { GadgetComponent } from "./component.ts";
|
||||||
|
import { GadgetLogLevel } from "./log.ts";
|
||||||
|
import { GadgetLogTransport } from "./log-transport.ts";
|
||||||
|
|
||||||
|
export type LogEmitFunction = (
|
||||||
|
event: string,
|
||||||
|
timestamp: Date,
|
||||||
|
component: GadgetComponent,
|
||||||
|
level: GadgetLogLevel,
|
||||||
|
message: string,
|
||||||
|
metadata?: unknown,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export class GadgetLogTransportSocket implements GadgetLogTransport {
|
||||||
|
private emit: LogEmitFunction;
|
||||||
|
|
||||||
|
constructor(emit: LogEmitFunction) {
|
||||||
|
this.emit = emit;
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeLog(
|
||||||
|
timestamp: Date,
|
||||||
|
component: GadgetComponent,
|
||||||
|
level: GadgetLogLevel,
|
||||||
|
message: string,
|
||||||
|
metadata?: unknown,
|
||||||
|
): Promise<void> {
|
||||||
|
this.emit("log", timestamp, component, level, message, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user