"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThreadManager = void 0; const database_1 = require("../database"); const git_integration_1 = require("../git/git-integration"); const embedding_service_1 = require("./embedding-service"); const uuid_1 = require("uuid"); const date_fns_1 = require("date-fns"); const locale_1 = require("date-fns/locale"); const path_1 = __importDefault(require("path")); const fs_extra_1 = __importDefault(require("fs-extra")); class ThreadManager { dbManager; threadsDAO; messagesDAO; fileChangesDAO; gitIntegration; embeddingService; claudeContextPath = path_1.default.join(process.cwd(), '.claude', '.threads', 'current-context.md'); constructor(dbManager) { this.dbManager = dbManager; this.embeddingService = new embedding_service_1.XenovaEmbeddingService(); this.threadsDAO = new database_1.ThreadsDAO(dbManager); this.messagesDAO = new database_1.MessagesDAO(dbManager, this.embeddingService); this.fileChangesDAO = new database_1.FileChangesDAO(dbManager); this.gitIntegration = new git_integration_1.GitIntegration(); fs_extra_1.default.ensureDirSync(path_1.default.dirname(this.claudeContextPath)); // Ensure directory exists on startup } get messagesDAOInstance() { return this.messagesDAO; } async updateClaudeMd(thread, messages) { // 格式化历史消息 const historyText = messages.map(msg => { const time = new Date(msg.timestamp).toLocaleString('zh-CN'); const roleText = msg.role === 'user' ? '用户' : '助手'; return `### [${time}] ${roleText}\n\n${msg.content}\n`; }).join('\n---\n\n'); const content = `# 📋 当前线程上下文\n\n**⚠️ 重要:上下文隔离规则**\n\n您当前在独立的对话线程中工作。请**严格遵守**以下规则:\n\n1. **只参考本文档中的历史对话**\n2. **忽略本线程之外的所有内容**\n3. **不要引用或提及其他线程的信息**\n\n---\n\n## 线程信息\n\n- 📋 标题:${thread.title}\n- 📝 描述:${thread.description || '无'}\n- 🆔 ID:${thread.id}\n- 🌿 Git 分支:${thread.gitBranch || '无'}\n- 🏷️ 标签:${thread.metadata.tags?.join(', ') || '无'}\n- 📊 消息数:${thread.messageCount}\n\n---\n\n## 历史对话\n\n${historyText || '暂无历史对话'}\n\n---\n\n**再次强调:请只参考上述对话内容进行回复,忽略本线程之外的所有历史记录。**\n`; // 写入文件 await fs_extra_1.default.writeFile(this.claudeContextPath, content, 'utf-8'); } async createThread(input) { const { title, description, tags, switchTo = true } = input; // 1. 生成 UUID const threadId = (0, uuid_1.v4)(); // 2. 准备 Git 分支名(但不立即创建,延迟到切换时) let gitBranch; if (await this.gitIntegration.isGitRepo()) { gitBranch = `thread/${threadId.substring(0, 8)}`; // 注意:分支将在首次切换到此 thread 时创建,避免阻塞 } // 3. 创建 Thread 记录 // 先创建为非激活状态,如果是 switchTo=true,后面会统一调用 setActive 处理互斥 let newThread = this.threadsDAO.create({ id: threadId, sessionId: threadId, // 相同 title, description, gitBranch, isActive: false, metadata: { filesChanged: 0, linesAdded: 0, linesDeleted: 0, tags: tags || [] } }); // 4. 处理切换逻辑 (默认为 true) if (switchTo) { this.threadsDAO.setActive(threadId); newThread.isActive = true; // 更新内存中的对象状态 // 更新 CLAUDE.md 上下文 await this.updateClaudeMd(newThread, []); } // 5. 生成启动命令 const launchCommand = `claude --session-id ${threadId}`; // 6. 返回结果(快速返回,不阻塞) return { thread: newThread, message: this.formatCreateMessage(newThread), launchCommand }; } formatCreateMessage(thread) { const shortId = thread.id.substring(0, 8); const gitBranchInfo = thread.gitBranch ? `- **Git 分支**: \`${thread.gitBranch}\`` : ''; return `### ✨ 新线程已创建 - **标题**: ${thread.title} - **ID**: \`${shortId}\` ${gitBranchInfo} **🚀 启动独立会话**: \`claude --session-id ${thread.id}\` 或: \`clt ${shortId}\` `; } formatCombinedSwitchMessage(thread, messages) { const shortId = thread.id.substring(0, 8); // Filter out commands and format context messages const contextMessages = messages.filter(msg => !msg.content.trim().startsWith('/')); const displayLimit = 5; // Reduced limit const recentMessages = contextMessages .slice(0, displayLimit) .reverse() .map(msg => { const time = (0, date_fns_1.formatDistanceToNow)(msg.timestamp, { locale: locale_1.zhCN }); const preview = msg.content.substring(0, 60).replace(/\n/g, ' '); const icon = msg.role === 'user' ? '👤' : '🤖'; return `- ${time} ${icon} ${preview}${msg.content.length > 60 ? '...' : ''}`; }) .join('\n'); const contextSection = contextMessages.length > 0 ? `**💬 最近消息**:\n${recentMessages}\n` : ''; const gitBranchInfo = thread.gitBranch ? `- **Git**: \`${thread.gitBranch}\`` : ''; return `### 🔄 已切换到线程 - **标题**: ${thread.title} - **ID**: \`${shortId}\` ${gitBranchInfo} - **统计**: ${messages.length} 消息 | ${thread.metadata.filesChanged} 文件变更 ${contextSection} **⚠️ 完全隔离上下文**: 推荐重启: \`exit\` 后运行 \`claude --session-id ${thread.id}\` `; } async getThread(id, includeMessages = false, includeFileChanges = false, messageLimit = 50) { const thread = this.threadsDAO.findById(id); if (!thread) { return { thread: null }; } let messages; if (includeMessages) { messages = this.messagesDAO.findByThreadId(id, { limit: messageLimit }); } let fileChanges; if (includeFileChanges) { fileChanges = this.fileChangesDAO.findByThreadId(id); } return { thread, messages, fileChanges }; } async listThreads(input) { const { threads, total } = this.threadsDAO.findAll(input); const currentActive = this.threadsDAO.getActive(); // Format the message for display const message = this.formatListThreadsMessage(threads, total, currentActive?.id); return { threads, total, currentThreadId: currentActive?.id, message }; } formatListThreadsMessage(threads, total, currentThreadId) { if (threads.length === 0) { return `📋 **线程列表**: 暂无线程。使用 \`/thread create \` 创建。`; } const rows = threads.map(thread => { const isActive = thread.id === currentThreadId; const status = isActive ? '✅' : ' '; const shortId = thread.id.substring(0, 8); const title = thread.title.length > 40 ? thread.title.substring(0, 37) + '...' : thread.title; const timeAgo = (0, date_fns_1.formatDistanceToNow)(thread.updatedAt, { locale: locale_1.zhCN, addSuffix: true }); // Compact format: Status | ID | Title (Msgs, Files) | Time return `\`${status} ${shortId}\` **${title}** (${thread.messageCount} msg, ${thread.metadata.filesChanged} files) - ${timeAgo}`; }).join('\n'); return `### 📋 线程列表 (总计: ${total}) ${rows} `; } findThreadsByPrefix(prefix) { return this.threadsDAO.findByPrefix(prefix); } async updateThread(id, updates) { const existingThread = this.threadsDAO.findById(id); if (!existingThread) { return { success: false, message: `Thread with ID ${id} not found.` }; } // Map UpdateThreadInput to Partial<Thread> const threadUpdates = { title: updates.title, description: updates.description, gitBranch: updates.gitBranch, }; if (updates.tags) { threadUpdates.metadata = { ...existingThread.metadata, tags: updates.tags }; } const updatedThread = this.threadsDAO.update(id, threadUpdates); if (!updatedThread) { return { success: false, message: `Failed to update thread with ID ${id}.` }; } return { success: true, thread: updatedThread, message: `Thread "${updatedThread.title}" (${updatedThread.id}) updated successfully.` }; } async deleteThread(id) { // Check if it's the active thread const currentActive = this.threadsDAO.getActive(); if (currentActive?.id === id) { return { success: false, message: "Cannot delete the currently active thread. Please switch to another thread first." }; } const deleted = this.threadsDAO.delete(id); if (!deleted) { return { success: false, message: `Thread with ID ${id} not found or failed to delete.` }; } // Due to ON DELETE CASCADE in SQLite schema, messages and file_changes are automatically deleted. return { success: true, message: `Thread with ID ${id} deleted successfully.` }; } async switchThread(id, options) { // 1. 查找目标 thread const targetThread = this.threadsDAO.findById(id); if (!targetThread) { return { success: false, message: `Thread with ID ${id} not found.` }; } // 2. 切换 Git 分支(如果需要,先创建) if (targetThread.gitBranch) { // 检查分支是否存在 const branchExists = await this.gitIntegration.branchExists(targetThread.gitBranch); if (!branchExists) { // 分支不存在,创建它(延迟创建策略) const created = await this.gitIntegration.createAndCheckoutBranch(targetThread.gitBranch); if (!created) { return { success: false, message: `Failed to create Git branch ${targetThread.gitBranch}` }; } } else { // 分支存在,直接切换 const switched = await this.gitIntegration.checkoutBranch(targetThread.gitBranch); if (!switched) { return { success: false, message: `Failed to switch to Git branch ${targetThread.gitBranch}` }; } } } // Set target thread as active this.threadsDAO.setActive(id); targetThread.isActive = true; // Update in-memory object // 3. 加载历史消息 (for CLAUDE.md and display) const messages = this.messagesDAO.findByThreadId(id, { limit: 50 }); // 4. 更新 CLAUDE.md(注入上下文提示) await this.updateClaudeMd(targetThread, messages); // 5. 生成切换命令 const launchCommand = `claude --session-id ${targetThread.id}`; // 6. 生成组合消息 const combinedMessage = this.formatCombinedSwitchMessage(targetThread, messages); return { success: true, thread: targetThread, messages, // 返回原始消息数组,供 Claude CLI 可能的进一步处理 message: combinedMessage, launchCommand }; } async getCurrentThread(includeMessages = false, includeFileChanges = false, messageLimit = 50) { const activeThread = this.threadsDAO.getActive(); if (!activeThread) { return { message: "No active thread found." }; } const { thread, messages, fileChanges } = await this.getThread(activeThread.id, includeMessages, includeFileChanges, messageLimit); if (!thread) { return { message: "Active thread not found in database (unexpected)." }; // Should not happen if getActive returns one } return { thread, messages, fileChanges }; } async addMessageToThread(threadId, role, content, metadata) { // Ensure thread exists and is active (or specified threadId is the active one) const activeThread = this.threadsDAO.getActive(); if (!activeThread || activeThread.id !== threadId) { throw new Error(`Thread with ID ${threadId} is not the active thread or does not exist. Cannot add message.`); } const message = await this.messagesDAO.create({ threadId, role, content, metadata }); return message; } async trackFileChange(threadId, filePath, changeType, linesAdded, linesDeleted, gitCommit) { // Ensure thread exists and is active const activeThread = this.threadsDAO.getActive(); if (!activeThread || activeThread.id !== threadId) { throw new Error(`Thread with ID ${threadId} is not the active thread or does not exist. Cannot track file change.`); } // Auto-detect stats if not provided if (linesAdded === undefined || linesDeleted === undefined || changeType === undefined) { const stats = await this.gitIntegration.getFileStats(filePath); if (linesAdded === undefined) linesAdded = stats.added; if (linesDeleted === undefined) linesDeleted = stats.deleted; if (changeType === undefined) changeType = stats.changeType; } // Auto-detect commit hash if not provided if (!gitCommit) { gitCommit = await this.gitIntegration.getCurrentCommit(); } const fileChange = this.fileChangesDAO.create({ threadId, filePath, changeType: changeType, linesAdded, linesDeleted, gitCommit }); return fileChange; } } exports.ThreadManager = ThreadManager;