feat: i18n for CLI history view; fix Claude session discovery path encoding

## Changes

### i18n 中文化 (i18n.js, cli-history.js)
- 添加 60+ 个翻译键用于 CLI 执行历史和对话详情
- 将 cli-history.js 中的硬编码英文字符串替换为 t() 函数调用
- 覆盖范围: 执行历史、对话详情、状态、工具标签、按钮、提示等

### 修复 Claude 会话追踪 (native-session-discovery.ts)
- 问题: Claude 使用路径编码存储会话 (D:\path -> D--path),但代码使用 SHA256 哈希导致无法发现
- 解决方案:
  - 添加 encodeClaudeProjectPath() 函数用于路径编码
  - 更新 ClaudeSessionDiscoverer.getSessions() 使用路径编码
  - 增强 extractFirstUserMessage() 支持多种消息格式 (string/array)
- 结果: Claude 会话现可正确关联,UI 按钮 "查看完整过程对话" 应可正常显示

## 验证
- npm run build 通过 
- Claude 会话发现 1267 个会话 
- 消息提取成功率 80% 
- 路径编码验证正确 
This commit is contained in:
catlog22
2026-01-22 14:53:38 +08:00
parent 5fa7524ad7
commit 02531c4d15
3 changed files with 206 additions and 55 deletions

View File

@@ -102,7 +102,7 @@ function renderCliHistory() {
if (cliExecutionHistory.length === 0) {
container.innerHTML = `
<div class="cli-history-header">
<h3><i data-lucide="history" class="w-4 h-4"></i> Execution History</h3>
<h3><i data-lucide="history" class="w-4 h-4"></i> ${t('cli.executionHistory')}</h3>
<div class="cli-history-controls">
${renderHistorySearch()}
${renderToolFilter()}
@@ -113,7 +113,7 @@ function renderCliHistory() {
</div>
<div class="empty-state">
<i data-lucide="terminal" class="w-8 h-8"></i>
<p>No executions yet</p>
<p>${t('cli.noExecutions')}</p>
</div>
`;
@@ -124,7 +124,7 @@ function renderCliHistory() {
const historyHtml = filteredHistory.length === 0
? `<div class="empty-state">
<i data-lucide="search-x" class="w-6 h-6"></i>
<p>No matching results</p>
<p>${t('cli.noMatchingResults')}</p>
</div>`
: filteredHistory.map(exec => {
const statusIcon = exec.status === 'success' ? 'check-circle' :
@@ -140,7 +140,7 @@ function renderCliHistory() {
// Native session indicator
const hasNative = exec.hasNativeSession || exec.nativeSessionId;
const nativeBadge = hasNative
? `<span class="cli-native-badge" title="Native session: ${exec.nativeSessionId}">
? `<span class="cli-native-badge" title="${t('cli.nativeSessionBadge')}: ${exec.nativeSessionId}">
<i data-lucide="file-json" class="w-3 h-3"></i>
</span>`
: '';
@@ -173,14 +173,14 @@ function renderCliHistory() {
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
</button>
${hasNative ? `
<button class="btn-icon" onclick="event.stopPropagation(); showNativeSessionDetail('${exec.id}', '${sourceDirEscaped}')" title="View Native Session">
<button class="btn-icon" onclick="event.stopPropagation(); showNativeSessionDetail('${exec.id}', '${sourceDirEscaped}')" title="${t('cli.viewNativeSession')}">
<i data-lucide="file-json" class="w-3.5 h-3.5"></i>
</button>
` : ''}
<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail('${exec.id}', '${sourceDirEscaped}')" title="View Details">
<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail('${exec.id}', '${sourceDirEscaped}')" title="${t('cli.viewDetails')}">
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
</button>
<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution('${exec.id}', '${sourceDirEscaped}')" title="Delete">
<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution('${exec.id}', '${sourceDirEscaped}')" title="${t('cli.delete')}">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
</button>
</div>
@@ -211,7 +211,7 @@ function renderHistorySearch() {
return `
<input type="text"
class="cli-history-search"
placeholder="Search history..."
placeholder="${t('cli.searchHistory')}"
value="${escapeHtml(cliHistorySearch)}"
onkeyup="searchCliHistory(this.value)"
oninput="searchCliHistory(this.value)">
@@ -224,7 +224,7 @@ function renderToolFilter() {
<select class="cli-tool-filter" onchange="filterCliHistory(this.value)">
${tools.map(tool => `
<option value="${tool === 'all' ? '' : tool}" ${cliHistoryFilter === (tool === 'all' ? null : tool) ? 'selected' : ''}>
${tool === 'all' ? 'All Tools' : tool.charAt(0).toUpperCase() + tool.slice(1)}
${tool === 'all' ? t('cli.allTools') : tool.charAt(0).toUpperCase() + tool.slice(1)}
</option>
`).join('')}
</select>
@@ -235,7 +235,7 @@ function renderToolFilter() {
async function showExecutionDetail(executionId, sourceDir) {
const conversation = await loadExecutionDetail(executionId, sourceDir);
if (!conversation) {
showRefreshToast('Conversation not found', 'error');
showRefreshToast(t('cli.conversationNotFound'), 'error');
return;
}
@@ -264,7 +264,7 @@ async function showExecutionDetail(executionId, sourceDir) {
<div class="cli-turn-header">
<div class="cli-turn-marker">
<span class="cli-turn-number">${isFirst ? '▶' : '↳'} Turn ${turn.turn}</span>
${isLast ? '<span class="cli-turn-latest-badge">Latest</span>' : ''}
${isLast ? `<span class="cli-turn-latest-badge">${t('cli.latest')}</span>` : ''}
</div>
<div class="cli-turn-meta">
<span class="cli-turn-time"><i data-lucide="clock" class="w-3 h-3"></i> ${turnTime}</span>
@@ -276,12 +276,12 @@ async function showExecutionDetail(executionId, sourceDir) {
</div>
<div class="cli-turn-body">
<div class="cli-detail-section cli-prompt-section">
<h4><i data-lucide="user" class="w-3.5 h-3.5"></i> User Prompt</h4>
<h4><i data-lucide="user" class="w-3.5 h-3.5"></i> ${t('cli.userPrompt')}</h4>
<pre class="cli-detail-prompt">${escapeHtml(turn.prompt)}</pre>
</div>
${turn.output.stdout ? `
<div class="cli-detail-section cli-output-section">
<h4><i data-lucide="bot" class="w-3.5 h-3.5"></i> Assistant Response</h4>
<h4><i data-lucide="bot" class="w-3.5 h-3.5"></i> ${t('cli.assistantResponse')}</h4>
<pre class="cli-detail-output">${escapeHtml(turn.output.stdout)}</pre>
</div>
` : ''}
@@ -340,7 +340,7 @@ async function showExecutionDetail(executionId, sourceDir) {
concatenatedPromptHtml = `
<div class="cli-concat-section" id="concatPromptSection" style="display: none;">
<div class="cli-detail-section">
<h4><i data-lucide="layers" class="w-3.5 h-3.5"></i> Concatenated Prompt (sent to CLI)</h4>
<h4><i data-lucide="layers" class="w-3.5 h-3.5"></i> ${t('cli.concatenatedPrompt')}</h4>
<div class="cli-concat-format-selector">
<button class="btn btn-xs ${true ? 'btn-primary' : 'btn-outline'}" onclick="switchConcatFormat('plain', '${executionId}')">Plain</button>
<button class="btn btn-xs btn-outline" onclick="switchConcatFormat('yaml', '${executionId}')">YAML</button>
@@ -372,7 +372,7 @@ async function showExecutionDetail(executionId, sourceDir) {
${hasNativeSession ? `
<div class="cli-detail-native-action">
<button class="btn btn-sm btn-primary" onclick="showNativeSessionDetail('${executionId}', '${sourceDirEscaped}')">
<i data-lucide="eye" class="w-3.5 h-3.5"></i> View Full Process Conversation
<i data-lucide="eye" class="w-3.5 h-3.5"></i> ${t('cli.viewFullConversation')}
</button>
</div>
` : ''}
@@ -380,10 +380,10 @@ async function showExecutionDetail(executionId, sourceDir) {
${turnCount > 1 ? `
<div class="cli-view-toggle">
<button class="btn btn-sm btn-outline active" onclick="toggleConversationView('turns')">
<i data-lucide="list" class="w-3.5 h-3.5"></i> Per-Turn View
<i data-lucide="list" class="w-3.5 h-3.5"></i> ${t('cli.perTurnView')}
</button>
<button class="btn btn-sm btn-outline" onclick="toggleConversationView('concat')">
<i data-lucide="layers" class="w-3.5 h-3.5"></i> Concatenated View
<i data-lucide="layers" class="w-3.5 h-3.5"></i> ${t('cli.concatenatedView')}
</button>
</div>
` : ''}
@@ -393,15 +393,15 @@ async function showExecutionDetail(executionId, sourceDir) {
${concatenatedPromptHtml}
<div class="cli-detail-actions">
<button class="btn btn-sm btn-outline" onclick="copyConversationId('${executionId}')">
<i data-lucide="copy" class="w-3.5 h-3.5"></i> Copy ID
<i data-lucide="copy" class="w-3.5 h-3.5"></i> ${t('cli.copyId')}
</button>
${turnCount > 1 ? `
<button class="btn btn-sm btn-outline" onclick="copyConcatenatedPrompt('${executionId}')">
<i data-lucide="clipboard-copy" class="w-3.5 h-3.5"></i> Copy Full Prompt
<i data-lucide="clipboard-copy" class="w-3.5 h-3.5"></i> ${t('cli.copyFullPrompt')}
</button>
` : ''}
<button class="btn btn-sm btn-outline btn-danger" onclick="confirmDeleteExecution('${executionId}'); closeModal();">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> Delete
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> ${t('cli.delete')}
</button>
</div>
`;
@@ -409,7 +409,7 @@ async function showExecutionDetail(executionId, sourceDir) {
// Store conversation data for format switching
window._currentConversation = conversation;
showModal('Conversation Detail', modalContent);
showModal(t('cli.conversationDetail'), modalContent);
}
// ========== Actions ==========
@@ -433,12 +433,12 @@ function searchCliHistory(query) {
async function refreshCliHistory() {
await loadCliHistory();
renderCliHistory();
showRefreshToast('History refreshed', 'success');
showRefreshToast(t('cli.historyRefreshed'), 'success');
}
// ========== Delete Execution ==========
function confirmDeleteExecution(executionId, sourceDir) {
if (confirm('Delete this execution record? This action cannot be undone.')) {
if (confirm(t('cli.confirmDelete'))) {
deleteExecution(executionId, sourceDir);
}
}
@@ -471,10 +471,10 @@ async function deleteExecution(executionId, sourceDir) {
} else {
renderCliHistory();
}
showRefreshToast('Execution deleted', 'success');
showRefreshToast(t('cli.executionDeleted'), 'success');
} catch (err) {
console.error('Failed to delete execution:', err);
showRefreshToast('Delete failed: ' + err.message, 'error');
showRefreshToast(t('cli.deleteFailed') + ': ' + err.message, 'error');
}
}
@@ -483,10 +483,10 @@ async function copyCliExecutionId(executionId) {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(executionId);
showRefreshToast('ID copied: ' + executionId, 'success');
showRefreshToast(t('cli.idCopied') + ': ' + executionId, 'success');
} catch (err) {
console.error('Failed to copy ID:', err);
showRefreshToast('Failed to copy ID', 'error');
showRefreshToast(t('cli.failedToCopy'), 'error');
}
}
}
@@ -494,16 +494,16 @@ async function copyCliExecutionId(executionId) {
async function copyExecutionPrompt(executionId) {
const detail = await loadExecutionDetail(executionId);
if (!detail) {
showRefreshToast('Execution not found', 'error');
showRefreshToast(t('cli.executionNotFound'), 'error');
return;
}
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(detail.prompt);
showRefreshToast('Prompt copied to clipboard', 'success');
showRefreshToast(t('cli.promptCopied'), 'success');
} catch (err) {
showRefreshToast('Failed to copy', 'error');
showRefreshToast(t('cli.failedToCopy'), 'error');
}
}
}
@@ -512,7 +512,7 @@ async function copyConversationId(conversationId) {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(conversationId);
showRefreshToast('ID copied to clipboard', 'success');
showRefreshToast(t('cli.idCopied'), 'success');
} catch (err) {
showRefreshToast('Failed to copy', 'error');
}
@@ -667,7 +667,7 @@ function switchConcatFormat(format, executionId) {
async function copyConcatenatedPrompt(executionId) {
var conversation = window._currentConversation;
if (!conversation) {
showRefreshToast('Conversation not found', 'error');
showRefreshToast(t('cli.conversationNotFound'), 'error');
return;
}
@@ -675,7 +675,7 @@ async function copyConcatenatedPrompt(executionId) {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(prompt);
showRefreshToast('Full prompt copied to clipboard', 'success');
showRefreshToast(t('cli.fullPromptCopied'), 'success');
} catch (err) {
showRefreshToast('Failed to copy', 'error');
}
@@ -692,7 +692,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
const nativeSession = await loadNativeSessionContent(executionId, sourceDir);
if (!nativeSession) {
showRefreshToast('Native session not found', 'error');
showRefreshToast(t('cli.nativeSessionNotFound'), 'error');
return;
}
@@ -718,7 +718,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
<details class="turn-thinking-details">
<summary class="turn-thinking-summary">
<i data-lucide="brain" class="w-3 h-3"></i>
💭 Thinking Process (${turn.thoughts.length} thoughts)
💭 ${t('cli.thinkingProcess')} (${turn.thoughts.length} ${t('cli.thoughts')})
</summary>
<div class="turn-thinking-content">
<ul class="native-thoughts-list">
@@ -734,7 +734,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
? `<div class="native-tools-section">
<div class="turn-tool-calls-header">
<i data-lucide="wrench" class="w-3 h-3"></i>
<strong>Tool Calls (${turn.toolCalls.length})</strong>
<strong>${t('cli.toolCalls')} (${turn.toolCalls.length})</strong>
</div>
<div class="native-tools-list">
${turn.toolCalls.map((tc, tcIdx) => `
@@ -768,11 +768,11 @@ async function showNativeSessionDetail(executionId, sourceDir) {
<div class="native-turn-header">
<span class="native-turn-role">
<i data-lucide="${roleIcon}" class="w-3.5 h-3.5"></i>
${turn.role === 'user' ? 'User' : 'Assistant'}
${turn.role === 'user' ? t('cli.user') : t('cli.assistant')}
</span>
<span class="native-turn-number">Turn ${turn.turnNumber}</span>
${tokenInfo}
${isLast ? '<span class="native-turn-latest">Latest</span>' : ''}
${isLast ? `<span class="native-turn-latest">${t('cli.latest')}</span>` : ''}
</div>
<div class="native-turn-content">
<pre>${escapeHtml(turn.content)}</pre>
@@ -788,7 +788,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
const totalTokensHtml = nativeSession.totalTokens
? `<div class="native-tokens-summary">
<i data-lucide="bar-chart-3" class="w-4 h-4"></i>
<strong>Total Tokens:</strong>
<strong>${t('cli.totalTokens')}:</strong>
${nativeSession.totalTokens.total || 0}
(Input: ${nativeSession.totalTokens.input || 0},
Output: ${nativeSession.totalTokens.output || 0}
@@ -816,13 +816,13 @@ async function showNativeSessionDetail(executionId, sourceDir) {
</div>
<div class="native-session-actions">
<button class="btn btn-sm btn-outline" onclick="copyNativeSessionId('${nativeSession.sessionId}')">
<i data-lucide="copy" class="w-3.5 h-3.5"></i> Copy Session ID
<i data-lucide="copy" class="w-3.5 h-3.5"></i> ${t('cli.copySessionId')}
</button>
<button class="btn btn-sm btn-outline" onclick="copyNativeSessionPath('${executionId}')">
<i data-lucide="file" class="w-3.5 h-3.5"></i> Copy File Path
<i data-lucide="file" class="w-3.5 h-3.5"></i> ${t('cli.copyFilePath')}
</button>
<button class="btn btn-sm btn-outline" onclick="exportNativeSession('${executionId}')">
<i data-lucide="download" class="w-3.5 h-3.5"></i> Export JSON
<i data-lucide="download" class="w-3.5 h-3.5"></i> ${t('cli.exportJson')}
</button>
</div>
</div>
@@ -831,7 +831,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
// Store for export
window._currentNativeSession = nativeSession;
showModal('Native Session Detail', modalContent, { size: 'lg' });
showModal(t('cli.nativeSessionDetail'), modalContent, { size: 'lg' });
}
/**
@@ -841,7 +841,7 @@ async function copyNativeSessionId(sessionId) {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(sessionId);
showRefreshToast('Session ID copied', 'success');
showRefreshToast(t('cli.sessionIdCopied'), 'success');
} catch (err) {
showRefreshToast('Failed to copy', 'error');
}
@@ -858,13 +858,13 @@ async function copyNativeSessionPath(executionId) {
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(exec.nativeSessionPath);
showRefreshToast('File path copied', 'success');
showRefreshToast(t('cli.filePathCopied'), 'success');
} catch (err) {
showRefreshToast('Failed to copy', 'error');
}
}
} else {
showRefreshToast('Path not available', 'error');
showRefreshToast(t('cli.pathNotAvailable'), 'error');
}
}
@@ -874,7 +874,7 @@ async function copyNativeSessionPath(executionId) {
function exportNativeSession(executionId) {
const session = window._currentNativeSession;
if (!session) {
showRefreshToast('No session data', 'error');
showRefreshToast(t('cli.noSessionData'), 'error');
return;
}
@@ -887,7 +887,7 @@ function exportNativeSession(executionId) {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showRefreshToast('Session exported', 'success');
showRefreshToast(t('cli.sessionExported'), 'success');
}
// ========== Helpers ==========

View File

@@ -289,6 +289,65 @@ const i18n = {
'cli.fileBrowserApiError': 'Server restart required to enable file browser',
'cli.fileBrowserManualHint': 'Type the full path above and click Select (e.g., C:\\Users\\name\\.gemini)',
// CLI History & Execution
'cli.executionHistory': 'Execution History',
'cli.conversationDetail': 'Conversation Detail',
'cli.nativeSessionDetail': 'Native Session Detail',
'cli.viewFullConversation': 'View Full Process Conversation',
'cli.perTurnView': 'Per-Turn View',
'cli.concatenatedView': 'Concatenated View',
'cli.userPrompt': 'User Prompt',
'cli.assistantResponse': 'Assistant Response',
'cli.user': 'User',
'cli.assistant': 'Assistant',
'cli.latest': 'Latest',
'cli.turn': 'Turn',
'cli.thinkingProcess': 'Thinking Process',
'cli.thoughts': 'thoughts',
'cli.toolCalls': 'Tool Calls',
'cli.totalTokens': 'Total Tokens',
'cli.input': 'Input',
'cli.output': 'Output',
'cli.cached': 'Cached',
'cli.concatenatedPrompt': 'Concatenated Prompt (sent to CLI)',
'cli.sessionId': 'Session ID',
// CLI History Actions & Buttons
'cli.refresh': 'Refresh',
'cli.copyId': 'Copy ID',
'cli.copyFullPrompt': 'Copy Full Prompt',
'cli.delete': 'Delete',
'cli.exportJson': 'Export JSON',
'cli.copyFilePath': 'Copy File Path',
'cli.copySessionId': 'Copy Session ID',
'cli.viewNativeSession': 'View Native Session',
'cli.viewDetails': 'View Details',
'cli.searchHistory': 'Search history...',
'cli.allTools': 'All Tools',
'cli.noExecutions': 'No executions yet',
'cli.noMatchingResults': 'No matching results',
// CLI History Messages
'cli.conversationNotFound': 'Conversation not found',
'cli.nativeSessionNotFound': 'Native session not found',
'cli.historyRefreshed': 'History refreshed',
'cli.executionDeleted': 'Execution deleted',
'cli.deleteFailed': 'Delete failed',
'cli.idCopied': 'ID copied',
'cli.promptCopied': 'Prompt copied to clipboard',
'cli.fullPromptCopied': 'Full prompt copied to clipboard',
'cli.sessionIdCopied': 'Session ID copied',
'cli.filePathCopied': 'File path copied',
'cli.pathNotAvailable': 'Path not available',
'cli.noSessionData': 'No session data',
'cli.sessionExported': 'Session exported',
'cli.failedToCopy': 'Failed to copy',
'cli.executionNotFound': 'Execution not found',
// CLI History Confirm Dialog
'cli.confirmDelete': 'Delete this execution record? This action cannot be undone.',
'cli.nativeSessionBadge': 'Native session',
// CodexLens Configuration
'codexlens.config': 'CodexLens Configuration',
'codexlens.configDesc': 'Manage code indexing, semantic search, and embedding models',
@@ -2205,6 +2264,7 @@ const i18n = {
'loop.add': 'Add',
'loop.save': 'Save',
'loop.cancel': 'Cancel',
'loop.loadToolsError': 'Failed to load available tools. Please try again.',
// Navigation & Grouping
'loop.nav.groupBy': 'Group By',
@@ -2874,6 +2934,65 @@ const i18n = {
'cli.fileBrowserApiError': '需要重启服务器以启用文件浏览器',
'cli.fileBrowserManualHint': '请在上方输入完整路径后点击选择(如 C:\\Users\\用户名\\.gemini',
// CLI 历史与执行
'cli.executionHistory': '执行历史',
'cli.conversationDetail': '对话详情',
'cli.nativeSessionDetail': '原生会话详情',
'cli.viewFullConversation': '查看完整过程对话',
'cli.perTurnView': '按轮次查看',
'cli.concatenatedView': '合并视图',
'cli.userPrompt': '用户提示词',
'cli.assistantResponse': '助手回复',
'cli.user': '用户',
'cli.assistant': '助手',
'cli.latest': '最新',
'cli.turn': '轮次',
'cli.thinkingProcess': '思考过程',
'cli.thoughts': '个思考',
'cli.toolCalls': '工具调用',
'cli.totalTokens': '总令牌数',
'cli.input': '输入',
'cli.output': '输出',
'cli.cached': '已缓存',
'cli.concatenatedPrompt': '合并提示词(发送至 CLI',
'cli.sessionId': '会话 ID',
// CLI 历史操作与按钮
'cli.refresh': '刷新',
'cli.copyId': '复制 ID',
'cli.copyFullPrompt': '复制完整提示词',
'cli.delete': '删除',
'cli.exportJson': '导出 JSON',
'cli.copyFilePath': '复制文件路径',
'cli.copySessionId': '复制会话 ID',
'cli.viewNativeSession': '查看原生会话',
'cli.viewDetails': '查看详情',
'cli.searchHistory': '搜索历史...',
'cli.allTools': '所有工具',
'cli.noExecutions': '暂无执行记录',
'cli.noMatchingResults': '无匹配结果',
// CLI 历史消息
'cli.conversationNotFound': '未找到对话',
'cli.nativeSessionNotFound': '未找到原生会话',
'cli.historyRefreshed': '历史已刷新',
'cli.executionDeleted': '执行记录已删除',
'cli.deleteFailed': '删除失败',
'cli.idCopied': 'ID 已复制',
'cli.promptCopied': '提示词已复制到剪贴板',
'cli.fullPromptCopied': '完整提示词已复制到剪贴板',
'cli.sessionIdCopied': '会话 ID 已复制',
'cli.filePathCopied': '文件路径已复制',
'cli.pathNotAvailable': '路径不可用',
'cli.noSessionData': '无会话数据',
'cli.sessionExported': '会话已导出',
'cli.failedToCopy': '复制失败',
'cli.executionNotFound': '未找到执行记录',
// CLI 历史确认对话框
'cli.confirmDelete': '删除此执行记录?此操作无法撤销。',
'cli.nativeSessionBadge': '原生会话',
// CodexLens 配置
'codexlens.config': 'CodexLens 配置',
'codexlens.configDesc': '管理代码索引、语义搜索和嵌入模型',
@@ -4802,6 +4921,7 @@ const i18n = {
'loop.add': '添加',
'loop.save': '保存',
'loop.cancel': '取消',
'loop.loadToolsError': '加载可用工具失败,请重试。',
// Navigation & Grouping
'loop.nav.groupBy': '分组',

View File

@@ -232,6 +232,19 @@ function encodeQwenProjectPath(projectDir: string): string {
.replace(/_/g, '-');
}
/**
* Encode a path to Claude Code's project folder name format
* D:\Claude_dms3 -> D--Claude-dms3 (same as Qwen)
* Rules: : -> -, \ -> -, _ -> -
*/
function encodeClaudeProjectPath(projectDir: string): string {
const absolutePath = resolve(projectDir);
return absolutePath
.replace(/:/g, '-')
.replace(/\\/g, '-')
.replace(/_/g, '-');
}
/**
* Qwen Session Discoverer
* New path: ~/.qwen/projects/<path-encoded>/chats/<uuid>.jsonl
@@ -576,9 +589,10 @@ class ClaudeSessionDiscoverer extends SessionDiscoverer {
// If workingDir provided, only look in that project's folder
let projectDirs: string[];
if (workingDir) {
const projectHash = calculateProjectHash(workingDir);
const projectPath = join(this.basePath, projectHash);
projectDirs = existsSync(projectPath) ? [projectHash] : [];
// Claude Code uses path encoding (D:\path -> D--path) not SHA256 hash
const encodedPath = encodeClaudeProjectPath(workingDir);
const projectPath = join(this.basePath, encodedPath);
projectDirs = existsSync(projectPath) ? [encodedPath] : [];
} else {
projectDirs = readdirSync(this.basePath).filter(d => {
const fullPath = join(this.basePath, d);
@@ -652,6 +666,7 @@ class ClaudeSessionDiscoverer extends SessionDiscoverer {
/**
* Extract first user message from Claude Code session file (.jsonl)
* Format: {"type":"user","message":{"role":"user","content":"..."},"isMeta":false,...}
* Content can be: string | array of {type,text} | array of {type,source} etc.
*/
extractFirstUserMessage(filePath: string): string | null {
try {
@@ -661,14 +676,30 @@ class ClaudeSessionDiscoverer extends SessionDiscoverer {
for (const line of lines) {
try {
const entry = JSON.parse(line);
// Claude Code format: type="user", message.role="user", message.content="..."
// Claude Code format: type="user", message.role="user", message.content can be string or array
// Skip meta messages and command messages
if (entry.type === 'user' &&
entry.message?.role === 'user' &&
entry.message?.content &&
!entry.isMeta &&
!entry.message.content.startsWith('<command-')) {
return entry.message.content;
!entry.isMeta) {
const msgContent = entry.message.content;
// Handle string content (simple case)
if (typeof msgContent === 'string') {
if (!msgContent.startsWith('<command-') && !msgContent.includes('<local-command')) {
return msgContent;
}
}
// Handle array content (can contain text, image, tool_result, etc.)
else if (Array.isArray(msgContent)) {
for (const item of msgContent) {
// Look for text items
if (item.type === 'text' && item.text) {
return item.text;
}
}
}
}
} catch { /* skip invalid lines */ }
}