chat session auto-naming with IDE update
This commit is contained in:
parent
4b33915c7d
commit
d26624ab93
@ -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.
|
||||||
|
|||||||
@ -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",
|
||||||
(
|
(
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
this.chatSession.name === "New Chat Session" &&
|
||||||
content,
|
this.chatSession.stats.turnCount === 1
|
||||||
);
|
) {
|
||||||
|
this.chatSession =
|
||||||
|
await ChatSessionService.generateSessionNameFromPrompt(
|
||||||
|
this.chatSession,
|
||||||
|
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, {});
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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, "");
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
16
packages/api/src/messages/web.ts
Normal file
16
packages/api/src/messages/web.ts
Normal 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;
|
||||||
Loading…
Reference in New Issue
Block a user