mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(mcp): add read_file tool and simplify edit/write returns
- edit_file: truncate diff to 15 lines, compact result format - write_file: return only path/bytes/message - read_file: new tool with multi-file, directory, regex support - paths: single file, array, or directory - pattern: glob filter (*.ts) - contentPattern: regex content search - maxDepth, maxFiles, includeContent options - Update tool-strategy.md documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -358,15 +358,20 @@ async function refreshCliHistory() {
|
||||
}
|
||||
|
||||
// ========== Delete Execution ==========
|
||||
function confirmDeleteExecution(executionId) {
|
||||
function confirmDeleteExecution(executionId, sourceDir) {
|
||||
if (confirm('Delete this execution record? This action cannot be undone.')) {
|
||||
deleteExecution(executionId);
|
||||
deleteExecution(executionId, sourceDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteExecution(executionId) {
|
||||
async function deleteExecution(executionId, sourceDir) {
|
||||
try {
|
||||
const response = await fetch(`/api/cli/execution?path=${encodeURIComponent(projectPath)}&id=${encodeURIComponent(executionId)}`, {
|
||||
// Build correct path - use sourceDir if provided for recursive items
|
||||
const basePath = sourceDir && sourceDir !== '.'
|
||||
? projectPath + '/' + sourceDir
|
||||
: projectPath;
|
||||
|
||||
const response = await fetch(`/api/cli/execution?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
@@ -375,9 +380,15 @@ async function deleteExecution(executionId) {
|
||||
throw new Error(error.error || 'Failed to delete');
|
||||
}
|
||||
|
||||
// Remove from local state
|
||||
cliExecutionHistory = cliExecutionHistory.filter(exec => exec.id !== executionId);
|
||||
renderCliHistory();
|
||||
// Reload fresh data from server and re-render
|
||||
await loadCliHistory();
|
||||
|
||||
// Render appropriate view based on current view
|
||||
if (typeof currentView !== 'undefined' && (currentView === 'history' || currentView === 'cli-history')) {
|
||||
renderCliHistoryView();
|
||||
} else {
|
||||
renderCliHistory();
|
||||
}
|
||||
showRefreshToast('Execution deleted', 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to delete execution:', err);
|
||||
|
||||
@@ -2,11 +2,26 @@
|
||||
// GLOBAL NOTIFICATION SYSTEM - Right Sidebar
|
||||
// ==========================================
|
||||
// Right-side slide-out toolbar for notifications and quick actions
|
||||
// Supports browser system notifications (cross-platform)
|
||||
|
||||
// Notification settings
|
||||
let notifSettings = {
|
||||
systemNotifEnabled: false,
|
||||
soundEnabled: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize global notification sidebar
|
||||
*/
|
||||
function initGlobalNotifications() {
|
||||
// Load settings from localStorage
|
||||
loadNotifSettings();
|
||||
|
||||
// Request notification permission if enabled
|
||||
if (notifSettings.systemNotifEnabled) {
|
||||
requestNotificationPermission();
|
||||
}
|
||||
|
||||
// Create sidebar if not exists
|
||||
if (!document.getElementById('notifSidebar')) {
|
||||
const sidebarHtml = `
|
||||
@@ -24,6 +39,19 @@ function initGlobalNotifications() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="notif-sidebar-settings" id="notifSettings">
|
||||
<label class="notif-setting-item">
|
||||
<input type="checkbox" id="systemNotifToggle" onchange="toggleSystemNotifications(this.checked)">
|
||||
<span class="notif-setting-label">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
||||
</svg>
|
||||
System Notifications
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="notif-sidebar-actions">
|
||||
<button class="notif-action-btn" onclick="markAllNotificationsRead()" title="Mark all read">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -60,12 +88,132 @@ function initGlobalNotifications() {
|
||||
container.id = 'notifSidebarContainer';
|
||||
container.innerHTML = sidebarHtml;
|
||||
document.body.appendChild(container);
|
||||
|
||||
// Initialize toggle state
|
||||
const toggle = document.getElementById('systemNotifToggle');
|
||||
if (toggle) {
|
||||
toggle.checked = notifSettings.systemNotifEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
renderGlobalNotifications();
|
||||
updateGlobalNotifBadge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load notification settings from localStorage
|
||||
*/
|
||||
function loadNotifSettings() {
|
||||
try {
|
||||
const saved = localStorage.getItem('ccw_notif_settings');
|
||||
if (saved) {
|
||||
notifSettings = { ...notifSettings, ...JSON.parse(saved) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Notif] Failed to load settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save notification settings to localStorage
|
||||
*/
|
||||
function saveNotifSettings() {
|
||||
try {
|
||||
localStorage.setItem('ccw_notif_settings', JSON.stringify(notifSettings));
|
||||
} catch (e) {
|
||||
console.error('[Notif] Failed to save settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle system notifications
|
||||
*/
|
||||
function toggleSystemNotifications(enabled) {
|
||||
notifSettings.systemNotifEnabled = enabled;
|
||||
saveNotifSettings();
|
||||
|
||||
if (enabled) {
|
||||
requestNotificationPermission();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request browser notification permission
|
||||
*/
|
||||
async function requestNotificationPermission() {
|
||||
if (!('Notification' in window)) {
|
||||
console.warn('[Notif] Browser does not support notifications');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Notification.permission !== 'denied') {
|
||||
const permission = await Notification.requestPermission();
|
||||
return permission === 'granted';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show system notification (browser notification)
|
||||
*/
|
||||
function showSystemNotification(notification) {
|
||||
if (!notifSettings.systemNotifEnabled) return;
|
||||
if (!('Notification' in window)) return;
|
||||
if (Notification.permission !== 'granted') return;
|
||||
|
||||
const typeIcon = {
|
||||
'info': 'ℹ️',
|
||||
'success': '✅',
|
||||
'warning': '⚠️',
|
||||
'error': '❌'
|
||||
}[notification.type] || '🔔';
|
||||
|
||||
const title = `${typeIcon} ${notification.message}`;
|
||||
let body = '';
|
||||
|
||||
if (notification.source) {
|
||||
body = `[${notification.source}]`;
|
||||
}
|
||||
|
||||
// Extract plain text from details if HTML formatted
|
||||
if (notification.details) {
|
||||
const detailText = notification.details.replace(/<[^>]*>/g, '').trim();
|
||||
if (detailText) {
|
||||
body += body ? '\n' + detailText : detailText;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const sysNotif = new Notification(title, {
|
||||
body: body.substring(0, 200),
|
||||
icon: '/favicon.ico',
|
||||
tag: `ccw-notif-${notification.id}`,
|
||||
requireInteraction: notification.type === 'error'
|
||||
});
|
||||
|
||||
// Click to open sidebar
|
||||
sysNotif.onclick = () => {
|
||||
window.focus();
|
||||
if (!isNotificationPanelVisible) {
|
||||
toggleNotifSidebar();
|
||||
}
|
||||
sysNotif.close();
|
||||
};
|
||||
|
||||
// Auto close after 5s (except errors)
|
||||
if (notification.type !== 'error') {
|
||||
setTimeout(() => sysNotif.close(), 5000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Notif] Failed to show system notification:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle notification sidebar visibility
|
||||
*/
|
||||
@@ -80,8 +228,6 @@ function toggleNotifSidebar() {
|
||||
sidebar.classList.add('open');
|
||||
overlay.classList.add('show');
|
||||
toggle.classList.add('hidden');
|
||||
// Mark notifications as read when opened
|
||||
markAllNotificationsRead();
|
||||
} else {
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('show');
|
||||
@@ -105,8 +251,11 @@ function toggleGlobalNotifications() {
|
||||
function addGlobalNotification(type, message, details = null, source = null) {
|
||||
// Format details if it's an object
|
||||
let formattedDetails = details;
|
||||
let rawDetails = details; // Keep raw for system notification
|
||||
|
||||
if (details && typeof details === 'object') {
|
||||
formattedDetails = formatNotificationJson(details);
|
||||
rawDetails = JSON.stringify(details, null, 2);
|
||||
} else if (typeof details === 'string') {
|
||||
// Try to parse and format if it looks like JSON
|
||||
const trimmed = details.trim();
|
||||
@@ -115,6 +264,7 @@ function addGlobalNotification(type, message, details = null, source = null) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
formattedDetails = formatNotificationJson(parsed);
|
||||
rawDetails = JSON.stringify(parsed, null, 2);
|
||||
} catch (e) {
|
||||
// Not valid JSON, use as-is
|
||||
formattedDetails = details;
|
||||
@@ -127,9 +277,11 @@ function addGlobalNotification(type, message, details = null, source = null) {
|
||||
type,
|
||||
message,
|
||||
details: formattedDetails,
|
||||
rawDetails: rawDetails,
|
||||
source,
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false
|
||||
read: false,
|
||||
expanded: false
|
||||
};
|
||||
|
||||
globalNotificationQueue.unshift(notification);
|
||||
@@ -147,10 +299,8 @@ function addGlobalNotification(type, message, details = null, source = null) {
|
||||
renderGlobalNotifications();
|
||||
updateGlobalNotifBadge();
|
||||
|
||||
// Show toast for important notifications
|
||||
if (type === 'error' || type === 'success') {
|
||||
showNotificationToast(notification);
|
||||
}
|
||||
// Show system notification instead of toast
|
||||
showSystemNotification(notification);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,36 +367,14 @@ function formatNotificationJson(obj) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a brief toast notification
|
||||
* Toggle notification item expansion
|
||||
*/
|
||||
function showNotificationToast(notification) {
|
||||
const typeIcon = {
|
||||
'info': 'ℹ️',
|
||||
'success': '✅',
|
||||
'warning': '⚠️',
|
||||
'error': '❌'
|
||||
}[notification.type] || 'ℹ️';
|
||||
|
||||
// Remove existing toast
|
||||
const existing = document.querySelector('.notif-toast');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `notif-toast type-${notification.type}`;
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon">${typeIcon}</span>
|
||||
<span class="toast-message">${escapeHtml(notification.message)}</span>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => toast.classList.add('show'));
|
||||
|
||||
// Auto-remove
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
function toggleNotifExpand(notifId) {
|
||||
const notif = globalNotificationQueue.find(n => n.id === notifId);
|
||||
if (notif) {
|
||||
notif.expanded = !notif.expanded;
|
||||
renderGlobalNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,26 +405,36 @@ function renderGlobalNotifications() {
|
||||
|
||||
const time = formatNotifTime(notif.timestamp);
|
||||
const sourceLabel = notif.source ? `<span class="notif-source">${escapeHtml(notif.source)}</span>` : '';
|
||||
const hasDetails = notif.details && notif.details.length > 0;
|
||||
const expandIcon = hasDetails ? (notif.expanded ? '▼' : '▶') : '';
|
||||
|
||||
// Details may already be HTML formatted or plain text
|
||||
// Details section - collapsed by default, show preview
|
||||
let detailsHtml = '';
|
||||
if (notif.details) {
|
||||
// Check if details is already HTML formatted (contains our json-* classes)
|
||||
if (typeof notif.details === 'string' && notif.details.includes('class="json-')) {
|
||||
detailsHtml = `<div class="notif-details-json">${notif.details}</div>`;
|
||||
if (hasDetails) {
|
||||
if (notif.expanded) {
|
||||
// Expanded view - show full details
|
||||
if (typeof notif.details === 'string' && notif.details.includes('class="json-')) {
|
||||
detailsHtml = `<div class="notif-details-json notif-details-expanded">${notif.details}</div>`;
|
||||
} else {
|
||||
detailsHtml = `<div class="notif-details notif-details-expanded">${escapeHtml(String(notif.details))}</div>`;
|
||||
}
|
||||
} else {
|
||||
detailsHtml = `<div class="notif-details">${escapeHtml(String(notif.details))}</div>`;
|
||||
// Collapsed view - show hint
|
||||
detailsHtml = `<div class="notif-details-hint">Click to view details</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="notif-item type-${notif.type} ${notif.read ? 'read' : ''}" data-id="${notif.id}">
|
||||
<div class="notif-item type-${notif.type} ${notif.read ? 'read' : ''} ${hasDetails ? 'has-details' : ''} ${notif.expanded ? 'expanded' : ''}"
|
||||
data-id="${notif.id}"
|
||||
onclick="toggleNotifExpand(${notif.id})">
|
||||
<div class="notif-item-header">
|
||||
<span class="notif-icon">${typeIcon}</span>
|
||||
<div class="notif-item-content">
|
||||
<span class="notif-message">${escapeHtml(notif.message)}</span>
|
||||
${sourceLabel}
|
||||
</div>
|
||||
${hasDetails ? `<span class="notif-expand-icon">${expandIcon}</span>` : ''}
|
||||
</div>
|
||||
${detailsHtml}
|
||||
<div class="notif-meta">
|
||||
|
||||
@@ -160,10 +160,15 @@ function handleNotification(data) {
|
||||
break;
|
||||
|
||||
case 'tool_execution':
|
||||
// Handle tool execution notifications from CLI
|
||||
// Handle tool execution notifications from MCP tools
|
||||
handleToolExecutionNotification(payload);
|
||||
break;
|
||||
|
||||
case 'cli_execution':
|
||||
// Handle CLI command notifications (ccw cli exec)
|
||||
handleCliCommandNotification(payload);
|
||||
break;
|
||||
|
||||
// CLI Tool Execution Events
|
||||
case 'CLI_EXECUTION_STARTED':
|
||||
if (typeof handleCliExecutionStarted === 'function') {
|
||||
@@ -195,7 +200,7 @@ function handleNotification(data) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tool execution notifications from CLI
|
||||
* Handle tool execution notifications from MCP tools
|
||||
* @param {Object} payload - Tool execution payload
|
||||
*/
|
||||
function handleToolExecutionNotification(payload) {
|
||||
@@ -210,19 +215,21 @@ function handleToolExecutionNotification(payload) {
|
||||
case 'started':
|
||||
notifType = 'info';
|
||||
message = `Executing ${toolName}...`;
|
||||
// Pass raw object for HTML formatting
|
||||
if (params) {
|
||||
details = formatJsonDetails(params, 150);
|
||||
details = params;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
notifType = 'success';
|
||||
message = `${toolName} completed`;
|
||||
// Pass raw object for HTML formatting
|
||||
if (result) {
|
||||
if (result._truncated) {
|
||||
details = result.preview;
|
||||
} else {
|
||||
details = formatJsonDetails(result, 200);
|
||||
details = result;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -238,13 +245,89 @@ function handleToolExecutionNotification(payload) {
|
||||
message = `${toolName}: ${status}`;
|
||||
}
|
||||
|
||||
// Add to global notifications
|
||||
// Add to global notifications - pass objects directly for HTML formatting
|
||||
if (typeof addGlobalNotification === 'function') {
|
||||
addGlobalNotification(notifType, message, details, 'MCP');
|
||||
}
|
||||
|
||||
// Log to console
|
||||
console.log(`[MCP] ${status}: ${toolName}`, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CLI command notifications (ccw cli exec)
|
||||
* @param {Object} payload - CLI execution payload
|
||||
*/
|
||||
function handleCliCommandNotification(payload) {
|
||||
const { event, tool, mode, prompt_preview, execution_id, success, duration_ms, status, error, turn_count, custom_id } = payload;
|
||||
|
||||
let notifType = 'info';
|
||||
let message = '';
|
||||
let details = null;
|
||||
|
||||
switch (event) {
|
||||
case 'started':
|
||||
notifType = 'info';
|
||||
message = `CLI ${tool} started`;
|
||||
// Pass structured object for rich display
|
||||
details = {
|
||||
mode: mode,
|
||||
prompt: prompt_preview
|
||||
};
|
||||
if (custom_id) {
|
||||
details.id = custom_id;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
if (success) {
|
||||
notifType = 'success';
|
||||
const turnStr = turn_count > 1 ? ` (turn ${turn_count})` : '';
|
||||
message = `CLI ${tool} completed${turnStr}`;
|
||||
// Pass structured object for rich display
|
||||
details = {
|
||||
duration: duration_ms ? `${(duration_ms / 1000).toFixed(1)}s` : '-',
|
||||
execution_id: execution_id
|
||||
};
|
||||
if (turn_count > 1) {
|
||||
details.turns = turn_count;
|
||||
}
|
||||
} else {
|
||||
notifType = 'error';
|
||||
message = `CLI ${tool} failed`;
|
||||
details = {
|
||||
status: status || 'Unknown error',
|
||||
execution_id: execution_id
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
notifType = 'error';
|
||||
message = `CLI ${tool} error`;
|
||||
details = error || 'Unknown error';
|
||||
break;
|
||||
|
||||
default:
|
||||
notifType = 'info';
|
||||
message = `CLI ${tool}: ${event}`;
|
||||
}
|
||||
|
||||
// Add to global notifications - pass objects for HTML formatting
|
||||
if (typeof addGlobalNotification === 'function') {
|
||||
addGlobalNotification(notifType, message, details, 'CLI');
|
||||
}
|
||||
|
||||
// Refresh CLI history if on history view
|
||||
if (event === 'completed' && typeof currentView !== 'undefined' &&
|
||||
(currentView === 'history' || currentView === 'cli-history')) {
|
||||
if (typeof loadCliHistory === 'function' && typeof renderCliHistoryView === 'function') {
|
||||
loadCliHistory().then(() => renderCliHistoryView());
|
||||
}
|
||||
}
|
||||
|
||||
// Log to console
|
||||
console.log(`[CLI] ${status}: ${toolName}`, payload);
|
||||
console.log(`[CLI Command] ${event}: ${tool}`, payload);
|
||||
}
|
||||
|
||||
// ========== Auto Refresh ==========
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// ==========================================
|
||||
// Right-side slide-out toolbar for task queue management
|
||||
|
||||
let isTaskQueueVisible = false;
|
||||
let isTaskQueueSidebarVisible = false;
|
||||
let taskQueueData = [];
|
||||
|
||||
/**
|
||||
@@ -65,13 +65,13 @@ function initTaskQueueSidebar() {
|
||||
* Toggle task queue sidebar visibility
|
||||
*/
|
||||
function toggleTaskQueueSidebar() {
|
||||
isTaskQueueVisible = !isTaskQueueVisible;
|
||||
isTaskQueueSidebarVisible = !isTaskQueueSidebarVisible;
|
||||
const sidebar = document.getElementById('taskQueueSidebar');
|
||||
const overlay = document.getElementById('taskQueueOverlay');
|
||||
const toggle = document.getElementById('taskQueueToggle');
|
||||
|
||||
if (sidebar && overlay && toggle) {
|
||||
if (isTaskQueueVisible) {
|
||||
if (isTaskQueueSidebarVisible) {
|
||||
// Close notification sidebar if open
|
||||
if (isNotificationPanelVisible && typeof toggleNotifSidebar === 'function') {
|
||||
toggleNotifSidebar();
|
||||
|
||||
Reference in New Issue
Block a user