Skip to content
Merged
11 changes: 11 additions & 0 deletions drizzle/0006_add_multi_chat_support.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Add multi-chat support fields to conversations table
ALTER TABLE conversations ADD COLUMN provider TEXT;
ALTER TABLE conversations ADD COLUMN is_active INTEGER DEFAULT 0;
ALTER TABLE conversations ADD COLUMN display_order INTEGER DEFAULT 0;
ALTER TABLE conversations ADD COLUMN metadata TEXT;

-- Update existing conversations to be active (first chat in each task)
UPDATE conversations SET is_active = 1, display_order = 0 WHERE is_active IS NULL;

-- Create index for quick active conversation lookup
CREATE INDEX IF NOT EXISTS idx_conversations_active ON conversations (task_id, is_active);
13 changes: 13 additions & 0 deletions drizzle/0007_add_is_main_to_conversations.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Add is_main column to conversations table
ALTER TABLE conversations ADD COLUMN is_main INTEGER NOT NULL DEFAULT 0;

-- Mark first conversation of each task as main (for backward compatibility)
UPDATE conversations
SET is_main = 1
WHERE id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at ASC) as rn
FROM conversations
) t
WHERE rn = 1
);
14 changes: 14 additions & 0 deletions drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@
"when": 1765592430357,
"tag": "0005_add_use_worktree_to_tasks",
"breakpoints": true
},
{
"idx": 6,
"version": "6",
"when": 1765592430358,
"tag": "0006_add_multi_chat_support",
"breakpoints": true
},
{
"idx": 7,
"version": "6",
"when": 1765592430359,
"tag": "0007_add_is_main_to_conversations",
"breakpoints": true
}
]
}
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.16",
"better-sqlite3": "^12.6.0",
"concurrently": "^8.2.2",
"drizzle-kit": "^0.24.1",
"electron": "^30.5.1",
Expand Down
6 changes: 6 additions & 0 deletions src/main/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ export const conversations = sqliteTable(
.notNull()
.references(() => tasks.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
provider: text('provider'), // AI provider for this chat (claude, codex, qwen, etc.)
isActive: integer('is_active').notNull().default(0), // 1 if this is the active chat for the task
isMain: integer('is_main').notNull().default(0), // 1 if this is the main/primary chat (gets full persistence)
displayOrder: integer('display_order').notNull().default(0), // Order in the tab bar
metadata: text('metadata'), // JSON for additional chat-specific data
createdAt: text('created_at')
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
Expand All @@ -67,6 +72,7 @@ export const conversations = sqliteTable(
},
(table) => ({
taskIdIdx: index('idx_conversations_task_id').on(table.taskId),
activeIdx: index('idx_conversations_active').on(table.taskId, table.isActive), // Index for quick active conversation lookup
})
);

Expand Down
112 changes: 112 additions & 0 deletions src/main/ipc/dbIpc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ipcMain } from 'electron';
import { log } from '../lib/logger';
import { databaseService } from '../services/DatabaseService';
import fs from 'fs';
import path from 'path';

export function registerDatabaseIpc() {
ipcMain.handle('db:getProjects', async () => {
Expand Down Expand Up @@ -111,6 +113,40 @@ export function registerDatabaseIpc() {
}
});

ipcMain.handle(
'db:cleanupSessionDirectory',
async (_, args: { taskPath: string; conversationId: string }) => {
try {
const sessionDir = path.join(args.taskPath, '.emdash-sessions', args.conversationId);

// Check if directory exists before trying to remove it
if (fs.existsSync(sessionDir)) {
// Remove the directory and its contents
fs.rmSync(sessionDir, { recursive: true, force: true });
log.info('Cleaned up session directory:', sessionDir);

// Also try to remove the parent .emdash-sessions if it's empty
const parentDir = path.join(args.taskPath, '.emdash-sessions');
try {
const entries = fs.readdirSync(parentDir);
if (entries.length === 0) {
fs.rmdirSync(parentDir);
log.info('Removed empty .emdash-sessions directory');
}
} catch (err) {
// Parent directory removal is optional
}
}

return { success: true };
} catch (error) {
log.warn('Failed to cleanup session directory:', error);
// This is best-effort, don't fail the operation
return { success: true };
}
}
);

ipcMain.handle('db:deleteTask', async (_, taskId: string) => {
try {
await databaseService.deleteTask(taskId);
Expand All @@ -120,4 +156,80 @@ export function registerDatabaseIpc() {
return { success: false, error: (error as Error).message };
}
});

// Multi-chat support handlers
ipcMain.handle(
'db:createConversation',
async (
_,
{
taskId,
title,
provider,
isMain,
}: { taskId: string; title: string; provider?: string; isMain?: boolean }
) => {
try {
const conversation = await databaseService.createConversation(
taskId,
title,
provider,
isMain
);
return { success: true, conversation };
} catch (error) {
log.error('Failed to create conversation:', error);
return { success: false, error: (error as Error).message };
}
}
);

ipcMain.handle(
'db:setActiveConversation',
async (_, { taskId, conversationId }: { taskId: string; conversationId: string }) => {
try {
await databaseService.setActiveConversation(taskId, conversationId);
return { success: true };
} catch (error) {
log.error('Failed to set active conversation:', error);
return { success: false, error: (error as Error).message };
}
}
);

ipcMain.handle('db:getActiveConversation', async (_, taskId: string) => {
try {
const conversation = await databaseService.getActiveConversation(taskId);
return { success: true, conversation };
} catch (error) {
log.error('Failed to get active conversation:', error);
return { success: false, error: (error as Error).message };
}
});

ipcMain.handle(
'db:reorderConversations',
async (_, { taskId, conversationIds }: { taskId: string; conversationIds: string[] }) => {
try {
await databaseService.reorderConversations(taskId, conversationIds);
return { success: true };
} catch (error) {
log.error('Failed to reorder conversations:', error);
return { success: false, error: (error as Error).message };
}
}
);

ipcMain.handle(
'db:updateConversationTitle',
async (_, { conversationId, title }: { conversationId: string; title: string }) => {
try {
await databaseService.updateConversationTitle(conversationId, title);
return { success: true };
} catch (error) {
log.error('Failed to update conversation title:', error);
return { success: false, error: (error as Error).message };
}
}
);
}
39 changes: 39 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,19 @@ contextBridge.exposeInMainWorld('electronAPI', {
getMessages: (conversationId: string) => ipcRenderer.invoke('db:getMessages', conversationId),
deleteConversation: (conversationId: string) =>
ipcRenderer.invoke('db:deleteConversation', conversationId),
cleanupSessionDirectory: (args: { taskPath: string; conversationId: string }) =>
ipcRenderer.invoke('db:cleanupSessionDirectory', args),

// Multi-chat support
createConversation: (params: { taskId: string; title: string; provider?: string }) =>
ipcRenderer.invoke('db:createConversation', params),
setActiveConversation: (params: { taskId: string; conversationId: string }) =>
ipcRenderer.invoke('db:setActiveConversation', params),
getActiveConversation: (taskId: string) => ipcRenderer.invoke('db:getActiveConversation', taskId),
reorderConversations: (params: { taskId: string; conversationIds: string[] }) =>
ipcRenderer.invoke('db:reorderConversations', params),
updateConversationTitle: (params: { conversationId: string; title: string }) =>
ipcRenderer.invoke('db:updateConversationTitle', params),

// Line comments management
lineCommentsCreate: (input: any) => ipcRenderer.invoke('lineComments:create', input),
Expand Down Expand Up @@ -637,6 +650,32 @@ export interface ElectronAPI {
conversationId: string
) => Promise<{ success: boolean; messages?: any[]; error?: string }>;
deleteConversation: (conversationId: string) => Promise<{ success: boolean; error?: string }>;
cleanupSessionDirectory: (args: {
taskPath: string;
conversationId: string;
}) => Promise<{ success: boolean }>;

// Multi-chat support
createConversation: (params: {
taskId: string;
title: string;
provider?: string;
}) => Promise<{ success: boolean; conversation?: any; error?: string }>;
setActiveConversation: (params: {
taskId: string;
conversationId: string;
}) => Promise<{ success: boolean; error?: string }>;
getActiveConversation: (
taskId: string
) => Promise<{ success: boolean; conversation?: any; error?: string }>;
reorderConversations: (params: {
taskId: string;
conversationIds: string[];
}) => Promise<{ success: boolean; error?: string }>;
updateConversationTitle: (params: {
conversationId: string;
title: string;
}) => Promise<{ success: boolean; error?: string }>;

// Host preview (non-container)
hostPreviewStart: (args: {
Expand Down
Loading