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:
catlog22
2025-12-13 17:28:03 +08:00
parent 029384c427
commit 675aff26ff
17 changed files with 1108 additions and 248 deletions

View File

@@ -536,16 +536,16 @@
position: fixed;
top: 70px;
right: 20px;
max-width: 360px;
padding: 12px 16px;
max-width: 280px;
padding: 8px 12px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
border-radius: 6px;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.12);
z-index: 1000;
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease-out;
@@ -577,11 +577,11 @@
}
.notification-icon {
font-size: 1.25rem;
font-size: 1rem;
}
.notification-message {
font-size: 0.875rem;
font-size: 0.8125rem;
color: hsl(var(--foreground));
flex: 1;
}
@@ -938,26 +938,29 @@
NOTIFICATION SIDEBAR (Right-Side Toolbar)
========================================== */
/* Sidebar Container */
/* Sidebar Container - Compact floating panel */
.notif-sidebar {
position: fixed;
top: 0;
right: 0;
width: 380px;
top: 60px;
right: 16px;
width: 320px;
max-width: 90vw;
height: 100vh;
max-height: 70vh;
background: hsl(var(--card));
border-left: 1px solid hsl(var(--border));
box-shadow: -4px 0 24px rgb(0 0 0 / 0.15);
border: 1px solid hsl(var(--border));
border-radius: 8px;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
z-index: 1100;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateX(calc(100% + 20px));
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.notif-sidebar.open {
transform: translateX(0);
opacity: 1;
}
/* Sidebar Header */
@@ -965,35 +968,36 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding: 12px 14px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--card));
background: hsl(var(--muted) / 0.3);
border-radius: 8px 8px 0 0;
}
.notif-sidebar-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 1rem;
gap: 8px;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.notif-title-icon {
font-size: 1.25rem;
font-size: 1rem;
}
.notif-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 6px;
min-width: 18px;
height: 18px;
padding: 0 5px;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-radius: 11px;
font-size: 0.75rem;
border-radius: 9px;
font-size: 0.6875rem;
font-weight: 600;
}
@@ -1001,11 +1005,11 @@
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
width: 26px;
height: 26px;
background: transparent;
border: none;
border-radius: 6px;
border-radius: 4px;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
@@ -1019,21 +1023,21 @@
/* Sidebar Actions */
.notif-sidebar-actions {
display: flex;
gap: 8px;
padding: 12px 20px;
gap: 6px;
padding: 8px 14px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
background: hsl(var(--background));
}
.notif-action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
gap: 4px;
padding: 4px 10px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 6px;
font-size: 0.8125rem;
border-radius: 4px;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
transition: all 0.15s;
@@ -1049,7 +1053,7 @@
.notif-sidebar-content {
flex: 1;
overflow-y: auto;
padding: 12px;
padding: 8px;
}
/* Empty State */
@@ -1058,36 +1062,74 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
padding: 32px 16px;
text-align: center;
}
.notif-empty-icon {
font-size: 3rem;
font-size: 2rem;
opacity: 0.3;
margin-bottom: 16px;
margin-bottom: 12px;
}
.notif-empty-text {
font-size: 1rem;
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
margin-bottom: 8px;
margin-bottom: 4px;
}
.notif-empty-hint {
font-size: 0.8125rem;
font-size: 0.75rem;
color: hsl(var(--muted-foreground) / 0.7);
}
/* Notification Settings */
.notif-sidebar-settings {
padding: 8px 12px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.3);
}
.notif-setting-item {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
.notif-setting-item input[type="checkbox"] {
width: 14px;
height: 14px;
cursor: pointer;
accent-color: hsl(var(--primary));
}
.notif-setting-label {
display: flex;
align-items: center;
gap: 6px;
}
.notif-setting-label svg {
opacity: 0.7;
}
/* Notification Items */
.notif-item {
padding: 12px 14px;
padding: 8px 10px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
margin-bottom: 8px;
border-radius: 6px;
margin-bottom: 6px;
transition: all 0.15s;
cursor: default;
}
.notif-item.has-details {
cursor: pointer;
}
.notif-item:hover {
@@ -1118,11 +1160,11 @@
.notif-item-header {
display: flex;
align-items: flex-start;
gap: 10px;
gap: 8px;
}
.notif-icon {
font-size: 1.125rem;
font-size: 0.9375rem;
flex-shrink: 0;
}
@@ -1133,33 +1175,61 @@
.notif-message {
display: block;
font-size: 0.875rem;
font-size: 0.8125rem;
font-weight: 500;
color: hsl(var(--foreground));
line-height: 1.4;
line-height: 1.3;
word-break: break-word;
}
.notif-source {
display: inline-block;
margin-top: 4px;
padding: 2px 8px;
margin-top: 3px;
padding: 1px 6px;
background: hsl(var(--muted));
border-radius: 4px;
font-size: 0.6875rem;
border-radius: 3px;
font-size: 0.625rem;
color: hsl(var(--muted-foreground));
font-family: var(--font-mono);
}
/* Expand Icon */
.notif-expand-icon {
flex-shrink: 0;
font-size: 0.625rem;
color: hsl(var(--muted-foreground));
opacity: 0.6;
transition: transform 0.2s;
}
.notif-item.expanded .notif-expand-icon {
transform: rotate(0deg);
}
/* Details Hint (collapsed state) */
.notif-details-hint {
margin-top: 4px;
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
opacity: 0.7;
font-style: italic;
}
/* Expanded Details */
.notif-details-expanded {
max-height: 300px;
overflow-y: auto;
}
/* Notification Details */
.notif-details {
margin-top: 10px;
padding: 10px 12px;
margin-top: 6px;
padding: 6px 8px;
background: hsl(var(--muted) / 0.5);
border-radius: 6px;
font-size: 0.8125rem;
border-radius: 4px;
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
line-height: 1.5;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
font-family: var(--font-mono);
@@ -1167,12 +1237,12 @@
/* JSON Formatted Details */
.notif-details-json {
margin-top: 10px;
padding: 10px 12px;
margin-top: 6px;
padding: 6px 8px;
background: hsl(var(--muted) / 0.5);
border-radius: 6px;
font-size: 0.8125rem;
line-height: 1.6;
border-radius: 4px;
font-size: 0.75rem;
line-height: 1.4;
font-family: var(--font-mono);
}
@@ -1233,13 +1303,13 @@
/* Notification Meta */
.notif-meta {
margin-top: 8px;
padding-top: 8px;
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid hsl(var(--border) / 0.5);
}
.notif-time {
font-size: 0.75rem;
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
}
@@ -1318,20 +1388,21 @@
/* Toast Notification */
.notif-toast {
position: fixed;
bottom: 24px;
right: 24px;
bottom: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 10px;
padding: 12px 18px;
gap: 8px;
padding: 8px 14px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.2);
border-radius: 6px;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
z-index: 1200;
transform: translateY(100px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
max-width: 280px;
}
.notif-toast.show {
@@ -1356,13 +1427,14 @@
}
.toast-icon {
font-size: 1.25rem;
font-size: 1rem;
}
.toast-message {
font-size: 0.875rem;
font-size: 0.8125rem;
color: hsl(var(--foreground));
max-width: 280px;
max-width: 220px;
line-height: 1.3;
}
/* Responsive */
@@ -1397,26 +1469,29 @@
TASK QUEUE SIDEBAR (Right-Side Toolbar)
========================================== */
/* Sidebar Container */
/* Sidebar Container - Compact floating panel (matches notif-sidebar) */
.task-queue-sidebar {
position: fixed;
top: 0;
right: 0;
width: 400px;
top: 60px;
right: 16px;
width: 360px;
max-width: 90vw;
height: 100vh;
max-height: 70vh;
background: hsl(var(--card));
border-left: 1px solid hsl(var(--border));
box-shadow: -4px 0 24px rgb(0 0 0 / 0.15);
border: 1px solid hsl(var(--border));
border-radius: 8px;
box-shadow: 0 8px 24px rgb(0 0 0 / 0.15);
z-index: 1100;
display: flex;
flex-direction: column;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateX(calc(100% + 20px));
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.task-queue-sidebar.open {
transform: translateX(0);
opacity: 1;
}
/* Sidebar Header */
@@ -1424,9 +1499,10 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding: 12px 14px;
border-bottom: 1px solid hsl(var(--border));
background: hsl(var(--card));
background: hsl(var(--muted) / 0.3);
border-radius: 8px 8px 0 0;
}
.task-queue-title {

View File

@@ -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);

View File

@@ -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">

View File

@@ -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 ==========

View File

@@ -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();

View File

@@ -79,7 +79,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
// Close task queue sidebar if open
if (isTaskQueueVisible && typeof toggleTaskQueueSidebar === 'function') {
if (isTaskQueueSidebarVisible && typeof toggleTaskQueueSidebar === 'function') {
toggleTaskQueueSidebar();
}
}

View File

@@ -58,15 +58,17 @@ async function renderCliManager() {
container.innerHTML = '<div class="status-manager">' +
'<div class="status-two-column">' +
'<div class="status-section" id="tools-section"></div>' +
'<div class="status-section" id="ccw-section"></div>' +
'<div class="cli-section" id="tools-section"></div>' +
'<div class="cli-section" id="ccw-section"></div>' +
'</div>' +
'<div class="status-section" id="ccw-endpoint-tools-section" style="margin-top: 1.5rem;"></div>' +
'<div class="cli-settings-section" id="cli-settings-section" style="margin-top: 1.5rem;"></div>' +
'<div class="cli-section" id="ccw-endpoint-tools-section" style="margin-top: 1.5rem;"></div>' +
'</div>';
// Render sub-panels
renderToolsSection();
renderCcwSection();
renderCliSettingsSection();
renderCcwEndpointToolsSection();
// Initialize Lucide icons
@@ -241,6 +243,74 @@ function renderCcwSection() {
if (window.lucide) lucide.createIcons();
}
// ========== CLI Settings Section (Full Width) ==========
function renderCliSettingsSection() {
var container = document.getElementById('cli-settings-section');
if (!container) return;
var settingsHtml = '<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="settings" class="w-4 h-4"></i> ' + t('cli.settings') + '</h3>' +
'</div>' +
'</div>' +
'<div class="cli-settings-grid">' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="layers" class="w-3 h-3"></i>' +
t('cli.promptFormat') +
'</label>' +
'<div class="cli-setting-control">' +
'<select class="cli-setting-select" onchange="setPromptFormat(this.value)">' +
'<option value="plain"' + (promptConcatFormat === 'plain' ? ' selected' : '') + '>Plain Text</option>' +
'<option value="yaml"' + (promptConcatFormat === 'yaml' ? ' selected' : '') + '>YAML</option>' +
'<option value="json"' + (promptConcatFormat === 'json' ? ' selected' : '') + '>JSON</option>' +
'</select>' +
'</div>' +
'<p class="cli-setting-desc">' + t('cli.promptFormatDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="database" class="w-3 h-3"></i>' +
t('cli.storageBackend') +
'</label>' +
'<div class="cli-setting-control">' +
'<span class="cli-setting-value">SQLite</span>' +
'</div>' +
'<p class="cli-setting-desc">' + t('cli.storageBackendDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="sparkles" class="w-3 h-3"></i>' +
t('cli.smartContext') +
'</label>' +
'<div class="cli-setting-control">' +
'<label class="cli-toggle">' +
'<input type="checkbox"' + (smartContextEnabled ? ' checked' : '') + ' onchange="setSmartContextEnabled(this.checked)">' +
'<span class="cli-toggle-slider"></span>' +
'</label>' +
'</div>' +
'<p class="cli-setting-desc">' + t('cli.smartContextDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item' + (!smartContextEnabled ? ' disabled' : '') + '">' +
'<label class="cli-setting-label">' +
'<i data-lucide="files" class="w-3 h-3"></i>' +
t('cli.maxContextFiles') +
'</label>' +
'<div class="cli-setting-control">' +
'<select class="cli-setting-select" onchange="setSmartContextMaxFiles(this.value)"' + (!smartContextEnabled ? ' disabled' : '') + '>' +
'<option value="5"' + (smartContextMaxFiles === 5 ? ' selected' : '') + '>5 files</option>' +
'<option value="10"' + (smartContextMaxFiles === 10 ? ' selected' : '') + '>10 files</option>' +
'<option value="20"' + (smartContextMaxFiles === 20 ? ' selected' : '') + '>20 files</option>' +
'</select>' +
'</div>' +
'<p class="cli-setting-desc">' + t('cli.maxContextFilesDesc') + '</p>' +
'</div>' +
'</div>';
container.innerHTML = settingsHtml;
if (window.lucide) lucide.createIcons();
}
// ========== CCW Endpoint Tools Section (Full Width) ==========
function renderCcwEndpointToolsSection() {
var container = document.getElementById('ccw-endpoint-tools-section');
@@ -744,7 +814,17 @@ async function executeCliFromDashboard() {
var response = await fetch('/api/cli/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool: tool, mode: mode, prompt: prompt, dir: projectPath })
body: JSON.stringify({
tool: tool,
mode: mode,
prompt: prompt,
dir: projectPath,
format: promptConcatFormat,
smartContext: {
enabled: smartContextEnabled,
maxFiles: smartContextMaxFiles
}
})
});
var result = await response.json();
@@ -777,6 +857,11 @@ function handleCliExecutionStarted(payload) {
};
cliExecutionOutput = '';
// Show toast notification
if (typeof addGlobalNotification === 'function') {
addGlobalNotification('info', 'CLI ' + payload.tool + ' started', payload.mode + ' mode', 'CLI');
}
if (currentView === 'cli-manager') {
var outputPanel = document.getElementById('cli-output-panel');
var outputContent = document.getElementById('cli-output-content');
@@ -806,6 +891,15 @@ function handleCliExecutionCompleted(payload) {
if (statusIndicator) statusIndicator.className = 'status-indicator ' + (payload.success ? 'success' : 'error');
if (statusText) statusText.textContent = payload.success ? 'Completed in ' + formatDuration(payload.duration_ms) : 'Failed: ' + payload.status;
// Show toast notification
if (typeof addGlobalNotification === 'function') {
if (payload.success) {
addGlobalNotification('success', 'CLI execution completed', formatDuration(payload.duration_ms), 'CLI');
} else {
addGlobalNotification('error', 'CLI execution failed', payload.status, 'CLI');
}
}
currentCliExecution = null;
if (currentView === 'cli-manager') {
loadCliHistory().then(function() { renderCliHistory(); });
@@ -819,5 +913,10 @@ function handleCliExecutionError(payload) {
if (statusIndicator) statusIndicator.className = 'status-indicator error';
if (statusText) statusText.textContent = 'Error: ' + payload.error;
// Show toast notification
if (typeof addGlobalNotification === 'function') {
addGlobalNotification('error', 'CLI execution error', payload.error, 'CLI');
}
currentCliExecution = null;
}

View File

@@ -94,7 +94,7 @@ async function renderCliHistoryView() {
'<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail(\'' + exec.id + '\')" title="View Details">' +
'<i data-lucide="eye" class="w-4 h-4"></i>' +
'</button>' +
'<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution(\'' + exec.id + '\')" title="Delete">' +
'<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution(\'' + exec.id + (exec.sourceDir ? '\',\'' + escapeHtml(exec.sourceDir) : '') + '\')" title="Delete">' +
'<i data-lucide="trash-2" class="w-4 h-4"></i>' +
'</button>' +
'</div>' +