mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
feat: Implement resume strategy engine and session content parser
- Added `resume-strategy.ts` to determine optimal resume approaches including native, prompt concatenation, and hybrid modes. - Introduced `determineResumeStrategy` function to evaluate various resume scenarios. - Created utility functions for building context prefixes and formatting outputs in plain, YAML, and JSON formats. - Added `session-content-parser.ts` to parse native CLI tool session files supporting Gemini/Qwen JSON and Codex JSONL formats. - Implemented parsing logic for different session formats, including error handling for invalid lines. - Provided functions to format conversations and extract user-assistant pairs from parsed sessions.
This commit is contained in:
@@ -1,20 +1,24 @@
|
||||
// CLI History Component
|
||||
// Displays execution history with filtering, search, and delete
|
||||
// Supports native session linking and full conversation parsing
|
||||
|
||||
// ========== CLI History State ==========
|
||||
let cliExecutionHistory = [];
|
||||
let cliHistoryFilter = null; // Filter by tool
|
||||
let cliHistorySearch = ''; // Search query
|
||||
let cliHistoryLimit = 50;
|
||||
let showNativeOnly = false; // Filter to show only native-linked executions
|
||||
|
||||
// ========== Data Loading ==========
|
||||
async function loadCliHistory(options = {}) {
|
||||
try {
|
||||
const { limit = cliHistoryLimit, tool = cliHistoryFilter, status = null } = options;
|
||||
|
||||
let url = `/api/cli/history?path=${encodeURIComponent(projectPath)}&limit=${limit}`;
|
||||
// Use history-native endpoint to get native session info
|
||||
let url = `/api/cli/history-native?path=${encodeURIComponent(projectPath)}&limit=${limit}`;
|
||||
if (tool) url += `&tool=${tool}`;
|
||||
if (status) url += `&status=${status}`;
|
||||
if (cliHistorySearch) url += `&search=${encodeURIComponent(cliHistorySearch)}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Failed to load CLI history');
|
||||
@@ -28,6 +32,32 @@ async function loadCliHistory(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load native session content for a specific execution
|
||||
async function loadNativeSessionContent(executionId) {
|
||||
try {
|
||||
const url = `/api/cli/native-session?path=${encodeURIComponent(projectPath)}&id=${encodeURIComponent(executionId)}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load native session:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Load enriched conversation (CCW + Native merged)
|
||||
async function loadEnrichedConversation(executionId) {
|
||||
try {
|
||||
const url = `/api/cli/enriched?path=${encodeURIComponent(projectPath)}&id=${encodeURIComponent(executionId)}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load enriched conversation:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExecutionDetail(executionId, sourceDir) {
|
||||
try {
|
||||
// If sourceDir provided, use it to build the correct path
|
||||
@@ -95,22 +125,39 @@ function renderCliHistory() {
|
||||
? `<span class="cli-turn-badge">${exec.turn_count} turns</span>`
|
||||
: '';
|
||||
|
||||
// Native session indicator
|
||||
const hasNative = exec.hasNativeSession || exec.nativeSessionId;
|
||||
const nativeBadge = hasNative
|
||||
? `<span class="cli-native-badge" title="Native session: ${exec.nativeSessionId}">
|
||||
<i data-lucide="file-json" class="w-3 h-3"></i>
|
||||
</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="cli-history-item">
|
||||
<div class="cli-history-item ${hasNative ? 'has-native' : ''}">
|
||||
<div class="cli-history-item-content" onclick="showExecutionDetail('${exec.id}')">
|
||||
<div class="cli-history-item-header">
|
||||
<span class="cli-tool-tag cli-tool-${exec.tool}">${exec.tool}</span>
|
||||
${turnBadge}
|
||||
<span class="cli-history-time">${timeAgo}</span>
|
||||
<i data-lucide="${statusIcon}" class="w-3.5 h-3.5 ${statusClass}"></i>
|
||||
<span class="cli-tool-tag cli-tool-${exec.tool}">${exec.tool.toUpperCase()}</span>
|
||||
<span class="cli-mode-tag">${exec.mode || 'analysis'}</span>
|
||||
<span class="cli-status-badge ${statusClass}">
|
||||
<i data-lucide="${statusIcon}" class="w-3 h-3"></i> ${exec.status}
|
||||
</span>
|
||||
${nativeBadge}
|
||||
</div>
|
||||
<div class="cli-history-prompt">${escapeHtml(exec.prompt_preview)}</div>
|
||||
<div class="cli-history-meta">
|
||||
<span>${duration}</span>
|
||||
<span>${exec.mode || 'analysis'}</span>
|
||||
<span><i data-lucide="clock" class="w-3 h-3"></i> ${timeAgo}</span>
|
||||
<span><i data-lucide="timer" class="w-3 h-3"></i> ${duration}</span>
|
||||
<span><i data-lucide="hash" class="w-3 h-3"></i> ${exec.id.split('-')[0]}</span>
|
||||
${turnBadge}
|
||||
</div>
|
||||
</div>
|
||||
<div class="cli-history-actions">
|
||||
${hasNative ? `
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); showNativeSessionDetail('${exec.id}')" title="View Native Session">
|
||||
<i data-lucide="file-json" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail('${exec.id}')" title="View Details">
|
||||
<i data-lucide="eye" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
@@ -588,6 +635,188 @@ async function copyConcatenatedPrompt(executionId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Native Session Detail ==========
|
||||
|
||||
/**
|
||||
* Show native session detail modal with full conversation content
|
||||
*/
|
||||
async function showNativeSessionDetail(executionId) {
|
||||
// Load native session content
|
||||
const nativeSession = await loadNativeSessionContent(executionId);
|
||||
|
||||
if (!nativeSession) {
|
||||
showRefreshToast('Native session not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build turns HTML from native session
|
||||
const turnsHtml = nativeSession.turns && nativeSession.turns.length > 0
|
||||
? nativeSession.turns.map((turn, idx) => {
|
||||
const isLast = idx === nativeSession.turns.length - 1;
|
||||
const roleIcon = turn.role === 'user' ? 'user' : 'bot';
|
||||
const roleClass = turn.role === 'user' ? 'user' : 'assistant';
|
||||
|
||||
// Token info
|
||||
const tokenInfo = turn.tokens
|
||||
? `<span class="native-turn-tokens">
|
||||
<i data-lucide="coins" class="w-3 h-3"></i>
|
||||
${turn.tokens.total || 0} tokens
|
||||
(in: ${turn.tokens.input || 0}, out: ${turn.tokens.output || 0}${turn.tokens.cached ? `, cached: ${turn.tokens.cached}` : ''})
|
||||
</span>`
|
||||
: '';
|
||||
|
||||
// Thoughts section
|
||||
const thoughtsHtml = turn.thoughts && turn.thoughts.length > 0
|
||||
? `<div class="native-thoughts-section">
|
||||
<h5><i data-lucide="brain" class="w-3 h-3"></i> Thoughts</h5>
|
||||
<ul class="native-thoughts-list">
|
||||
${turn.thoughts.map(t => `<li>${escapeHtml(t)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
// Tool calls section
|
||||
const toolCallsHtml = turn.toolCalls && turn.toolCalls.length > 0
|
||||
? `<div class="native-tools-section">
|
||||
<h5><i data-lucide="wrench" class="w-3 h-3"></i> Tool Calls (${turn.toolCalls.length})</h5>
|
||||
<div class="native-tools-list">
|
||||
${turn.toolCalls.map(tc => `
|
||||
<div class="native-tool-call">
|
||||
<span class="native-tool-name">${escapeHtml(tc.name)}</span>
|
||||
${tc.output ? `<pre class="native-tool-output">${escapeHtml(tc.output.substring(0, 500))}${tc.output.length > 500 ? '...' : ''}</pre>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="native-turn ${roleClass} ${isLast ? 'latest' : ''}">
|
||||
<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'}
|
||||
</span>
|
||||
<span class="native-turn-number">Turn ${turn.turnNumber}</span>
|
||||
${tokenInfo}
|
||||
${isLast ? '<span class="native-turn-latest">Latest</span>' : ''}
|
||||
</div>
|
||||
<div class="native-turn-content">
|
||||
<pre>${escapeHtml(turn.content)}</pre>
|
||||
</div>
|
||||
${thoughtsHtml}
|
||||
${toolCallsHtml}
|
||||
</div>
|
||||
`;
|
||||
}).join('')
|
||||
: '<p class="text-muted-foreground">No conversation turns found</p>';
|
||||
|
||||
// Total tokens summary
|
||||
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>
|
||||
${nativeSession.totalTokens.total || 0}
|
||||
(Input: ${nativeSession.totalTokens.input || 0},
|
||||
Output: ${nativeSession.totalTokens.output || 0}
|
||||
${nativeSession.totalTokens.cached ? `, Cached: ${nativeSession.totalTokens.cached}` : ''})
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const modalContent = `
|
||||
<div class="native-session-detail">
|
||||
<div class="native-session-header">
|
||||
<div class="native-session-info">
|
||||
<span class="cli-tool-tag cli-tool-${nativeSession.tool}">${nativeSession.tool.toUpperCase()}</span>
|
||||
${nativeSession.model ? `<span class="native-model"><i data-lucide="cpu" class="w-3 h-3"></i> ${nativeSession.model}</span>` : ''}
|
||||
<span class="native-session-id"><i data-lucide="fingerprint" class="w-3 h-3"></i> ${nativeSession.sessionId}</span>
|
||||
</div>
|
||||
<div class="native-session-meta">
|
||||
<span><i data-lucide="calendar" class="w-3 h-3"></i> ${new Date(nativeSession.startTime).toLocaleString()}</span>
|
||||
${nativeSession.workingDir ? `<span><i data-lucide="folder" class="w-3 h-3"></i> ${nativeSession.workingDir}</span>` : ''}
|
||||
${nativeSession.projectHash ? `<span><i data-lucide="hash" class="w-3 h-3"></i> ${nativeSession.projectHash.substring(0, 12)}...</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
${totalTokensHtml}
|
||||
<div class="native-turns-container">
|
||||
${turnsHtml}
|
||||
</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
|
||||
</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
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Store for export
|
||||
window._currentNativeSession = nativeSession;
|
||||
|
||||
showModal('Native Session Detail', modalContent, 'modal-lg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy native session ID to clipboard
|
||||
*/
|
||||
async function copyNativeSessionId(sessionId) {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(sessionId);
|
||||
showRefreshToast('Session ID copied', 'success');
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed to copy', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy native session file path
|
||||
*/
|
||||
async function copyNativeSessionPath(executionId) {
|
||||
// Find execution in history
|
||||
const exec = cliExecutionHistory.find(e => e.id === executionId);
|
||||
if (exec && exec.nativeSessionPath) {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(exec.nativeSessionPath);
|
||||
showRefreshToast('File path copied', 'success');
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed to copy', 'error');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showRefreshToast('Path not available', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export native session as JSON file
|
||||
*/
|
||||
function exportNativeSession(executionId) {
|
||||
const session = window._currentNativeSession;
|
||||
if (!session) {
|
||||
showRefreshToast('No session data', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(session, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `native-session-${session.sessionId}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
showRefreshToast('Session exported', 'success');
|
||||
}
|
||||
|
||||
// ========== Helpers ==========
|
||||
function formatDuration(ms) {
|
||||
if (ms >= 60000) {
|
||||
|
||||
@@ -12,6 +12,9 @@ let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; /
|
||||
let smartContextEnabled = localStorage.getItem('ccw-smart-context') === 'true';
|
||||
let smartContextMaxFiles = parseInt(localStorage.getItem('ccw-smart-context-max-files') || '10', 10);
|
||||
|
||||
// Native Resume settings
|
||||
let nativeResumeEnabled = localStorage.getItem('ccw-native-resume') !== 'false'; // default true
|
||||
|
||||
// ========== Initialization ==========
|
||||
function initCliStatus() {
|
||||
// Load CLI status on init
|
||||
@@ -256,6 +259,19 @@ function renderCliStatus() {
|
||||
</div>
|
||||
<p class="cli-setting-desc">Auto-analyze prompt and add relevant file paths</p>
|
||||
</div>
|
||||
<div class="cli-setting-item">
|
||||
<label class="cli-setting-label">
|
||||
<i data-lucide="refresh-cw" class="w-3 h-3"></i>
|
||||
Native Resume
|
||||
</label>
|
||||
<div class="cli-setting-control">
|
||||
<label class="cli-toggle">
|
||||
<input type="checkbox" ${nativeResumeEnabled ? 'checked' : ''} onchange="setNativeResumeEnabled(this.checked)">
|
||||
<span class="cli-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="cli-setting-desc">Use native tool resume (gemini -r, qwen --resume, codex resume)</p>
|
||||
</div>
|
||||
<div class="cli-setting-item ${!smartContextEnabled ? 'disabled' : ''}">
|
||||
<label class="cli-setting-label">
|
||||
<i data-lucide="files" class="w-3 h-3"></i>
|
||||
@@ -326,6 +342,12 @@ function setSmartContextMaxFiles(max) {
|
||||
showRefreshToast(`Smart Context max files set to ${max}`, 'success');
|
||||
}
|
||||
|
||||
function setNativeResumeEnabled(enabled) {
|
||||
nativeResumeEnabled = enabled;
|
||||
localStorage.setItem('ccw-native-resume', enabled.toString());
|
||||
showRefreshToast(`Native Resume ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
||||
}
|
||||
|
||||
async function refreshAllCliStatus() {
|
||||
await Promise.all([loadCliToolStatus(), loadCodexLensStatus()]);
|
||||
renderCliStatus();
|
||||
|
||||
@@ -12,8 +12,8 @@ const HOOK_TEMPLATES = {
|
||||
'ccw-notify': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'curl',
|
||||
args: ['-s', '-X', 'POST', '-H', 'Content-Type: application/json', '-d', '{"type":"summary_written","filePath":"$CLAUDE_FILE_PATHS"}', 'http://localhost:3456/api/hook'],
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); FILE_PATH=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); [ -n "$FILE_PATH" ] && curl -s -X POST -H "Content-Type: application/json" -d "{\\"type\\":\\"file_written\\",\\"filePath\\":\\"$FILE_PATH\\"}" http://localhost:3456/api/hook || true'],
|
||||
description: 'Notify CCW dashboard when files are written',
|
||||
category: 'notification'
|
||||
},
|
||||
@@ -21,7 +21,7 @@ const HOOK_TEMPLATES = {
|
||||
event: 'PostToolUse',
|
||||
matcher: '',
|
||||
command: 'bash',
|
||||
args: ['-c', 'echo "[$(date)] Tool: $CLAUDE_TOOL_NAME, Files: $CLAUDE_FILE_PATHS" >> ~/.claude/tool-usage.log'],
|
||||
args: ['-c', 'INPUT=$(cat); TOOL=$(echo "$INPUT" | jq -r ".tool_name // empty"); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); echo "[$(date)] Tool: $TOOL, File: $FILE" >> ~/.claude/tool-usage.log'],
|
||||
description: 'Log all tool executions to a file',
|
||||
category: 'logging'
|
||||
},
|
||||
@@ -29,7 +29,7 @@ const HOOK_TEMPLATES = {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'bash',
|
||||
args: ['-c', 'for f in $CLAUDE_FILE_PATHS; do if [[ "$f" =~ \\.(js|ts|jsx|tsx)$ ]]; then npx eslint "$f" --fix 2>/dev/null || true; fi; done'],
|
||||
args: ['-c', 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // empty"); if [[ "$FILE" =~ \\.(js|ts|jsx|tsx)$ ]]; then npx eslint "$FILE" --fix 2>/dev/null || true; fi'],
|
||||
description: 'Run ESLint on JavaScript/TypeScript files after write',
|
||||
category: 'quality'
|
||||
},
|
||||
@@ -37,7 +37,7 @@ const HOOK_TEMPLATES = {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'bash',
|
||||
args: ['-c', 'for f in $CLAUDE_FILE_PATHS; do git add "$f" 2>/dev/null || true; done'],
|
||||
args: ['-c', 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // empty"); [ -n "$FILE" ] && git add "$FILE" 2>/dev/null || true'],
|
||||
description: 'Automatically stage written files to git',
|
||||
category: 'git'
|
||||
},
|
||||
@@ -45,7 +45,7 @@ const HOOK_TEMPLATES = {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'bash',
|
||||
args: ['-c', 'if [ -d ".codexlens" ] && [ -n "$CLAUDE_FILE_PATHS" ]; then python -m codexlens update $CLAUDE_FILE_PATHS --json 2>/dev/null || ~/.codexlens/venv/bin/python -m codexlens update $CLAUDE_FILE_PATHS --json 2>/dev/null || true; fi'],
|
||||
args: ['-c', 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); [ -d ".codexlens" ] && [ -n "$FILE" ] && (python -m codexlens update "$FILE" --json 2>/dev/null || ~/.codexlens/venv/bin/python -m codexlens update "$FILE" --json 2>/dev/null || true)'],
|
||||
description: 'Auto-update code index when files are written or edited',
|
||||
category: 'indexing'
|
||||
},
|
||||
@@ -80,7 +80,7 @@ const HOOK_TEMPLATES = {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'bash',
|
||||
args: ['-c', 'ccw tool exec skill_context_loader \'{"keywords":"$SKILL_KEYWORDS","skills":"$SKILL_NAMES","prompt":"$CLAUDE_PROMPT"}\''],
|
||||
args: ['-c', 'ccw tool exec skill_context_loader --stdin'],
|
||||
description: 'Load SKILL context based on keyword matching in user prompt',
|
||||
category: 'skill',
|
||||
configurable: true,
|
||||
@@ -93,10 +93,37 @@ const HOOK_TEMPLATES = {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'bash',
|
||||
args: ['-c', 'ccw tool exec skill_context_loader \'{"mode":"auto","prompt":"$CLAUDE_PROMPT"}\''],
|
||||
args: ['-c', 'ccw tool exec skill_context_loader --stdin --mode auto'],
|
||||
description: 'Auto-detect and load SKILL based on skill name in prompt',
|
||||
category: 'skill',
|
||||
configurable: false
|
||||
},
|
||||
'memory-file-read': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Read|mcp__ccw-tools__read_file',
|
||||
command: 'ccw',
|
||||
args: ['memory', 'track', '--type', 'file', '--action', 'read', '--stdin'],
|
||||
description: 'Track file reads to build context heatmap',
|
||||
category: 'memory',
|
||||
timeout: 5000
|
||||
},
|
||||
'memory-file-write': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write|Edit|mcp__ccw-tools__write_file|mcp__ccw-tools__edit_file',
|
||||
command: 'ccw',
|
||||
args: ['memory', 'track', '--type', 'file', '--action', 'write', '--stdin'],
|
||||
description: 'Track file modifications to identify core modules',
|
||||
category: 'memory',
|
||||
timeout: 5000
|
||||
},
|
||||
'memory-prompt-track': {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'ccw',
|
||||
args: ['memory', 'track', '--type', 'topic', '--action', 'mention', '--stdin'],
|
||||
description: 'Record user prompts for pattern analysis',
|
||||
category: 'memory',
|
||||
timeout: 5000
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,6 +174,33 @@ const WIZARD_TEMPLATES = {
|
||||
configFields: [],
|
||||
requiresSkillDiscovery: true,
|
||||
customRenderer: 'renderSkillContextConfig'
|
||||
},
|
||||
'memory-setup': {
|
||||
name: 'Memory Module Setup',
|
||||
description: 'Configure automatic context tracking',
|
||||
icon: 'brain',
|
||||
options: [
|
||||
{
|
||||
id: 'file-read',
|
||||
name: 'File Read Tracker',
|
||||
description: 'Track file reads to build context heatmap',
|
||||
templateId: 'memory-file-read'
|
||||
},
|
||||
{
|
||||
id: 'file-write',
|
||||
name: 'File Write Tracker',
|
||||
description: 'Track file modifications to identify core modules',
|
||||
templateId: 'memory-file-write'
|
||||
},
|
||||
{
|
||||
id: 'prompts',
|
||||
name: 'Prompt Tracker',
|
||||
description: 'Record user prompts for pattern analysis',
|
||||
templateId: 'memory-prompt-track'
|
||||
}
|
||||
],
|
||||
configFields: [],
|
||||
multiSelect: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,8 +235,60 @@ async function loadHookConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert internal hook format to Claude Code format
|
||||
* Internal: { command, args, matcher, timeout }
|
||||
* Claude Code: { matcher, hooks: [{ type: "command", command: "...", timeout }] }
|
||||
*/
|
||||
function convertToClaudeCodeFormat(hookData) {
|
||||
// If already in correct format, return as-is
|
||||
if (hookData.hooks && Array.isArray(hookData.hooks)) {
|
||||
return hookData;
|
||||
}
|
||||
|
||||
// Build command string from command + args
|
||||
let commandStr = hookData.command || '';
|
||||
if (hookData.args && Array.isArray(hookData.args)) {
|
||||
// Join args, properly quoting if needed
|
||||
const quotedArgs = hookData.args.map(arg => {
|
||||
if (arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) {
|
||||
return `"${arg.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
commandStr = `${commandStr} ${quotedArgs.join(' ')}`.trim();
|
||||
}
|
||||
|
||||
const converted = {
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: commandStr
|
||||
}]
|
||||
};
|
||||
|
||||
// Add matcher if present (not needed for UserPromptSubmit, Stop, etc.)
|
||||
if (hookData.matcher) {
|
||||
converted.matcher = hookData.matcher;
|
||||
}
|
||||
|
||||
// Add timeout if present (in seconds for Claude Code)
|
||||
if (hookData.timeout) {
|
||||
converted.hooks[0].timeout = Math.ceil(hookData.timeout / 1000);
|
||||
}
|
||||
|
||||
// Preserve replaceIndex for updates
|
||||
if (hookData.replaceIndex !== undefined) {
|
||||
converted.replaceIndex = hookData.replaceIndex;
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
async function saveHook(scope, event, hookData) {
|
||||
try {
|
||||
// Convert to Claude Code format before saving
|
||||
const convertedHookData = convertToClaudeCodeFormat(hookData);
|
||||
|
||||
const response = await fetch('/api/hooks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -190,7 +296,7 @@ async function saveHook(scope, event, hookData) {
|
||||
projectPath: projectPath,
|
||||
scope: scope,
|
||||
event: event,
|
||||
hookData: hookData
|
||||
hookData: convertedHookData
|
||||
})
|
||||
});
|
||||
|
||||
@@ -419,6 +525,11 @@ function openHookWizardModal(wizardId) {
|
||||
wizardConfig[field.key] = field.default;
|
||||
});
|
||||
|
||||
// Initialize selectedOptions for multi-select wizards
|
||||
if (wizard.multiSelect) {
|
||||
wizardConfig.selectedOptions = [];
|
||||
}
|
||||
|
||||
const modal = document.getElementById('hookWizardModal');
|
||||
if (modal) {
|
||||
renderWizardModalContent();
|
||||
@@ -445,8 +556,10 @@ function renderWizardModalContent() {
|
||||
|
||||
// Get translated wizard name and description
|
||||
const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') :
|
||||
wizardId === 'memory-setup' ? t('hook.wizard.memorySetup') :
|
||||
wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name;
|
||||
const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') :
|
||||
wizardId === 'memory-setup' ? t('hook.wizard.memorySetupDesc') :
|
||||
wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description;
|
||||
|
||||
// Helper to get translated option names
|
||||
@@ -455,6 +568,11 @@ function renderWizardModalContent() {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEnd');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdate');
|
||||
}
|
||||
if (wizardId === 'memory-setup') {
|
||||
if (optId === 'file-read') return t('hook.wizard.fileReadTracker');
|
||||
if (optId === 'file-write') return t('hook.wizard.fileWriteTracker');
|
||||
if (optId === 'prompts') return t('hook.wizard.promptTracker');
|
||||
}
|
||||
if (wizardId === 'skill-context') {
|
||||
if (optId === 'keyword') return t('hook.wizard.keywordMatching');
|
||||
if (optId === 'auto') return t('hook.wizard.autoDetection');
|
||||
@@ -467,6 +585,11 @@ function renderWizardModalContent() {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc');
|
||||
}
|
||||
if (wizardId === 'memory-setup') {
|
||||
if (optId === 'file-read') return t('hook.wizard.fileReadTrackerDesc');
|
||||
if (optId === 'file-write') return t('hook.wizard.fileWriteTrackerDesc');
|
||||
if (optId === 'prompts') return t('hook.wizard.promptTrackerDesc');
|
||||
}
|
||||
if (wizardId === 'skill-context') {
|
||||
if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc');
|
||||
if (optId === 'auto') return t('hook.wizard.autoDetectionDesc');
|
||||
@@ -508,9 +631,23 @@ function renderWizardModalContent() {
|
||||
|
||||
<!-- Trigger Type Selection -->
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-foreground">${t('hook.wizard.whenToTrigger')}</label>
|
||||
<label class="block text-sm font-medium text-foreground">${wizard.multiSelect ? t('hook.wizard.selectTrackers') : t('hook.wizard.whenToTrigger')}</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
${wizard.options.map(opt => `
|
||||
${wizard.multiSelect ? wizard.options.map(opt => {
|
||||
const isSelected = wizardConfig.selectedOptions?.includes(opt.id) || false;
|
||||
return `
|
||||
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-all ${isSelected ? 'border-primary bg-primary/5' : 'border-border hover:border-muted-foreground'}">
|
||||
<input type="checkbox" name="wizardTrigger" value="${opt.id}"
|
||||
${isSelected ? 'checked' : ''}
|
||||
onchange="toggleWizardOption('${opt.id}')"
|
||||
class="mt-1">
|
||||
<div class="flex-1">
|
||||
<span class="font-medium text-foreground">${escapeHtml(getOptionName(opt.id))}</span>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(getOptionDesc(opt.id))}</p>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
}).join('') : wizard.options.map(opt => `
|
||||
<label class="flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-all ${selectedOption === opt.id ? 'border-primary bg-primary/5' : 'border-border hover:border-muted-foreground'}">
|
||||
<input type="radio" name="wizardTrigger" value="${opt.id}"
|
||||
${selectedOption === opt.id ? 'checked' : ''}
|
||||
@@ -609,6 +746,21 @@ function updateWizardTrigger(triggerId) {
|
||||
renderWizardModalContent();
|
||||
}
|
||||
|
||||
function toggleWizardOption(optionId) {
|
||||
if (!wizardConfig.selectedOptions) {
|
||||
wizardConfig.selectedOptions = [];
|
||||
}
|
||||
|
||||
const index = wizardConfig.selectedOptions.indexOf(optionId);
|
||||
if (index === -1) {
|
||||
wizardConfig.selectedOptions.push(optionId);
|
||||
} else {
|
||||
wizardConfig.selectedOptions.splice(index, 1);
|
||||
}
|
||||
|
||||
renderWizardModalContent();
|
||||
}
|
||||
|
||||
function updateWizardConfig(key, value) {
|
||||
wizardConfig[key] = value;
|
||||
// Update command preview
|
||||
@@ -793,6 +945,75 @@ async function submitHookWizard() {
|
||||
if (!currentWizardTemplate) return;
|
||||
|
||||
const wizard = currentWizardTemplate;
|
||||
const scope = document.querySelector('input[name="wizardScope"]:checked')?.value || 'project';
|
||||
|
||||
// Handle multi-select wizards
|
||||
if (wizard.multiSelect) {
|
||||
const selectedOptions = wizardConfig.selectedOptions || [];
|
||||
if (selectedOptions.length === 0) {
|
||||
showRefreshToast('Please select at least one option', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Install each selected hook (skip if already exists)
|
||||
let installedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const optionId of selectedOptions) {
|
||||
const selectedOption = wizard.options.find(o => o.id === optionId);
|
||||
if (!selectedOption) continue;
|
||||
|
||||
const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId];
|
||||
if (!baseTemplate) continue;
|
||||
|
||||
// Check if hook already exists
|
||||
const existingHooks = scope === 'global'
|
||||
? hookConfig.global?.hooks?.[baseTemplate.event] || []
|
||||
: hookConfig.project?.hooks?.[baseTemplate.event] || [];
|
||||
|
||||
const hookList = Array.isArray(existingHooks) ? existingHooks : [existingHooks];
|
||||
const alreadyExists = hookList.some(h => {
|
||||
// Check by matcher and command
|
||||
const existingMatcher = h.matcher || '';
|
||||
const templateMatcher = baseTemplate.matcher || '';
|
||||
const existingCmd = h.hooks?.[0]?.command || h.command || '';
|
||||
const templateCmd = baseTemplate.command + ' ' + (baseTemplate.args || []).join(' ');
|
||||
return existingMatcher === templateMatcher && existingCmd.includes(baseTemplate.command);
|
||||
});
|
||||
|
||||
if (alreadyExists) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const hookData = {
|
||||
command: baseTemplate.command,
|
||||
args: baseTemplate.args
|
||||
};
|
||||
|
||||
if (baseTemplate.matcher) {
|
||||
hookData.matcher = baseTemplate.matcher;
|
||||
}
|
||||
|
||||
if (baseTemplate.timeout) {
|
||||
hookData.timeout = baseTemplate.timeout;
|
||||
}
|
||||
|
||||
await saveHook(scope, baseTemplate.event, hookData);
|
||||
installedCount++;
|
||||
}
|
||||
|
||||
closeHookWizardModal();
|
||||
|
||||
if (skippedCount > 0 && installedCount === 0) {
|
||||
showRefreshToast(`All ${skippedCount} hook(s) already installed`, 'info');
|
||||
} else if (skippedCount > 0) {
|
||||
showRefreshToast(`Installed ${installedCount}, skipped ${skippedCount} (already exists)`, 'success');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle single-select wizards
|
||||
const triggerType = wizardConfig.triggerType || wizard.options[0].id;
|
||||
const selectedOption = wizard.options.find(o => o.id === triggerType);
|
||||
if (!selectedOption) return;
|
||||
@@ -800,7 +1021,6 @@ async function submitHookWizard() {
|
||||
const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId];
|
||||
if (!baseTemplate) return;
|
||||
|
||||
const scope = document.querySelector('input[name="wizardScope"]:checked')?.value || 'project';
|
||||
const command = generateWizardCommand();
|
||||
|
||||
const hookData = {
|
||||
|
||||
@@ -104,6 +104,10 @@ function initNavigation() {
|
||||
renderCliHistoryView();
|
||||
} else if (currentView === 'hook-manager') {
|
||||
renderHookManager();
|
||||
} else if (currentView === 'memory') {
|
||||
renderMemoryView();
|
||||
} else if (currentView === 'prompt-history') {
|
||||
renderPromptHistoryView();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -128,6 +132,10 @@ function updateContentTitle() {
|
||||
titleEl.textContent = t('title.cliHistory');
|
||||
} else if (currentView === 'hook-manager') {
|
||||
titleEl.textContent = t('title.hookManager');
|
||||
} else if (currentView === 'memory') {
|
||||
titleEl.textContent = t('title.memoryModule');
|
||||
} else if (currentView === 'prompt-history') {
|
||||
titleEl.textContent = t('title.promptHistory');
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
|
||||
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
// ==========================================
|
||||
// TASK QUEUE SIDEBAR - Right Sidebar
|
||||
// ==========================================
|
||||
// Right-side slide-out toolbar for task queue management
|
||||
// Right-side slide-out toolbar for task queue and CLI execution management
|
||||
|
||||
let isTaskQueueSidebarVisible = false;
|
||||
let taskQueueData = [];
|
||||
let cliQueueData = [];
|
||||
let currentQueueTab = 'tasks'; // 'tasks' | 'cli'
|
||||
let cliCategoryFilter = 'all'; // 'all' | 'user' | 'internal' | 'insight'
|
||||
|
||||
/**
|
||||
* Initialize task queue sidebar
|
||||
*/
|
||||
function initTaskQueueSidebar() {
|
||||
// Create sidebar if not exists
|
||||
// Create sidebar if not exists - check for container to handle partial creation
|
||||
var existingContainer = document.getElementById('taskQueueContainer');
|
||||
if (existingContainer) {
|
||||
existingContainer.remove();
|
||||
}
|
||||
if (!document.getElementById('taskQueueSidebar')) {
|
||||
const sidebarHtml = `
|
||||
<div class="task-queue-sidebar" id="taskQueueSidebar">
|
||||
<div class="task-queue-header">
|
||||
<div class="task-queue-title">
|
||||
<span class="task-queue-title-icon">📋</span>
|
||||
<span>Task Queue</span>
|
||||
<span>Execution Queue</span>
|
||||
<span class="task-queue-count-badge" id="taskQueueCountBadge">0</span>
|
||||
</div>
|
||||
<button class="task-queue-close" onclick="toggleTaskQueueSidebar()" title="Close">
|
||||
@@ -27,12 +34,28 @@ function initTaskQueueSidebar() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="task-queue-filters">
|
||||
<div class="task-queue-tabs">
|
||||
<button class="task-queue-tab active" data-tab="tasks" onclick="switchQueueTab('tasks')">
|
||||
📋 Tasks <span class="tab-badge" id="tasksTabBadge">0</span>
|
||||
</button>
|
||||
<button class="task-queue-tab" data-tab="cli" onclick="switchQueueTab('cli')">
|
||||
⚡ CLI <span class="tab-badge" id="cliTabBadge">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="task-queue-filters" id="taskQueueFilters">
|
||||
<button class="task-filter-btn active" data-filter="all" onclick="filterTaskQueue('all')">All</button>
|
||||
<button class="task-filter-btn" data-filter="in_progress" onclick="filterTaskQueue('in_progress')">In Progress</button>
|
||||
<button class="task-filter-btn" data-filter="pending" onclick="filterTaskQueue('pending')">Pending</button>
|
||||
</div>
|
||||
|
||||
<div class="task-queue-filters" id="cliQueueFilters" style="display: none;">
|
||||
<button class="cli-filter-btn active" data-filter="all" onclick="filterCliQueue('all')">All</button>
|
||||
<button class="cli-filter-btn" data-filter="user" onclick="filterCliQueue('user')">🔵 User</button>
|
||||
<button class="cli-filter-btn" data-filter="insight" onclick="filterCliQueue('insight')">🟣 Insight</button>
|
||||
<button class="cli-filter-btn" data-filter="internal" onclick="filterCliQueue('internal')">🟢 Internal</button>
|
||||
</div>
|
||||
|
||||
<div class="task-queue-content" id="taskQueueContent">
|
||||
<div class="task-queue-empty-state">
|
||||
<div class="task-queue-empty-icon">📋</div>
|
||||
@@ -40,9 +63,17 @@ function initTaskQueueSidebar() {
|
||||
<div class="task-queue-empty-hint">Active workflow tasks will appear here</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-queue-content" id="cliQueueContent" style="display: none;">
|
||||
<div class="task-queue-empty-state">
|
||||
<div class="task-queue-empty-icon">⚡</div>
|
||||
<div class="task-queue-empty-text">No CLI executions</div>
|
||||
<div class="task-queue-empty-hint">CLI tool executions will appear here</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-queue-toggle" id="taskQueueToggle" onclick="toggleTaskQueueSidebar()" title="Task Queue">
|
||||
<div class="task-queue-toggle" id="taskQueueToggle" onclick="toggleTaskQueueSidebar()" title="Execution Queue">
|
||||
<span class="toggle-icon">📋</span>
|
||||
<span class="toggle-badge" id="taskQueueToggleBadge"></span>
|
||||
</div>
|
||||
@@ -57,7 +88,9 @@ function initTaskQueueSidebar() {
|
||||
}
|
||||
|
||||
updateTaskQueueData();
|
||||
updateCliQueueData();
|
||||
renderTaskQueue();
|
||||
renderCliQueue();
|
||||
updateTaskQueueBadge();
|
||||
}
|
||||
|
||||
@@ -96,8 +129,14 @@ function toggleTaskQueueSidebar() {
|
||||
function updateTaskQueueData() {
|
||||
taskQueueData = [];
|
||||
|
||||
// Safety check for global state
|
||||
if (typeof workflowData === 'undefined' || !workflowData) {
|
||||
console.warn('[TaskQueue] workflowData not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect tasks from active sessions
|
||||
const activeSessions = workflowData.activeSessions || [];
|
||||
var activeSessions = workflowData.activeSessions || [];
|
||||
|
||||
activeSessions.forEach(session => {
|
||||
const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
@@ -115,7 +154,10 @@ function updateTaskQueueData() {
|
||||
});
|
||||
|
||||
// Also check lite task sessions
|
||||
Object.keys(liteTaskDataStore).forEach(key => {
|
||||
if (typeof liteTaskDataStore === 'undefined' || !liteTaskDataStore) {
|
||||
return;
|
||||
}
|
||||
Object.keys(liteTaskDataStore).forEach(function(key) {
|
||||
const liteSession = liteTaskDataStore[key];
|
||||
if (liteSession && liteSession.tasks) {
|
||||
liteSession.tasks.forEach(task => {
|
||||
@@ -142,9 +184,13 @@ function updateTaskQueueData() {
|
||||
/**
|
||||
* Render task queue list
|
||||
*/
|
||||
function renderTaskQueue(filter = 'all') {
|
||||
const contentEl = document.getElementById('taskQueueContent');
|
||||
if (!contentEl) return;
|
||||
function renderTaskQueue(filter) {
|
||||
filter = filter || 'all';
|
||||
var contentEl = document.getElementById('taskQueueContent');
|
||||
if (!contentEl) {
|
||||
console.warn('[TaskQueue] taskQueueContent element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
let filteredTasks = taskQueueData;
|
||||
if (filter !== 'all') {
|
||||
@@ -260,6 +306,156 @@ function updateTaskQueueBadge() {
|
||||
*/
|
||||
function refreshTaskQueue() {
|
||||
updateTaskQueueData();
|
||||
updateCliQueueData();
|
||||
renderTaskQueue();
|
||||
renderCliQueue();
|
||||
updateTaskQueueBadge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch between Tasks and CLI tabs
|
||||
*/
|
||||
function switchQueueTab(tab) {
|
||||
currentQueueTab = tab;
|
||||
|
||||
// Update tab button states
|
||||
document.querySelectorAll('.task-queue-tab').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||||
});
|
||||
|
||||
// Show/hide filters and content
|
||||
const taskFilters = document.getElementById('taskQueueFilters');
|
||||
const cliFilters = document.getElementById('cliQueueFilters');
|
||||
const taskContent = document.getElementById('taskQueueContent');
|
||||
const cliContent = document.getElementById('cliQueueContent');
|
||||
|
||||
if (tab === 'tasks') {
|
||||
if (taskFilters) taskFilters.style.display = 'flex';
|
||||
if (cliFilters) cliFilters.style.display = 'none';
|
||||
if (taskContent) taskContent.style.display = 'block';
|
||||
if (cliContent) cliContent.style.display = 'none';
|
||||
} else {
|
||||
if (taskFilters) taskFilters.style.display = 'none';
|
||||
if (cliFilters) cliFilters.style.display = 'flex';
|
||||
if (taskContent) taskContent.style.display = 'none';
|
||||
if (cliContent) cliContent.style.display = 'block';
|
||||
// Refresh CLI data when switching to CLI tab
|
||||
updateCliQueueData();
|
||||
renderCliQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CLI queue data from API
|
||||
*/
|
||||
async function updateCliQueueData() {
|
||||
try {
|
||||
// Fetch recent CLI executions with category info
|
||||
const response = await fetch(`/api/cli/history-native?path=${encodeURIComponent(projectPath)}&limit=20`);
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
cliQueueData = data.executions || [];
|
||||
} catch (err) {
|
||||
console.warn('[TaskQueue] Failed to load CLI queue:', err);
|
||||
cliQueueData = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CLI queue list
|
||||
*/
|
||||
function renderCliQueue() {
|
||||
const contentEl = document.getElementById('cliQueueContent');
|
||||
if (!contentEl) return;
|
||||
|
||||
// Filter by category
|
||||
let filtered = cliQueueData;
|
||||
if (cliCategoryFilter !== 'all') {
|
||||
filtered = cliQueueData.filter(exec => (exec.category || 'user') === cliCategoryFilter);
|
||||
}
|
||||
|
||||
// Update tab badge
|
||||
const cliTabBadge = document.getElementById('cliTabBadge');
|
||||
if (cliTabBadge) {
|
||||
cliTabBadge.textContent = cliQueueData.length;
|
||||
cliTabBadge.style.display = cliQueueData.length > 0 ? 'inline' : 'none';
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
const emptyText = cliCategoryFilter === 'all'
|
||||
? 'No CLI executions'
|
||||
: `No ${cliCategoryFilter} executions`;
|
||||
contentEl.innerHTML = `
|
||||
<div class="task-queue-empty-state">
|
||||
<div class="task-queue-empty-icon">⚡</div>
|
||||
<div class="task-queue-empty-text">${emptyText}</div>
|
||||
<div class="task-queue-empty-hint">CLI tool executions will appear here</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
contentEl.innerHTML = filtered.map(exec => {
|
||||
const category = exec.category || 'user';
|
||||
const categoryIcon = { user: '🔵', internal: '🟢', insight: '🟣' }[category] || '⚪';
|
||||
const statusIcon = exec.status === 'success' ? '✅' : exec.status === 'timeout' ? '⏰' : '❌';
|
||||
const timeAgo = getCliTimeAgo(new Date(exec.updated_at || exec.timestamp));
|
||||
const promptPreview = (exec.prompt_preview || '').substring(0, 60);
|
||||
|
||||
return `
|
||||
<div class="cli-queue-item category-${category}" onclick="showCliExecutionFromQueue('${escapeHtml(exec.id)}')">
|
||||
<div class="cli-queue-item-header">
|
||||
<span class="cli-queue-category-icon">${categoryIcon}</span>
|
||||
<span class="cli-queue-tool-tag cli-tool-${exec.tool}">${exec.tool.toUpperCase()}</span>
|
||||
<span class="cli-queue-status">${statusIcon}</span>
|
||||
<span class="cli-queue-time">${timeAgo}</span>
|
||||
</div>
|
||||
<div class="cli-queue-prompt">${escapeHtml(promptPreview)}${promptPreview.length >= 60 ? '...' : ''}</div>
|
||||
<div class="cli-queue-meta">
|
||||
<span class="cli-queue-id">#${exec.id.split('-')[0]}</span>
|
||||
${exec.turn_count > 1 ? `<span class="cli-queue-turns">${exec.turn_count} turns</span>` : ''}
|
||||
${exec.hasNativeSession ? '<span class="cli-queue-native">📎</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter CLI queue by category
|
||||
*/
|
||||
function filterCliQueue(category) {
|
||||
cliCategoryFilter = category;
|
||||
|
||||
// Update filter button states
|
||||
document.querySelectorAll('.cli-filter-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.filter === category);
|
||||
});
|
||||
|
||||
renderCliQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show CLI execution detail from queue
|
||||
*/
|
||||
function showCliExecutionFromQueue(executionId) {
|
||||
toggleTaskQueueSidebar();
|
||||
|
||||
// Use the showExecutionDetail function from cli-history.js if available
|
||||
if (typeof showExecutionDetail === 'function') {
|
||||
showExecutionDetail(executionId);
|
||||
} else {
|
||||
console.warn('[TaskQueue] showExecutionDetail not available');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to format time ago
|
||||
*/
|
||||
function getCliTimeAgo(date) {
|
||||
const seconds = Math.floor((new Date() - date) / 1000);
|
||||
if (seconds < 60) return 'now';
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
||||
return `${Math.floor(seconds / 86400)}d`;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ const i18n = {
|
||||
'nav.explorer': 'Explorer',
|
||||
'nav.status': 'Status',
|
||||
'nav.history': 'History',
|
||||
'nav.memory': 'Memory',
|
||||
'nav.contextMemory': 'Context',
|
||||
'nav.promptHistory': 'Prompts',
|
||||
|
||||
// Sidebar - Sessions section
|
||||
'nav.sessions': 'Sessions',
|
||||
@@ -78,6 +81,8 @@ const i18n = {
|
||||
'title.sessionDetail': 'Session Detail',
|
||||
'title.liteTaskDetail': 'Lite Task Detail',
|
||||
'title.hookManager': 'Hook Manager',
|
||||
'title.memoryModule': 'Memory Module',
|
||||
'title.promptHistory': 'Prompt History',
|
||||
|
||||
// Search
|
||||
'search.placeholder': 'Search...',
|
||||
@@ -207,6 +212,8 @@ const i18n = {
|
||||
'cli.storageBackendDesc': 'CLI history stored in SQLite with FTS search',
|
||||
'cli.smartContext': 'Smart Context',
|
||||
'cli.smartContextDesc': 'Auto-analyze prompt and add relevant file paths',
|
||||
'cli.nativeResume': 'Native Resume',
|
||||
'cli.nativeResumeDesc': 'Use native tool resume (gemini -r, qwen --resume, codex resume)',
|
||||
'cli.maxContextFiles': 'Max Context Files',
|
||||
'cli.maxContextFilesDesc': 'Maximum files to include in smart context',
|
||||
|
||||
@@ -424,6 +431,15 @@ const i18n = {
|
||||
'hook.wizard.keywordMatchingDesc': 'Load specific SKILLs when keywords are detected in prompt',
|
||||
'hook.wizard.autoDetection': 'Auto Detection',
|
||||
'hook.wizard.autoDetectionDesc': 'Automatically detect and load SKILLs by name in prompt',
|
||||
'hook.wizard.memorySetup': 'Memory Module Setup',
|
||||
'hook.wizard.memorySetupDesc': 'Configure automatic context tracking (lightweight metadata recording)',
|
||||
'hook.wizard.fileReadTracker': 'File Read Tracker',
|
||||
'hook.wizard.fileReadTrackerDesc': 'Track file reads to build context heatmap',
|
||||
'hook.wizard.fileWriteTracker': 'File Write Tracker',
|
||||
'hook.wizard.fileWriteTrackerDesc': 'Track file modifications to identify core modules',
|
||||
'hook.wizard.promptTracker': 'Prompt Tracker',
|
||||
'hook.wizard.promptTrackerDesc': 'Record user prompts for pattern analysis',
|
||||
'hook.wizard.selectTrackers': 'Select Trackers',
|
||||
|
||||
// Hook Wizard Labels
|
||||
'hook.wizard.cliTools': 'CLI Tools:',
|
||||
@@ -510,6 +526,71 @@ const i18n = {
|
||||
'footer.generated': 'Generated:',
|
||||
'footer.version': 'CCW Dashboard v1.0',
|
||||
|
||||
// Prompt History
|
||||
'prompt.timeline': 'Prompt Timeline',
|
||||
'prompt.searchPlaceholder': 'Search prompts...',
|
||||
'prompt.allProjects': 'All Projects',
|
||||
'prompt.currentProject': 'Current Project',
|
||||
'prompt.noPromptsFound': 'No Prompts Found',
|
||||
'prompt.noPromptsText': 'No prompts found matching your search criteria.',
|
||||
'prompt.insights': 'Insights & Suggestions',
|
||||
'prompt.analyze': 'Analyze',
|
||||
'prompt.analyzing': 'Analyzing...',
|
||||
'prompt.selectTool': 'Select Tool',
|
||||
'prompt.quality': 'Quality',
|
||||
'prompt.intent': 'Intent',
|
||||
'prompt.project': 'Project',
|
||||
'prompt.session': 'Session',
|
||||
'prompt.noInsights': 'No insights yet',
|
||||
'prompt.noInsightsText': 'Select a CLI tool and click Analyze to generate insights.',
|
||||
'prompt.loadingInsights': 'Generating insights...',
|
||||
'prompt.insightsError': 'Failed to generate insights',
|
||||
'prompt.intent.implement': 'Implement',
|
||||
'prompt.intent.fix': 'Fix',
|
||||
'prompt.intent.explore': 'Explore',
|
||||
'prompt.intent.debug': 'Debug',
|
||||
'prompt.intent.refactor': 'Refactor',
|
||||
'prompt.intent.test': 'Test',
|
||||
'prompt.intent.document': 'Document',
|
||||
'prompt.intent.general': 'General',
|
||||
'prompt.timeJustNow': 'Just now',
|
||||
'prompt.timeMinutesAgo': '{count} min ago',
|
||||
'prompt.timeHoursAgo': '{count} hours ago',
|
||||
'prompt.timeDaysAgo': '{count} days ago',
|
||||
|
||||
// Memory Module
|
||||
'memory.contextHotspots': 'Context Hotspots',
|
||||
'memory.mostRead': 'Most Read Files',
|
||||
'memory.mostEdited': 'Most Edited Files',
|
||||
'memory.today': 'Today',
|
||||
'memory.week': 'Week',
|
||||
'memory.allTime': 'All Time',
|
||||
'memory.noData': 'No data available',
|
||||
'memory.memoryGraph': 'Memory Graph',
|
||||
'memory.nodes': 'nodes',
|
||||
'memory.resetView': 'Reset View',
|
||||
'memory.file': 'File',
|
||||
'memory.module': 'Module',
|
||||
'memory.component': 'Component',
|
||||
'memory.noGraphData': 'No graph data available',
|
||||
'memory.d3NotLoaded': 'D3.js not loaded',
|
||||
'memory.recentContext': 'Recent Context',
|
||||
'memory.activities': 'activities',
|
||||
'memory.searchContext': 'Search context...',
|
||||
'memory.noRecentActivity': 'No recent activity',
|
||||
'memory.reads': 'Reads',
|
||||
'memory.edits': 'Edits',
|
||||
'memory.prompts': 'Prompts',
|
||||
'memory.nodeDetails': 'Node Details',
|
||||
'memory.heat': 'Heat',
|
||||
'memory.associations': 'Associations',
|
||||
'memory.type': 'Type',
|
||||
'memory.relatedNodes': 'Related Nodes',
|
||||
'memory.noAssociations': 'No associations found',
|
||||
'memory.justNow': 'Just now',
|
||||
'memory.minutesAgo': 'minutes ago',
|
||||
'memory.hoursAgo': 'hours ago',
|
||||
|
||||
// Common
|
||||
'common.cancel': 'Cancel',
|
||||
'common.create': 'Create',
|
||||
@@ -547,6 +628,9 @@ const i18n = {
|
||||
'nav.explorer': '文件浏览器',
|
||||
'nav.status': '状态',
|
||||
'nav.history': '历史',
|
||||
'nav.memory': '记忆',
|
||||
'nav.contextMemory': '活动',
|
||||
'nav.promptHistory': '洞察',
|
||||
|
||||
// Sidebar - Sessions section
|
||||
'nav.sessions': '会话',
|
||||
@@ -598,7 +682,9 @@ const i18n = {
|
||||
'title.sessionDetail': '会话详情',
|
||||
'title.liteTaskDetail': '轻量任务详情',
|
||||
'title.hookManager': '钩子管理',
|
||||
|
||||
'title.memoryModule': '记忆模块',
|
||||
'title.promptHistory': '提示历史',
|
||||
|
||||
// Search
|
||||
'search.placeholder': '搜索...',
|
||||
|
||||
@@ -727,6 +813,8 @@ const i18n = {
|
||||
'cli.storageBackendDesc': 'CLI 历史使用 SQLite 存储,支持全文搜索',
|
||||
'cli.smartContext': '智能上下文',
|
||||
'cli.smartContextDesc': '自动分析提示词并添加相关文件路径',
|
||||
'cli.nativeResume': '原生恢复',
|
||||
'cli.nativeResumeDesc': '使用工具原生恢复命令 (gemini -r, qwen --resume, codex resume)',
|
||||
'cli.maxContextFiles': '最大上下文文件数',
|
||||
'cli.maxContextFilesDesc': '智能上下文包含的最大文件数',
|
||||
|
||||
@@ -944,6 +1032,15 @@ const i18n = {
|
||||
'hook.wizard.keywordMatchingDesc': '当提示中检测到关键词时加载特定 SKILL',
|
||||
'hook.wizard.autoDetection': '自动检测',
|
||||
'hook.wizard.autoDetectionDesc': '根据提示中的名称自动检测并加载 SKILL',
|
||||
'hook.wizard.memorySetup': '记忆模块设置',
|
||||
'hook.wizard.memorySetupDesc': '配置自动上下文跟踪(轻量级元数据记录)',
|
||||
'hook.wizard.fileReadTracker': '文件读取追踪器',
|
||||
'hook.wizard.fileReadTrackerDesc': '追踪文件读取以构建上下文热图',
|
||||
'hook.wizard.fileWriteTracker': '文件写入追踪器',
|
||||
'hook.wizard.fileWriteTrackerDesc': '追踪文件修改以识别核心模块',
|
||||
'hook.wizard.promptTracker': '提示追踪器',
|
||||
'hook.wizard.promptTrackerDesc': '记录用户提示用于模式分析',
|
||||
'hook.wizard.selectTrackers': '选择追踪器',
|
||||
|
||||
// Hook Wizard Labels
|
||||
'hook.wizard.cliTools': 'CLI 工具:',
|
||||
@@ -1029,7 +1126,72 @@ const i18n = {
|
||||
// Footer
|
||||
'footer.generated': '生成时间:',
|
||||
'footer.version': 'CCW 控制面板 v1.0',
|
||||
|
||||
|
||||
// Prompt History
|
||||
'prompt.timeline': '提示词时间线',
|
||||
'prompt.searchPlaceholder': '搜索提示词...',
|
||||
'prompt.allProjects': '所有项目',
|
||||
'prompt.currentProject': '当前项目',
|
||||
'prompt.noPromptsFound': '未找到提示词',
|
||||
'prompt.noPromptsText': '没有符合搜索条件的提示词。',
|
||||
'prompt.insights': '洞察与建议',
|
||||
'prompt.analyze': '分析',
|
||||
'prompt.analyzing': '分析中...',
|
||||
'prompt.selectTool': '选择工具',
|
||||
'prompt.quality': '质量',
|
||||
'prompt.intent': '意图',
|
||||
'prompt.project': '项目',
|
||||
'prompt.session': '会话',
|
||||
'prompt.noInsights': '暂无洞察',
|
||||
'prompt.noInsightsText': '选择 CLI 工具并点击分析以生成洞察。',
|
||||
'prompt.loadingInsights': '正在生成洞察...',
|
||||
'prompt.insightsError': '生成洞察失败',
|
||||
'prompt.intent.implement': '实现',
|
||||
'prompt.intent.fix': '修复',
|
||||
'prompt.intent.explore': '探索',
|
||||
'prompt.intent.debug': '调试',
|
||||
'prompt.intent.refactor': '重构',
|
||||
'prompt.intent.test': '测试',
|
||||
'prompt.intent.document': '文档',
|
||||
'prompt.intent.general': '通用',
|
||||
'prompt.timeJustNow': '刚刚',
|
||||
'prompt.timeMinutesAgo': '{count} 分钟前',
|
||||
'prompt.timeHoursAgo': '{count} 小时前',
|
||||
'prompt.timeDaysAgo': '{count} 天前',
|
||||
|
||||
// Memory Module
|
||||
'memory.contextHotspots': '上下文热点',
|
||||
'memory.mostRead': '最常读取的文件',
|
||||
'memory.mostEdited': '最常编辑的文件',
|
||||
'memory.today': '今天',
|
||||
'memory.week': '本周',
|
||||
'memory.allTime': '全部时间',
|
||||
'memory.noData': '无可用数据',
|
||||
'memory.memoryGraph': '记忆图谱',
|
||||
'memory.nodes': '节点',
|
||||
'memory.resetView': '重置视图',
|
||||
'memory.file': '文件',
|
||||
'memory.module': '模块',
|
||||
'memory.component': '组件',
|
||||
'memory.noGraphData': '无图谱数据',
|
||||
'memory.d3NotLoaded': 'D3.js 未加载',
|
||||
'memory.recentContext': '最近上下文',
|
||||
'memory.activities': '活动',
|
||||
'memory.searchContext': '搜索上下文...',
|
||||
'memory.noRecentActivity': '无最近活动',
|
||||
'memory.reads': '读取',
|
||||
'memory.edits': '编辑',
|
||||
'memory.prompts': '提示',
|
||||
'memory.nodeDetails': '节点详情',
|
||||
'memory.heat': '热度',
|
||||
'memory.associations': '关联',
|
||||
'memory.type': '类型',
|
||||
'memory.relatedNodes': '相关节点',
|
||||
'memory.noAssociations': '未找到关联',
|
||||
'memory.justNow': '刚刚',
|
||||
'memory.minutesAgo': '分钟前',
|
||||
'memory.hoursAgo': '小时前',
|
||||
|
||||
// Common
|
||||
'common.cancel': '取消',
|
||||
'common.create': '创建',
|
||||
|
||||
@@ -291,6 +291,19 @@ function renderCliSettingsSection() {
|
||||
'</div>' +
|
||||
'<p class="cli-setting-desc">' + t('cli.smartContextDesc') + '</p>' +
|
||||
'</div>' +
|
||||
'<div class="cli-setting-item">' +
|
||||
'<label class="cli-setting-label">' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3"></i>' +
|
||||
t('cli.nativeResume') +
|
||||
'</label>' +
|
||||
'<div class="cli-setting-control">' +
|
||||
'<label class="cli-toggle">' +
|
||||
'<input type="checkbox"' + (nativeResumeEnabled ? ' checked' : '') + ' onchange="setNativeResumeEnabled(this.checked)">' +
|
||||
'<span class="cli-toggle-slider"></span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<p class="cli-setting-desc">' + t('cli.nativeResumeDesc') + '</p>' +
|
||||
'</div>' +
|
||||
'<div class="cli-setting-item' + (!smartContextEnabled ? ' disabled' : '') + '">' +
|
||||
'<label class="cli-setting-label">' +
|
||||
'<i data-lucide="files" class="w-3 h-3"></i>' +
|
||||
|
||||
@@ -11,10 +11,8 @@ async function renderHookManager() {
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Load hook config if not already loaded
|
||||
if (!hookConfig.global.hooks && !hookConfig.project.hooks) {
|
||||
await loadHookConfig();
|
||||
}
|
||||
// Always reload hook config to get latest data
|
||||
await loadHookConfig();
|
||||
|
||||
const globalHooks = hookConfig.global?.hooks || {};
|
||||
const projectHooks = hookConfig.project?.hooks || {};
|
||||
@@ -84,8 +82,9 @@ async function renderHookManager() {
|
||||
<span class="text-sm text-muted-foreground">${t('hook.wizardsDesc')}</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
${renderWizardCard('memory-update')}
|
||||
${renderWizardCard('memory-setup')}
|
||||
${renderWizardCard('skill-context')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,8 +205,10 @@ function renderWizardCard(wizardId) {
|
||||
|
||||
// Get translated wizard name and description
|
||||
const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') :
|
||||
wizardId === 'memory-setup' ? t('hook.wizard.memorySetup') :
|
||||
wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name;
|
||||
const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') :
|
||||
wizardId === 'memory-setup' ? t('hook.wizard.memorySetupDesc') :
|
||||
wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description;
|
||||
|
||||
// Translate options
|
||||
@@ -216,6 +217,11 @@ function renderWizardCard(wizardId) {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEnd');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdate');
|
||||
}
|
||||
if (wizardId === 'memory-setup') {
|
||||
if (optId === 'file-read') return t('hook.wizard.fileReadTracker');
|
||||
if (optId === 'file-write') return t('hook.wizard.fileWriteTracker');
|
||||
if (optId === 'prompts') return t('hook.wizard.promptTracker');
|
||||
}
|
||||
if (wizardId === 'skill-context') {
|
||||
if (optId === 'keyword') return t('hook.wizard.keywordMatching');
|
||||
if (optId === 'auto') return t('hook.wizard.autoDetection');
|
||||
@@ -228,6 +234,11 @@ function renderWizardCard(wizardId) {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc');
|
||||
}
|
||||
if (wizardId === 'memory-setup') {
|
||||
if (optId === 'file-read') return t('hook.wizard.fileReadTrackerDesc');
|
||||
if (optId === 'file-write') return t('hook.wizard.fileWriteTrackerDesc');
|
||||
if (optId === 'prompts') return t('hook.wizard.promptTrackerDesc');
|
||||
}
|
||||
if (wizardId === 'skill-context') {
|
||||
if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc');
|
||||
if (optId === 'auto') return t('hook.wizard.autoDetectionDesc');
|
||||
@@ -236,8 +247,9 @@ function renderWizardCard(wizardId) {
|
||||
};
|
||||
|
||||
// Determine what to show in the tools/skills section
|
||||
const toolsSection = wizard.requiresSkillDiscovery
|
||||
? `
|
||||
let toolsSection = '';
|
||||
if (wizard.requiresSkillDiscovery) {
|
||||
toolsSection = `
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.event')}</span>
|
||||
<span class="px-2 py-0.5 bg-amber-500/10 text-amber-500 rounded">UserPromptSubmit</span>
|
||||
@@ -246,8 +258,12 @@ function renderWizardCard(wizardId) {
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
||||
<span class="text-muted-foreground ml-2">${t('hook.wizard.loading')}</span>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
`;
|
||||
} else if (wizard.multiSelect) {
|
||||
// memory-setup: lightweight tracking, no CLI tools
|
||||
toolsSection = '';
|
||||
} else {
|
||||
toolsSection = `
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.cliTools')}</span>
|
||||
<span class="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded">gemini</span>
|
||||
@@ -255,6 +271,7 @@ function renderWizardCard(wizardId) {
|
||||
<span class="px-2 py-0.5 bg-green-500/10 text-green-500 rounded">codex</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="hook-wizard-card bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20 rounded-lg p-5 hover:shadow-lg transition-all">
|
||||
@@ -308,8 +325,10 @@ function renderHooksByEvent(hooks, scope) {
|
||||
|
||||
return hookList.map((hook, index) => {
|
||||
const matcher = hook.matcher || 'All tools';
|
||||
const command = hook.command || 'N/A';
|
||||
// Support both old format (hook.command) and new Claude Code format (hook.hooks[0].command)
|
||||
const command = hook.hooks?.[0]?.command || hook.command || 'N/A';
|
||||
const args = hook.args || [];
|
||||
const timeout = hook.hooks?.[0]?.timeout || hook.timeout;
|
||||
|
||||
return `
|
||||
<div class="hook-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all">
|
||||
@@ -424,18 +443,28 @@ function isHookTemplateInstalled(templateId) {
|
||||
const template = HOOK_TEMPLATES[templateId];
|
||||
if (!template) return false;
|
||||
|
||||
// Build expected command string
|
||||
const templateCmd = template.command + (template.args ? ' ' + template.args.join(' ') : '');
|
||||
|
||||
// Check project hooks
|
||||
const projectHooks = hookConfig.project?.hooks?.[template.event];
|
||||
if (projectHooks) {
|
||||
const hookList = Array.isArray(projectHooks) ? projectHooks : [projectHooks];
|
||||
if (hookList.some(h => h.command === template.command)) return true;
|
||||
if (hookList.some(h => {
|
||||
// Check both old format (h.command) and new format (h.hooks[0].command)
|
||||
const cmd = h.hooks?.[0]?.command || h.command || '';
|
||||
return cmd.includes(template.command);
|
||||
})) return true;
|
||||
}
|
||||
|
||||
// Check global hooks
|
||||
const globalHooks = hookConfig.global?.hooks?.[template.event];
|
||||
if (globalHooks) {
|
||||
const hookList = Array.isArray(globalHooks) ? globalHooks : [globalHooks];
|
||||
if (hookList.some(h => h.command === template.command)) return true;
|
||||
if (hookList.some(h => {
|
||||
const cmd = h.hooks?.[0]?.command || h.command || '';
|
||||
return cmd.includes(template.command);
|
||||
})) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -448,6 +477,12 @@ async function installHookTemplate(templateId, scope) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already installed
|
||||
if (isHookTemplateInstalled(templateId)) {
|
||||
showRefreshToast('Hook already installed', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const hookData = {
|
||||
command: template.command,
|
||||
args: template.args
|
||||
|
||||
588
ccw/src/templates/dashboard-js/views/memory.js
Normal file
588
ccw/src/templates/dashboard-js/views/memory.js
Normal file
@@ -0,0 +1,588 @@
|
||||
// Memory Module View
|
||||
// Three-column layout: Context Hotspots | Memory Graph | Recent Context
|
||||
|
||||
// ========== Memory State ==========
|
||||
var memoryStats = null;
|
||||
var memoryGraphData = null;
|
||||
var recentContext = [];
|
||||
var memoryTimeFilter = 'all'; // 'today', 'week', 'all'
|
||||
var selectedNode = null;
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderMemoryView() {
|
||||
var container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
// Hide stats grid and search for memory view
|
||||
var statsGrid = document.getElementById('statsGrid');
|
||||
var searchInput = document.getElementById('searchInput');
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="memory-view loading">' +
|
||||
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
|
||||
'<p>' + t('common.loading') + '</p>' +
|
||||
'</div>';
|
||||
|
||||
// Load data
|
||||
await Promise.all([
|
||||
loadMemoryStats(),
|
||||
loadMemoryGraph(),
|
||||
loadRecentContext()
|
||||
]);
|
||||
|
||||
// Render three-column layout
|
||||
container.innerHTML = '<div class="memory-view">' +
|
||||
'<div class="memory-columns">' +
|
||||
'<div class="memory-column left" id="memory-hotspots"></div>' +
|
||||
'<div class="memory-column center" id="memory-graph"></div>' +
|
||||
'<div class="memory-column right" id="memory-context"></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Render each column
|
||||
renderHotspotsColumn();
|
||||
renderGraphColumn();
|
||||
renderContextColumn();
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== Data Loading ==========
|
||||
async function loadMemoryStats() {
|
||||
try {
|
||||
var response = await fetch('/api/memory/stats?filter=' + memoryTimeFilter);
|
||||
if (!response.ok) throw new Error('Failed to load memory stats');
|
||||
var data = await response.json();
|
||||
memoryStats = data.stats || { mostRead: [], mostEdited: [] };
|
||||
return memoryStats;
|
||||
} catch (err) {
|
||||
console.error('Failed to load memory stats:', err);
|
||||
memoryStats = { mostRead: [], mostEdited: [] };
|
||||
return memoryStats;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMemoryGraph() {
|
||||
try {
|
||||
var response = await fetch('/api/memory/graph');
|
||||
if (!response.ok) throw new Error('Failed to load memory graph');
|
||||
var data = await response.json();
|
||||
memoryGraphData = data.graph || { nodes: [], edges: [] };
|
||||
return memoryGraphData;
|
||||
} catch (err) {
|
||||
console.error('Failed to load memory graph:', err);
|
||||
memoryGraphData = { nodes: [], edges: [] };
|
||||
return memoryGraphData;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecentContext() {
|
||||
try {
|
||||
var response = await fetch('/api/memory/recent');
|
||||
if (!response.ok) throw new Error('Failed to load recent context');
|
||||
var data = await response.json();
|
||||
recentContext = data.recent || [];
|
||||
return recentContext;
|
||||
} catch (err) {
|
||||
console.error('Failed to load recent context:', err);
|
||||
recentContext = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Left Column: Context Hotspots ==========
|
||||
function renderHotspotsColumn() {
|
||||
var container = document.getElementById('memory-hotspots');
|
||||
if (!container) return;
|
||||
|
||||
var mostRead = memoryStats.mostRead || [];
|
||||
var mostEdited = memoryStats.mostEdited || [];
|
||||
|
||||
container.innerHTML = '<div class="memory-section">' +
|
||||
'<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="flame" class="w-4 h-4"></i> ' + t('memory.contextHotspots') + '</h3>' +
|
||||
'</div>' +
|
||||
'<div class="section-header-actions">' +
|
||||
'<button class="btn-icon" onclick="refreshMemoryData()" title="' + t('common.refresh') + '">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="memory-filters">' +
|
||||
'<button class="filter-btn ' + (memoryTimeFilter === 'today' ? 'active' : '') + '" onclick="setMemoryTimeFilter(\'today\')">' + t('memory.today') + '</button>' +
|
||||
'<button class="filter-btn ' + (memoryTimeFilter === 'week' ? 'active' : '') + '" onclick="setMemoryTimeFilter(\'week\')">' + t('memory.week') + '</button>' +
|
||||
'<button class="filter-btn ' + (memoryTimeFilter === 'all' ? 'active' : '') + '" onclick="setMemoryTimeFilter(\'all\')">' + t('memory.allTime') + '</button>' +
|
||||
'</div>' +
|
||||
'<div class="hotspot-lists">' +
|
||||
'<div class="hotspot-list-container">' +
|
||||
'<h4 class="hotspot-list-title"><i data-lucide="eye" class="w-3.5 h-3.5"></i> ' + t('memory.mostRead') + '</h4>' +
|
||||
renderHotspotList(mostRead, 'read') +
|
||||
'</div>' +
|
||||
'<div class="hotspot-list-container">' +
|
||||
'<h4 class="hotspot-list-title"><i data-lucide="pencil" class="w-3.5 h-3.5"></i> ' + t('memory.mostEdited') + '</h4>' +
|
||||
renderHotspotList(mostEdited, 'edit') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderHotspotList(items, type) {
|
||||
if (!items || items.length === 0) {
|
||||
return '<div class="hotspot-empty">' +
|
||||
'<i data-lucide="inbox" class="w-6 h-6"></i>' +
|
||||
'<p>' + t('memory.noData') + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
return '<div class="hotspot-list">' +
|
||||
items.map(function(item, index) {
|
||||
var heat = item.heat || item.count || 0;
|
||||
var heatClass = heat > 50 ? 'high' : heat > 20 ? 'medium' : 'low';
|
||||
var path = item.path || item.file || 'Unknown';
|
||||
var fileName = path.split('/').pop().split('\\').pop();
|
||||
|
||||
return '<div class="hotspot-item" onclick="highlightNode(\'' + escapeHtml(path) + '\')">' +
|
||||
'<div class="hotspot-rank">' + (index + 1) + '</div>' +
|
||||
'<div class="hotspot-info">' +
|
||||
'<div class="hotspot-name" title="' + escapeHtml(path) + '">' + escapeHtml(fileName) + '</div>' +
|
||||
'<div class="hotspot-path">' + escapeHtml(path.substring(0, path.lastIndexOf(fileName))) + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="hotspot-heat ' + heatClass + '">' +
|
||||
'<span class="heat-badge">' + heat + '</span>' +
|
||||
'<i data-lucide="' + (type === 'read' ? 'eye' : 'pencil') + '" class="w-3 h-3"></i>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// ========== Center Column: Memory Graph ==========
|
||||
function renderGraphColumn() {
|
||||
var container = document.getElementById('memory-graph');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="memory-section">' +
|
||||
'<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="network" class="w-4 h-4"></i> ' + t('memory.memoryGraph') + '</h3>' +
|
||||
'<span class="section-count">' + (memoryGraphData.nodes || []).length + ' ' + t('memory.nodes') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="section-header-actions">' +
|
||||
'<button class="btn-icon" onclick="resetGraphView()" title="' + t('memory.resetView') + '">' +
|
||||
'<i data-lucide="maximize-2" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="memory-graph-container" id="memoryGraphSvg"></div>' +
|
||||
'<div class="memory-graph-legend">' +
|
||||
'<div class="legend-item"><span class="legend-dot file"></span> ' + t('memory.file') + '</div>' +
|
||||
'<div class="legend-item"><span class="legend-dot module"></span> ' + t('memory.module') + '</div>' +
|
||||
'<div class="legend-item"><span class="legend-dot component"></span> ' + t('memory.component') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Render D3 graph
|
||||
renderMemoryGraph(memoryGraphData);
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderMemoryGraph(graphData) {
|
||||
if (!graphData || !graphData.nodes || graphData.nodes.length === 0) {
|
||||
var container = document.getElementById('memoryGraphSvg');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="graph-empty-state">' +
|
||||
'<i data-lucide="network" class="w-12 h-12"></i>' +
|
||||
'<p>' + t('memory.noGraphData') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if D3 is available
|
||||
if (typeof d3 === 'undefined') {
|
||||
var container = document.getElementById('memoryGraphSvg');
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="graph-error">' +
|
||||
'<i data-lucide="alert-triangle" class="w-8 h-8"></i>' +
|
||||
'<p>' + t('memory.d3NotLoaded') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var container = document.getElementById('memoryGraphSvg');
|
||||
if (!container) return;
|
||||
|
||||
var width = container.clientWidth || 600;
|
||||
var height = container.clientHeight || 500;
|
||||
|
||||
// Clear existing
|
||||
container.innerHTML = '';
|
||||
|
||||
var svg = d3.select('#memoryGraphSvg')
|
||||
.append('svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.attr('class', 'memory-graph-svg');
|
||||
|
||||
// Create force simulation
|
||||
var simulation = d3.forceSimulation(graphData.nodes)
|
||||
.force('link', d3.forceLink(graphData.edges).id(function(d) { return d.id; }).distance(100))
|
||||
.force('charge', d3.forceManyBody().strength(-300))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collision', d3.forceCollide().radius(function(d) { return (d.heat || 10) + 5; }));
|
||||
|
||||
// Draw edges
|
||||
var link = svg.append('g')
|
||||
.selectAll('line')
|
||||
.data(graphData.edges)
|
||||
.enter()
|
||||
.append('line')
|
||||
.attr('class', 'graph-edge')
|
||||
.attr('stroke-width', function(d) { return Math.sqrt(d.weight || 1); });
|
||||
|
||||
// Draw nodes
|
||||
var node = svg.append('g')
|
||||
.selectAll('circle')
|
||||
.data(graphData.nodes)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', function(d) { return 'graph-node ' + (d.type || 'file'); })
|
||||
.attr('r', function(d) { return (d.heat || 10); })
|
||||
.attr('data-id', function(d) { return d.id; })
|
||||
.call(d3.drag()
|
||||
.on('start', dragstarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragended))
|
||||
.on('click', function(event, d) {
|
||||
selectNode(d);
|
||||
});
|
||||
|
||||
// Node labels
|
||||
var label = svg.append('g')
|
||||
.selectAll('text')
|
||||
.data(graphData.nodes)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('class', 'graph-label')
|
||||
.text(function(d) { return d.name || d.id; })
|
||||
.attr('x', 8)
|
||||
.attr('y', 3);
|
||||
|
||||
// Update positions on simulation tick
|
||||
simulation.on('tick', function() {
|
||||
link
|
||||
.attr('x1', function(d) { return d.source.x; })
|
||||
.attr('y1', function(d) { return d.source.y; })
|
||||
.attr('x2', function(d) { return d.target.x; })
|
||||
.attr('y2', function(d) { return d.target.y; });
|
||||
|
||||
node
|
||||
.attr('cx', function(d) { return d.x; })
|
||||
.attr('cy', function(d) { return d.y; });
|
||||
|
||||
label
|
||||
.attr('x', function(d) { return d.x + 8; })
|
||||
.attr('y', function(d) { return d.y + 3; });
|
||||
});
|
||||
|
||||
// Drag functions
|
||||
function dragstarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x;
|
||||
d.fy = d.y;
|
||||
}
|
||||
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x;
|
||||
d.fy = event.y;
|
||||
}
|
||||
|
||||
function dragended(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null;
|
||||
d.fy = null;
|
||||
}
|
||||
}
|
||||
|
||||
function selectNode(node) {
|
||||
selectedNode = node;
|
||||
|
||||
// Highlight in graph
|
||||
d3.selectAll('.graph-node').classed('selected', false);
|
||||
d3.selectAll('.graph-node[data-id="' + node.id + '"]').classed('selected', true);
|
||||
|
||||
// Show node details in context column
|
||||
showNodeDetails(node);
|
||||
}
|
||||
|
||||
function highlightNode(path) {
|
||||
var node = memoryGraphData.nodes.find(function(n) { return n.path === path || n.id === path; });
|
||||
if (node) {
|
||||
selectNode(node);
|
||||
// Center graph on node if possible
|
||||
if (typeof d3 !== 'undefined') {
|
||||
var container = document.getElementById('memoryGraphSvg');
|
||||
if (container) {
|
||||
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetGraphView() {
|
||||
selectedNode = null;
|
||||
d3.selectAll('.graph-node').classed('selected', false);
|
||||
renderContextColumn();
|
||||
}
|
||||
|
||||
// ========== Right Column: Recent Context ==========
|
||||
function renderContextColumn() {
|
||||
var container = document.getElementById('memory-context');
|
||||
if (!container) return;
|
||||
|
||||
if (selectedNode) {
|
||||
showNodeDetails(selectedNode);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="memory-section">' +
|
||||
'<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="clock" class="w-4 h-4"></i> ' + t('memory.recentContext') + '</h3>' +
|
||||
'<span class="section-count">' + recentContext.length + ' ' + t('memory.activities') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="context-search">' +
|
||||
'<input type="text" id="contextSearchInput" class="context-search-input" placeholder="' + t('memory.searchContext') + '" onkeyup="filterRecentContext(this.value)">' +
|
||||
'<i data-lucide="search" class="w-4 h-4 search-icon"></i>' +
|
||||
'</div>' +
|
||||
renderContextTimeline(recentContext) +
|
||||
renderContextStats() +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderContextTimeline(prompts) {
|
||||
if (!prompts || prompts.length === 0) {
|
||||
return '<div class="context-empty">' +
|
||||
'<i data-lucide="inbox" class="w-8 h-8"></i>' +
|
||||
'<p>' + t('memory.noRecentActivity') + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
return '<div class="context-timeline">' +
|
||||
prompts.map(function(item) {
|
||||
var timestamp = item.timestamp ? formatTimestamp(item.timestamp) : 'Unknown time';
|
||||
var type = item.type || 'unknown';
|
||||
var typeIcon = type === 'read' ? 'eye' : type === 'edit' ? 'pencil' : 'file-text';
|
||||
var files = item.files || [];
|
||||
|
||||
return '<div class="timeline-item">' +
|
||||
'<div class="timeline-icon ' + type + '">' +
|
||||
'<i data-lucide="' + typeIcon + '" class="w-3.5 h-3.5"></i>' +
|
||||
'</div>' +
|
||||
'<div class="timeline-content">' +
|
||||
'<div class="timeline-header">' +
|
||||
'<span class="timeline-type">' + escapeHtml(type.charAt(0).toUpperCase() + type.slice(1)) + '</span>' +
|
||||
'<span class="timeline-time">' + timestamp + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="timeline-prompt">' + escapeHtml(item.prompt || item.description || 'No description') + '</div>' +
|
||||
(files.length > 0 ? '<div class="timeline-files">' +
|
||||
files.slice(0, 3).map(function(f) {
|
||||
return '<span class="file-tag" onclick="highlightNode(\'' + escapeHtml(f) + '\')">' +
|
||||
'<i data-lucide="file" class="w-3 h-3"></i> ' + escapeHtml(f.split('/').pop().split('\\').pop()) +
|
||||
'</span>';
|
||||
}).join('') +
|
||||
(files.length > 3 ? '<span class="file-tag more">+' + (files.length - 3) + ' more</span>' : '') +
|
||||
'</div>' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderContextStats() {
|
||||
var totalReads = recentContext.filter(function(c) { return c.type === 'read'; }).length;
|
||||
var totalEdits = recentContext.filter(function(c) { return c.type === 'edit'; }).length;
|
||||
var totalPrompts = recentContext.filter(function(c) { return c.type === 'prompt'; }).length;
|
||||
|
||||
return '<div class="context-stats">' +
|
||||
'<div class="context-stat-item">' +
|
||||
'<i data-lucide="eye" class="w-4 h-4"></i>' +
|
||||
'<span class="stat-label">' + t('memory.reads') + '</span>' +
|
||||
'<span class="stat-value">' + totalReads + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="context-stat-item">' +
|
||||
'<i data-lucide="pencil" class="w-4 h-4"></i>' +
|
||||
'<span class="stat-label">' + t('memory.edits') + '</span>' +
|
||||
'<span class="stat-value">' + totalEdits + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="context-stat-item">' +
|
||||
'<i data-lucide="message-square" class="w-4 h-4"></i>' +
|
||||
'<span class="stat-label">' + t('memory.prompts') + '</span>' +
|
||||
'<span class="stat-value">' + totalPrompts + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function showNodeDetails(node) {
|
||||
var container = document.getElementById('memory-context');
|
||||
if (!container) return;
|
||||
|
||||
var associations = memoryGraphData.edges
|
||||
.filter(function(e) { return e.source.id === node.id || e.target.id === node.id; })
|
||||
.map(function(e) {
|
||||
var other = e.source.id === node.id ? e.target : e.source;
|
||||
return { node: other, weight: e.weight || 1 };
|
||||
})
|
||||
.sort(function(a, b) { return b.weight - a.weight; });
|
||||
|
||||
container.innerHTML = '<div class="memory-section">' +
|
||||
'<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="info" class="w-4 h-4"></i> ' + t('memory.nodeDetails') + '</h3>' +
|
||||
'</div>' +
|
||||
'<div class="section-header-actions">' +
|
||||
'<button class="btn-icon" onclick="resetGraphView()" title="' + t('common.close') + '">' +
|
||||
'<i data-lucide="x" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="node-details">' +
|
||||
'<div class="node-detail-header">' +
|
||||
'<div class="node-detail-icon ' + (node.type || 'file') + '">' +
|
||||
'<i data-lucide="' + (node.type === 'module' ? 'package' : node.type === 'component' ? 'box' : 'file') + '" class="w-5 h-5"></i>' +
|
||||
'</div>' +
|
||||
'<div class="node-detail-info">' +
|
||||
'<div class="node-detail-name">' + escapeHtml(node.name || node.id) + '</div>' +
|
||||
'<div class="node-detail-path">' + escapeHtml(node.path || node.id) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="node-detail-stats">' +
|
||||
'<div class="detail-stat">' +
|
||||
'<span class="detail-stat-label">' + t('memory.heat') + '</span>' +
|
||||
'<span class="detail-stat-value">' + (node.heat || 0) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-stat">' +
|
||||
'<span class="detail-stat-label">' + t('memory.associations') + '</span>' +
|
||||
'<span class="detail-stat-value">' + associations.length + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-stat">' +
|
||||
'<span class="detail-stat-label">' + t('memory.type') + '</span>' +
|
||||
'<span class="detail-stat-value">' + (node.type || 'file') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
(associations.length > 0 ? '<div class="node-associations">' +
|
||||
'<h4 class="associations-title">' + t('memory.relatedNodes') + '</h4>' +
|
||||
'<div class="associations-list">' +
|
||||
associations.slice(0, 10).map(function(a) {
|
||||
return '<div class="association-item" onclick="selectNode(' + JSON.stringify(a.node).replace(/"/g, '"') + ')">' +
|
||||
'<div class="association-node">' +
|
||||
'<i data-lucide="' + (a.node.type === 'module' ? 'package' : a.node.type === 'component' ? 'box' : 'file') + '" class="w-3.5 h-3.5"></i>' +
|
||||
'<span>' + escapeHtml(a.node.name || a.node.id) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="association-weight">' + a.weight + '</div>' +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
(associations.length > 10 ? '<div class="associations-more">+' + (associations.length - 10) + ' more</div>' : '') +
|
||||
'</div>' +
|
||||
'</div>' : '<div class="node-no-associations">' + t('memory.noAssociations') + '</div>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== Actions ==========
|
||||
async function setMemoryTimeFilter(filter) {
|
||||
memoryTimeFilter = filter;
|
||||
await loadMemoryStats();
|
||||
renderHotspotsColumn();
|
||||
}
|
||||
|
||||
async function refreshMemoryData() {
|
||||
await Promise.all([
|
||||
loadMemoryStats(),
|
||||
loadMemoryGraph(),
|
||||
loadRecentContext()
|
||||
]);
|
||||
renderHotspotsColumn();
|
||||
renderGraphColumn();
|
||||
renderContextColumn();
|
||||
}
|
||||
|
||||
function filterRecentContext(query) {
|
||||
var filtered = recentContext;
|
||||
if (query && query.trim()) {
|
||||
var q = query.toLowerCase();
|
||||
filtered = recentContext.filter(function(item) {
|
||||
var promptMatch = (item.prompt || '').toLowerCase().includes(q);
|
||||
var filesMatch = (item.files || []).some(function(f) { return f.toLowerCase().includes(q); });
|
||||
return promptMatch || filesMatch;
|
||||
});
|
||||
}
|
||||
|
||||
var container = document.getElementById('memory-context');
|
||||
if (!container) return;
|
||||
|
||||
var timeline = container.querySelector('.context-timeline');
|
||||
if (timeline) {
|
||||
timeline.outerHTML = renderContextTimeline(filtered);
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== WebSocket Event Handlers ==========
|
||||
function handleMemoryUpdated(payload) {
|
||||
// Refresh graph and stats without full re-render
|
||||
if (payload.type === 'stats') {
|
||||
loadMemoryStats().then(function() { renderHotspotsColumn(); });
|
||||
} else if (payload.type === 'graph') {
|
||||
loadMemoryGraph().then(function() { renderGraphColumn(); });
|
||||
} else if (payload.type === 'context') {
|
||||
loadRecentContext().then(function() { renderContextColumn(); });
|
||||
} else {
|
||||
// Full refresh
|
||||
refreshMemoryData();
|
||||
}
|
||||
|
||||
// Highlight updated node if provided
|
||||
if (payload.nodeId) {
|
||||
highlightNode(payload.nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Utilities ==========
|
||||
function formatTimestamp(timestamp) {
|
||||
var date = new Date(timestamp);
|
||||
var now = new Date();
|
||||
var diff = now - date;
|
||||
|
||||
// Less than 1 minute
|
||||
if (diff < 60000) {
|
||||
return t('memory.justNow');
|
||||
}
|
||||
// Less than 1 hour
|
||||
if (diff < 3600000) {
|
||||
var minutes = Math.floor(diff / 60000);
|
||||
return minutes + ' ' + t('memory.minutesAgo');
|
||||
}
|
||||
// Less than 1 day
|
||||
if (diff < 86400000) {
|
||||
var hours = Math.floor(diff / 3600000);
|
||||
return hours + ' ' + t('memory.hoursAgo');
|
||||
}
|
||||
// Otherwise show date
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
||||
}
|
||||
538
ccw/src/templates/dashboard-js/views/prompt-history.js
Normal file
538
ccw/src/templates/dashboard-js/views/prompt-history.js
Normal file
@@ -0,0 +1,538 @@
|
||||
// Prompt History View
|
||||
// Displays prompt history and optimization insights
|
||||
|
||||
// ========== State ==========
|
||||
var promptHistoryData = [];
|
||||
var promptInsights = null;
|
||||
var promptHistorySearch = '';
|
||||
var promptHistoryDateFilter = null;
|
||||
var promptHistoryProjectFilter = null;
|
||||
var selectedPromptId = null;
|
||||
|
||||
// ========== Data Loading ==========
|
||||
async function loadPromptHistory() {
|
||||
try {
|
||||
// Use native Claude history.jsonl as primary source
|
||||
var response = await fetch('/api/memory/native-history?path=' + encodeURIComponent(projectPath) + '&limit=200');
|
||||
if (!response.ok) throw new Error('Failed to load prompt history');
|
||||
var data = await response.json();
|
||||
promptHistoryData = data.prompts || [];
|
||||
console.log('[PromptHistory] Loaded', promptHistoryData.length, 'prompts from native history');
|
||||
return promptHistoryData;
|
||||
} catch (err) {
|
||||
console.error('Failed to load prompt history:', err);
|
||||
promptHistoryData = [];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPromptInsights() {
|
||||
try {
|
||||
var response = await fetch('/api/memory/insights?path=' + encodeURIComponent(projectPath));
|
||||
if (!response.ok) throw new Error('Failed to load insights');
|
||||
var data = await response.json();
|
||||
promptInsights = data.insights || null;
|
||||
return promptInsights;
|
||||
} catch (err) {
|
||||
console.error('Failed to load insights:', err);
|
||||
promptInsights = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Rendering ==========
|
||||
async function renderPromptHistoryView() {
|
||||
var container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
// Hide stats grid and search
|
||||
var statsGrid = document.getElementById('statsGrid');
|
||||
var searchInput = document.getElementById('searchInput');
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Load data
|
||||
await Promise.all([loadPromptHistory(), loadPromptInsights()]);
|
||||
|
||||
// Calculate stats
|
||||
var totalPrompts = promptHistoryData.length;
|
||||
var intentDistribution = calculateIntentDistribution(promptHistoryData);
|
||||
var avgLength = calculateAverageLength(promptHistoryData);
|
||||
var qualityDistribution = calculateQualityDistribution(promptHistoryData);
|
||||
|
||||
container.innerHTML = '<div class="prompt-history-view">' +
|
||||
'<div class="prompt-history-header">' +
|
||||
renderStatsSection(totalPrompts, intentDistribution, avgLength, qualityDistribution) +
|
||||
'</div>' +
|
||||
'<div class="prompt-history-content">' +
|
||||
'<div class="prompt-history-left">' +
|
||||
renderPromptTimeline() +
|
||||
'</div>' +
|
||||
'<div class="prompt-history-right">' +
|
||||
renderInsightsPanel() +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderStatsSection(totalPrompts, intentDist, avgLength, qualityDist) {
|
||||
var topIntent = intentDist.length > 0 ? intentDist[0].intent : 'N/A';
|
||||
var topIntentCount = intentDist.length > 0 ? intentDist[0].count : 0;
|
||||
var intentLabel = t('prompt.intent.' + topIntent) || topIntent;
|
||||
|
||||
return '<div class="prompt-stats-grid">' +
|
||||
'<div class="prompt-stat-card">' +
|
||||
'<div class="stat-icon"><i data-lucide="message-square" class="w-5 h-5"></i></div>' +
|
||||
'<div class="stat-content">' +
|
||||
'<div class="stat-value">' + totalPrompts + '</div>' +
|
||||
'<div class="stat-label">' + (isZh() ? '总提示词' : 'Total Prompts') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="prompt-stat-card">' +
|
||||
'<div class="stat-icon"><i data-lucide="target" class="w-5 h-5"></i></div>' +
|
||||
'<div class="stat-content">' +
|
||||
'<div class="stat-value">' + intentLabel + '</div>' +
|
||||
'<div class="stat-label">' + (isZh() ? '主要意图' : 'Top Intent') + ' (' + topIntentCount + ')</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="prompt-stat-card">' +
|
||||
'<div class="stat-icon"><i data-lucide="align-left" class="w-5 h-5"></i></div>' +
|
||||
'<div class="stat-content">' +
|
||||
'<div class="stat-value">' + avgLength + '</div>' +
|
||||
'<div class="stat-label">' + (isZh() ? '平均长度' : 'Avg Length') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="prompt-stat-card">' +
|
||||
'<div class="stat-icon"><i data-lucide="bar-chart-2" class="w-5 h-5"></i></div>' +
|
||||
'<div class="stat-content">' +
|
||||
'<div class="stat-value">' + renderQualityBadge(qualityDist.average) + '</div>' +
|
||||
'<div class="stat-label">' + t('prompt.quality') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderPromptTimeline() {
|
||||
var filteredPrompts = filterPrompts(promptHistoryData);
|
||||
var groupedBySession = groupPromptsBySession(filteredPrompts);
|
||||
|
||||
var html = '<div class="prompt-timeline-header">' +
|
||||
'<h3><i data-lucide="clock" class="w-4 h-4"></i> ' + t('prompt.timeline') + '</h3>' +
|
||||
'<div class="prompt-timeline-filters">' +
|
||||
'<div class="prompt-search-wrapper">' +
|
||||
'<i data-lucide="search" class="w-4 h-4"></i>' +
|
||||
'<input type="text" class="prompt-search-input" placeholder="' + t('prompt.searchPlaceholder') + '" ' +
|
||||
'value="' + escapeHtml(promptHistorySearch) + '" ' +
|
||||
'oninput="searchPrompts(this.value)">' +
|
||||
'</div>' +
|
||||
'<button class="btn-icon" onclick="refreshPromptHistory()" title="' + t('common.refresh') + '">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if (filteredPrompts.length === 0) {
|
||||
html += '<div class="prompt-empty-state">' +
|
||||
'<i data-lucide="message-circle-off" class="w-12 h-12"></i>' +
|
||||
'<h3>' + t('prompt.noPromptsFound') + '</h3>' +
|
||||
'<p>' + t('prompt.noPromptsText') + '</p>' +
|
||||
'</div>';
|
||||
} else {
|
||||
html += '<div class="prompt-timeline-list">';
|
||||
for (var sessionId in groupedBySession) {
|
||||
html += renderSessionGroup(sessionId, groupedBySession[sessionId]);
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderSessionGroup(sessionId, prompts) {
|
||||
var sessionDate = new Date(prompts[0].timestamp).toLocaleDateString();
|
||||
var shortSessionId = sessionId.substring(0, 8);
|
||||
|
||||
var html = '<div class="prompt-session-group">' +
|
||||
'<div class="prompt-session-header">' +
|
||||
'<span class="prompt-session-id">' +
|
||||
'<i data-lucide="layers" class="w-3.5 h-3.5"></i> ' +
|
||||
shortSessionId +
|
||||
'</span>' +
|
||||
'<span class="prompt-session-date">' + sessionDate + '</span>' +
|
||||
'<span class="prompt-session-count">' + prompts.length + ' prompt' + (prompts.length > 1 ? 's' : '') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="prompt-session-items">';
|
||||
|
||||
for (var i = 0; i < prompts.length; i++) {
|
||||
html += renderPromptItem(prompts[i]);
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderPromptItem(prompt) {
|
||||
var isExpanded = selectedPromptId === prompt.id;
|
||||
var timeAgo = getTimeAgo(new Date(prompt.timestamp));
|
||||
var preview = prompt.text.substring(0, 120) + (prompt.text.length > 120 ? '...' : '');
|
||||
var qualityClass = getQualityClass(prompt.quality_score);
|
||||
|
||||
var html = '<div class="prompt-item' + (isExpanded ? ' prompt-item-expanded' : '') + '" ' +
|
||||
'onclick="togglePromptExpand(\'' + prompt.id + '\')">' +
|
||||
'<div class="prompt-item-header">' +
|
||||
'<span class="prompt-intent-tag">' + (prompt.intent || 'unknown') + '</span>' +
|
||||
'<span class="prompt-quality-badge ' + qualityClass + '">' +
|
||||
'<i data-lucide="sparkles" class="w-3 h-3"></i> ' + (prompt.quality_score || 0) +
|
||||
'</span>' +
|
||||
'<span class="prompt-time">' + timeAgo + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="prompt-item-preview">' + escapeHtml(preview) + '</div>';
|
||||
|
||||
if (isExpanded) {
|
||||
html += '<div class="prompt-item-full">' +
|
||||
'<div class="prompt-full-text">' + escapeHtml(prompt.text) + '</div>' +
|
||||
'<div class="prompt-item-meta">' +
|
||||
'<span><i data-lucide="type" class="w-3 h-3"></i> ' + prompt.text.length + ' chars</span>' +
|
||||
(prompt.project ? '<span><i data-lucide="folder" class="w-3 h-3"></i> ' + escapeHtml(prompt.project) + '</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="prompt-item-actions-full">' +
|
||||
'<button class="btn btn-sm btn-outline" onclick="event.stopPropagation(); copyPrompt(\'' + prompt.id + '\')">' +
|
||||
'<i data-lucide="copy" class="w-3.5 h-3.5"></i> ' + t('common.copy') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderInsightsPanel() {
|
||||
var html = '<div class="insights-panel-header">' +
|
||||
'<h3><i data-lucide="lightbulb" class="w-4 h-4"></i> ' + t('prompt.insights') + '</h3>' +
|
||||
'<div class="insights-actions">' +
|
||||
'<select id="insightsTool" class="insights-tool-select">' +
|
||||
'<option value="gemini">Gemini</option>' +
|
||||
'<option value="qwen">Qwen</option>' +
|
||||
'</select>' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="triggerCliInsightsAnalysis()" id="analyzeBtn">' +
|
||||
'<i data-lucide="sparkles" class="w-3.5 h-3.5"></i> ' + t('prompt.analyze') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Show loading state
|
||||
if (window.insightsAnalyzing) {
|
||||
html += '<div class="insights-loading">' +
|
||||
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-6 h-6 animate-spin"></i></div>' +
|
||||
'<p>' + t('prompt.loadingInsights') + '</p>' +
|
||||
'</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
if (!promptInsights || !promptInsights.patterns || promptInsights.patterns.length === 0) {
|
||||
html += '<div class="insights-empty-state">' +
|
||||
'<i data-lucide="brain" class="w-10 h-10"></i>' +
|
||||
'<p>' + t('prompt.noInsights') + '</p>' +
|
||||
'<p class="insights-hint">' + t('prompt.noInsightsText') + '</p>' +
|
||||
'</div>';
|
||||
} else {
|
||||
html += '<div class="insights-list">';
|
||||
|
||||
// Render detected patterns
|
||||
if (promptInsights.patterns && promptInsights.patterns.length > 0) {
|
||||
html += '<div class="insights-section">' +
|
||||
'<h4><i data-lucide="alert-circle" class="w-4 h-4"></i> Detected Patterns</h4>';
|
||||
for (var i = 0; i < promptInsights.patterns.length; i++) {
|
||||
html += renderPatternCard(promptInsights.patterns[i]);
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Render suggestions
|
||||
if (promptInsights.suggestions && promptInsights.suggestions.length > 0) {
|
||||
html += '<div class="insights-section">' +
|
||||
'<h4><i data-lucide="zap" class="w-4 h-4"></i> Optimization Suggestions</h4>';
|
||||
for (var j = 0; j < promptInsights.suggestions.length; j++) {
|
||||
html += renderSuggestionCard(promptInsights.suggestions[j]);
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Render similar successful prompts
|
||||
if (promptInsights.similar_prompts && promptInsights.similar_prompts.length > 0) {
|
||||
html += '<div class="insights-section">' +
|
||||
'<h4><i data-lucide="stars" class="w-4 h-4"></i> Similar Successful Prompts</h4>';
|
||||
for (var k = 0; k < promptInsights.similar_prompts.length; k++) {
|
||||
html += renderSimilarPromptCard(promptInsights.similar_prompts[k]);
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderPatternCard(pattern) {
|
||||
var iconMap = {
|
||||
'vague': 'help-circle',
|
||||
'correction': 'rotate-ccw',
|
||||
'repetitive': 'repeat',
|
||||
'incomplete': 'alert-triangle'
|
||||
};
|
||||
var icon = iconMap[pattern.type] || 'info';
|
||||
var severityClass = pattern.severity || 'medium';
|
||||
|
||||
return '<div class="pattern-card pattern-' + severityClass + '">' +
|
||||
'<div class="pattern-header">' +
|
||||
'<i data-lucide="' + icon + '" class="w-4 h-4"></i>' +
|
||||
'<span class="pattern-type">' + (pattern.type || 'Unknown') + '</span>' +
|
||||
'<span class="pattern-count">' + (pattern.occurrences || 0) + 'x</span>' +
|
||||
'</div>' +
|
||||
'<div class="pattern-description">' + escapeHtml(pattern.description || '') + '</div>' +
|
||||
'<div class="pattern-suggestion">' +
|
||||
'<i data-lucide="arrow-right" class="w-3 h-3"></i> ' +
|
||||
escapeHtml(pattern.suggestion || '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderSuggestionCard(suggestion) {
|
||||
return '<div class="suggestion-card">' +
|
||||
'<div class="suggestion-title">' +
|
||||
'<i data-lucide="sparkle" class="w-3.5 h-3.5"></i> ' +
|
||||
escapeHtml(suggestion.title || 'Suggestion') +
|
||||
'</div>' +
|
||||
'<div class="suggestion-description">' + escapeHtml(suggestion.description || '') + '</div>' +
|
||||
(suggestion.example ?
|
||||
'<div class="suggestion-example">' +
|
||||
'<div class="suggestion-example-label">Example:</div>' +
|
||||
'<code>' + escapeHtml(suggestion.example) + '</code>' +
|
||||
'</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderSimilarPromptCard(prompt) {
|
||||
var similarity = Math.round((prompt.similarity || 0) * 100);
|
||||
var preview = prompt.text.substring(0, 80) + (prompt.text.length > 80 ? '...' : '');
|
||||
|
||||
return '<div class="similar-prompt-card" onclick="showPromptDetail(\'' + prompt.id + '\')">' +
|
||||
'<div class="similar-prompt-header">' +
|
||||
'<span class="similar-prompt-similarity">' + similarity + '% match</span>' +
|
||||
'<span class="similar-prompt-intent">' + (prompt.intent || 'unknown') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="similar-prompt-preview">' + escapeHtml(preview) + '</div>' +
|
||||
'<div class="similar-prompt-meta">' +
|
||||
'<span class="similar-prompt-quality">' +
|
||||
'<i data-lucide="star" class="w-3 h-3"></i> ' + (prompt.quality_score || 0) +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderProjectOptions() {
|
||||
var projects = getUniqueProjects(promptHistoryData);
|
||||
var html = '';
|
||||
for (var i = 0; i < projects.length; i++) {
|
||||
var selected = projects[i] === promptHistoryProjectFilter ? 'selected' : '';
|
||||
html += '<option value="' + escapeHtml(projects[i]) + '" ' + selected + '>' +
|
||||
escapeHtml(projects[i]) + '</option>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderQualityBadge(score) {
|
||||
if (score >= 80) return '<span class="quality-badge high">' + score + '</span>';
|
||||
if (score >= 60) return '<span class="quality-badge medium">' + score + '</span>';
|
||||
return '<span class="quality-badge low">' + score + '</span>';
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
function calculateIntentDistribution(prompts) {
|
||||
var distribution = {};
|
||||
for (var i = 0; i < prompts.length; i++) {
|
||||
var intent = prompts[i].intent || 'unknown';
|
||||
distribution[intent] = (distribution[intent] || 0) + 1;
|
||||
}
|
||||
|
||||
var result = [];
|
||||
for (var key in distribution) {
|
||||
result.push({ intent: key, count: distribution[key] });
|
||||
}
|
||||
result.sort(function(a, b) { return b.count - a.count; });
|
||||
return result;
|
||||
}
|
||||
|
||||
function calculateAverageLength(prompts) {
|
||||
if (prompts.length === 0) return 0;
|
||||
var total = 0;
|
||||
for (var i = 0; i < prompts.length; i++) {
|
||||
total += (prompts[i].text || '').length;
|
||||
}
|
||||
return Math.round(total / prompts.length);
|
||||
}
|
||||
|
||||
function calculateQualityDistribution(prompts) {
|
||||
if (prompts.length === 0) return { average: 0, distribution: {} };
|
||||
|
||||
var total = 0;
|
||||
var distribution = { high: 0, medium: 0, low: 0 };
|
||||
|
||||
for (var i = 0; i < prompts.length; i++) {
|
||||
var score = prompts[i].quality_score || 0;
|
||||
total += score;
|
||||
|
||||
if (score >= 80) distribution.high++;
|
||||
else if (score >= 60) distribution.medium++;
|
||||
else distribution.low++;
|
||||
}
|
||||
|
||||
return {
|
||||
average: Math.round(total / prompts.length),
|
||||
distribution: distribution
|
||||
};
|
||||
}
|
||||
|
||||
function getQualityClass(score) {
|
||||
if (score >= 80) return 'quality-high';
|
||||
if (score >= 60) return 'quality-medium';
|
||||
return 'quality-low';
|
||||
}
|
||||
|
||||
function filterPrompts(prompts) {
|
||||
return prompts.filter(function(prompt) {
|
||||
var matchesSearch = !promptHistorySearch ||
|
||||
prompt.text.toLowerCase().includes(promptHistorySearch.toLowerCase());
|
||||
var matchesProject = !promptHistoryProjectFilter ||
|
||||
prompt.project === promptHistoryProjectFilter;
|
||||
var matchesDate = !promptHistoryDateFilter ||
|
||||
isSameDay(new Date(prompt.timestamp), promptHistoryDateFilter);
|
||||
|
||||
return matchesSearch && matchesProject && matchesDate;
|
||||
});
|
||||
}
|
||||
|
||||
function groupPromptsBySession(prompts) {
|
||||
var grouped = {};
|
||||
for (var i = 0; i < prompts.length; i++) {
|
||||
var sessionId = prompts[i].session_id || 'unknown';
|
||||
if (!grouped[sessionId]) grouped[sessionId] = [];
|
||||
grouped[sessionId].push(prompts[i]);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function getUniqueProjects(prompts) {
|
||||
var projects = {};
|
||||
for (var i = 0; i < prompts.length; i++) {
|
||||
if (prompts[i].project) projects[prompts[i].project] = true;
|
||||
}
|
||||
return Object.keys(projects).sort();
|
||||
}
|
||||
|
||||
function isSameDay(date1, date2) {
|
||||
return date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate();
|
||||
}
|
||||
|
||||
// ========== Actions ==========
|
||||
function searchPrompts(query) {
|
||||
promptHistorySearch = query;
|
||||
renderPromptHistoryView();
|
||||
}
|
||||
|
||||
function filterByProject(project) {
|
||||
promptHistoryProjectFilter = project || null;
|
||||
renderPromptHistoryView();
|
||||
}
|
||||
|
||||
function togglePromptExpand(promptId) {
|
||||
if (selectedPromptId === promptId) {
|
||||
selectedPromptId = null;
|
||||
} else {
|
||||
selectedPromptId = promptId;
|
||||
}
|
||||
renderPromptHistoryView();
|
||||
}
|
||||
|
||||
function copyPrompt(promptId) {
|
||||
var prompt = promptHistoryData.find(function(p) { return p.id === promptId; });
|
||||
if (!prompt) return;
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(prompt.text).then(function() {
|
||||
showRefreshToast('Prompt copied to clipboard', 'success');
|
||||
}).catch(function() {
|
||||
showRefreshToast('Failed to copy prompt', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showPromptDetail(promptId) {
|
||||
togglePromptExpand(promptId);
|
||||
}
|
||||
|
||||
async function refreshPromptHistory() {
|
||||
await Promise.all([loadPromptHistory(), loadPromptInsights()]);
|
||||
renderPromptHistoryView();
|
||||
showRefreshToast('Prompt history refreshed', 'success');
|
||||
}
|
||||
|
||||
// ========== CLI-based Insights Analysis ==========
|
||||
async function triggerCliInsightsAnalysis() {
|
||||
if (promptHistoryData.length === 0) {
|
||||
showRefreshToast(t('prompt.noPromptsFound'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var toolSelect = document.getElementById('insightsTool');
|
||||
var tool = toolSelect ? toolSelect.value : 'gemini';
|
||||
var analyzeBtn = document.getElementById('analyzeBtn');
|
||||
|
||||
// Show loading state
|
||||
window.insightsAnalyzing = true;
|
||||
if (analyzeBtn) {
|
||||
analyzeBtn.disabled = true;
|
||||
analyzeBtn.innerHTML = '<i data-lucide="loader-2" class="w-3.5 h-3.5 animate-spin"></i> ' + t('prompt.analyzing');
|
||||
}
|
||||
renderPromptHistoryView();
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/memory/insights/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
path: projectPath,
|
||||
tool: tool,
|
||||
lang: getLang(), // Send current language preference
|
||||
prompts: promptHistoryData.slice(0, 50) // Send top 50 prompts for analysis
|
||||
})
|
||||
});
|
||||
|
||||
var data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
// Update insights with CLI analysis results
|
||||
if (data.insights) {
|
||||
promptInsights = data.insights;
|
||||
console.log('[PromptHistory] Insights parsed:', promptInsights);
|
||||
}
|
||||
|
||||
showRefreshToast(t('toast.completed') + ' (' + tool + ')', 'success');
|
||||
} catch (err) {
|
||||
console.error('CLI insights analysis failed:', err);
|
||||
showRefreshToast(t('prompt.insightsError') + ': ' + err.message, 'error');
|
||||
} finally {
|
||||
window.insightsAnalyzing = false;
|
||||
renderPromptHistoryView();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user