chat session auto-naming with IDE update

This commit is contained in:
Rob Colbert 2026-05-09 09:58:47 -04:00
parent 4b33915c7d
commit d26624ab93
11 changed files with 292 additions and 121 deletions

View File

@ -34,6 +34,9 @@ Defined in `packages/api/src/messages/socket.ts`.
* `crashRecoveryResponse`: Command to `discard` or `retry` a stalled work order. * `crashRecoveryResponse`: Command to `discard` or `retry` a stalled work order.
* `requestTermination`: Command to immediately terminate the drone process. * `requestTermination`: Command to immediately terminate the drone process.
### Web -> IDE (Server to Client)
* `sessionUpdated`: Notify the IDE that a chat session property has changed (e.g. auto-generated name).
--- ---
## 3. Core Sequences & Routing ## 3. Core Sequences & Routing
@ -42,8 +45,12 @@ Defined in `packages/api/src/messages/socket.ts`.
1. **IDE** emits `submitPrompt(content)`. 1. **IDE** emits `submitPrompt(content)`.
2. **Web (`CodeSession.ts`)**: 2. **Web (`CodeSession.ts`)**:
* Creates a `ChatTurn` document (status: `processing`). * Creates a `ChatTurn` document (status: `processing`).
* Increments the chat session's `stats.turnCount`.
* Finds the target `DroneSession`. * Finds the target `DroneSession`.
* Caches the updated session and signals the **IDE** to enter Processing state.
* Emits `processWorkOrder` to the **Drone**. * Emits `processWorkOrder` to the **Drone**.
* On first prompt (name is still the default), calls AI API to auto-generate session name.
* Emits `sessionUpdated({ name })` to **IDE** if the name changed.
3. **Drone (`gadget-drone.ts`)**: 3. **Drone (`gadget-drone.ts`)**:
* Writes a local `.gadget/work-order.json` cache (for crash recovery). * Writes a local `.gadget/work-order.json` cache (for crash recovery).
* Calls `AgentService.process()`. * Calls `AgentService.process()`.
@ -117,6 +124,13 @@ type RequestTerminationMessage = (
) => void; ) => void;
``` ```
### Web -> IDE
```typescript
type SessionUpdatedMessage = (
updates: Partial<IChatSession>
) => void;
```
### Drone -> Web (Streaming) ### Drone -> Web (Streaming)
```typescript ```typescript
type ThinkingMessage = (content: string) => void; type ThinkingMessage = (content: string) => void;
@ -183,8 +197,9 @@ All indexes are kept in sync during connection and disconnection.
## 7. Extending the Protocol ## 7. Extending the Protocol
To add a new message: To add a new message:
1. Add the message type to `packages/api/src/messages/ide.ts` or `drone.ts`. 1. Add the message type to `packages/api/src/messages/ide.ts`, `drone.ts`, or `web.ts`.
2. Register it in `ClientToServerEvents` or `ServerToClientEvents` in `packages/api/src/messages/socket.ts`. 2. Register it in `ClientToServerEvents` or `ServerToClientEvents` in `packages/api/src/messages/socket.ts`.
3. Implement the sender (emit) in the Client (`ide` or `drone`). 3. Re-export from `packages/api/src/index.ts`.
4. Implement the handler in the corresponding `CodeSession` or `DroneSession` on the Web server. 4. Implement the sender (emit) in the Client (`ide` or `drone`) or Server (`CodeSession`/`DroneSession`).
5. Implement the forward-path routing if needed. 5. Implement the handler in the corresponding class or frontend component.
6. Implement the forward-path routing if needed.

View File

@ -1,5 +1,6 @@
import { createContext } from "react"; import { createContext } from "react";
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import type { ChatSession } from "./api";
const SOCKET_URL = ""; const SOCKET_URL = "";
@ -90,6 +91,7 @@ export interface SocketEvents {
role: "user" | "assistant" | "system"; role: "user" | "assistant" | "system";
}) => void; }) => void;
workspaceModeChanged: (mode: string) => void; workspaceModeChanged: (mode: string) => void;
sessionUpdated: (updates: Partial<ChatSession>) => void;
connect: () => void; connect: () => void;
disconnect: (reason: string) => void; disconnect: (reason: string) => void;
error: (error: Error) => void; error: (error: Error) => void;
@ -161,6 +163,10 @@ class SocketClient {
this.emit("workspaceModeChanged", mode); this.emit("workspaceModeChanged", mode);
}); });
this.socket.on("sessionUpdated", (updates: unknown) => {
this.emit("sessionUpdated", updates as Partial<ChatSession>);
});
this.socket.on( this.socket.on(
"log", "log",
( (

View File

@ -189,12 +189,17 @@ export default function ChatSessionView() {
} }
}; };
const handleSessionUpdated = useCallback((updates: Partial<ChatSession>) => {
setSession(prev => prev ? { ...prev, ...updates } : null);
}, []);
const setupSocketListeners = () => { const setupSocketListeners = () => {
socketClient.on('thinking', handleThinking); socketClient.on('thinking', handleThinking);
socketClient.on('response', handleResponse); socketClient.on('response', handleResponse);
socketClient.on('toolCall', handleToolCall); socketClient.on('toolCall', handleToolCall);
socketClient.on('workOrderComplete', handleWorkOrderComplete); socketClient.on('workOrderComplete', handleWorkOrderComplete);
socketClient.on('workspaceModeChanged', handleWorkspaceModeChanged); socketClient.on('workspaceModeChanged', handleWorkspaceModeChanged);
socketClient.on('sessionUpdated', handleSessionUpdated);
socketClient.on('log:entry', handleLogEntry); socketClient.on('log:entry', handleLogEntry);
socketClient.on('status', handleStatus); socketClient.on('status', handleStatus);
}; };
@ -205,6 +210,7 @@ export default function ChatSessionView() {
socketClient.off('toolCall', handleToolCall); socketClient.off('toolCall', handleToolCall);
socketClient.off('workOrderComplete', handleWorkOrderComplete); socketClient.off('workOrderComplete', handleWorkOrderComplete);
socketClient.off('workspaceModeChanged', handleWorkspaceModeChanged); socketClient.off('workspaceModeChanged', handleWorkspaceModeChanged);
socketClient.off('sessionUpdated', handleSessionUpdated);
socketClient.off('log:entry', handleLogEntry); socketClient.off('log:entry', handleLogEntry);
socketClient.off('status', handleStatus); socketClient.off('status', handleStatus);
}; };

View File

@ -253,13 +253,23 @@ export class CodeSession extends SocketSession {
); );
/* /*
* Call out to have the session's name auto-generated from this prompt * Auto-generate a session name from the first prompt. Only do this when
* if this is the first prompt. * the name is still the default (user hasn't set a custom name) and
* we're on the first turn (turnCount === 1 after the increment above).
*/ */
this.chatSession = await ChatSessionService.generateSessionNameFromPrompt( if (
this.chatSession.name === "New Chat Session" &&
this.chatSession.stats.turnCount === 1
) {
this.chatSession =
await ChatSessionService.generateSessionNameFromPrompt(
this.chatSession, this.chatSession,
content, content,
); );
const update: Partial<IChatSession> = { name: this.chatSession.name };
this.log.debug("emitting sessionUpdated message", { update });
this.socket.emit("sessionUpdated", update);
}
} catch (error) { } catch (error) {
this.log.error("prompt rejected", { error }); this.log.error("prompt rejected", { error });
cb(false, {}); cb(false, {});

View File

@ -21,12 +21,12 @@ import {
import { IAiProvider } from "@gadget/api"; import { IAiProvider } from "@gadget/api";
import Project from "../models/project.js"; import Project from "../models/project.ts";
import ChatTurn from "../models/chat-turn.js"; import ChatTurn from "../models/chat-turn.ts";
import ChatSession from "../models/chat-session.js"; import ChatSession from "../models/chat-session.ts";
import AiProvider from "../models/ai-provider.js"; import AiProvider from "../models/ai-provider.ts";
import { DtpService } from "../lib/service.js"; import { DtpService } from "../lib/service.ts";
import { PopulateOptions } from "mongoose"; import { PopulateOptions } from "mongoose";
import { import {
AiApi, AiApi,
@ -425,7 +425,7 @@ class ChatSessionService extends DtpService {
const newSession = await ChatSession.findOneAndUpdate( const newSession = await ChatSession.findOneAndUpdate(
{ _id: session._id }, { _id: session._id },
{ $set: { name: response.response || "New Session" } }, { $set: { name: response.response || "New Session" } },
{ new: true, populate: this.populateChatSession, lean: true }, { new: true, populate: this.populateChatSession },
); );
if (!newSession) { if (!newSession) {
const error = new Error("chat session has been removed"); const error = new Error("chat session has been removed");
@ -433,10 +433,6 @@ class ChatSessionService extends DtpService {
throw error; throw error;
} }
//TODO: emit the `sessionUpdated` message to the CodeSession in the IDE
// for this chat session, letting it know the name of the chat session has
// changed. The IDE will then update it's displays and state for the User.
return newSession; return newSession;
} }

View File

@ -9,12 +9,16 @@ import {
ChatTurnStatus, ChatTurnStatus,
} from "@gadget/api"; } from "@gadget/api";
import SocketService from "../src/services/socket"; import SocketService from "../src/services/socket";
import ChatSessionService from "../src/services/chat-session";
import { ChatTurn } from "../src/models/chat-turn"; import { ChatTurn } from "../src/models/chat-turn";
import ChatSession from "../src/models/chat-session";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
// Mock dependencies // Mock dependencies
vi.mock("../src/services/socket"); vi.mock("../src/services/socket");
vi.mock("../src/services/chat-session");
vi.mock("../src/models/chat-turn"); vi.mock("../src/models/chat-turn");
vi.mock("../src/models/chat-session");
describe("CodeSession", () => { describe("CodeSession", () => {
let mockSocket: any; let mockSocket: any;
@ -54,12 +58,18 @@ describe("CodeSession", () => {
mockChatSession = { mockChatSession = {
_id: nanoid(), _id: nanoid(),
name: "Test Session", name: "New Chat Session",
mode: "build", mode: "build",
provider: nanoid(), provider: nanoid(),
selectedModel: "llama3.1", selectedModel: "llama3.1",
user: mockUser, user: mockUser,
project: mockProject, project: mockProject,
stats: {
turnCount: 0,
toolCallCount: 0,
inputTokens: 0,
outputTokens: 0,
},
} as IChatSession; } as IChatSession;
codeSession = new CodeSession(mockSocket, mockUser); codeSession = new CodeSession(mockSocket, mockUser);
@ -68,7 +78,6 @@ describe("CodeSession", () => {
describe("setSelectedDrone", () => { describe("setSelectedDrone", () => {
it("should set the selected drone", () => { it("should set the selected drone", () => {
codeSession.setSelectedDrone(mockDrone); codeSession.setSelectedDrone(mockDrone);
// Can't directly access private property, but we can verify through behavior
expect(() => codeSession.setSelectedDrone(mockDrone)).not.toThrow(); expect(() => codeSession.setSelectedDrone(mockDrone)).not.toThrow();
}); });
}); });
@ -83,91 +92,12 @@ describe("CodeSession", () => {
}); });
describe("onSubmitPrompt", () => { describe("onSubmitPrompt", () => {
it("should return error if no drone is selected", async () => { let mockTurn: any;
codeSession.setChatSession(mockChatSession, mockProject); let mockDroneSession: any;
const cb = vi.fn(); let cb: any;
await codeSession.onSubmitPrompt("test prompt", cb); beforeEach(() => {
mockTurn = {
expect(cb).toHaveBeenCalledWith(false, { message: "No drone selected" });
});
it("should return error if no chat session is active", async () => {
codeSession.setSelectedDrone(mockDrone);
const cb = vi.fn();
await codeSession.onSubmitPrompt("test prompt", cb);
expect(cb).toHaveBeenCalledWith(false, { message: "No chat session active" });
});
it("should return error if no project is selected", async () => {
codeSession.setSelectedDrone(mockDrone);
codeSession.setChatSession(mockChatSession, undefined as any);
const cb = vi.fn();
await codeSession.onSubmitPrompt("test prompt", cb);
expect(cb).toHaveBeenCalledWith(false, { message: "No project selected" });
});
it("should create a ChatTurn and emit processWorkOrder to drone", async () => {
codeSession.setSelectedDrone(mockDrone);
codeSession.setChatSession(mockChatSession, mockProject);
const mockTurn = {
_id: nanoid(),
save: vi.fn().mockResolvedValue(undefined),
};
vi.mocked(ChatTurn).mockImplementation(function () {
return mockTurn as any;
});
(vi.mocked(ChatTurn) as any).populate = vi
.fn()
.mockResolvedValue(mockTurn);
const mockDroneSession = {
socket: {
emit: vi.fn(),
},
setChatSessionId: vi.fn(),
setCurrentTurnId: vi.fn(),
};
vi.mocked(SocketService.getDroneSession).mockReturnValue(
mockDroneSession as any,
);
const cb = vi.fn();
await codeSession.onSubmitPrompt("test prompt", cb);
expect(ChatTurn).toHaveBeenCalledWith(
expect.objectContaining({
user: mockUser._id,
project: mockProject._id,
session: mockChatSession._id,
provider: mockChatSession.provider,
llm: mockChatSession.selectedModel,
status: ChatTurnStatus.Processing,
prompts: expect.objectContaining({
user: "test prompt",
}),
}),
);
expect(mockTurn.save).toHaveBeenCalled();
expect(mockDroneSession.socket.emit).toHaveBeenCalledWith(
"processWorkOrder",
mockDrone,
mockTurn,
expect.any(Function),
);
});
it("should update ChatTurn to Error status if drone rejects work order", async () => {
codeSession.setSelectedDrone(mockDrone);
codeSession.setChatSession(mockChatSession, mockProject);
const mockTurn = {
_id: nanoid(), _id: nanoid(),
status: ChatTurnStatus.Processing, status: ChatTurnStatus.Processing,
errorMessage: "", errorMessage: "",
@ -180,12 +110,9 @@ describe("CodeSession", () => {
.fn() .fn()
.mockResolvedValue(mockTurn); .mockResolvedValue(mockTurn);
const mockDroneSession = { mockDroneSession = {
socket: { socket: {
emit: vi.fn((event, ...args) => { emit: vi.fn(),
const callback = args[args.length - 1];
callback(false, "Drone is busy");
}),
}, },
setChatSessionId: vi.fn(), setChatSessionId: vi.fn(),
setCurrentTurnId: vi.fn(), setCurrentTurnId: vi.fn(),
@ -194,20 +121,189 @@ describe("CodeSession", () => {
mockDroneSession as any, mockDroneSession as any,
); );
const cb = vi.fn(); vi.mocked(ChatSessionService.createTurn).mockResolvedValue(
mockTurn as any,
);
cb = vi.fn();
codeSession.setSelectedDrone(mockDrone);
codeSession.setChatSession(mockChatSession, mockProject);
});
it("should return error if no drone is selected", async () => {
codeSession = new CodeSession(mockSocket, mockUser);
codeSession.setChatSession(mockChatSession, mockProject);
await codeSession.onSubmitPrompt("test prompt", cb);
expect(cb).toHaveBeenCalledWith(false, { message: "No drone selected" });
});
it("should return error if no chat session is active", async () => {
codeSession = new CodeSession(mockSocket, mockUser);
codeSession.setSelectedDrone(mockDrone);
await codeSession.onSubmitPrompt("test prompt", cb);
expect(cb).toHaveBeenCalledWith(false, {
message: "No chat session active",
});
});
it("should return error if no project is selected", async () => {
codeSession.setChatSession(mockChatSession, undefined as any);
await codeSession.onSubmitPrompt("test prompt", cb);
expect(cb).toHaveBeenCalledWith(false, { message: "No project selected" });
});
it("should create a ChatTurn, increment turnCount, and emit processWorkOrder", async () => {
const updatedSession = {
...mockChatSession,
stats: { ...mockChatSession.stats, turnCount: 1 },
};
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
updatedSession as any,
);
vi.mocked(
ChatSessionService.generateSessionNameFromPrompt,
).mockResolvedValue(updatedSession as any);
await codeSession.onSubmitPrompt("test prompt", cb);
expect(ChatSessionService.createTurn).toHaveBeenCalledWith(
mockChatSession,
"test prompt",
);
expect(ChatSession.findOneAndUpdate).toHaveBeenCalledWith(
{ _id: mockChatSession._id },
{ $inc: { "stats.turnCount": 1 } },
expect.objectContaining({ new: true }),
);
expect(mockDroneSession.socket.emit).toHaveBeenCalledWith(
"processWorkOrder",
mockDrone,
mockTurn,
expect.any(Function),
);
});
it("should update ChatTurn to Error status if drone rejects work order", async () => {
const updatedSession = {
...mockChatSession,
stats: { ...mockChatSession.stats, turnCount: 1 },
};
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
updatedSession as any,
);
mockDroneSession.socket.emit = vi.fn((event: string, ...args: any[]) => {
const callback = args[args.length - 1];
callback(false, "Drone is busy");
});
await codeSession.onSubmitPrompt("test prompt", cb); await codeSession.onSubmitPrompt("test prompt", cb);
expect(mockTurn.status).toBe(ChatTurnStatus.Error); expect(mockTurn.status).toBe(ChatTurnStatus.Error);
expect(mockTurn.errorMessage).toBe("Drone is busy"); expect(mockTurn.errorMessage).toBe("Drone is busy");
expect(mockTurn.save).toHaveBeenCalled(); expect(mockTurn.save).toHaveBeenCalled();
}); });
it("should reject prompt and delete turn if session was removed during increment", async () => {
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(null);
vi.mocked(ChatSessionService.delete).mockResolvedValue(undefined);
await codeSession.onSubmitPrompt("test prompt", cb);
expect(ChatSessionService.delete).toHaveBeenCalledWith(mockChatSession._id);
expect(cb).toHaveBeenCalledWith(false, {});
});
it("should auto-generate session name and emit sessionUpdated on first prompt with default name", async () => {
const updatedSession = {
...mockChatSession,
stats: { ...mockChatSession.stats, turnCount: 1 },
};
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
updatedSession as any,
);
const namedSession = {
...updatedSession,
name: "Build a REST API",
};
vi.mocked(
ChatSessionService.generateSessionNameFromPrompt,
).mockResolvedValue(namedSession as any);
await codeSession.onSubmitPrompt("build a rest api", cb);
expect(
ChatSessionService.generateSessionNameFromPrompt,
).toHaveBeenCalledWith(updatedSession, "build a rest api");
expect(mockSocket.emit).toHaveBeenCalledWith("sessionUpdated", {
name: "Build a REST API",
});
});
it("should NOT auto-generate name if session already has a custom name", async () => {
const customSession = {
...mockChatSession,
name: "My Custom Session",
};
codeSession.setChatSession(customSession, mockProject);
const updatedSession = {
...customSession,
stats: { ...customSession.stats, turnCount: 1 },
};
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
updatedSession as any,
);
await codeSession.onSubmitPrompt("test prompt", cb);
expect(
ChatSessionService.generateSessionNameFromPrompt,
).not.toHaveBeenCalled();
expect(mockSocket.emit).not.toHaveBeenCalledWith(
"sessionUpdated",
expect.anything(),
);
});
it("should NOT auto-generate name on subsequent prompts (turnCount > 1)", async () => {
const multiTurnSession = {
...mockChatSession,
stats: { ...mockChatSession.stats, turnCount: 5 },
};
codeSession.setChatSession(multiTurnSession, mockProject);
const updatedSession = {
...multiTurnSession,
stats: { ...multiTurnSession.stats, turnCount: 6 },
};
vi.mocked(ChatSession.findOneAndUpdate).mockResolvedValue(
updatedSession as any,
);
await codeSession.onSubmitPrompt("test prompt", cb);
expect(
ChatSessionService.generateSessionNameFromPrompt,
).not.toHaveBeenCalled();
expect(mockSocket.emit).not.toHaveBeenCalledWith(
"sessionUpdated",
expect.anything(),
);
});
}); });
describe("onRequestSessionLock", () => { describe("onRequestSessionLock", () => {
it("should set selected drone, chat session, and project on success", () => { it("should set selected drone, chat session, and project on success", () => {
const mockDroneSession = { const mockDroneSession = {
socket: { socket: {
emit: vi.fn((event, ...args) => { emit: vi.fn((event: string, ...args: any[]) => {
const callback = args[args.length - 1]; const callback = args[args.length - 1];
callback(true, mockChatSession._id); callback(true, mockChatSession._id);
}), }),
@ -233,7 +329,7 @@ describe("CodeSession", () => {
it("should not set session data on failure", () => { it("should not set session data on failure", () => {
const mockDroneSession = { const mockDroneSession = {
socket: { socket: {
emit: vi.fn((event, ...args) => { emit: vi.fn((event: string, ...args: any[]) => {
const callback = args[args.length - 1]; const callback = args[args.length - 1];
callback(false, ""); callback(false, "");
}), }),

View File

@ -12,7 +12,12 @@ 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, GadgetLog, GadgetLogTransportSocket, 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 {
@ -210,7 +215,14 @@ class GadgetDrone extends GadgetProcess {
const socketTransport = new GadgetLogTransportSocket( const socketTransport = new GadgetLogTransportSocket(
(event, timestamp, component, level, message, metadata): void => { (event, timestamp, component, level, message, metadata): void => {
(this.socket as any)?.emit(event, timestamp, component, level, message, metadata); (this.socket as any)?.emit(
event,
timestamp,
{ name: component.name, slug: component.slug },
level,
message,
metadata,
);
}, },
); );
GadgetLog.addDefaultTransport(socketTransport); GadgetLog.addDefaultTransport(socketTransport);

View File

@ -147,18 +147,27 @@ export class OllamaAiApi extends AiApi {
stream: true, stream: true,
}); });
const content = {
response: "",
thinking: "",
};
let lastChunk; let lastChunk;
for await (const chunk of response) { for await (const chunk of response) {
lastChunk = chunk; lastChunk = chunk;
if (streamCallback) {
if (chunk.thinking) { if (chunk.thinking) {
content.thinking += chunk.thinking;
if (streamCallback) {
await streamCallback({ await streamCallback({
type: "thinking", type: "thinking",
data: chunk.thinking, data: chunk.thinking,
}); });
} }
}
if (chunk.response) { if (chunk.response) {
content.response += chunk.response;
if (streamCallback) {
await streamCallback({ await streamCallback({
type: "response", type: "response",
data: chunk.response, data: chunk.response,
@ -166,13 +175,15 @@ export class OllamaAiApi extends AiApi {
} }
} }
} }
this.log.debug("generate call is done", content);
assert(lastChunk, "no stream response chunks received"); assert(lastChunk, "no stream response chunks received");
return { return {
done: lastChunk.done, done: lastChunk.done,
doneReason: lastChunk.done_reason, doneReason: lastChunk.done_reason,
response: lastChunk.response, response: content.response,
thinking: lastChunk.thinking, thinking: content.thinking,
stats: { stats: {
duration: { duration: {
seconds: lastChunk.total_duration, seconds: lastChunk.total_duration,

View File

@ -30,6 +30,7 @@ export * from "./interfaces/user.ts";
export * from "./messages/ide.ts"; export * from "./messages/ide.ts";
export * from "./messages/drone.ts"; export * from "./messages/drone.ts";
export * from "./messages/web.ts";
export * from "./messages/socket.ts"; export * from "./messages/socket.ts";
/* /*

View File

@ -15,6 +15,7 @@ import {
WorkspaceModeChangedMessage, WorkspaceModeChangedMessage,
LogMessage, LogMessage,
} from "./drone.ts"; } from "./drone.ts";
import { SessionUpdatedMessage } from "./web.ts";
import { import {
ReleaseSessionLockMessage, ReleaseSessionLockMessage,
RequestSessionLockMessage, RequestSessionLockMessage,
@ -91,6 +92,7 @@ export interface ServerToClientEvents {
toolCall: ToolCallMessage; toolCall: ToolCallMessage;
workOrderComplete: WorkOrderCompleteMessage; workOrderComplete: WorkOrderCompleteMessage;
workspaceModeChanged: WorkspaceModeChangedMessage; workspaceModeChanged: WorkspaceModeChangedMessage;
sessionUpdated: SessionUpdatedMessage;
} }
export interface SocketData { export interface SocketData {

View File

@ -0,0 +1,16 @@
// src/messages/web.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0
import { IChatSession } from "../interfaces/chat-session.ts";
/*
* sessionUpdated
* Sent from gadget-code:web to gadget-code:ide when a chat session property
* has changed (e.g. auto-generated name). The IDE should merge these updates
* into its local state.
*/
export type SessionUpdatedMessage = (
updates: Partial<IChatSession>,
) => void;