mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: Enhance JSON streaming parsing and UI updates
- Added a function to parse JSON streaming content in core-memory.js, extracting readable text from messages. - Updated memory detail view to utilize the new parsing function for content and summary. - Introduced an enableReview option in rules-manager.js, allowing users to toggle review functionality in rule creation. - Simplified skill creation modal in skills-manager.js by removing generation type selection UI. - Improved CLI executor to handle tool calls for file writing, ensuring proper output parsing. - Adjusted CLI command tests to set timeout to 0 for immediate execution. - Updated file watcher to implement a true debounce mechanism and added a pending queue status for UI updates. - Enhanced watcher manager to handle queue changes and provide JSON output for better integration with TypeScript backend. - Established TypeScript naming conventions documentation to standardize code style across the project.
This commit is contained in:
@@ -472,6 +472,65 @@ function handleNotification(data) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CODEXLENS_WATCHER_UPDATE':
|
||||
// Handle CodexLens watcher real-time updates (file changes detected)
|
||||
if (typeof handleWatcherStatusUpdate === 'function') {
|
||||
handleWatcherStatusUpdate(payload);
|
||||
}
|
||||
console.log('[CodexLens] Watcher update:', payload.events_processed, 'events');
|
||||
break;
|
||||
|
||||
case 'CODEXLENS_WATCHER_QUEUE_UPDATE':
|
||||
// Handle pending queue status updates
|
||||
if (typeof updatePendingQueueUI === 'function') {
|
||||
updatePendingQueueUI(payload.queue);
|
||||
}
|
||||
// Add activity log entries only for NEW files (not already logged)
|
||||
if (payload.queue && payload.queue.files && payload.queue.files.length > 0) {
|
||||
if (typeof addWatcherLogEntry === 'function') {
|
||||
// Track logged files to avoid duplicates
|
||||
window._watcherLoggedFiles = window._watcherLoggedFiles || new Set();
|
||||
var newFiles = payload.queue.files.filter(function(f) {
|
||||
return !window._watcherLoggedFiles.has(f);
|
||||
});
|
||||
// Only show first few new files to avoid spam
|
||||
newFiles.slice(0, 5).forEach(function(fileName) {
|
||||
window._watcherLoggedFiles.add(fileName);
|
||||
addWatcherLogEntry('modified', fileName);
|
||||
});
|
||||
// Clear tracking when queue is empty (after flush)
|
||||
if (payload.queue.file_count === 0) {
|
||||
window._watcherLoggedFiles.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('[CodexLens] Queue update:', payload.queue?.file_count, 'files pending');
|
||||
break;
|
||||
|
||||
case 'CODEXLENS_WATCHER_INDEX_COMPLETE':
|
||||
// Handle index completion event
|
||||
if (typeof updateLastIndexResult === 'function') {
|
||||
updateLastIndexResult(payload.result);
|
||||
}
|
||||
// Clear logged files tracking after index completes
|
||||
if (window._watcherLoggedFiles) {
|
||||
window._watcherLoggedFiles.clear();
|
||||
}
|
||||
// Add activity log entry for index completion
|
||||
if (typeof addWatcherLogEntry === 'function' && payload.result) {
|
||||
var summary = 'Indexed ' + (payload.result.files_indexed || 0) + ' files';
|
||||
addWatcherLogEntry('indexed', summary);
|
||||
}
|
||||
// Show toast notification
|
||||
if (typeof showRefreshToast === 'function' && payload.result) {
|
||||
var indexMsg = 'Indexed ' + (payload.result.files_indexed || 0) + ' files, ' +
|
||||
(payload.result.symbols_added || 0) + ' symbols';
|
||||
var toastType = (payload.result.errors && payload.result.errors.length > 0) ? 'warning' : 'success';
|
||||
showRefreshToast(indexMsg, toastType);
|
||||
}
|
||||
console.log('[CodexLens] Index complete:', payload.result?.files_indexed, 'files indexed');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[WS] Unknown notification type:', type);
|
||||
}
|
||||
|
||||
@@ -1363,6 +1363,8 @@ const i18n = {
|
||||
'rules.extractScopeRequired': 'Analysis scope is required',
|
||||
'rules.extractFocus': 'Focus Areas',
|
||||
'rules.extractFocusHint': 'Comma-separated aspects to focus on (e.g., naming, error-handling)',
|
||||
'rules.enableReview': 'Enable Quality Review',
|
||||
'rules.enableReviewHint': 'AI will verify the generated rule for clarity, actionability, and proper formatting',
|
||||
'rules.cliGenerating': 'Generating rule via CLI (this may take a few minutes)...',
|
||||
|
||||
// CLAUDE.md Manager
|
||||
@@ -3375,6 +3377,8 @@ const i18n = {
|
||||
'rules.extractScopeRequired': '分析范围是必需的',
|
||||
'rules.extractFocus': '关注领域',
|
||||
'rules.extractFocusHint': '以逗号分隔的关注方面(例如:命名规范, 错误处理)',
|
||||
'rules.enableReview': '启用质量审查',
|
||||
'rules.enableReviewHint': 'AI 将验证生成的规则是否清晰、可操作且格式正确',
|
||||
'rules.cliGenerating': '正在通过 CLI 生成规则(可能需要几分钟)...',
|
||||
|
||||
// CLAUDE.md Manager
|
||||
|
||||
@@ -636,12 +636,6 @@ function renderFileMetadata() {
|
||||
'<option value="gemini">Gemini</option>' +
|
||||
'<option value="qwen">Qwen</option>' +
|
||||
'</select>' +
|
||||
'<label>' + (t('claude.mode') || 'Mode') + '</label>' +
|
||||
'<select id="cliModeSelect" class="sync-select">' +
|
||||
'<option value="update">' + (t('claude.modeUpdate') || 'Update (Smart Merge)') + '</option>' +
|
||||
'<option value="generate">' + (t('claude.modeGenerate') || 'Generate (Full Replace)') + '</option>' +
|
||||
'<option value="append">' + (t('claude.modeAppend') || 'Append') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<button class="btn btn-sm btn-primary full-width sync-button" onclick="syncFileWithCLI()" id="cliSyncButton">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> ' +
|
||||
@@ -664,7 +658,7 @@ async function syncFileWithCLI() {
|
||||
if (!selectedFile) return;
|
||||
|
||||
var tool = document.getElementById('cliToolSelect').value;
|
||||
var mode = document.getElementById('cliModeSelect').value;
|
||||
var mode = 'generate'; // Default to full replace mode
|
||||
|
||||
// Show progress
|
||||
showSyncProgress(true, tool);
|
||||
|
||||
@@ -4536,10 +4536,25 @@ window.toggleWatcher = async function toggleWatcher() {
|
||||
// Check current status first
|
||||
try {
|
||||
console.log('[CodexLens] Checking watcher status...');
|
||||
var statusResponse = await fetch('/api/codexlens/watch/status');
|
||||
// Pass path parameter to get specific watcher status
|
||||
var statusResponse = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath));
|
||||
var statusResult = await statusResponse.json();
|
||||
console.log('[CodexLens] Status result:', statusResult);
|
||||
var isRunning = statusResult.success && statusResult.running;
|
||||
|
||||
// Handle both single watcher response and array response
|
||||
var isRunning = false;
|
||||
if (statusResult.success) {
|
||||
if (typeof statusResult.running === 'boolean') {
|
||||
isRunning = statusResult.running;
|
||||
} else if (statusResult.watchers && Array.isArray(statusResult.watchers)) {
|
||||
var normalizedPath = projectPath.toLowerCase().replace(/\\/g, '/');
|
||||
var matchingWatcher = statusResult.watchers.find(function(w) {
|
||||
var watcherPath = (w.root_path || '').toLowerCase().replace(/\\/g, '/');
|
||||
return watcherPath === normalizedPath || watcherPath.includes(normalizedPath) || normalizedPath.includes(watcherPath);
|
||||
});
|
||||
isRunning = matchingWatcher ? matchingWatcher.running : false;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle: if running, stop; if stopped, start
|
||||
var action = isRunning ? 'stop' : 'start';
|
||||
@@ -4592,7 +4607,8 @@ function updateWatcherUI(running, stats) {
|
||||
var uptimeDisplay = document.getElementById('watcherUptimeDisplay');
|
||||
|
||||
if (filesCount) filesCount.textContent = stats.files_watched || '-';
|
||||
if (changesCount) changesCount.textContent = stats.changes_detected || '0';
|
||||
// Support both changes_detected and events_processed
|
||||
if (changesCount) changesCount.textContent = stats.events_processed || stats.changes_detected || '0';
|
||||
if (uptimeDisplay) uptimeDisplay.textContent = formatUptime(stats.uptime_seconds);
|
||||
}
|
||||
|
||||
@@ -4628,17 +4644,25 @@ function startWatcherPolling() {
|
||||
if (watcherPollInterval) return; // Already polling
|
||||
|
||||
watcherStartTime = Date.now();
|
||||
var projectPath = window.CCW_PROJECT_ROOT || '.';
|
||||
|
||||
watcherPollInterval = setInterval(async function() {
|
||||
try {
|
||||
var response = await fetch('/api/codexlens/watch/status');
|
||||
// Must include path parameter to get specific watcher status
|
||||
var response = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath));
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success && result.running) {
|
||||
// Update uptime
|
||||
// Update uptime from server response
|
||||
var uptimeDisplay = document.getElementById('watcherUptimeDisplay');
|
||||
if (uptimeDisplay) {
|
||||
var uptime = (Date.now() - watcherStartTime) / 1000;
|
||||
uptimeDisplay.textContent = formatUptime(uptime);
|
||||
if (uptimeDisplay && result.uptime_seconds !== undefined) {
|
||||
uptimeDisplay.textContent = formatUptime(result.uptime_seconds);
|
||||
}
|
||||
|
||||
// Update changes count from events_processed
|
||||
if (result.events_processed !== undefined) {
|
||||
var changesCount = document.getElementById('watcherChangesCount');
|
||||
if (changesCount) changesCount.textContent = result.events_processed;
|
||||
}
|
||||
|
||||
// Update files count if available
|
||||
@@ -4653,8 +4677,8 @@ function startWatcherPolling() {
|
||||
addWatcherLogEntry(event.type, event.path);
|
||||
});
|
||||
}
|
||||
} else if (!result.running) {
|
||||
// Watcher stopped externally
|
||||
} else if (result.success && result.running === false) {
|
||||
// Watcher stopped externally (only if running is explicitly false)
|
||||
updateWatcherUI(false);
|
||||
stopWatcherPolling();
|
||||
}
|
||||
@@ -4699,13 +4723,15 @@ function addWatcherLogEntry(type, path) {
|
||||
'created': 'text-success',
|
||||
'modified': 'text-warning',
|
||||
'deleted': 'text-destructive',
|
||||
'renamed': 'text-primary'
|
||||
'renamed': 'text-primary',
|
||||
'indexed': 'text-success'
|
||||
};
|
||||
var typeIcons = {
|
||||
'created': '+',
|
||||
'modified': '~',
|
||||
'deleted': '-',
|
||||
'renamed': '→'
|
||||
'renamed': '→',
|
||||
'indexed': '✓'
|
||||
};
|
||||
|
||||
var colorClass = typeColors[type] || 'text-muted-foreground';
|
||||
@@ -4748,13 +4774,35 @@ function clearWatcherLog() {
|
||||
*/
|
||||
async function initWatcherStatus() {
|
||||
try {
|
||||
var response = await fetch('/api/codexlens/watch/status');
|
||||
var projectPath = window.CCW_PROJECT_ROOT || '.';
|
||||
// Pass path parameter to get specific watcher status
|
||||
var response = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath));
|
||||
var result = await response.json();
|
||||
if (result.success) {
|
||||
updateWatcherUI(result.running, {
|
||||
files_watched: result.files_watched,
|
||||
// Handle both single watcher response (with path param) and array response (without path param)
|
||||
var running = result.running;
|
||||
var uptime = result.uptime_seconds || 0;
|
||||
var filesWatched = result.files_watched;
|
||||
|
||||
// If response has watchers array (no path param), find matching watcher
|
||||
if (result.watchers && Array.isArray(result.watchers)) {
|
||||
var normalizedPath = projectPath.toLowerCase().replace(/\\/g, '/');
|
||||
var matchingWatcher = result.watchers.find(function(w) {
|
||||
var watcherPath = (w.root_path || '').toLowerCase().replace(/\\/g, '/');
|
||||
return watcherPath === normalizedPath || watcherPath.includes(normalizedPath) || normalizedPath.includes(watcherPath);
|
||||
});
|
||||
if (matchingWatcher) {
|
||||
running = matchingWatcher.running;
|
||||
uptime = matchingWatcher.uptime_seconds || 0;
|
||||
} else {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateWatcherUI(running, {
|
||||
files_watched: filesWatched,
|
||||
changes_detected: 0,
|
||||
uptime_seconds: result.uptime_seconds
|
||||
uptime_seconds: uptime
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -5846,6 +5894,45 @@ function buildWatcherControlContent(status, defaultPath) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Pending Queue Section (shown when running)
|
||||
'<div id="watcherPendingQueue" class="tool-config-section" style="display:' + (running ? 'block' : 'none') + '">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<h4 class="flex items-center gap-2 m-0">' +
|
||||
'<i data-lucide="clock" class="w-4 h-4"></i>' +
|
||||
(t('codexlens.pendingChanges') || 'Pending Changes') +
|
||||
'</h4>' +
|
||||
'<button onclick="flushWatcherNow()" class="btn btn-sm btn-primary" id="flushNowBtn" disabled>' +
|
||||
'<i data-lucide="zap" class="w-3 h-3 mr-1"></i>' +
|
||||
(t('codexlens.indexNow') || 'Index Now') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center justify-between p-3 bg-muted/20 rounded-lg mb-2">' +
|
||||
'<div>' +
|
||||
'<span class="text-2xl font-bold text-warning" id="pendingFileCount">0</span>' +
|
||||
'<span class="text-sm text-muted-foreground ml-1">' + (t('codexlens.filesWaiting') || 'files waiting') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="text-right">' +
|
||||
'<div class="text-lg font-mono" id="countdownTimer">--:--</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' + (t('codexlens.untilNextIndex') || 'until next index') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="pendingFilesList" class="max-h-24 overflow-y-auto space-y-1 text-sm"></div>' +
|
||||
'</div>' +
|
||||
|
||||
// Last Index Result (shown when running)
|
||||
'<div id="watcherLastIndex" class="tool-config-section" style="display:none">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<h4 class="flex items-center gap-2 m-0">' +
|
||||
'<i data-lucide="check-circle" class="w-4 h-4"></i>' +
|
||||
(t('codexlens.lastIndexResult') || 'Last Index Result') +
|
||||
'</h4>' +
|
||||
'<button onclick="showIndexHistory()" class="text-xs text-muted-foreground hover:text-foreground">' +
|
||||
(t('codexlens.viewHistory') || 'View History') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="grid grid-cols-4 gap-2 text-center" id="lastIndexStats"></div>' +
|
||||
'</div>' +
|
||||
|
||||
// Start Configuration (shown when not running)
|
||||
'<div id="watcherStartConfig" class="tool-config-section" style="display:' + (running ? 'none' : 'block') + '">' +
|
||||
'<h4>' + (t('codexlens.watcherConfig') || 'Configuration') + '</h4>' +
|
||||
@@ -5857,7 +5944,7 @@ function buildWatcherControlContent(status, defaultPath) {
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + (t('codexlens.debounceMs') || 'Debounce (ms)') + '</label>' +
|
||||
'<input type="number" id="watcherDebounce" value="1000" min="100" max="10000" step="100" ' +
|
||||
'<input type="number" id="watcherDebounce" value="60000" min="1000" max="120000" step="1000" ' +
|
||||
'class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" />' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.debounceHint') || 'Time to wait before processing file changes') + '</p>' +
|
||||
'</div>' +
|
||||
@@ -5986,6 +6073,204 @@ function stopWatcherStatusPolling() {
|
||||
clearInterval(watcherPollingInterval);
|
||||
watcherPollingInterval = null;
|
||||
}
|
||||
stopCountdownTimer();
|
||||
}
|
||||
|
||||
// Countdown timer for pending queue
|
||||
var countdownInterval = null;
|
||||
var currentCountdownSeconds = 0;
|
||||
|
||||
function startCountdownTimer(seconds) {
|
||||
currentCountdownSeconds = seconds;
|
||||
if (countdownInterval) return;
|
||||
|
||||
countdownInterval = setInterval(function() {
|
||||
var timerEl = document.getElementById('countdownTimer');
|
||||
if (!timerEl) {
|
||||
stopCountdownTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentCountdownSeconds <= 0) {
|
||||
timerEl.textContent = '--:--';
|
||||
} else {
|
||||
currentCountdownSeconds--;
|
||||
timerEl.textContent = formatCountdown(currentCountdownSeconds);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopCountdownTimer() {
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatCountdown(seconds) {
|
||||
if (seconds <= 0) return '--:--';
|
||||
var mins = Math.floor(seconds / 60);
|
||||
var secs = seconds % 60;
|
||||
return (mins < 10 ? '0' : '') + mins + ':' + (secs < 10 ? '0' : '') + secs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately flush pending queue and trigger indexing
|
||||
*/
|
||||
async function flushWatcherNow() {
|
||||
var btn = document.getElementById('flushNowBtn');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-3 h-3 mr-1 animate-spin"></i> Indexing...';
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
try {
|
||||
var watchPath = document.getElementById('watcherPath');
|
||||
var path = watchPath ? watchPath.value.trim() : '';
|
||||
|
||||
var response = await fetch('/api/codexlens/watch/flush', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: path || undefined })
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast(t('codexlens.indexTriggered') || 'Indexing triggered', 'success');
|
||||
} else {
|
||||
showRefreshToast(t('common.error') + ': ' + result.error, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i data-lucide="zap" class="w-3 h-3 mr-1"></i>' + (t('codexlens.indexNow') || 'Index Now');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
window.flushWatcherNow = flushWatcherNow;
|
||||
|
||||
/**
|
||||
* Show index history in a modal
|
||||
*/
|
||||
async function showIndexHistory() {
|
||||
try {
|
||||
var watchPath = document.getElementById('watcherPath');
|
||||
var path = watchPath ? watchPath.value.trim() : '';
|
||||
|
||||
var response = await fetch('/api/codexlens/watch/history?limit=10&path=' + encodeURIComponent(path));
|
||||
var result = await response.json();
|
||||
|
||||
if (!result.success || !result.history || result.history.length === 0) {
|
||||
showRefreshToast(t('codexlens.noHistory') || 'No index history available', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
var historyHtml = result.history.slice().reverse().map(function(h, i) {
|
||||
var timestamp = h.timestamp ? new Date(h.timestamp * 1000).toLocaleString() : 'Unknown';
|
||||
return '<div class="p-3 border-b border-border last:border-0">' +
|
||||
'<div class="flex justify-between items-center mb-2">' +
|
||||
'<span class="text-sm font-medium">#' + (result.history.length - i) + '</span>' +
|
||||
'<span class="text-xs text-muted-foreground">' + timestamp + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="grid grid-cols-4 gap-2 text-center text-sm">' +
|
||||
'<div><span class="text-success">' + (h.files_indexed || 0) + '</span> indexed</div>' +
|
||||
'<div><span class="text-warning">' + (h.files_removed || 0) + '</span> removed</div>' +
|
||||
'<div><span class="text-primary">+' + (h.symbols_added || 0) + '</span> symbols</div>' +
|
||||
'<div><span class="text-destructive">' + ((h.errors && h.errors.length) || 0) + '</span> errors</div>' +
|
||||
'</div>' +
|
||||
(h.errors && h.errors.length > 0 ? '<div class="mt-2 text-xs text-destructive">' +
|
||||
h.errors.slice(0, 2).map(function(e) { return '<div>• ' + e + '</div>'; }).join('') +
|
||||
(h.errors.length > 2 ? '<div>... and ' + (h.errors.length - 2) + ' more</div>' : '') +
|
||||
'</div>' : '') +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
var modal = document.createElement('div');
|
||||
modal.id = 'indexHistoryModal';
|
||||
modal.className = 'modal-backdrop';
|
||||
modal.innerHTML = '<div class="modal-container max-w-md">' +
|
||||
'<div class="modal-header">' +
|
||||
'<h2 class="text-lg font-bold">' + (t('codexlens.indexHistory') || 'Index History') + '</h2>' +
|
||||
'<button onclick="document.getElementById(\'indexHistoryModal\').remove()" class="text-muted-foreground hover:text-foreground">' +
|
||||
'<i data-lucide="x" class="w-5 h-5"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="modal-body max-h-96 overflow-y-auto">' + historyHtml + '</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(modal);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
window.showIndexHistory = showIndexHistory;
|
||||
|
||||
/**
|
||||
* Update pending queue UI elements
|
||||
*/
|
||||
function updatePendingQueueUI(queue) {
|
||||
var countEl = document.getElementById('pendingFileCount');
|
||||
var timerEl = document.getElementById('countdownTimer');
|
||||
var listEl = document.getElementById('pendingFilesList');
|
||||
var flushBtn = document.getElementById('flushNowBtn');
|
||||
|
||||
if (countEl) countEl.textContent = queue.file_count || 0;
|
||||
|
||||
if (queue.countdown_seconds > 0) {
|
||||
currentCountdownSeconds = queue.countdown_seconds;
|
||||
if (timerEl) timerEl.textContent = formatCountdown(queue.countdown_seconds);
|
||||
startCountdownTimer(queue.countdown_seconds);
|
||||
} else {
|
||||
if (timerEl) timerEl.textContent = '--:--';
|
||||
}
|
||||
|
||||
if (flushBtn) flushBtn.disabled = (queue.file_count || 0) === 0;
|
||||
|
||||
if (listEl && queue.files) {
|
||||
listEl.innerHTML = queue.files.map(function(f) {
|
||||
return '<div class="flex items-center gap-2 text-muted-foreground">' +
|
||||
'<i data-lucide="file" class="w-3 h-3"></i>' +
|
||||
'<span class="truncate">' + f + '</span>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last index result UI
|
||||
*/
|
||||
function updateLastIndexResult(result) {
|
||||
var statsEl = document.getElementById('lastIndexStats');
|
||||
var sectionEl = document.getElementById('watcherLastIndex');
|
||||
|
||||
if (sectionEl) sectionEl.style.display = 'block';
|
||||
if (statsEl) {
|
||||
statsEl.innerHTML = '<div class="p-2 bg-success/10 rounded">' +
|
||||
'<div class="text-lg font-bold text-success">' + (result.files_indexed || 0) + '</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Indexed</div>' +
|
||||
'</div>' +
|
||||
'<div class="p-2 bg-warning/10 rounded">' +
|
||||
'<div class="text-lg font-bold text-warning">' + (result.files_removed || 0) + '</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Removed</div>' +
|
||||
'</div>' +
|
||||
'<div class="p-2 bg-primary/10 rounded">' +
|
||||
'<div class="text-lg font-bold text-primary">' + (result.symbols_added || 0) + '</div>' +
|
||||
'<div class="text-xs text-muted-foreground">+Symbols</div>' +
|
||||
'</div>' +
|
||||
'<div class="p-2 bg-destructive/10 rounded">' +
|
||||
'<div class="text-lg font-bold text-destructive">' + ((result.errors && result.errors.length) || 0) + '</div>' +
|
||||
'<div class="text-xs text-muted-foreground">Errors</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Clear pending queue after indexing
|
||||
updatePendingQueueUI({ file_count: 0, files: [], countdown_seconds: 0 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5999,12 +6284,37 @@ function closeWatcherModal() {
|
||||
|
||||
/**
|
||||
* Handle watcher status update from WebSocket
|
||||
* @param {Object} payload - { running: boolean, path?: string, error?: string }
|
||||
* @param {Object} payload - { running: boolean, path?: string, error?: string, events_processed?: number, uptime_seconds?: number }
|
||||
*/
|
||||
function handleWatcherStatusUpdate(payload) {
|
||||
var toggle = document.getElementById('watcherToggle');
|
||||
var statsDiv = document.getElementById('watcherStats');
|
||||
var configDiv = document.getElementById('watcherStartConfig');
|
||||
var eventsCountEl = document.getElementById('watcherEventsCount');
|
||||
var uptimeEl = document.getElementById('watcherUptime');
|
||||
|
||||
// Update events count if provided (real-time updates)
|
||||
if (payload.events_processed !== undefined && eventsCountEl) {
|
||||
eventsCountEl.textContent = payload.events_processed;
|
||||
}
|
||||
|
||||
// Update uptime if provided
|
||||
if (payload.uptime_seconds !== undefined && uptimeEl) {
|
||||
var seconds = payload.uptime_seconds;
|
||||
var formatted = seconds < 60 ? seconds + 's' :
|
||||
seconds < 3600 ? Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's' :
|
||||
Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
|
||||
uptimeEl.textContent = formatted;
|
||||
}
|
||||
|
||||
// Also update main page watcher status badge if it exists
|
||||
var statusBadge = document.getElementById('watcherStatusBadge');
|
||||
if (statusBadge && payload.running !== undefined) {
|
||||
updateWatcherUI(payload.running, {
|
||||
events_processed: payload.events_processed,
|
||||
uptime_seconds: payload.uptime_seconds
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.error) {
|
||||
// Watcher failed - update UI to show stopped state
|
||||
@@ -6018,8 +6328,8 @@ function handleWatcherStatusUpdate(payload) {
|
||||
if (statsDiv) statsDiv.style.display = 'block';
|
||||
if (configDiv) configDiv.style.display = 'none';
|
||||
startWatcherStatusPolling();
|
||||
} else {
|
||||
// Watcher stopped normally
|
||||
} else if (payload.running === false) {
|
||||
// Watcher stopped normally (only if running is explicitly false)
|
||||
if (toggle) toggle.checked = false;
|
||||
if (statsDiv) statsDiv.style.display = 'none';
|
||||
if (configDiv) configDiv.style.display = 'block';
|
||||
|
||||
@@ -1,6 +1,56 @@
|
||||
// Core Memory View
|
||||
// Manages strategic context entries with knowledge graph and evolution tracking
|
||||
|
||||
/**
|
||||
* Parse JSON streaming content and extract readable text
|
||||
* Handles Gemini/Qwen format: {"type":"message","content":"...","delta":true}
|
||||
*/
|
||||
function parseJsonStreamContent(content) {
|
||||
if (!content || typeof content !== 'string') return content;
|
||||
|
||||
// Check if content looks like JSON streaming (multiple JSON objects)
|
||||
if (!content.includes('{"type":')) return content;
|
||||
|
||||
const lines = content.split('\n');
|
||||
const extractedParts = [];
|
||||
let hasJsonLines = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
// Try to parse as JSON
|
||||
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
||||
try {
|
||||
const obj = JSON.parse(trimmed);
|
||||
// Extract content from message type
|
||||
if (obj.type === 'message' && obj.content) {
|
||||
extractedParts.push(obj.content);
|
||||
hasJsonLines = true;
|
||||
}
|
||||
// Skip init/result/error types (metadata)
|
||||
else if (obj.type === 'init' || obj.type === 'result' || obj.type === 'error') {
|
||||
hasJsonLines = true;
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not valid JSON, keep as plain text
|
||||
extractedParts.push(trimmed);
|
||||
}
|
||||
} else {
|
||||
// Plain text line
|
||||
extractedParts.push(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// If we found JSON lines, return extracted content
|
||||
if (hasJsonLines && extractedParts.length > 0) {
|
||||
return extractedParts.join('');
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Notification function
|
||||
function showNotification(message, type = 'info') {
|
||||
// Create notification container if it doesn't exist
|
||||
@@ -527,20 +577,24 @@ async function viewMemoryDetail(memoryId) {
|
||||
const modal = document.getElementById('memoryDetailModal');
|
||||
document.getElementById('memoryDetailTitle').textContent = memory.id;
|
||||
|
||||
// Parse content and summary in case they contain JSON streaming format
|
||||
const parsedContent = parseJsonStreamContent(memory.content);
|
||||
const parsedSummary = parseJsonStreamContent(memory.summary);
|
||||
|
||||
const body = document.getElementById('memoryDetailBody');
|
||||
body.innerHTML = `
|
||||
<div class="memory-detail-content">
|
||||
${memory.summary
|
||||
${parsedSummary
|
||||
? `<div class="detail-section">
|
||||
<h3>${t('coreMemory.summary')}</h3>
|
||||
<div class="detail-text">${escapeHtml(memory.summary)}</div>
|
||||
<div class="detail-text">${escapeHtml(parsedSummary)}</div>
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
|
||||
<div class="detail-section">
|
||||
<h3>${t('coreMemory.content')}</h3>
|
||||
<pre class="detail-code">${escapeHtml(memory.content)}</pre>
|
||||
<pre class="detail-code">${escapeHtml(parsedContent)}</pre>
|
||||
</div>
|
||||
|
||||
${(() => {
|
||||
@@ -564,7 +618,7 @@ async function viewMemoryDetail(memoryId) {
|
||||
${memory.raw_output
|
||||
? `<div class="detail-section">
|
||||
<h3>${t('coreMemory.rawOutput')}</h3>
|
||||
<pre class="detail-code">${escapeHtml(memory.raw_output)}</pre>
|
||||
<pre class="detail-code">${escapeHtml(parseJsonStreamContent(memory.raw_output))}</pre>
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
|
||||
@@ -347,7 +347,8 @@ var ruleCreateState = {
|
||||
generationType: 'description',
|
||||
description: '',
|
||||
extractScope: '',
|
||||
extractFocus: ''
|
||||
extractFocus: '',
|
||||
enableReview: false
|
||||
};
|
||||
|
||||
function openRuleCreateModal() {
|
||||
@@ -363,7 +364,8 @@ function openRuleCreateModal() {
|
||||
generationType: 'description',
|
||||
description: '',
|
||||
extractScope: '',
|
||||
extractFocus: ''
|
||||
extractFocus: '',
|
||||
enableReview: false
|
||||
};
|
||||
|
||||
// Create modal HTML
|
||||
@@ -506,6 +508,18 @@ function openRuleCreateModal() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Review Option (CLI mode only) -->
|
||||
<div id="ruleReviewSection" style="display: ${ruleCreateState.mode === 'cli-generate' ? 'block' : 'none'}">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="ruleEnableReview"
|
||||
class="w-4 h-4 text-primary bg-background border-border rounded focus:ring-2 focus:ring-primary"
|
||||
${ruleCreateState.enableReview ? 'checked' : ''}
|
||||
onchange="toggleRuleReview()">
|
||||
<span class="text-sm font-medium text-foreground">${t('rules.enableReview')}</span>
|
||||
</label>
|
||||
<p class="text-xs text-muted-foreground mt-1 ml-6">${t('rules.enableReviewHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Conditional Rule Toggle (Manual mode only) -->
|
||||
<div id="ruleConditionalSection" style="display: ${ruleCreateState.mode === 'input' ? 'block' : 'none'}">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
@@ -667,11 +681,13 @@ function switchRuleCreateMode(mode) {
|
||||
const generationTypeSection = document.getElementById('ruleGenerationTypeSection');
|
||||
const descriptionSection = document.getElementById('ruleDescriptionSection');
|
||||
const extractSection = document.getElementById('ruleExtractSection');
|
||||
const reviewSection = document.getElementById('ruleReviewSection');
|
||||
const conditionalSection = document.getElementById('ruleConditionalSection');
|
||||
const contentSection = document.getElementById('ruleContentSection');
|
||||
|
||||
if (mode === 'cli-generate') {
|
||||
if (generationTypeSection) generationTypeSection.style.display = 'block';
|
||||
if (reviewSection) reviewSection.style.display = 'block';
|
||||
if (conditionalSection) conditionalSection.style.display = 'none';
|
||||
if (contentSection) contentSection.style.display = 'none';
|
||||
|
||||
@@ -687,6 +703,7 @@ function switchRuleCreateMode(mode) {
|
||||
if (generationTypeSection) generationTypeSection.style.display = 'none';
|
||||
if (descriptionSection) descriptionSection.style.display = 'none';
|
||||
if (extractSection) extractSection.style.display = 'none';
|
||||
if (reviewSection) reviewSection.style.display = 'none';
|
||||
if (conditionalSection) conditionalSection.style.display = 'block';
|
||||
if (contentSection) contentSection.style.display = 'block';
|
||||
}
|
||||
@@ -724,6 +741,11 @@ function switchRuleGenerationType(type) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRuleReview() {
|
||||
const checkbox = document.getElementById('ruleEnableReview');
|
||||
ruleCreateState.enableReview = checkbox ? checkbox.checked : false;
|
||||
}
|
||||
|
||||
async function createRule() {
|
||||
const fileNameInput = document.getElementById('ruleFileName');
|
||||
const subdirectoryInput = document.getElementById('ruleSubdirectory');
|
||||
@@ -784,7 +806,8 @@ async function createRule() {
|
||||
generationType: ruleCreateState.generationType,
|
||||
description: ruleCreateState.generationType === 'description' ? description : undefined,
|
||||
extractScope: ruleCreateState.generationType === 'extract' ? extractScope : undefined,
|
||||
extractFocus: ruleCreateState.generationType === 'extract' ? extractFocus : undefined
|
||||
extractFocus: ruleCreateState.generationType === 'extract' ? extractFocus : undefined,
|
||||
enableReview: ruleCreateState.enableReview || undefined
|
||||
};
|
||||
|
||||
// Show progress message
|
||||
|
||||
@@ -510,35 +510,8 @@ function openSkillCreateModal() {
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.skillNameHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Generation Type Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.generationType')}</label>
|
||||
<div class="flex gap-3">
|
||||
<button class="flex-1 px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.generationType === 'description' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchSkillGenerationType('description')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium text-sm">${t('skills.fromDescription')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.fromDescriptionHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex-1 px-4 py-3 text-left border-2 rounded-lg transition-all opacity-50 cursor-not-allowed"
|
||||
disabled>
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="layout-template" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium text-sm">${t('skills.fromTemplate')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.comingSoon')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Text Area (for 'description' type) -->
|
||||
<div id="skillDescriptionArea" style="display: ${skillCreateState.generationType === 'description' ? 'block' : 'none'}">
|
||||
<!-- Description Text Area -->
|
||||
<div id="skillDescriptionArea">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.descriptionLabel')} <span class="text-destructive">*</span></label>
|
||||
<textarea id="skillDescription"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
@@ -683,19 +656,6 @@ function switchSkillCreateMode(mode) {
|
||||
}
|
||||
}
|
||||
|
||||
function switchSkillGenerationType(type) {
|
||||
skillCreateState.generationType = type;
|
||||
|
||||
// Toggle visibility of description area
|
||||
const descriptionArea = document.getElementById('skillDescriptionArea');
|
||||
if (descriptionArea) {
|
||||
descriptionArea.style.display = type === 'description' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Update generation type button styles (only the description button is active, template is disabled)
|
||||
// No need to update button styles since template button is disabled
|
||||
}
|
||||
|
||||
function browseSkillFolder() {
|
||||
// Use browser prompt for now (Phase 3 will implement file browser)
|
||||
const path = prompt(t('skills.enterFolderPath'), skillCreateState.sourcePath);
|
||||
|
||||
Reference in New Issue
Block a user