Compare commits
2 Commits
fix/worksp
...
feat/ms19-
| Author | SHA1 | Date | |
|---|---|---|---|
| e41fedb3c2 | |||
| 5ba77d8952 |
@@ -0,0 +1,23 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "TerminalSessionStatus" AS ENUM ('ACTIVE', 'CLOSED');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "terminal_sessions" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"workspace_id" UUID NOT NULL,
|
||||||
|
"name" TEXT NOT NULL DEFAULT 'Terminal',
|
||||||
|
"status" "TerminalSessionStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"closed_at" TIMESTAMPTZ,
|
||||||
|
|
||||||
|
CONSTRAINT "terminal_sessions_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "terminal_sessions_workspace_id_idx" ON "terminal_sessions"("workspace_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "terminal_sessions_workspace_id_status_idx" ON "terminal_sessions"("workspace_id", "status");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "terminal_sessions" ADD CONSTRAINT "terminal_sessions_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -206,6 +206,11 @@ enum CredentialScope {
|
|||||||
SYSTEM
|
SYSTEM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TerminalSessionStatus {
|
||||||
|
ACTIVE
|
||||||
|
CLOSED
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// MODELS
|
// MODELS
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -297,6 +302,7 @@ model Workspace {
|
|||||||
federationEventSubscriptions FederationEventSubscription[]
|
federationEventSubscriptions FederationEventSubscription[]
|
||||||
llmUsageLogs LlmUsageLog[]
|
llmUsageLogs LlmUsageLog[]
|
||||||
userCredentials UserCredential[]
|
userCredentials UserCredential[]
|
||||||
|
terminalSessions TerminalSession[]
|
||||||
|
|
||||||
@@index([ownerId])
|
@@index([ownerId])
|
||||||
@@map("workspaces")
|
@@map("workspaces")
|
||||||
@@ -1507,3 +1513,23 @@ model LlmUsageLog {
|
|||||||
@@index([conversationId])
|
@@index([conversationId])
|
||||||
@@map("llm_usage_logs")
|
@@map("llm_usage_logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// TERMINAL MODULE
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
model TerminalSession {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
workspaceId String @map("workspace_id") @db.Uuid
|
||||||
|
name String @default("Terminal")
|
||||||
|
status TerminalSessionStatus @default(ACTIVE)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
||||||
|
closedAt DateTime? @map("closed_at") @db.Timestamptz
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([workspaceId])
|
||||||
|
@@index([workspaceId, status])
|
||||||
|
@@map("terminal_sessions")
|
||||||
|
}
|
||||||
|
|||||||
53
apps/api/src/terminal/terminal-session.dto.ts
Normal file
53
apps/api/src/terminal/terminal-session.dto.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Terminal Session DTOs
|
||||||
|
*
|
||||||
|
* Data Transfer Objects for terminal session persistence endpoints.
|
||||||
|
* Validated using class-validator decorators.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IsString, IsOptional, MaxLength, IsEnum, IsUUID } from "class-validator";
|
||||||
|
import { TerminalSessionStatus } from "@prisma/client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for creating a new terminal session record.
|
||||||
|
*/
|
||||||
|
export class CreateTerminalSessionDto {
|
||||||
|
@IsString()
|
||||||
|
@IsUUID()
|
||||||
|
workspaceId!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(128)
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for querying terminal sessions by workspace.
|
||||||
|
*/
|
||||||
|
export class FindTerminalSessionsByWorkspaceDto {
|
||||||
|
@IsString()
|
||||||
|
@IsUUID()
|
||||||
|
workspaceId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response shape for a terminal session.
|
||||||
|
*/
|
||||||
|
export class TerminalSessionResponseDto {
|
||||||
|
id!: string;
|
||||||
|
workspaceId!: string;
|
||||||
|
name!: string;
|
||||||
|
status!: TerminalSessionStatus;
|
||||||
|
createdAt!: Date;
|
||||||
|
closedAt!: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for filtering terminal sessions by status.
|
||||||
|
*/
|
||||||
|
export class TerminalSessionStatusFilterDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(TerminalSessionStatus)
|
||||||
|
status?: TerminalSessionStatus;
|
||||||
|
}
|
||||||
229
apps/api/src/terminal/terminal-session.service.spec.ts
Normal file
229
apps/api/src/terminal/terminal-session.service.spec.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* TerminalSessionService Tests
|
||||||
|
*
|
||||||
|
* Unit tests for database-backed terminal session CRUD:
|
||||||
|
* create, findByWorkspace, close, and findById.
|
||||||
|
* PrismaService is mocked to isolate the service logic.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
|
import { NotFoundException } from "@nestjs/common";
|
||||||
|
import { TerminalSessionStatus } from "@prisma/client";
|
||||||
|
import type { TerminalSession } from "@prisma/client";
|
||||||
|
import { TerminalSessionService } from "./terminal-session.service";
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Helpers
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
function makeSession(overrides: Partial<TerminalSession> = {}): TerminalSession {
|
||||||
|
return {
|
||||||
|
id: "session-uuid-1",
|
||||||
|
workspaceId: "workspace-uuid-1",
|
||||||
|
name: "Terminal",
|
||||||
|
status: TerminalSessionStatus.ACTIVE,
|
||||||
|
createdAt: new Date("2026-02-25T00:00:00Z"),
|
||||||
|
closedAt: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Mock PrismaService
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
function makeMockPrisma() {
|
||||||
|
return {
|
||||||
|
terminalSession: {
|
||||||
|
create: vi.fn(),
|
||||||
|
findMany: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Tests
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
describe("TerminalSessionService", () => {
|
||||||
|
let service: TerminalSessionService;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let mockPrisma: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockPrisma = makeMockPrisma();
|
||||||
|
service = new TerminalSessionService(mockPrisma);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// create
|
||||||
|
// ==========================================
|
||||||
|
describe("create", () => {
|
||||||
|
it("should call prisma.terminalSession.create with workspaceId only when no name provided", async () => {
|
||||||
|
const session = makeSession();
|
||||||
|
mockPrisma.terminalSession.create.mockResolvedValueOnce(session);
|
||||||
|
|
||||||
|
const result = await service.create("workspace-uuid-1");
|
||||||
|
|
||||||
|
expect(mockPrisma.terminalSession.create).toHaveBeenCalledWith({
|
||||||
|
data: { workspaceId: "workspace-uuid-1" },
|
||||||
|
});
|
||||||
|
expect(result).toEqual(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include name in create data when name is provided", async () => {
|
||||||
|
const session = makeSession({ name: "My Terminal" });
|
||||||
|
mockPrisma.terminalSession.create.mockResolvedValueOnce(session);
|
||||||
|
|
||||||
|
const result = await service.create("workspace-uuid-1", "My Terminal");
|
||||||
|
|
||||||
|
expect(mockPrisma.terminalSession.create).toHaveBeenCalledWith({
|
||||||
|
data: { workspaceId: "workspace-uuid-1", name: "My Terminal" },
|
||||||
|
});
|
||||||
|
expect(result).toEqual(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the created session", async () => {
|
||||||
|
const session = makeSession();
|
||||||
|
mockPrisma.terminalSession.create.mockResolvedValueOnce(session);
|
||||||
|
|
||||||
|
const result = await service.create("workspace-uuid-1");
|
||||||
|
|
||||||
|
expect(result.id).toBe("session-uuid-1");
|
||||||
|
expect(result.status).toBe(TerminalSessionStatus.ACTIVE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// findByWorkspace
|
||||||
|
// ==========================================
|
||||||
|
describe("findByWorkspace", () => {
|
||||||
|
it("should query for ACTIVE sessions in the given workspace, ordered by createdAt desc", async () => {
|
||||||
|
const sessions = [makeSession(), makeSession({ id: "session-uuid-2" })];
|
||||||
|
mockPrisma.terminalSession.findMany.mockResolvedValueOnce(sessions);
|
||||||
|
|
||||||
|
const result = await service.findByWorkspace("workspace-uuid-1");
|
||||||
|
|
||||||
|
expect(mockPrisma.terminalSession.findMany).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
workspaceId: "workspace-uuid-1",
|
||||||
|
status: TerminalSessionStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an empty array when no active sessions exist", async () => {
|
||||||
|
mockPrisma.terminalSession.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const result = await service.findByWorkspace("workspace-uuid-empty");
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not include CLOSED sessions", async () => {
|
||||||
|
// The where clause enforces ACTIVE status — verify it is present
|
||||||
|
mockPrisma.terminalSession.findMany.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
await service.findByWorkspace("workspace-uuid-1");
|
||||||
|
|
||||||
|
const callArgs = mockPrisma.terminalSession.findMany.mock.calls[0][0] as {
|
||||||
|
where: { status: TerminalSessionStatus };
|
||||||
|
};
|
||||||
|
expect(callArgs.where.status).toBe(TerminalSessionStatus.ACTIVE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// close
|
||||||
|
// ==========================================
|
||||||
|
describe("close", () => {
|
||||||
|
it("should set status to CLOSED and set closedAt when session exists", async () => {
|
||||||
|
const existingSession = makeSession();
|
||||||
|
const closedSession = makeSession({
|
||||||
|
status: TerminalSessionStatus.CLOSED,
|
||||||
|
closedAt: new Date("2026-02-25T01:00:00Z"),
|
||||||
|
});
|
||||||
|
|
||||||
|
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(existingSession);
|
||||||
|
mockPrisma.terminalSession.update.mockResolvedValueOnce(closedSession);
|
||||||
|
|
||||||
|
const result = await service.close("session-uuid-1");
|
||||||
|
|
||||||
|
expect(mockPrisma.terminalSession.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { id: "session-uuid-1" },
|
||||||
|
});
|
||||||
|
expect(mockPrisma.terminalSession.update).toHaveBeenCalledWith({
|
||||||
|
where: { id: "session-uuid-1" },
|
||||||
|
data: {
|
||||||
|
status: TerminalSessionStatus.CLOSED,
|
||||||
|
closedAt: expect.any(Date),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.status).toBe(TerminalSessionStatus.CLOSED);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundException when session does not exist", async () => {
|
||||||
|
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
await expect(service.close("nonexistent-id")).rejects.toThrow(NotFoundException);
|
||||||
|
expect(mockPrisma.terminalSession.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should include a non-null closedAt timestamp on close", async () => {
|
||||||
|
const existingSession = makeSession();
|
||||||
|
const closedSession = makeSession({
|
||||||
|
status: TerminalSessionStatus.CLOSED,
|
||||||
|
closedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(existingSession);
|
||||||
|
mockPrisma.terminalSession.update.mockResolvedValueOnce(closedSession);
|
||||||
|
|
||||||
|
const result = await service.close("session-uuid-1");
|
||||||
|
|
||||||
|
expect(result.closedAt).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// findById
|
||||||
|
// ==========================================
|
||||||
|
describe("findById", () => {
|
||||||
|
it("should return the session when it exists", async () => {
|
||||||
|
const session = makeSession();
|
||||||
|
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(session);
|
||||||
|
|
||||||
|
const result = await service.findById("session-uuid-1");
|
||||||
|
|
||||||
|
expect(mockPrisma.terminalSession.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { id: "session-uuid-1" },
|
||||||
|
});
|
||||||
|
expect(result).toEqual(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when session does not exist", async () => {
|
||||||
|
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const result = await service.findById("no-such-id");
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find CLOSED sessions as well as ACTIVE ones", async () => {
|
||||||
|
const closedSession = makeSession({
|
||||||
|
status: TerminalSessionStatus.CLOSED,
|
||||||
|
closedAt: new Date(),
|
||||||
|
});
|
||||||
|
mockPrisma.terminalSession.findUnique.mockResolvedValueOnce(closedSession);
|
||||||
|
|
||||||
|
const result = await service.findById("session-uuid-1");
|
||||||
|
|
||||||
|
expect(result?.status).toBe(TerminalSessionStatus.CLOSED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
96
apps/api/src/terminal/terminal-session.service.ts
Normal file
96
apps/api/src/terminal/terminal-session.service.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* TerminalSessionService
|
||||||
|
*
|
||||||
|
* Manages database persistence for terminal sessions.
|
||||||
|
* Provides CRUD operations on the TerminalSession model,
|
||||||
|
* enabling session tracking, recovery, and workspace-level listing.
|
||||||
|
*
|
||||||
|
* Session lifecycle:
|
||||||
|
* - create: record a new terminal session with ACTIVE status
|
||||||
|
* - findByWorkspace: return all ACTIVE sessions for a workspace
|
||||||
|
* - close: mark a session as CLOSED, set closedAt timestamp
|
||||||
|
* - findById: retrieve a single session by ID
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
|
||||||
|
import { TerminalSessionStatus } from "@prisma/client";
|
||||||
|
import type { TerminalSession } from "@prisma/client";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TerminalSessionService {
|
||||||
|
private readonly logger = new Logger(TerminalSessionService.name);
|
||||||
|
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new terminal session record in the database.
|
||||||
|
*
|
||||||
|
* @param workspaceId - The workspace this session belongs to
|
||||||
|
* @param name - Optional display name for the session (defaults to "Terminal")
|
||||||
|
* @returns The created TerminalSession record
|
||||||
|
*/
|
||||||
|
async create(workspaceId: string, name?: string): Promise<TerminalSession> {
|
||||||
|
this.logger.log(
|
||||||
|
`Creating terminal session for workspace ${workspaceId}${name !== undefined ? ` (name: ${name})` : ""}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: { workspaceId: string; name?: string } = { workspaceId };
|
||||||
|
if (name !== undefined) {
|
||||||
|
data.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.terminalSession.create({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all ACTIVE terminal sessions for a workspace.
|
||||||
|
*
|
||||||
|
* @param workspaceId - The workspace to query
|
||||||
|
* @returns Array of active TerminalSession records, ordered by creation time (newest first)
|
||||||
|
*/
|
||||||
|
async findByWorkspace(workspaceId: string): Promise<TerminalSession[]> {
|
||||||
|
return this.prisma.terminalSession.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
status: TerminalSessionStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a terminal session by setting its status to CLOSED and recording closedAt.
|
||||||
|
*
|
||||||
|
* @param id - The session ID to close
|
||||||
|
* @returns The updated TerminalSession record
|
||||||
|
* @throws NotFoundException if the session does not exist
|
||||||
|
*/
|
||||||
|
async close(id: string): Promise<TerminalSession> {
|
||||||
|
const existing = await this.prisma.terminalSession.findUnique({ where: { id } });
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
throw new NotFoundException(`Terminal session ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Closing terminal session ${id} (workspace: ${existing.workspaceId})`);
|
||||||
|
|
||||||
|
return this.prisma.terminalSession.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
status: TerminalSessionStatus.CLOSED,
|
||||||
|
closedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a terminal session by ID.
|
||||||
|
*
|
||||||
|
* @param id - The session ID to retrieve
|
||||||
|
* @returns The TerminalSession record, or null if not found
|
||||||
|
*/
|
||||||
|
async findById(id: string): Promise<TerminalSession | null> {
|
||||||
|
return this.prisma.terminalSession.findUnique({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,10 +5,11 @@
|
|||||||
*
|
*
|
||||||
* Imports:
|
* Imports:
|
||||||
* - AuthModule for WebSocket authentication (verifySession)
|
* - AuthModule for WebSocket authentication (verifySession)
|
||||||
* - PrismaModule for workspace membership queries
|
* - PrismaModule for workspace membership queries and session persistence
|
||||||
*
|
*
|
||||||
* Providers:
|
* Providers:
|
||||||
* - TerminalService: manages PTY session lifecycle
|
* - TerminalService: manages PTY session lifecycle (in-memory)
|
||||||
|
* - TerminalSessionService: persists session records to the database
|
||||||
* - TerminalGateway: WebSocket gateway on /terminal namespace
|
* - TerminalGateway: WebSocket gateway on /terminal namespace
|
||||||
*
|
*
|
||||||
* The module does not export providers; terminal sessions are
|
* The module does not export providers; terminal sessions are
|
||||||
@@ -18,11 +19,13 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { TerminalGateway } from "./terminal.gateway";
|
import { TerminalGateway } from "./terminal.gateway";
|
||||||
import { TerminalService } from "./terminal.service";
|
import { TerminalService } from "./terminal.service";
|
||||||
|
import { TerminalSessionService } from "./terminal-session.service";
|
||||||
import { AuthModule } from "../auth/auth.module";
|
import { AuthModule } from "../auth/auth.module";
|
||||||
import { PrismaModule } from "../prisma/prisma.module";
|
import { PrismaModule } from "../prisma/prisma.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule, PrismaModule],
|
imports: [AuthModule, PrismaModule],
|
||||||
providers: [TerminalGateway, TerminalService],
|
providers: [TerminalGateway, TerminalService, TerminalSessionService],
|
||||||
|
exports: [TerminalSessionService],
|
||||||
})
|
})
|
||||||
export class TerminalModule {}
|
export class TerminalModule {}
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
|||||||
const response = await sendChatMessage(request);
|
const response = await sendChatMessage(request);
|
||||||
|
|
||||||
const assistantMessage: Message = {
|
const assistantMessage: Message = {
|
||||||
id: `assistant-${Date.now().toString()}`,
|
id: `assistant-${Date.now().toString()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: response.message.content,
|
content: response.message.content,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -328,7 +328,7 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const errorMessage: Message = {
|
const errorMessage: Message = {
|
||||||
id: `error-${String(Date.now())}`,
|
id: `error-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "Something went wrong. Please try again.",
|
content: "Something went wrong. Please try again.",
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
|||||||
Reference in New Issue
Block a user