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`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user