mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
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:
@@ -102,7 +102,7 @@ function renderCliHistory() {
|
|||||||
if (cliExecutionHistory.length === 0) {
|
if (cliExecutionHistory.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="cli-history-header">
|
<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">
|
<div class="cli-history-controls">
|
||||||
${renderHistorySearch()}
|
${renderHistorySearch()}
|
||||||
${renderToolFilter()}
|
${renderToolFilter()}
|
||||||
@@ -113,7 +113,7 @@ function renderCliHistory() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<i data-lucide="terminal" class="w-8 h-8"></i>
|
<i data-lucide="terminal" class="w-8 h-8"></i>
|
||||||
<p>No executions yet</p>
|
<p>${t('cli.noExecutions')}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ function renderCliHistory() {
|
|||||||
const historyHtml = filteredHistory.length === 0
|
const historyHtml = filteredHistory.length === 0
|
||||||
? `<div class="empty-state">
|
? `<div class="empty-state">
|
||||||
<i data-lucide="search-x" class="w-6 h-6"></i>
|
<i data-lucide="search-x" class="w-6 h-6"></i>
|
||||||
<p>No matching results</p>
|
<p>${t('cli.noMatchingResults')}</p>
|
||||||
</div>`
|
</div>`
|
||||||
: filteredHistory.map(exec => {
|
: filteredHistory.map(exec => {
|
||||||
const statusIcon = exec.status === 'success' ? 'check-circle' :
|
const statusIcon = exec.status === 'success' ? 'check-circle' :
|
||||||
@@ -140,7 +140,7 @@ function renderCliHistory() {
|
|||||||
// Native session indicator
|
// Native session indicator
|
||||||
const hasNative = exec.hasNativeSession || exec.nativeSessionId;
|
const hasNative = exec.hasNativeSession || exec.nativeSessionId;
|
||||||
const nativeBadge = hasNative
|
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>
|
<i data-lucide="file-json" class="w-3 h-3"></i>
|
||||||
</span>`
|
</span>`
|
||||||
: '';
|
: '';
|
||||||
@@ -173,14 +173,14 @@ function renderCliHistory() {
|
|||||||
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||||
</button>
|
</button>
|
||||||
${hasNative ? `
|
${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>
|
<i data-lucide="file-json" class="w-3.5 h-3.5"></i>
|
||||||
</button>
|
</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>
|
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
|
||||||
</button>
|
</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>
|
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -211,7 +211,7 @@ function renderHistorySearch() {
|
|||||||
return `
|
return `
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="cli-history-search"
|
class="cli-history-search"
|
||||||
placeholder="Search history..."
|
placeholder="${t('cli.searchHistory')}"
|
||||||
value="${escapeHtml(cliHistorySearch)}"
|
value="${escapeHtml(cliHistorySearch)}"
|
||||||
onkeyup="searchCliHistory(this.value)"
|
onkeyup="searchCliHistory(this.value)"
|
||||||
oninput="searchCliHistory(this.value)">
|
oninput="searchCliHistory(this.value)">
|
||||||
@@ -224,7 +224,7 @@ function renderToolFilter() {
|
|||||||
<select class="cli-tool-filter" onchange="filterCliHistory(this.value)">
|
<select class="cli-tool-filter" onchange="filterCliHistory(this.value)">
|
||||||
${tools.map(tool => `
|
${tools.map(tool => `
|
||||||
<option value="${tool === 'all' ? '' : tool}" ${cliHistoryFilter === (tool === 'all' ? null : tool) ? 'selected' : ''}>
|
<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>
|
</option>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</select>
|
</select>
|
||||||
@@ -235,7 +235,7 @@ function renderToolFilter() {
|
|||||||
async function showExecutionDetail(executionId, sourceDir) {
|
async function showExecutionDetail(executionId, sourceDir) {
|
||||||
const conversation = await loadExecutionDetail(executionId, sourceDir);
|
const conversation = await loadExecutionDetail(executionId, sourceDir);
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
showRefreshToast('Conversation not found', 'error');
|
showRefreshToast(t('cli.conversationNotFound'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ async function showExecutionDetail(executionId, sourceDir) {
|
|||||||
<div class="cli-turn-header">
|
<div class="cli-turn-header">
|
||||||
<div class="cli-turn-marker">
|
<div class="cli-turn-marker">
|
||||||
<span class="cli-turn-number">${isFirst ? '▶' : '↳'} Turn ${turn.turn}</span>
|
<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>
|
||||||
<div class="cli-turn-meta">
|
<div class="cli-turn-meta">
|
||||||
<span class="cli-turn-time"><i data-lucide="clock" class="w-3 h-3"></i> ${turnTime}</span>
|
<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>
|
||||||
<div class="cli-turn-body">
|
<div class="cli-turn-body">
|
||||||
<div class="cli-detail-section cli-prompt-section">
|
<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>
|
<pre class="cli-detail-prompt">${escapeHtml(turn.prompt)}</pre>
|
||||||
</div>
|
</div>
|
||||||
${turn.output.stdout ? `
|
${turn.output.stdout ? `
|
||||||
<div class="cli-detail-section cli-output-section">
|
<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>
|
<pre class="cli-detail-output">${escapeHtml(turn.output.stdout)}</pre>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
@@ -340,7 +340,7 @@ async function showExecutionDetail(executionId, sourceDir) {
|
|||||||
concatenatedPromptHtml = `
|
concatenatedPromptHtml = `
|
||||||
<div class="cli-concat-section" id="concatPromptSection" style="display: none;">
|
<div class="cli-concat-section" id="concatPromptSection" style="display: none;">
|
||||||
<div class="cli-detail-section">
|
<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">
|
<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 ${true ? 'btn-primary' : 'btn-outline'}" onclick="switchConcatFormat('plain', '${executionId}')">Plain</button>
|
||||||
<button class="btn btn-xs btn-outline" onclick="switchConcatFormat('yaml', '${executionId}')">YAML</button>
|
<button class="btn btn-xs btn-outline" onclick="switchConcatFormat('yaml', '${executionId}')">YAML</button>
|
||||||
@@ -372,7 +372,7 @@ async function showExecutionDetail(executionId, sourceDir) {
|
|||||||
${hasNativeSession ? `
|
${hasNativeSession ? `
|
||||||
<div class="cli-detail-native-action">
|
<div class="cli-detail-native-action">
|
||||||
<button class="btn btn-sm btn-primary" onclick="showNativeSessionDetail('${executionId}', '${sourceDirEscaped}')">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
@@ -380,10 +380,10 @@ async function showExecutionDetail(executionId, sourceDir) {
|
|||||||
${turnCount > 1 ? `
|
${turnCount > 1 ? `
|
||||||
<div class="cli-view-toggle">
|
<div class="cli-view-toggle">
|
||||||
<button class="btn btn-sm btn-outline active" onclick="toggleConversationView('turns')">
|
<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>
|
||||||
<button class="btn btn-sm btn-outline" onclick="toggleConversationView('concat')">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
@@ -393,15 +393,15 @@ async function showExecutionDetail(executionId, sourceDir) {
|
|||||||
${concatenatedPromptHtml}
|
${concatenatedPromptHtml}
|
||||||
<div class="cli-detail-actions">
|
<div class="cli-detail-actions">
|
||||||
<button class="btn btn-sm btn-outline" onclick="copyConversationId('${executionId}')">
|
<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>
|
</button>
|
||||||
${turnCount > 1 ? `
|
${turnCount > 1 ? `
|
||||||
<button class="btn btn-sm btn-outline" onclick="copyConcatenatedPrompt('${executionId}')">
|
<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>
|
||||||
` : ''}
|
` : ''}
|
||||||
<button class="btn btn-sm btn-outline btn-danger" onclick="confirmDeleteExecution('${executionId}'); closeModal();">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -409,7 +409,7 @@ async function showExecutionDetail(executionId, sourceDir) {
|
|||||||
// Store conversation data for format switching
|
// Store conversation data for format switching
|
||||||
window._currentConversation = conversation;
|
window._currentConversation = conversation;
|
||||||
|
|
||||||
showModal('Conversation Detail', modalContent);
|
showModal(t('cli.conversationDetail'), modalContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Actions ==========
|
// ========== Actions ==========
|
||||||
@@ -433,12 +433,12 @@ function searchCliHistory(query) {
|
|||||||
async function refreshCliHistory() {
|
async function refreshCliHistory() {
|
||||||
await loadCliHistory();
|
await loadCliHistory();
|
||||||
renderCliHistory();
|
renderCliHistory();
|
||||||
showRefreshToast('History refreshed', 'success');
|
showRefreshToast(t('cli.historyRefreshed'), 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Delete Execution ==========
|
// ========== Delete Execution ==========
|
||||||
function confirmDeleteExecution(executionId, sourceDir) {
|
function confirmDeleteExecution(executionId, sourceDir) {
|
||||||
if (confirm('Delete this execution record? This action cannot be undone.')) {
|
if (confirm(t('cli.confirmDelete'))) {
|
||||||
deleteExecution(executionId, sourceDir);
|
deleteExecution(executionId, sourceDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,10 +471,10 @@ async function deleteExecution(executionId, sourceDir) {
|
|||||||
} else {
|
} else {
|
||||||
renderCliHistory();
|
renderCliHistory();
|
||||||
}
|
}
|
||||||
showRefreshToast('Execution deleted', 'success');
|
showRefreshToast(t('cli.executionDeleted'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to delete execution:', 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) {
|
if (navigator.clipboard) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(executionId);
|
await navigator.clipboard.writeText(executionId);
|
||||||
showRefreshToast('ID copied: ' + executionId, 'success');
|
showRefreshToast(t('cli.idCopied') + ': ' + executionId, 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy ID:', 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) {
|
async function copyExecutionPrompt(executionId) {
|
||||||
const detail = await loadExecutionDetail(executionId);
|
const detail = await loadExecutionDetail(executionId);
|
||||||
if (!detail) {
|
if (!detail) {
|
||||||
showRefreshToast('Execution not found', 'error');
|
showRefreshToast(t('cli.executionNotFound'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(detail.prompt);
|
await navigator.clipboard.writeText(detail.prompt);
|
||||||
showRefreshToast('Prompt copied to clipboard', 'success');
|
showRefreshToast(t('cli.promptCopied'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showRefreshToast('Failed to copy', 'error');
|
showRefreshToast(t('cli.failedToCopy'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,7 +512,7 @@ async function copyConversationId(conversationId) {
|
|||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(conversationId);
|
await navigator.clipboard.writeText(conversationId);
|
||||||
showRefreshToast('ID copied to clipboard', 'success');
|
showRefreshToast(t('cli.idCopied'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showRefreshToast('Failed to copy', 'error');
|
showRefreshToast('Failed to copy', 'error');
|
||||||
}
|
}
|
||||||
@@ -667,7 +667,7 @@ function switchConcatFormat(format, executionId) {
|
|||||||
async function copyConcatenatedPrompt(executionId) {
|
async function copyConcatenatedPrompt(executionId) {
|
||||||
var conversation = window._currentConversation;
|
var conversation = window._currentConversation;
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
showRefreshToast('Conversation not found', 'error');
|
showRefreshToast(t('cli.conversationNotFound'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,7 +675,7 @@ async function copyConcatenatedPrompt(executionId) {
|
|||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(prompt);
|
await navigator.clipboard.writeText(prompt);
|
||||||
showRefreshToast('Full prompt copied to clipboard', 'success');
|
showRefreshToast(t('cli.fullPromptCopied'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showRefreshToast('Failed to copy', 'error');
|
showRefreshToast('Failed to copy', 'error');
|
||||||
}
|
}
|
||||||
@@ -692,7 +692,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
|
|||||||
const nativeSession = await loadNativeSessionContent(executionId, sourceDir);
|
const nativeSession = await loadNativeSessionContent(executionId, sourceDir);
|
||||||
|
|
||||||
if (!nativeSession) {
|
if (!nativeSession) {
|
||||||
showRefreshToast('Native session not found', 'error');
|
showRefreshToast(t('cli.nativeSessionNotFound'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,7 +718,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
|
|||||||
<details class="turn-thinking-details">
|
<details class="turn-thinking-details">
|
||||||
<summary class="turn-thinking-summary">
|
<summary class="turn-thinking-summary">
|
||||||
<i data-lucide="brain" class="w-3 h-3"></i>
|
<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>
|
</summary>
|
||||||
<div class="turn-thinking-content">
|
<div class="turn-thinking-content">
|
||||||
<ul class="native-thoughts-list">
|
<ul class="native-thoughts-list">
|
||||||
@@ -734,7 +734,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
|
|||||||
? `<div class="native-tools-section">
|
? `<div class="native-tools-section">
|
||||||
<div class="turn-tool-calls-header">
|
<div class="turn-tool-calls-header">
|
||||||
<i data-lucide="wrench" class="w-3 h-3"></i>
|
<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>
|
||||||
<div class="native-tools-list">
|
<div class="native-tools-list">
|
||||||
${turn.toolCalls.map((tc, tcIdx) => `
|
${turn.toolCalls.map((tc, tcIdx) => `
|
||||||
@@ -768,11 +768,11 @@ async function showNativeSessionDetail(executionId, sourceDir) {
|
|||||||
<div class="native-turn-header">
|
<div class="native-turn-header">
|
||||||
<span class="native-turn-role">
|
<span class="native-turn-role">
|
||||||
<i data-lucide="${roleIcon}" class="w-3.5 h-3.5"></i>
|
<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>
|
||||||
<span class="native-turn-number">Turn ${turn.turnNumber}</span>
|
<span class="native-turn-number">Turn ${turn.turnNumber}</span>
|
||||||
${tokenInfo}
|
${tokenInfo}
|
||||||
${isLast ? '<span class="native-turn-latest">Latest</span>' : ''}
|
${isLast ? `<span class="native-turn-latest">${t('cli.latest')}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="native-turn-content">
|
<div class="native-turn-content">
|
||||||
<pre>${escapeHtml(turn.content)}</pre>
|
<pre>${escapeHtml(turn.content)}</pre>
|
||||||
@@ -788,7 +788,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
|
|||||||
const totalTokensHtml = nativeSession.totalTokens
|
const totalTokensHtml = nativeSession.totalTokens
|
||||||
? `<div class="native-tokens-summary">
|
? `<div class="native-tokens-summary">
|
||||||
<i data-lucide="bar-chart-3" class="w-4 h-4"></i>
|
<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}
|
${nativeSession.totalTokens.total || 0}
|
||||||
(Input: ${nativeSession.totalTokens.input || 0},
|
(Input: ${nativeSession.totalTokens.input || 0},
|
||||||
Output: ${nativeSession.totalTokens.output || 0}
|
Output: ${nativeSession.totalTokens.output || 0}
|
||||||
@@ -816,13 +816,13 @@ async function showNativeSessionDetail(executionId, sourceDir) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="native-session-actions">
|
<div class="native-session-actions">
|
||||||
<button class="btn btn-sm btn-outline" onclick="copyNativeSessionId('${nativeSession.sessionId}')">
|
<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>
|
||||||
<button class="btn btn-sm btn-outline" onclick="copyNativeSessionPath('${executionId}')">
|
<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>
|
||||||
<button class="btn btn-sm btn-outline" onclick="exportNativeSession('${executionId}')">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -831,7 +831,7 @@ async function showNativeSessionDetail(executionId, sourceDir) {
|
|||||||
// Store for export
|
// Store for export
|
||||||
window._currentNativeSession = nativeSession;
|
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) {
|
if (navigator.clipboard) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(sessionId);
|
await navigator.clipboard.writeText(sessionId);
|
||||||
showRefreshToast('Session ID copied', 'success');
|
showRefreshToast(t('cli.sessionIdCopied'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showRefreshToast('Failed to copy', 'error');
|
showRefreshToast('Failed to copy', 'error');
|
||||||
}
|
}
|
||||||
@@ -858,13 +858,13 @@ async function copyNativeSessionPath(executionId) {
|
|||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(exec.nativeSessionPath);
|
await navigator.clipboard.writeText(exec.nativeSessionPath);
|
||||||
showRefreshToast('File path copied', 'success');
|
showRefreshToast(t('cli.filePathCopied'), 'success');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showRefreshToast('Failed to copy', 'error');
|
showRefreshToast('Failed to copy', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showRefreshToast('Path not available', 'error');
|
showRefreshToast(t('cli.pathNotAvailable'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,7 +874,7 @@ async function copyNativeSessionPath(executionId) {
|
|||||||
function exportNativeSession(executionId) {
|
function exportNativeSession(executionId) {
|
||||||
const session = window._currentNativeSession;
|
const session = window._currentNativeSession;
|
||||||
if (!session) {
|
if (!session) {
|
||||||
showRefreshToast('No session data', 'error');
|
showRefreshToast(t('cli.noSessionData'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,7 +887,7 @@ function exportNativeSession(executionId) {
|
|||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
showRefreshToast('Session exported', 'success');
|
showRefreshToast(t('cli.sessionExported'), 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Helpers ==========
|
// ========== Helpers ==========
|
||||||
|
|||||||
@@ -289,6 +289,65 @@ const i18n = {
|
|||||||
'cli.fileBrowserApiError': 'Server restart required to enable file browser',
|
'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.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 Configuration
|
||||||
'codexlens.config': 'CodexLens Configuration',
|
'codexlens.config': 'CodexLens Configuration',
|
||||||
'codexlens.configDesc': 'Manage code indexing, semantic search, and embedding models',
|
'codexlens.configDesc': 'Manage code indexing, semantic search, and embedding models',
|
||||||
@@ -2205,6 +2264,7 @@ const i18n = {
|
|||||||
'loop.add': 'Add',
|
'loop.add': 'Add',
|
||||||
'loop.save': 'Save',
|
'loop.save': 'Save',
|
||||||
'loop.cancel': 'Cancel',
|
'loop.cancel': 'Cancel',
|
||||||
|
'loop.loadToolsError': 'Failed to load available tools. Please try again.',
|
||||||
|
|
||||||
// Navigation & Grouping
|
// Navigation & Grouping
|
||||||
'loop.nav.groupBy': 'Group By',
|
'loop.nav.groupBy': 'Group By',
|
||||||
@@ -2874,6 +2934,65 @@ const i18n = {
|
|||||||
'cli.fileBrowserApiError': '需要重启服务器以启用文件浏览器',
|
'cli.fileBrowserApiError': '需要重启服务器以启用文件浏览器',
|
||||||
'cli.fileBrowserManualHint': '请在上方输入完整路径后点击选择(如 C:\\Users\\用户名\\.gemini)',
|
'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 配置
|
||||||
'codexlens.config': 'CodexLens 配置',
|
'codexlens.config': 'CodexLens 配置',
|
||||||
'codexlens.configDesc': '管理代码索引、语义搜索和嵌入模型',
|
'codexlens.configDesc': '管理代码索引、语义搜索和嵌入模型',
|
||||||
@@ -4802,6 +4921,7 @@ const i18n = {
|
|||||||
'loop.add': '添加',
|
'loop.add': '添加',
|
||||||
'loop.save': '保存',
|
'loop.save': '保存',
|
||||||
'loop.cancel': '取消',
|
'loop.cancel': '取消',
|
||||||
|
'loop.loadToolsError': '加载可用工具失败,请重试。',
|
||||||
|
|
||||||
// Navigation & Grouping
|
// Navigation & Grouping
|
||||||
'loop.nav.groupBy': '分组',
|
'loop.nav.groupBy': '分组',
|
||||||
|
|||||||
@@ -232,6 +232,19 @@ function encodeQwenProjectPath(projectDir: string): string {
|
|||||||
.replace(/_/g, '-');
|
.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
|
* Qwen Session Discoverer
|
||||||
* New path: ~/.qwen/projects/<path-encoded>/chats/<uuid>.jsonl
|
* 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
|
// If workingDir provided, only look in that project's folder
|
||||||
let projectDirs: string[];
|
let projectDirs: string[];
|
||||||
if (workingDir) {
|
if (workingDir) {
|
||||||
const projectHash = calculateProjectHash(workingDir);
|
// Claude Code uses path encoding (D:\path -> D--path) not SHA256 hash
|
||||||
const projectPath = join(this.basePath, projectHash);
|
const encodedPath = encodeClaudeProjectPath(workingDir);
|
||||||
projectDirs = existsSync(projectPath) ? [projectHash] : [];
|
const projectPath = join(this.basePath, encodedPath);
|
||||||
|
projectDirs = existsSync(projectPath) ? [encodedPath] : [];
|
||||||
} else {
|
} else {
|
||||||
projectDirs = readdirSync(this.basePath).filter(d => {
|
projectDirs = readdirSync(this.basePath).filter(d => {
|
||||||
const fullPath = join(this.basePath, 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)
|
* Extract first user message from Claude Code session file (.jsonl)
|
||||||
* Format: {"type":"user","message":{"role":"user","content":"..."},"isMeta":false,...}
|
* 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 {
|
extractFirstUserMessage(filePath: string): string | null {
|
||||||
try {
|
try {
|
||||||
@@ -661,14 +676,30 @@ class ClaudeSessionDiscoverer extends SessionDiscoverer {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
try {
|
try {
|
||||||
const entry = JSON.parse(line);
|
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
|
// Skip meta messages and command messages
|
||||||
if (entry.type === 'user' &&
|
if (entry.type === 'user' &&
|
||||||
entry.message?.role === 'user' &&
|
entry.message?.role === 'user' &&
|
||||||
entry.message?.content &&
|
entry.message?.content &&
|
||||||
!entry.isMeta &&
|
!entry.isMeta) {
|
||||||
!entry.message.content.startsWith('<command-')) {
|
|
||||||
return entry.message.content;
|
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 */ }
|
} catch { /* skip invalid lines */ }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user