Support concurrent chats (#1478)
Fixes #212 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Add concurrent chat support with per-chat state, chat activity UI, IPC per-chat handling, and accompanying tests. > > - **Frontend (Chat concurrency)** > - Replace global chat atoms with per-chat maps: `chatMessagesByIdAtom`, `isStreamingByIdAtom`, `chatErrorByIdAtom`, `chatStreamCountByIdAtom`, `recentStreamChatIdsAtom`. > - Update `ChatPanel`, `ChatInput`, `MessagesList`, `DyadMarkdownParser`, and `useVersions` to read/write per-chat state. > - Add `useSelectChat` to centralize selecting/navigating chats; wire into `ChatList`. > - **UI** > - Add chat activity popover: `ChatActivityButton` and list; integrate into `preview_panel/ActionHeader` (renamed from `PreviewHeader`) and swap in `TitleBar`. > - **IPC/Main** > - Send error payloads with `chatId` on `chat:response:error`; update `ipc_client` to route errors per chat. > - Persist streaming partial assistant content periodically; improve cancellation/end handling. > - Make `FileUploadsState` per-chat (`addFileUpload({chatId,fileId}, ...)`, `clear(chatId)`, `getFileUploadsForChat(chatId)`); update handlers/processors accordingly. > - **Testing** > - Add e2e `concurrent_chat.spec.ts` and snapshots; extend helpers (`snapshotMessages` timeout, chat activity helpers). > - Fake LLM server: support `tc=` with options, optional sleep delay to simulate concurrency. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9035f30b73a1f2e5a366a0cac1c63411742b16f3. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
@@ -9,8 +9,8 @@ export interface FileUploadInfo {
|
||||
|
||||
export class FileUploadsState {
|
||||
private static instance: FileUploadsState;
|
||||
private currentChatId: number | null = null;
|
||||
private fileUploadsMap = new Map<string, FileUploadInfo>();
|
||||
// Map of chatId -> (fileId -> fileInfo)
|
||||
private uploadsByChat = new Map<number, Map<string, FileUploadInfo>>();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -22,45 +22,54 @@ export class FileUploadsState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize file uploads state for a specific chat and message
|
||||
* Ensure a map exists for a chatId
|
||||
*/
|
||||
public initialize({ chatId }: { chatId: number }): void {
|
||||
this.currentChatId = chatId;
|
||||
this.fileUploadsMap.clear();
|
||||
logger.debug(`Initialized file uploads state for chat ${chatId}`);
|
||||
private ensureChat(chatId: number): Map<string, FileUploadInfo> {
|
||||
let map = this.uploadsByChat.get(chatId);
|
||||
if (!map) {
|
||||
map = new Map<string, FileUploadInfo>();
|
||||
this.uploadsByChat.set(chatId, map);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a file upload mapping
|
||||
* Add a file upload mapping to a specific chat
|
||||
*/
|
||||
public addFileUpload(fileId: string, fileInfo: FileUploadInfo): void {
|
||||
this.fileUploadsMap.set(fileId, fileInfo);
|
||||
logger.log(`Added file upload: ${fileId} -> ${fileInfo.originalName}`);
|
||||
public addFileUpload(
|
||||
{ chatId, fileId }: { chatId: number; fileId: string },
|
||||
fileInfo: FileUploadInfo,
|
||||
): void {
|
||||
const map = this.ensureChat(chatId);
|
||||
map.set(fileId, fileInfo);
|
||||
logger.log(
|
||||
`Added file upload for chat ${chatId}: ${fileId} -> ${fileInfo.originalName}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current file uploads map
|
||||
* Get a copy of the file uploads map for a specific chat
|
||||
*/
|
||||
public getFileUploadsForChat(chatId: number): Map<string, FileUploadInfo> {
|
||||
if (this.currentChatId !== chatId) {
|
||||
return new Map();
|
||||
}
|
||||
return new Map(this.fileUploadsMap);
|
||||
const map = this.uploadsByChat.get(chatId);
|
||||
return new Map(map ?? []);
|
||||
}
|
||||
|
||||
// Removed getCurrentChatId(): no longer applicable in per-chat state
|
||||
|
||||
/**
|
||||
* Clear state for a specific chat
|
||||
*/
|
||||
public clear(chatId: number): void {
|
||||
this.uploadsByChat.delete(chatId);
|
||||
logger.debug(`Cleared file uploads state for chat ${chatId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current chat ID
|
||||
* Clear all uploads (primarily for tests or full reset)
|
||||
*/
|
||||
public getCurrentChatId(): number | null {
|
||||
return this.currentChatId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the current state
|
||||
*/
|
||||
public clear(): void {
|
||||
this.currentChatId = null;
|
||||
this.fileUploadsMap.clear();
|
||||
logger.debug("Cleared file uploads state");
|
||||
public clearAll(): void {
|
||||
this.uploadsByChat.clear();
|
||||
logger.debug("Cleared all file uploads state");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user