mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
// Modular dashboard JS files (in dependency order)
|
||||
const MODULE_FILES = [
|
||||
// i18n (must be first for translations)
|
||||
'dashboard-js/i18n.js',
|
||||
// Base (no dependencies)
|
||||
'dashboard-js/state.js',
|
||||
'dashboard-js/utils.js',
|
||||
@@ -16,6 +18,14 @@ const MODULE_FILES = [
|
||||
'dashboard-js/components/task-drawer-core.js',
|
||||
'dashboard-js/components/tabs-context.js',
|
||||
'dashboard-js/components/tabs-other.js',
|
||||
'dashboard-js/components/carousel.js',
|
||||
'dashboard-js/components/notifications.js',
|
||||
'dashboard-js/components/global-notifications.js',
|
||||
'dashboard-js/components/cli-status.js',
|
||||
'dashboard-js/components/cli-history.js',
|
||||
'dashboard-js/components/mcp-manager.js',
|
||||
'dashboard-js/components/hook-manager.js',
|
||||
'dashboard-js/components/version-check.js',
|
||||
// Views
|
||||
'dashboard-js/views/home.js',
|
||||
'dashboard-js/views/project-overview.js',
|
||||
@@ -23,6 +33,11 @@ const MODULE_FILES = [
|
||||
'dashboard-js/views/fix-session.js',
|
||||
'dashboard-js/views/lite-tasks.js',
|
||||
'dashboard-js/views/session-detail.js',
|
||||
'dashboard-js/views/cli-manager.js',
|
||||
'dashboard-js/views/explorer.js',
|
||||
'dashboard-js/views/mcp-manager.js',
|
||||
'dashboard-js/views/hook-manager.js',
|
||||
'dashboard-js/views/history.js',
|
||||
// Navigation & Main
|
||||
'dashboard-js/components/navigation.js',
|
||||
'dashboard-js/main.js'
|
||||
|
||||
@@ -21,10 +21,13 @@ const MODULE_CSS_FILES = [
|
||||
'05-context.css',
|
||||
'06-cards.css',
|
||||
'07-managers.css',
|
||||
'08-review.css'
|
||||
'08-review.css',
|
||||
'09-explorer.css',
|
||||
'10-cli.css'
|
||||
];
|
||||
|
||||
const MODULE_FILES = [
|
||||
'i18n.js', // Must be loaded first for translations
|
||||
'utils.js',
|
||||
'state.js',
|
||||
'api.js',
|
||||
@@ -37,12 +40,25 @@ const MODULE_FILES = [
|
||||
'components/task-drawer-core.js',
|
||||
'components/task-drawer-renderers.js',
|
||||
'components/flowchart.js',
|
||||
'components/carousel.js',
|
||||
'components/notifications.js',
|
||||
'components/global-notifications.js',
|
||||
'components/cli-status.js',
|
||||
'components/cli-history.js',
|
||||
'components/mcp-manager.js',
|
||||
'components/hook-manager.js',
|
||||
'components/version-check.js',
|
||||
'views/home.js',
|
||||
'views/project-overview.js',
|
||||
'views/session-detail.js',
|
||||
'views/review-session.js',
|
||||
'views/lite-tasks.js',
|
||||
'views/fix-session.js',
|
||||
'views/cli-manager.js',
|
||||
'views/explorer.js',
|
||||
'views/mcp-manager.js',
|
||||
'views/hook-manager.js',
|
||||
'views/history.js',
|
||||
'main.js'
|
||||
];
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ function handlePostRequest(req, res, handler) {
|
||||
|
||||
// Modular JS files in dependency order
|
||||
const MODULE_FILES = [
|
||||
'i18n.js', // Must be loaded first for translations
|
||||
'utils.js',
|
||||
'state.js',
|
||||
'api.js',
|
||||
|
||||
@@ -110,7 +110,7 @@ function renderCarouselSlide(direction = 'none') {
|
||||
<div class="carousel-empty flex items-center justify-center h-full text-muted-foreground">
|
||||
<div class="text-center">
|
||||
<div class="text-3xl mb-2">🎯</div>
|
||||
<p class="text-sm">No active sessions</p>
|
||||
<p class="text-sm">${t('carousel.noActiveSessions')}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -172,7 +172,7 @@ function renderCarouselSlide(direction = 'none') {
|
||||
<!-- Progress -->
|
||||
<div class="mb-2">
|
||||
<div class="flex items-center justify-between text-xs mb-1">
|
||||
<span class="text-muted-foreground">Progress</span>
|
||||
<span class="text-muted-foreground">${t('session.progress')}</span>
|
||||
<span class="text-foreground font-medium">${completed}/${taskCount}</span>
|
||||
</div>
|
||||
<div class="h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
@@ -196,7 +196,7 @@ function renderCarouselSlide(direction = 'none') {
|
||||
|
||||
<!-- Right Column: Task List -->
|
||||
<div class="w-[45%] flex flex-col border-l border-border pl-3">
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1.5">Recent Tasks</div>
|
||||
<div class="text-xs font-medium text-muted-foreground mb-1.5">${t('tab.tasks')}</div>
|
||||
<div class="task-list flex-1 space-y-1 overflow-hidden">
|
||||
${displayTasks.length > 0 ? displayTasks.map(task => `
|
||||
<div class="flex items-center gap-1.5 text-xs">
|
||||
@@ -204,7 +204,7 @@ function renderCarouselSlide(direction = 'none') {
|
||||
<span class="truncate flex-1 ${task.status === 'in_progress' ? 'text-foreground font-medium' : 'text-muted-foreground'}">${escapeHtml(task.title || task.id || 'Task')}</span>
|
||||
</div>
|
||||
`).join('') : `
|
||||
<div class="text-xs text-muted-foreground">No tasks yet</div>
|
||||
<div class="text-xs text-muted-foreground">${t('empty.noTasks')}</div>
|
||||
`}
|
||||
</div>
|
||||
<!-- Progress percentage -->
|
||||
|
||||
@@ -440,8 +440,59 @@ function renderWizardModalContent() {
|
||||
if (!container || !currentWizardTemplate) return;
|
||||
|
||||
const wizard = currentWizardTemplate;
|
||||
const wizardId = wizard.id;
|
||||
const selectedOption = wizardConfig.triggerType || wizard.options[0].id;
|
||||
|
||||
// Get translated wizard name and description
|
||||
const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') :
|
||||
wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name;
|
||||
const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') :
|
||||
wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description;
|
||||
|
||||
// Helper to get translated option names
|
||||
const getOptionName = (optId) => {
|
||||
if (wizardId === 'memory-update') {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEnd');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdate');
|
||||
}
|
||||
if (wizardId === 'skill-context') {
|
||||
if (optId === 'keyword') return t('hook.wizard.keywordMatching');
|
||||
if (optId === 'auto') return t('hook.wizard.autoDetection');
|
||||
}
|
||||
return wizard.options.find(o => o.id === optId)?.name || '';
|
||||
};
|
||||
|
||||
const getOptionDesc = (optId) => {
|
||||
if (wizardId === 'memory-update') {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc');
|
||||
}
|
||||
if (wizardId === 'skill-context') {
|
||||
if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc');
|
||||
if (optId === 'auto') return t('hook.wizard.autoDetectionDesc');
|
||||
}
|
||||
return wizard.options.find(o => o.id === optId)?.description || '';
|
||||
};
|
||||
|
||||
// Helper to get translated field labels
|
||||
const getFieldLabel = (fieldKey) => {
|
||||
const labels = {
|
||||
'tool': t('hook.wizard.cliTool'),
|
||||
'interval': t('hook.wizard.intervalSeconds'),
|
||||
'strategy': t('hook.wizard.updateStrategy')
|
||||
};
|
||||
return labels[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.label || fieldKey;
|
||||
};
|
||||
|
||||
const getFieldDesc = (fieldKey) => {
|
||||
const descs = {
|
||||
'tool': t('hook.wizard.toolForDocGen'),
|
||||
'interval': t('hook.wizard.timeBetweenUpdates'),
|
||||
'strategy': t('hook.wizard.relatedStrategy')
|
||||
};
|
||||
return descs[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.description || '';
|
||||
};
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="space-y-6">
|
||||
<!-- Wizard Header -->
|
||||
@@ -450,14 +501,14 @@ function renderWizardModalContent() {
|
||||
<i data-lucide="${wizard.icon}" class="w-6 h-6 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-foreground">${escapeHtml(wizard.name)}</h3>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(wizard.description)}</p>
|
||||
<h3 class="text-lg font-semibold text-foreground">${escapeHtml(wizardName)}</h3>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(wizardDesc)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trigger Type Selection -->
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-foreground">When to Trigger</label>
|
||||
<label class="block text-sm font-medium text-foreground">${t('hook.wizard.whenToTrigger')}</label>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
${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'}">
|
||||
@@ -466,8 +517,8 @@ function renderWizardModalContent() {
|
||||
onchange="updateWizardTrigger('${opt.id}')"
|
||||
class="mt-1">
|
||||
<div class="flex-1">
|
||||
<span class="font-medium text-foreground">${escapeHtml(opt.name)}</span>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(opt.description)}</p>
|
||||
<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('')}
|
||||
@@ -476,18 +527,20 @@ function renderWizardModalContent() {
|
||||
|
||||
<!-- Configuration Fields -->
|
||||
<div class="space-y-4">
|
||||
<label class="block text-sm font-medium text-foreground">Configuration</label>
|
||||
<label class="block text-sm font-medium text-foreground">${t('hook.wizard.configuration')}</label>
|
||||
${wizard.customRenderer ? window[wizard.customRenderer]() : wizard.configFields.map(field => {
|
||||
// Check if field should be shown for current trigger type
|
||||
const shouldShow = !field.showFor || field.showFor.includes(selectedOption);
|
||||
if (!shouldShow) return '';
|
||||
|
||||
const value = wizardConfig[field.key] ?? field.default;
|
||||
const fieldLabel = getFieldLabel(field.key);
|
||||
const fieldDesc = getFieldDesc(field.key);
|
||||
|
||||
if (field.type === 'select') {
|
||||
return `
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm text-muted-foreground">${escapeHtml(field.label)}</label>
|
||||
<label class="block text-sm text-muted-foreground">${escapeHtml(fieldLabel)}</label>
|
||||
<select id="wizard_${field.key}"
|
||||
onchange="updateWizardConfig('${field.key}', this.value)"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary">
|
||||
@@ -495,13 +548,13 @@ function renderWizardModalContent() {
|
||||
<option value="${opt}" ${value === opt ? 'selected' : ''}>${opt}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
${field.description ? `<p class="text-xs text-muted-foreground">${escapeHtml(field.description)}</p>` : ''}
|
||||
${fieldDesc ? `<p class="text-xs text-muted-foreground">${escapeHtml(fieldDesc)}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'number') {
|
||||
return `
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm text-muted-foreground">${escapeHtml(field.label)}</label>
|
||||
<label class="block text-sm text-muted-foreground">${escapeHtml(fieldLabel)}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" id="wizard_${field.key}"
|
||||
value="${value}"
|
||||
@@ -512,7 +565,7 @@ function renderWizardModalContent() {
|
||||
class="flex-1 px-3 py-2 bg-background border border-border rounded-lg text-foreground focus:outline-none focus:ring-2 focus:ring-primary">
|
||||
<span class="text-sm text-muted-foreground">${formatIntervalDisplay(value)}</span>
|
||||
</div>
|
||||
${field.description ? `<p class="text-xs text-muted-foreground">${escapeHtml(field.description)}</p>` : ''}
|
||||
${fieldDesc ? `<p class="text-xs text-muted-foreground">${escapeHtml(fieldDesc)}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -522,7 +575,7 @@ function renderWizardModalContent() {
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-foreground">Generated Command Preview</label>
|
||||
<label class="block text-sm font-medium text-foreground">${t('hook.wizard.commandPreview')}</label>
|
||||
<div class="bg-muted/50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
|
||||
<pre id="wizardCommandPreview" class="whitespace-pre-wrap text-muted-foreground">${escapeHtml(generateWizardCommand())}</pre>
|
||||
</div>
|
||||
@@ -530,16 +583,16 @@ function renderWizardModalContent() {
|
||||
|
||||
<!-- Scope Selection -->
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium text-foreground">Install To</label>
|
||||
<label class="block text-sm font-medium text-foreground">${t('hook.wizard.installTo')}</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="wizardScope" value="project" checked>
|
||||
<span class="text-sm text-foreground">Project</span>
|
||||
<span class="text-sm text-foreground">${t('hook.scopeProject').split('(')[0]}</span>
|
||||
<span class="text-xs text-muted-foreground">(.claude/settings.json)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="wizardScope" value="global">
|
||||
<span class="text-sm text-foreground">Global</span>
|
||||
<span class="text-sm text-foreground">${t('hook.scopeGlobal').split('(')[0]}</span>
|
||||
<span class="text-xs text-muted-foreground">(~/.claude/settings.json)</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -593,10 +646,10 @@ function renderSkillContextConfig() {
|
||||
return '<div class="bg-muted/30 rounded-lg p-4 text-sm text-muted-foreground">' +
|
||||
'<div class="flex items-center gap-2 mb-2">' +
|
||||
'<i data-lucide="info" class="w-4 h-4"></i>' +
|
||||
'<span class="font-medium">Auto Detection Mode</span>' +
|
||||
'<span class="font-medium">' + t('hook.wizard.autoDetectionMode') + '</span>' +
|
||||
'</div>' +
|
||||
'<p>SKILLs will be automatically loaded when their name appears in your prompt.</p>' +
|
||||
'<p class="mt-2">Available SKILLs: ' + skillBadges + '</p>' +
|
||||
'<p>' + t('hook.wizard.autoDetectionInfo') + '</p>' +
|
||||
'<p class="mt-2">' + t('hook.wizard.availableSkills') + ' ' + skillBadges + '</p>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -604,8 +657,8 @@ function renderSkillContextConfig() {
|
||||
if (skillConfigs.length === 0) {
|
||||
configListHtml = '<div class="text-center py-6 text-muted-foreground text-sm border border-dashed border-border rounded-lg">' +
|
||||
'<i data-lucide="package" class="w-8 h-8 mx-auto mb-2 opacity-50"></i>' +
|
||||
'<p>No SKILLs configured yet</p>' +
|
||||
'<p class="text-xs mt-1">Click "Add SKILL" to configure keyword triggers</p>' +
|
||||
'<p>' + t('hook.wizard.noSkillsConfigured') + '</p>' +
|
||||
'<p class="text-xs mt-1">' + t('hook.wizard.clickAddSkill') + '</p>' +
|
||||
'</div>';
|
||||
} else {
|
||||
configListHtml = skillConfigs.map(function(config, idx) {
|
||||
@@ -617,7 +670,7 @@ function renderSkillContextConfig() {
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<select onchange="updateSkillConfig(' + idx + ', \'skill\', this.value)" ' +
|
||||
'class="px-2 py-1 text-sm bg-background border border-border rounded text-foreground">' +
|
||||
'<option value="">Select SKILL...</option>' +
|
||||
'<option value="">' + t('hook.wizard.selectSkill') + '</option>' +
|
||||
skillOptions +
|
||||
'</select>' +
|
||||
'<button onclick="removeSkillConfig(' + idx + ')" ' +
|
||||
@@ -626,7 +679,7 @@ function renderSkillContextConfig() {
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="space-y-1">' +
|
||||
'<label class="text-xs text-muted-foreground">Trigger Keywords (comma-separated)</label>' +
|
||||
'<label class="text-xs text-muted-foreground">' + t('hook.wizard.triggerKeywords') + '</label>' +
|
||||
'<input type="text" ' +
|
||||
'value="' + (config.keywords || '') + '" ' +
|
||||
'onchange="updateSkillConfig(' + idx + ', \'keywords\', this.value)" ' +
|
||||
@@ -641,16 +694,16 @@ function renderSkillContextConfig() {
|
||||
if (availableSkills.length === 0) {
|
||||
noSkillsWarning = '<div class="text-xs text-amber-500 flex items-center gap-1">' +
|
||||
'<i data-lucide="alert-triangle" class="w-3 h-3"></i>' +
|
||||
'No SKILLs found. Create SKILL packages in .claude/skills/' +
|
||||
t('hook.wizard.noSkillsFound') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
return '<div class="space-y-4">' +
|
||||
'<div class="flex items-center justify-between">' +
|
||||
'<span class="text-sm font-medium text-foreground">Configure SKILLs</span>' +
|
||||
'<span class="text-sm font-medium text-foreground">' + t('hook.wizard.configureSkills') + '</span>' +
|
||||
'<button type="button" onclick="addSkillConfig()" ' +
|
||||
'class="px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center gap-1">' +
|
||||
'<i data-lucide="plus" class="w-3 h-3"></i> Add SKILL' +
|
||||
'<i data-lucide="plus" class="w-3 h-3"></i> ' + t('hook.wizard.addSkill') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="skillConfigsList" class="space-y-3">' + configListHtml + '</div>' +
|
||||
|
||||
@@ -117,25 +117,27 @@ function setActiveNavItem(item) {
|
||||
function updateContentTitle() {
|
||||
const titleEl = document.getElementById('contentTitle');
|
||||
if (currentView === 'project-overview') {
|
||||
titleEl.textContent = 'Project Overview';
|
||||
titleEl.textContent = t('title.projectOverview');
|
||||
} else if (currentView === 'mcp-manager') {
|
||||
titleEl.textContent = 'MCP Server Management';
|
||||
titleEl.textContent = t('title.mcpManagement');
|
||||
} else if (currentView === 'explorer') {
|
||||
titleEl.textContent = 'File Explorer';
|
||||
titleEl.textContent = t('title.fileExplorer');
|
||||
} else if (currentView === 'cli-manager') {
|
||||
titleEl.textContent = 'CLI Tools & CCW';
|
||||
titleEl.textContent = t('title.cliTools');
|
||||
} else if (currentView === 'cli-history') {
|
||||
titleEl.textContent = 'CLI Execution History';
|
||||
titleEl.textContent = t('title.cliHistory');
|
||||
} else if (currentView === 'hook-manager') {
|
||||
titleEl.textContent = t('title.hookManager');
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' };
|
||||
titleEl.textContent = names[currentLiteType] || 'Lite Tasks';
|
||||
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
|
||||
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
||||
} else if (currentView === 'sessionDetail') {
|
||||
titleEl.textContent = 'Session Detail';
|
||||
titleEl.textContent = t('title.sessionDetail');
|
||||
} else if (currentView === 'liteTaskDetail') {
|
||||
titleEl.textContent = 'Lite Task Detail';
|
||||
titleEl.textContent = t('title.liteTaskDetail');
|
||||
} else {
|
||||
const names = { 'all': 'All Sessions', 'active': 'Active Sessions', 'archived': 'Archived Sessions' };
|
||||
titleEl.textContent = names[currentFilter] || 'Sessions';
|
||||
const names = { 'all': t('title.allSessions'), 'active': t('title.activeSessions'), 'archived': t('title.archivedSessions') };
|
||||
titleEl.textContent = names[currentFilter] || t('title.sessions');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +207,7 @@ async function refreshWorkspace() {
|
||||
renderProjectOverview();
|
||||
}
|
||||
|
||||
showRefreshToast('Workspace refreshed', 'success');
|
||||
showRefreshToast(t('toast.workspaceRefreshed'), 'success');
|
||||
}
|
||||
} else {
|
||||
// Non-server mode: just reload page
|
||||
@@ -213,7 +215,7 @@ async function refreshWorkspace() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Refresh failed:', error);
|
||||
showRefreshToast('Refresh failed: ' + error.message, 'error');
|
||||
showRefreshToast(t('toast.refreshFailed', { error: error.message }), 'error');
|
||||
} finally {
|
||||
btn.classList.remove('refreshing');
|
||||
btn.disabled = false;
|
||||
|
||||
1150
ccw/src/templates/dashboard-js/i18n.js
Normal file
1150
ccw/src/templates/dashboard-js/i18n.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize Lucide icons (must be first to render SVG icons)
|
||||
try { lucide.createIcons(); } catch (e) { console.error('Lucide icons init failed:', e); }
|
||||
|
||||
// Initialize i18n (must be early to translate static content)
|
||||
try { initI18n(); } catch (e) { console.error('I18n init failed:', e); }
|
||||
|
||||
// Initialize components with error handling to prevent cascading failures
|
||||
try { initTheme(); } catch (e) { console.error('Theme init failed:', e); }
|
||||
try { initSidebar(); } catch (e) { console.error('Sidebar init failed:', e); }
|
||||
|
||||
@@ -79,9 +79,9 @@ function renderToolsSection() {
|
||||
if (!container) return;
|
||||
|
||||
var toolDescriptions = {
|
||||
gemini: 'Google AI for code analysis',
|
||||
qwen: 'Alibaba AI assistant',
|
||||
codex: 'OpenAI code generation'
|
||||
gemini: t('cli.geminiDesc'),
|
||||
qwen: t('cli.qwenDesc'),
|
||||
codex: t('cli.codexDesc')
|
||||
};
|
||||
|
||||
var tools = ['gemini', 'qwen', 'codex'];
|
||||
@@ -97,17 +97,17 @@ function renderToolsSection() {
|
||||
'<span class="tool-status-dot ' + (isAvailable ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">' + tool.charAt(0).toUpperCase() + tool.slice(1) +
|
||||
(isDefault ? '<span class="tool-default-badge">Default</span>' : '') +
|
||||
(isDefault ? '<span class="tool-default-badge">' + t('cli.default') + '</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="tool-item-desc">' + toolDescriptions[tool] + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(isAvailable
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> Ready</span>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>') +
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> ' + t('cli.ready') + '</span>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>') +
|
||||
(isAvailable && !isDefault
|
||||
? '<button class="btn-sm btn-outline" onclick="setDefaultCliTool(\'' + tool + '\')"><i data-lucide="star" class="w-3 h-3"></i> Set Default</button>'
|
||||
? '<button class="btn-sm btn-outline" onclick="setDefaultCliTool(\'' + tool + '\')"><i data-lucide="star" class="w-3 h-3"></i> ' + t('cli.setDefault') + '</button>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
@@ -119,15 +119,15 @@ function renderToolsSection() {
|
||||
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span></div>' +
|
||||
'<div class="tool-item-desc">' + (codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine') + '</div>' +
|
||||
'<div class="tool-item-desc">' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(codexLensStatus.ready
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
|
||||
'<button class="btn-sm btn-outline" onclick="initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> Init Index</button>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> Install</button>') +
|
||||
'<button class="btn-sm btn-outline" onclick="initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> ' + t('cli.initIndex') + '</button>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> ' + t('cli.install') + '</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
@@ -153,10 +153,10 @@ function renderToolsSection() {
|
||||
|
||||
container.innerHTML = '<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>' +
|
||||
'<span class="section-count">' + available + '/' + tools.length + ' available</span>' +
|
||||
'<h3><i data-lucide="terminal" class="w-4 h-4"></i> ' + t('cli.tools') + '</h3>' +
|
||||
'<span class="section-count">' + available + '/' + tools.length + ' ' + t('cli.available') + '</span>' +
|
||||
'</div>' +
|
||||
'<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh Status">' +
|
||||
'<button class="btn-icon" onclick="refreshAllCliStatus()" title="' + t('cli.refreshStatus') + '">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
@@ -179,9 +179,9 @@ function renderCcwSection() {
|
||||
if (ccwInstallations.length === 0) {
|
||||
installationsHtml = '<div class="ccw-empty-state">' +
|
||||
'<i data-lucide="package-x" class="w-8 h-8"></i>' +
|
||||
'<p>No installations found</p>' +
|
||||
'<p>' + t('ccw.noInstallations') + '</p>' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="showCcwInstallModal()">' +
|
||||
'<i data-lucide="download" class="w-3 h-3"></i> Install CCW</button>' +
|
||||
'<i data-lucide="download" class="w-3 h-3"></i> ' + t('ccw.installCcw') + '</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
installationsHtml = '<div class="ccw-list">';
|
||||
@@ -224,8 +224,8 @@ function renderCcwSection() {
|
||||
|
||||
container.innerHTML = '<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="package" class="w-4 h-4"></i> CCW Install</h3>' +
|
||||
'<span class="section-count">' + ccwInstallations.length + ' installation' + (ccwInstallations.length !== 1 ? 's' : '') + '</span>' +
|
||||
'<h3><i data-lucide="package" class="w-4 h-4"></i> ' + t('ccw.install') + '</h3>' +
|
||||
'<span class="section-count">' + ccwInstallations.length + ' ' + (ccwInstallations.length !== 1 ? t('ccw.installationsPlural') : t('ccw.installations')) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="section-header-actions">' +
|
||||
'<button class="btn-icon" onclick="showCcwInstallModal()" title="Add Installation">' +
|
||||
@@ -252,9 +252,9 @@ function renderCcwEndpointToolsSection() {
|
||||
if (!ccwEndpointTools || ccwEndpointTools.length === 0) {
|
||||
toolsHtml = '<div class="ccw-empty-state">' +
|
||||
'<i data-lucide="wrench" class="w-8 h-8"></i>' +
|
||||
'<p>No endpoint tools found</p>' +
|
||||
'<p>' + t('ccw.noEndpointTools') + '</p>' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="loadCcwEndpointTools().then(function() { renderCcwEndpointToolsSection(); if (window.lucide) lucide.createIcons(); })">' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3"></i> Refresh</button>' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3"></i> ' + t('common.refresh') + '</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
toolsHtml = '<div class="endpoint-tools-grid">' +
|
||||
@@ -284,8 +284,8 @@ function renderCcwEndpointToolsSection() {
|
||||
|
||||
container.innerHTML = '<div class="section-header">' +
|
||||
'<div class="section-header-left">' +
|
||||
'<h3><i data-lucide="server" class="w-4 h-4"></i> CCW Endpoint Tools</h3>' +
|
||||
'<span class="section-count">' + count + ' tool' + (count !== 1 ? 's' : '') + '</span>' +
|
||||
'<h3><i data-lucide="server" class="w-4 h-4"></i> ' + t('ccw.endpointTools') + '</h3>' +
|
||||
'<span class="section-count">' + count + ' ' + (count !== 1 ? t('ccw.tools') : t('ccw.tool')) + '</span>' +
|
||||
'</div>' +
|
||||
'<button class="btn-icon" onclick="loadCcwEndpointTools().then(function() { renderCcwEndpointToolsSection(); if (window.lucide) lucide.createIcons(); })" title="Refresh">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i>' +
|
||||
@@ -664,7 +664,7 @@ function runCcwInstall(mode, customPath) {
|
||||
}
|
||||
|
||||
async function runCcwUpgrade() {
|
||||
showRefreshToast('Starting upgrade...', 'info');
|
||||
showRefreshToast(t('ccw.upgradeStarting'), 'info');
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/ccw/upgrade', {
|
||||
@@ -676,7 +676,7 @@ async function runCcwUpgrade() {
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast('Upgrade completed! Refreshing...', 'success');
|
||||
showRefreshToast(t('ccw.upgradeCompleted'), 'success');
|
||||
// Reload installations after upgrade
|
||||
setTimeout(function() {
|
||||
loadCcwInstallations().then(function() {
|
||||
@@ -684,15 +684,15 @@ async function runCcwUpgrade() {
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
showRefreshToast('Upgrade failed: ' + (result.error || 'Unknown error'), 'error');
|
||||
showRefreshToast(t('ccw.upgradeFailed', { error: result.error || 'Unknown error' }), 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast('Upgrade error: ' + err.message, 'error');
|
||||
showRefreshToast(t('ccw.upgradeFailed', { error: err.message }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmCcwUninstall(installPath) {
|
||||
if (confirm('Uninstall CCW from this location?\n' + (installPath || 'Current installation'))) {
|
||||
if (confirm(t('ccw.uninstallConfirm') + '\n' + (installPath || 'Current installation'))) {
|
||||
var command = installPath
|
||||
? 'ccw uninstall --path "' + installPath + '"'
|
||||
: 'ccw uninstall';
|
||||
@@ -720,7 +720,7 @@ async function executeCliFromDashboard() {
|
||||
var prompt = promptEl ? promptEl.value.trim() : '';
|
||||
|
||||
if (!prompt) {
|
||||
showRefreshToast('Please enter a prompt', 'error');
|
||||
showRefreshToast(t('toast.enterPrompt'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -756,11 +756,11 @@ async function executeCliFromDashboard() {
|
||||
|
||||
await loadCliHistory();
|
||||
renderCliHistory();
|
||||
showRefreshToast(result.success ? 'Completed' : (result.error || 'Failed'), result.success ? 'success' : 'error');
|
||||
showRefreshToast(result.success ? t('toast.completed') : (result.error || t('toast.failed')), result.success ? 'success' : 'error');
|
||||
} catch (error) {
|
||||
if (statusIndicator) statusIndicator.className = 'status-indicator error';
|
||||
if (statusText) statusText.textContent = 'Error: ' + error.message;
|
||||
showRefreshToast('Error: ' + error.message, 'error');
|
||||
showRefreshToast(t('toast.error', { error: error.message }), 'error');
|
||||
}
|
||||
|
||||
currentCliExecution = null;
|
||||
|
||||
@@ -60,33 +60,33 @@ async function renderExplorer() {
|
||||
<div class="explorer-tree-header">
|
||||
<div class="explorer-tree-title">
|
||||
<i data-lucide="folder-tree" class="explorer-icon"></i>
|
||||
<span class="explorer-title-text">Explorer</span>
|
||||
<span class="explorer-title-text">${t('explorer.title')}</span>
|
||||
</div>
|
||||
<button class="explorer-refresh-btn" onclick="refreshExplorerTree()" title="Refresh">
|
||||
<button class="explorer-refresh-btn" onclick="refreshExplorerTree()" title="${t('explorer.refresh')}">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="explorer-tree-content" id="explorerTreeContent">
|
||||
<div class="explorer-loading">Loading file tree...</div>
|
||||
<div class="explorer-loading">${t('explorer.loading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Preview -->
|
||||
<div class="explorer-preview-panel">
|
||||
<div class="explorer-preview-header" id="explorerPreviewHeader">
|
||||
<span class="preview-filename">Select a file to preview</span>
|
||||
<span class="preview-filename">${t('explorer.selectFile')}</span>
|
||||
</div>
|
||||
<div class="explorer-preview-content" id="explorerPreviewContent">
|
||||
<div class="explorer-preview-empty">
|
||||
<div class="preview-empty-icon"><i data-lucide="file-text" class="w-12 h-12"></i></div>
|
||||
<div class="preview-empty-text">Select a file from the tree to preview its contents</div>
|
||||
<div class="preview-empty-text">${t('explorer.selectFileHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Action Button -->
|
||||
<div class="explorer-fab" onclick="toggleTaskQueue()" title="Task Queue">
|
||||
<div class="explorer-fab" onclick="toggleTaskQueue()" title="${t('taskQueue.title')}">
|
||||
<span class="fab-icon"><i data-lucide="list-todo" class="w-5 h-5"></i></span>
|
||||
<span class="fab-badge" id="fabBadge">0</span>
|
||||
</div>
|
||||
@@ -94,12 +94,12 @@ async function renderExplorer() {
|
||||
<!-- Task Queue Panel -->
|
||||
<div class="task-queue-panel" id="taskQueuePanel">
|
||||
<div class="task-queue-header">
|
||||
<span class="task-queue-title"><i data-lucide="clipboard-list" class="w-4 h-4 inline-block mr-1"></i> Update Tasks</span>
|
||||
<span class="task-queue-title"><i data-lucide="clipboard-list" class="w-4 h-4 inline-block mr-1"></i> ${t('taskQueue.title')}</span>
|
||||
<button class="task-queue-close" onclick="toggleTaskQueue()">×</button>
|
||||
</div>
|
||||
<div class="task-queue-toolbar">
|
||||
<div class="queue-cli-selector">
|
||||
<label>CLI:</label>
|
||||
<label>${t('taskQueue.cli')}</label>
|
||||
<select id="queueCliTool" onchange="updateDefaultCliTool(this.value)">
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="qwen">Qwen</option>
|
||||
@@ -107,21 +107,21 @@ async function renderExplorer() {
|
||||
</select>
|
||||
</div>
|
||||
<div class="task-queue-actions">
|
||||
<button class="queue-action-btn" onclick="openAddTaskModal()" title="Add update task">
|
||||
<button class="queue-action-btn" onclick="openAddTaskModal()" title="${t('taskQueue.addTask')}">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="queue-action-btn queue-start-btn" onclick="startTaskQueue()" id="startQueueBtn" disabled title="Start all tasks">
|
||||
<button class="queue-action-btn queue-start-btn" onclick="startTaskQueue()" id="startQueueBtn" disabled title="${t('taskQueue.startAll')}">
|
||||
<i data-lucide="play" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="queue-action-btn queue-clear-btn" onclick="clearCompletedTasks()" title="Clear completed">
|
||||
<button class="queue-action-btn queue-clear-btn" onclick="clearCompletedTasks()" title="${t('taskQueue.clearCompleted')}">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-queue-list" id="taskQueueList">
|
||||
<div class="task-queue-empty">
|
||||
<span>No tasks in queue</span>
|
||||
<p>Hover folder and click <i data-lucide="file" class="w-3 h-3 inline"></i> or <i data-lucide="folder" class="w-3 h-3 inline"></i> to add tasks</p>
|
||||
<span>${t('taskQueue.noTasks')}</span>
|
||||
<p>${t('taskQueue.noTasksHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@ async function loadExplorerTree(dirPath) {
|
||||
*/
|
||||
function renderTreeLevel(files, parentPath, depth) {
|
||||
if (!files || files.length === 0) {
|
||||
return `<div class="tree-empty" style="padding-left: ${depth * 16 + 8}px">Empty directory</div>`;
|
||||
return `<div class="tree-empty" style="padding-left: ${depth * 16 + 8}px">${t('explorer.emptyDir')}</div>`;
|
||||
}
|
||||
|
||||
return files.map(file => {
|
||||
@@ -190,10 +190,10 @@ function renderTreeLevel(files, parentPath, depth) {
|
||||
</span>
|
||||
` : ''}
|
||||
<div class="tree-folder-actions">
|
||||
<button class="tree-update-btn" onclick="event.stopPropagation(); addFolderToQueue('${escapeHtml(file.path)}', 'single-layer')" title="Update CLAUDE.md (current folder only)">
|
||||
<button class="tree-update-btn" onclick="event.stopPropagation(); addFolderToQueue('${escapeHtml(file.path)}', 'single-layer')" title="${t('explorer.currentFolderOnly')}">
|
||||
<span class="update-icon"><i data-lucide="file" class="w-3.5 h-3.5"></i></span>
|
||||
</button>
|
||||
<button class="tree-update-btn tree-update-multi" onclick="event.stopPropagation(); addFolderToQueue('${escapeHtml(file.path)}', 'multi-layer')" title="Update CLAUDE.md (with subdirectories)">
|
||||
<button class="tree-update-btn tree-update-multi" onclick="event.stopPropagation(); addFolderToQueue('${escapeHtml(file.path)}', 'multi-layer')" title="${t('explorer.withSubdirs')}">
|
||||
<span class="update-icon"><i data-lucide="folder-tree" class="w-3.5 h-3.5"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -425,8 +425,8 @@ async function previewFile(filePath) {
|
||||
</div>
|
||||
${isMarkdown ? `
|
||||
<div class="preview-header-tabs" id="previewHeaderTabs">
|
||||
<button class="preview-tab active" data-tab="rendered" onclick="switchPreviewTab(this, 'rendered')">Preview</button>
|
||||
<button class="preview-tab" data-tab="source" onclick="switchPreviewTab(this, 'source')">Source</button>
|
||||
<button class="preview-tab active" data-tab="rendered" onclick="switchPreviewTab(this, 'rendered')">${t('explorer.preview')}</button>
|
||||
<button class="preview-tab" data-tab="source" onclick="switchPreviewTab(this, 'source')">${t('explorer.source')}</button>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
@@ -458,7 +458,7 @@ async function previewFile(filePath) {
|
||||
previewContent.innerHTML = `
|
||||
<div class="preview-info">
|
||||
<span class="preview-lang">${data.language}</span>
|
||||
<span class="preview-lines">${data.lines} lines</span>
|
||||
<span class="preview-lines">${data.lines} ${t('explorer.lines')}</span>
|
||||
<span class="preview-size">${formatFileSize(data.size)}</span>
|
||||
</div>
|
||||
<pre class="preview-code"><code class="language-${data.language}">${escapeHtml(data.content)}</code></pre>
|
||||
@@ -725,8 +725,8 @@ function renderTaskQueue() {
|
||||
if (updateTaskQueue.length === 0) {
|
||||
listEl.innerHTML = `
|
||||
<div class="task-queue-empty">
|
||||
<span>No tasks in queue</span>
|
||||
<p>Right-click a folder or click "Add Task" to queue CLAUDE.md updates</p>
|
||||
<span>${t('taskQueue.noTasks')}</span>
|
||||
<p>${t('taskQueue.noTasksHint')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -735,8 +735,8 @@ function renderTaskQueue() {
|
||||
listEl.innerHTML = updateTaskQueue.map(task => {
|
||||
const folderName = task.path.split('/').pop() || task.path;
|
||||
const strategyLabel = task.strategy === 'multi-layer'
|
||||
? '<i data-lucide="folder-tree" class="w-3 h-3 inline"></i> With subdirs'
|
||||
: '<i data-lucide="file" class="w-3 h-3 inline"></i> Current only';
|
||||
? '<i data-lucide="folder-tree" class="w-3 h-3 inline"></i> ' + t('taskQueue.withSubdirs')
|
||||
: '<i data-lucide="file" class="w-3 h-3 inline"></i> ' + t('taskQueue.currentOnly');
|
||||
const statusIcon = {
|
||||
'pending': '<i data-lucide="clock" class="w-4 h-4"></i>',
|
||||
'running': '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>',
|
||||
@@ -796,7 +796,7 @@ async function executeTask(task) {
|
||||
|
||||
// Update status to running
|
||||
task.status = 'running';
|
||||
task.message = 'Processing...';
|
||||
task.message = t('taskQueue.processing');
|
||||
renderTaskQueue();
|
||||
|
||||
addGlobalNotification('info', `Processing: ${folderName}`, `Strategy: ${task.strategy}, Tool: ${task.tool}`, 'Explorer');
|
||||
@@ -816,12 +816,12 @@ async function executeTask(task) {
|
||||
|
||||
if (result.success) {
|
||||
task.status = 'completed';
|
||||
task.message = 'Updated successfully';
|
||||
task.message = t('taskQueue.updated');
|
||||
addGlobalNotification('success', `Completed: ${folderName}`, result.message, 'Explorer');
|
||||
return { success: true };
|
||||
} else {
|
||||
task.status = 'failed';
|
||||
task.message = result.error || 'Update failed';
|
||||
task.message = result.error || t('taskQueue.failed');
|
||||
addGlobalNotification('error', `Failed: ${folderName}`, result.error || 'Unknown error', 'Explorer');
|
||||
return { success: false };
|
||||
}
|
||||
@@ -848,7 +848,7 @@ async function startTaskQueue() {
|
||||
isTaskRunning = true;
|
||||
document.getElementById('startQueueBtn').disabled = true;
|
||||
|
||||
addGlobalNotification('info', `Starting ${pendingTasks.length} task(s) in parallel...`, null, 'Explorer');
|
||||
addGlobalNotification('info', t('taskQueue.startingTasks', { count: pendingTasks.length }), null, 'Explorer');
|
||||
|
||||
// Execute all tasks in parallel
|
||||
const results = await Promise.all(pendingTasks.map(task => executeTask(task)));
|
||||
@@ -861,7 +861,7 @@ async function startTaskQueue() {
|
||||
// Summary notification
|
||||
addGlobalNotification(
|
||||
failCount === 0 ? 'success' : 'warning',
|
||||
`Queue completed: ${successCount} succeeded, ${failCount} failed`,
|
||||
t('taskQueue.queueCompleted', { success: successCount, failed: failCount }),
|
||||
null,
|
||||
'Explorer'
|
||||
);
|
||||
|
||||
@@ -74,8 +74,8 @@ function renderSessions() {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" style="grid-column: 1/-1;">
|
||||
<div class="empty-icon"><i data-lucide="inbox" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No Sessions Found</div>
|
||||
<div class="empty-text">No workflow sessions match your current filter.</div>
|
||||
<div class="empty-title">${t('empty.noSessions')}</div>
|
||||
<div class="empty-text">${t('empty.noSessionsText')}</div>
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
@@ -111,13 +111,13 @@ function renderSessionCard(session) {
|
||||
if (!isActive) {
|
||||
// Archived sessions always show as ARCHIVED regardless of status field
|
||||
statusClass = 'archived';
|
||||
statusText = 'ARCHIVED';
|
||||
statusText = t('session.status.archived');
|
||||
} else if (isPlanning) {
|
||||
statusClass = 'planning';
|
||||
statusText = 'PLANNING';
|
||||
statusText = t('session.status.planning');
|
||||
} else {
|
||||
statusClass = 'active';
|
||||
statusText = 'ACTIVE';
|
||||
statusText = t('session.status.active');
|
||||
}
|
||||
|
||||
// Store session data for modal
|
||||
@@ -146,11 +146,11 @@ function renderSessionCard(session) {
|
||||
<div class="session-body">
|
||||
<div class="session-meta">
|
||||
<span class="session-meta-item"><i data-lucide="calendar" class="w-3.5 h-3.5 inline mr-1"></i>${formatDate(date)}</span>
|
||||
<span class="session-meta-item"><i data-lucide="list-checks" class="w-3.5 h-3.5 inline mr-1"></i>${taskCount} tasks</span>
|
||||
<span class="session-meta-item"><i data-lucide="list-checks" class="w-3.5 h-3.5 inline mr-1"></i>${taskCount} ${t('session.tasks')}</span>
|
||||
</div>
|
||||
${taskCount > 0 && !isPlanning ? `
|
||||
<div class="progress-container">
|
||||
<span class="progress-label">Progress</span>
|
||||
<span class="progress-label">${t('session.progress')}</span>
|
||||
<div class="progress-bar-wrapper">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${progress}%"></div>
|
||||
@@ -188,13 +188,13 @@ function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, isPla
|
||||
let statusClass, statusText;
|
||||
if (!isActive) {
|
||||
statusClass = 'archived';
|
||||
statusText = 'ARCHIVED';
|
||||
statusText = t('session.status.archived');
|
||||
} else if (isPlanning) {
|
||||
statusClass = 'planning';
|
||||
statusText = 'PLANNING';
|
||||
statusText = t('session.status.planning');
|
||||
} else {
|
||||
statusClass = 'active';
|
||||
statusText = 'ACTIVE';
|
||||
statusText = t('session.status.active');
|
||||
}
|
||||
|
||||
// Card class includes planning modifier for special styling (only for active sessions)
|
||||
@@ -214,7 +214,7 @@ function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, isPla
|
||||
<div class="session-body">
|
||||
<div class="session-meta">
|
||||
<span class="session-meta-item"><i data-lucide="calendar" class="w-3.5 h-3.5 inline mr-1"></i>${formatDate(date)}</span>
|
||||
<span class="session-meta-item"><i data-lucide="search" class="w-3.5 h-3.5 inline mr-1"></i>${totalFindings} findings</span>
|
||||
<span class="session-meta-item"><i data-lucide="search" class="w-3.5 h-3.5 inline mr-1"></i>${totalFindings} ${t('session.findings')}</span>
|
||||
</div>
|
||||
${totalFindings > 0 ? `
|
||||
<div class="review-findings-summary">
|
||||
@@ -225,7 +225,7 @@ function renderReviewSessionCard(session, sessionKey, typeBadge, isActive, isPla
|
||||
${lowCount > 0 ? `<span class="finding-count low"><i data-lucide="check-circle" class="w-3 h-3 inline"></i> ${lowCount}</span>` : ''}
|
||||
</div>
|
||||
<div class="dimensions-info">
|
||||
${dimensions.length} dimensions
|
||||
${dimensions.length} ${t('session.dimensions')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
@@ -29,21 +29,21 @@ async function renderHookManager() {
|
||||
<div class="hook-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">Project Hooks</h3>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-primary-light text-primary">.claude/settings.json</span>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('hook.projectHooks')}</h3>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-primary-light text-primary">${t('hook.projectFile')}</span>
|
||||
<button class="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
|
||||
onclick="openHookCreateModal()">
|
||||
<span>+</span> New Hook
|
||||
<span>+</span> ${t('hook.newHook')}
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${projectHookCount} hooks configured</span>
|
||||
<span class="text-sm text-muted-foreground">${projectHookCount} ${t('hook.hooksConfigured')}</span>
|
||||
</div>
|
||||
|
||||
${projectHookCount === 0 ? `
|
||||
<div class="hook-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="webhook" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">No hooks configured for this project</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">Create a hook to automate actions on tool usage</p>
|
||||
<p class="text-muted-foreground">${t('empty.noHooks')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('empty.createHookHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="hook-grid grid gap-3">
|
||||
@@ -56,16 +56,16 @@ async function renderHookManager() {
|
||||
<div class="hook-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">Global Hooks</h3>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-muted text-muted-foreground">~/.claude/settings.json</span>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('hook.globalHooks')}</h3>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-muted text-muted-foreground">${t('hook.globalFile')}</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${globalHookCount} hooks configured</span>
|
||||
<span class="text-sm text-muted-foreground">${globalHookCount} ${t('hook.hooksConfigured')}</span>
|
||||
</div>
|
||||
|
||||
${globalHookCount === 0 ? `
|
||||
<div class="hook-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<p class="text-muted-foreground">No global hooks configured</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">Global hooks apply to all Claude Code sessions</p>
|
||||
<p class="text-muted-foreground">${t('empty.noGlobalHooks')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('empty.globalHooksHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="hook-grid grid gap-3">
|
||||
@@ -78,10 +78,10 @@ async function renderHookManager() {
|
||||
<div class="hook-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">Hook Wizards</h3>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-success/20 text-success">Guided Setup</span>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('hook.wizards')}</h3>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-success/20 text-success">${t('hook.guidedSetup')}</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">Configure complex hooks with guided wizards</span>
|
||||
<span class="text-sm text-muted-foreground">${t('hook.wizardsDesc')}</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -93,23 +93,23 @@ async function renderHookManager() {
|
||||
<!-- Quick Install Templates -->
|
||||
<div class="hook-section">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-foreground">Quick Install Templates</h3>
|
||||
<span class="text-sm text-muted-foreground">One-click hook installation</span>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('hook.quickInstall')}</h3>
|
||||
<span class="text-sm text-muted-foreground">${t('hook.oneClick')}</span>
|
||||
</div>
|
||||
|
||||
<div class="hook-templates-grid grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
${renderQuickInstallCard('codexlens-update', 'CodexLens Auto-Sync', 'Auto-update code index when files are written or edited', 'PostToolUse', 'Write|Edit')}
|
||||
${renderQuickInstallCard('ccw-notify', 'CCW Dashboard Notify', 'Notify CCW dashboard when files are written', 'PostToolUse', 'Write')}
|
||||
${renderQuickInstallCard('log-tool', 'Tool Usage Logger', 'Log all tool executions to a file', 'PostToolUse', 'All')}
|
||||
${renderQuickInstallCard('lint-check', 'Auto Lint Check', 'Run ESLint on JavaScript/TypeScript files after write', 'PostToolUse', 'Write')}
|
||||
${renderQuickInstallCard('git-add', 'Auto Git Stage', 'Automatically stage written files to git', 'PostToolUse', 'Write')}
|
||||
${renderQuickInstallCard('codexlens-update', t('hook.tpl.codexlensSync'), t('hook.tpl.codexlensSyncDesc'), 'PostToolUse', 'Write|Edit')}
|
||||
${renderQuickInstallCard('ccw-notify', t('hook.tpl.ccwDashboardNotify'), t('hook.tpl.ccwDashboardNotifyDesc'), 'PostToolUse', 'Write')}
|
||||
${renderQuickInstallCard('log-tool', t('hook.tpl.toolLogger'), t('hook.tpl.toolLoggerDesc'), 'PostToolUse', 'All')}
|
||||
${renderQuickInstallCard('lint-check', t('hook.tpl.autoLint'), t('hook.tpl.autoLintDesc'), 'PostToolUse', 'Write')}
|
||||
${renderQuickInstallCard('git-add', t('hook.tpl.autoGitStage'), t('hook.tpl.autoGitStageDesc'), 'PostToolUse', 'Write')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hook Environment Variables Reference -->
|
||||
<div class="hook-section mt-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-foreground">Environment Variables Reference</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('hook.envVarsRef')}</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-card border border-border rounded-lg p-4">
|
||||
@@ -117,29 +117,29 @@ async function renderHookManager() {
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">$CLAUDE_FILE_PATHS</code>
|
||||
<span class="text-muted-foreground">Space-separated file paths affected</span>
|
||||
<span class="text-muted-foreground">${t('hook.filePaths')}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">$CLAUDE_TOOL_NAME</code>
|
||||
<span class="text-muted-foreground">Name of the tool being executed</span>
|
||||
<span class="text-muted-foreground">${t('hook.toolName')}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">$CLAUDE_TOOL_INPUT</code>
|
||||
<span class="text-muted-foreground">JSON input passed to the tool</span>
|
||||
<span class="text-muted-foreground">${t('hook.toolInput')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">$CLAUDE_SESSION_ID</code>
|
||||
<span class="text-muted-foreground">Current Claude session ID</span>
|
||||
<span class="text-muted-foreground">${t('hook.sessionId')}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">$CLAUDE_PROJECT_DIR</code>
|
||||
<span class="text-muted-foreground">Current project directory path</span>
|
||||
<span class="text-muted-foreground">${t('hook.projectDir')}</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">$CLAUDE_WORKING_DIR</code>
|
||||
<span class="text-muted-foreground">Current working directory</span>
|
||||
<span class="text-muted-foreground">${t('hook.workingDir')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,6 +153,9 @@ async function renderHookManager() {
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
// Load available SKILLs for skill-context wizard
|
||||
loadAvailableSkills();
|
||||
}
|
||||
|
||||
// Load available SKILLs for skill-context wizard
|
||||
@@ -161,25 +164,25 @@ async function loadAvailableSkills() {
|
||||
const response = await fetch(`/api/skills?path=${encodeURIComponent(projectPath)}`);
|
||||
if (!response.ok) throw new Error('Failed to load skills');
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
const container = document.getElementById('skill-discovery-skill-context');
|
||||
if (container && data.skills) {
|
||||
if (data.skills.length === 0) {
|
||||
container.innerHTML = `
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
|
||||
<span class="text-muted-foreground ml-2">No SKILLs found in .claude/skills/</span>
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
||||
<span class="text-muted-foreground ml-2">${t('hook.wizard.noSkillsFound').split('.')[0]}</span>
|
||||
`;
|
||||
} else {
|
||||
const skillBadges = data.skills.map(skill => `
|
||||
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description)}">${escapeHtml(skill.name)}</span>
|
||||
`).join('');
|
||||
container.innerHTML = `
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">${skillBadges}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Store skills for wizard use
|
||||
window.availableSkills = data.skills || [];
|
||||
} catch (err) {
|
||||
@@ -187,8 +190,8 @@ async function loadAvailableSkills() {
|
||||
const container = document.getElementById('skill-discovery-skill-context');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
|
||||
<span class="text-destructive ml-2">Error loading skills</span>
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
||||
<span class="text-destructive ml-2">${t('toast.loadFailed', { error: err.message })}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -201,21 +204,52 @@ function renderWizardCard(wizardId) {
|
||||
const wizard = WIZARD_TEMPLATES[wizardId];
|
||||
if (!wizard) return '';
|
||||
|
||||
// Get translated wizard name and description
|
||||
const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') :
|
||||
wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name;
|
||||
const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') :
|
||||
wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description;
|
||||
|
||||
// Translate options
|
||||
const getOptionName = (wizardId, optId) => {
|
||||
if (wizardId === 'memory-update') {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEnd');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdate');
|
||||
}
|
||||
if (wizardId === 'skill-context') {
|
||||
if (optId === 'keyword') return t('hook.wizard.keywordMatching');
|
||||
if (optId === 'auto') return t('hook.wizard.autoDetection');
|
||||
}
|
||||
return wizard.options.find(o => o.id === optId)?.name || '';
|
||||
};
|
||||
|
||||
const getOptionDesc = (wizardId, optId) => {
|
||||
if (wizardId === 'memory-update') {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc');
|
||||
}
|
||||
if (wizardId === 'skill-context') {
|
||||
if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc');
|
||||
if (optId === 'auto') return t('hook.wizard.autoDetectionDesc');
|
||||
}
|
||||
return wizard.options.find(o => o.id === optId)?.description || '';
|
||||
};
|
||||
|
||||
// Determine what to show in the tools/skills section
|
||||
const toolsSection = wizard.requiresSkillDiscovery
|
||||
const toolsSection = wizard.requiresSkillDiscovery
|
||||
? `
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Event:</span>
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.event')}</span>
|
||||
<span class="px-2 py-0.5 bg-amber-500/10 text-amber-500 rounded">UserPromptSubmit</span>
|
||||
</div>
|
||||
<div id="skill-discovery-${wizardId}" class="text-xs text-muted-foreground mb-4">
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">Available SKILLs:</span>
|
||||
<span class="text-muted-foreground ml-2">Loading...</span>
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
||||
<span class="text-muted-foreground ml-2">${t('hook.wizard.loading')}</span>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground mb-4">
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">CLI Tools:</span>
|
||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.cliTools')}</span>
|
||||
<span class="px-2 py-0.5 bg-blue-500/10 text-blue-500 rounded">gemini</span>
|
||||
<span class="px-2 py-0.5 bg-purple-500/10 text-purple-500 rounded">qwen</span>
|
||||
<span class="px-2 py-0.5 bg-green-500/10 text-green-500 rounded">codex</span>
|
||||
@@ -230,8 +264,8 @@ function renderWizardCard(wizardId) {
|
||||
<i data-lucide="${wizard.icon}" class="w-6 h-6 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(wizard.name)}</h4>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(wizard.description)}</p>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(wizardName)}</h4>
|
||||
<p class="text-sm text-muted-foreground">${escapeHtml(wizardDesc)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,7 +274,7 @@ function renderWizardCard(wizardId) {
|
||||
${wizard.options.map(opt => `
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<i data-lucide="check" class="w-4 h-4 text-success"></i>
|
||||
<span>${escapeHtml(opt.name)}: ${escapeHtml(opt.description)}</span>
|
||||
<span>${escapeHtml(getOptionName(wizardId, opt.id))}: ${escapeHtml(getOptionDesc(wizardId, opt.id))}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
@@ -250,7 +284,7 @@ function renderWizardCard(wizardId) {
|
||||
<button class="w-full px-4 py-2.5 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2"
|
||||
onclick="openHookWizardModal('${wizardId}')">
|
||||
<i data-lucide="wand-2" class="w-4 h-4"></i>
|
||||
Open Wizard
|
||||
${t('hook.openWizard')}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -333,6 +367,7 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
|
||||
const isInstalled = isHookTemplateInstalled(templateId);
|
||||
const template = HOOK_TEMPLATES[templateId];
|
||||
const category = template?.category || 'general';
|
||||
const categoryTranslated = t(`hook.category.${category}`) || category;
|
||||
|
||||
return `
|
||||
<div class="hook-template-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${isInstalled ? 'border-success bg-success-light/30' : ''}">
|
||||
@@ -346,7 +381,7 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
|
||||
</div>
|
||||
<button class="p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded transition-colors"
|
||||
onclick="viewTemplateDetails('${templateId}')"
|
||||
title="View template details">
|
||||
title="${t('hook.viewDetails')}">
|
||||
<i data-lucide="eye" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -356,9 +391,9 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
|
||||
<span class="font-mono bg-muted px-1 py-0.5 rounded">${event}</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
Matches: <span class="font-medium">${matcher}</span>
|
||||
${t('hook.wizard.matches')} <span class="font-medium">${matcher}</span>
|
||||
</span>
|
||||
<span class="px-1.5 py-0.5 bg-primary/10 text-primary rounded text-xs">${category}</span>
|
||||
<span class="px-1.5 py-0.5 bg-primary/10 text-primary rounded text-xs">${categoryTranslated}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -366,18 +401,18 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)
|
||||
<button class="flex-1 px-3 py-1.5 text-sm bg-destructive/10 text-destructive rounded hover:bg-destructive/20 transition-colors"
|
||||
data-template="${templateId}"
|
||||
data-action="uninstall">
|
||||
Uninstall
|
||||
${t('hook.uninstall')}
|
||||
</button>
|
||||
` : `
|
||||
<button class="flex-1 px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-template="${templateId}"
|
||||
data-action="install-project">
|
||||
Install (Project)
|
||||
${t('hook.installProject')}
|
||||
</button>
|
||||
<button class="px-3 py-1.5 text-sm bg-muted text-foreground rounded hover:bg-hover transition-colors"
|
||||
data-template="${templateId}"
|
||||
data-action="install-global">
|
||||
Global
|
||||
${t('hook.installGlobal')}
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
@@ -486,7 +521,7 @@ function attachHookEventListeners() {
|
||||
const event = button.dataset.event;
|
||||
const index = parseInt(button.dataset.index);
|
||||
|
||||
if (confirm(`Remove this ${event} hook?`)) {
|
||||
if (confirm(t('hook.deleteConfirm', { event: event }))) {
|
||||
await removeHook(scope, event, index);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@ function renderLiteTasks() {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon"><i data-lucide="zap" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No ${currentLiteType} Sessions</div>
|
||||
<div class="empty-text">No sessions found in .workflow/.${currentLiteType}/</div>
|
||||
<div class="empty-title">${t('empty.noLiteSessions', { type: currentLiteType })}</div>
|
||||
<div class="empty-text">${t('empty.noLiteSessionsText', { type: currentLiteType })}</div>
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
@@ -55,13 +55,13 @@ function renderLiteTaskCard(session) {
|
||||
<div class="session-header">
|
||||
<div class="session-title">${escapeHtml(session.id)}</div>
|
||||
<span class="session-status ${session.type}">
|
||||
${session.type === 'lite-plan' ? '<i data-lucide="file-edit" class="w-3 h-3 inline"></i> PLAN' : '<i data-lucide="wrench" class="w-3 h-3 inline"></i> FIX'}
|
||||
${session.type === 'lite-plan' ? '<i data-lucide="file-edit" class="w-3 h-3 inline"></i> ' + t('lite.plan') : '<i data-lucide="wrench" class="w-3 h-3 inline"></i> ' + t('lite.fix')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="session-body">
|
||||
<div class="session-meta">
|
||||
<span class="session-meta-item"><i data-lucide="calendar" class="w-3.5 h-3.5 inline mr-1"></i>${formatDate(session.createdAt)}</span>
|
||||
<span class="session-meta-item"><i data-lucide="list-checks" class="w-3.5 h-3.5 inline mr-1"></i>${tasks.length} tasks</span>
|
||||
<span class="session-meta-item"><i data-lucide="list-checks" class="w-3.5 h-3.5 inline mr-1"></i>${tasks.length} ${t('session.tasks')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,12 +116,12 @@ function showLiteTaskDetailPage(sessionKey) {
|
||||
<!-- Session Info Bar -->
|
||||
<div class="detail-info-bar">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Created:</span>
|
||||
<span class="info-label">${t('detail.created')}</span>
|
||||
<span class="info-value">${formatDate(session.createdAt)}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Tasks:</span>
|
||||
<span class="info-value">${tasks.length} tasks</span>
|
||||
<span class="info-label">${t('detail.tasks')}</span>
|
||||
<span class="info-value">${tasks.length} ${t('session.tasks')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,27 +129,27 @@ function showLiteTaskDetailPage(sessionKey) {
|
||||
<div class="detail-tabs">
|
||||
<button class="detail-tab active" data-tab="tasks" onclick="switchLiteDetailTab('tasks')">
|
||||
<span class="tab-icon"><i data-lucide="list-checks" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Tasks</span>
|
||||
<span class="tab-text">${t('tab.tasks')}</span>
|
||||
<span class="tab-count">${tasks.length}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="plan" onclick="switchLiteDetailTab('plan')">
|
||||
<span class="tab-icon"><i data-lucide="ruler" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Plan</span>
|
||||
<span class="tab-text">${t('tab.plan')}</span>
|
||||
</button>
|
||||
${session.type === 'lite-fix' ? `
|
||||
<button class="detail-tab" data-tab="diagnoses" onclick="switchLiteDetailTab('diagnoses')">
|
||||
<span class="tab-icon"><i data-lucide="stethoscope" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Diagnoses</span>
|
||||
<span class="tab-text">${t('tab.diagnoses')}</span>
|
||||
${session.diagnoses?.items?.length ? `<span class="tab-count">${session.diagnoses.items.length}</span>` : ''}
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="detail-tab" data-tab="context" onclick="switchLiteDetailTab('context')">
|
||||
<span class="tab-icon"><i data-lucide="package" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Context</span>
|
||||
<span class="tab-text">${t('tab.context')}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="summary" onclick="switchLiteDetailTab('summary')">
|
||||
<span class="tab-icon"><i data-lucide="file-text" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Summary</span>
|
||||
<span class="tab-text">${t('tab.summary')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -232,8 +232,8 @@ function renderLiteTasksTab(session, tasks, completed, inProgress, pending) {
|
||||
return `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon"><i data-lucide="clipboard-list" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No Tasks</div>
|
||||
<div class="empty-text">This session has no tasks defined.</div>
|
||||
<div class="empty-title">${t('empty.noTasks')}</div>
|
||||
<div class="empty-text">${t('empty.noTasksText')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -506,8 +506,8 @@ function renderDiagnosesTab(session) {
|
||||
return `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon"><i data-lucide="stethoscope" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No Diagnoses</div>
|
||||
<div class="empty-text">No diagnosis-*.json files found for this session.</div>
|
||||
<div class="empty-title">${t('empty.noDiagnoses')}</div>
|
||||
<div class="empty-text">${t('empty.noDiagnosesText')}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -41,20 +41,20 @@ async function renderMcpManager() {
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">Current Project MCP Servers</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.currentProject')}</h3>
|
||||
<button class="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
|
||||
onclick="openMcpCreateModal()">
|
||||
<span>+</span> New Server
|
||||
<span>+</span> ${t('mcp.newServer')}
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${currentProjectServerNames.length} servers configured</span>
|
||||
<span class="text-sm text-muted-foreground">${currentProjectServerNames.length} ${t('mcp.serversConfigured')}</span>
|
||||
</div>
|
||||
|
||||
${currentProjectServerNames.length === 0 ? `
|
||||
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="plug" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">No MCP servers configured for this project</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">Add servers from the available list below</p>
|
||||
<p class="text-muted-foreground">${t('empty.noMcpServers')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('empty.addMcpServersHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
@@ -109,13 +109,13 @@ async function renderMcpManager() {
|
||||
<!-- Available MCP Servers from Other Projects -->
|
||||
<div class="mcp-section">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-foreground">Available from Other Projects</h3>
|
||||
<span class="text-sm text-muted-foreground">${otherProjectServers.length} servers available</span>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.availableOther')}</h3>
|
||||
<span class="text-sm text-muted-foreground">${otherProjectServers.length} ${t('mcp.serversAvailable')}</span>
|
||||
</div>
|
||||
|
||||
${otherProjectServers.length === 0 ? `
|
||||
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<p class="text-muted-foreground">No additional MCP servers found in other projects</p>
|
||||
<p class="text-muted-foreground">${t('empty.noAdditionalMcp')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
@@ -129,17 +129,17 @@ async function renderMcpManager() {
|
||||
<!-- All Projects MCP Overview Table -->
|
||||
<div class="mcp-section mt-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-foreground">All Projects MCP Overview</h3>
|
||||
<span class="text-sm text-muted-foreground">${Object.keys(mcpAllProjects).length} projects</span>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.allProjects')}</h3>
|
||||
<span class="text-sm text-muted-foreground">${Object.keys(mcpAllProjects).length} ${t('mcp.projects')}</span>
|
||||
</div>
|
||||
|
||||
<div class="mcp-projects-table bg-card border border-border rounded-lg overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-muted/50">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-foreground border-b border-border">Project</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-foreground border-b border-border">MCP Servers</th>
|
||||
<th class="text-center px-4 py-3 text-sm font-semibold text-foreground border-b border-border w-24">Status</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-foreground border-b border-border">${t('mcp.project')}</th>
|
||||
<th class="text-left px-4 py-3 text-sm font-semibold text-foreground border-b border-border">${t('mcp.servers')}</th>
|
||||
<th class="text-center px-4 py-3 text-sm font-semibold text-foreground border-b border-border w-24">${t('mcp.status')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -158,7 +158,7 @@ async function renderMcpManager() {
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-foreground truncate text-sm" title="${escapeHtml(path)}">
|
||||
${escapeHtml(path.split('\\').pop() || path)}
|
||||
${isCurrentProject ? '<span class="ml-2 text-xs text-primary font-medium">(Current)</span>' : ''}
|
||||
${isCurrentProject ? `<span class="ml-2 text-xs text-primary font-medium">${t('mcp.current')}</span>` : ''}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground truncate">${escapeHtml(path)}</div>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@ async function renderMcpManager() {
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
${serverNames.length === 0
|
||||
? '<span class="text-xs text-muted-foreground italic">No MCP servers</span>'
|
||||
? `<span class="text-xs text-muted-foreground italic">${t('mcp.noMcpServers')}</span>`
|
||||
: serverNames.map(serverName => {
|
||||
const isEnabled = !projectDisabled.includes(serverName);
|
||||
return `
|
||||
@@ -247,7 +247,7 @@ function renderMcpServerCard(serverName, serverConfig, isEnabled, isInCurrentPro
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-action="remove">
|
||||
Remove from project
|
||||
${t('mcp.removeFromProject')}
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -271,7 +271,7 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-action="add">
|
||||
Add
|
||||
${t('mcp.add')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -306,7 +306,7 @@ function renderGlobalServerCard(serverName, serverConfig, source = 'user') {
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-action="add">
|
||||
Add to Project
|
||||
${t('mcp.addToProject')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -403,7 +403,7 @@ function attachMcpEventListeners() {
|
||||
document.querySelectorAll('.mcp-server-card button[data-action="remove"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const serverName = e.target.dataset.serverName;
|
||||
if (confirm(`Remove MCP server "${serverName}" from this project?`)) {
|
||||
if (confirm(t('mcp.removeConfirm', { name: serverName }))) {
|
||||
await removeMcpServerFromProject(serverName);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,14 +43,14 @@ function showSessionDetailPage(sessionKey) {
|
||||
<div class="detail-header">
|
||||
<button class="btn-back" onclick="goBackToSessions()">
|
||||
<span class="back-icon">←</span>
|
||||
<span>Back to Sessions</span>
|
||||
<span>${t('detail.backToSessions')}</span>
|
||||
</button>
|
||||
<div class="detail-title-row">
|
||||
<h2 class="detail-session-id">${escapeHtml(session.session_id)}</h2>
|
||||
<div class="detail-badges">
|
||||
<span class="session-type-badge ${session.type || 'workflow'}">${session.type || 'workflow'}</span>
|
||||
<span class="session-status ${isActive ? 'active' : 'archived'}">
|
||||
${isActive ? 'ACTIVE' : 'ARCHIVED'}
|
||||
${isActive ? t('session.status.active') : t('session.status.archived')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,22 +59,22 @@ function showSessionDetailPage(sessionKey) {
|
||||
<!-- Session Info Bar -->
|
||||
<div class="detail-info-bar">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Created:</span>
|
||||
<span class="info-label">${t('detail.created')}</span>
|
||||
<span class="info-value">${formatDate(session.created_at)}</span>
|
||||
</div>
|
||||
${session.archived_at ? `
|
||||
<div class="info-item">
|
||||
<span class="info-label">Archived:</span>
|
||||
<span class="info-label">${t('detail.archived')}</span>
|
||||
<span class="info-value">${formatDate(session.archived_at)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="info-item">
|
||||
<span class="info-label">Project:</span>
|
||||
<span class="info-label">${t('detail.project')}</span>
|
||||
<span class="info-value">${escapeHtml(session.project || '-')}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Tasks:</span>
|
||||
<span class="info-value">${completed}/${tasks.length} completed</span>
|
||||
<span class="info-label">${t('detail.tasks')}</span>
|
||||
<span class="info-value">${completed}/${tasks.length} ${t('detail.completed')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,26 +82,26 @@ function showSessionDetailPage(sessionKey) {
|
||||
<div class="detail-tabs">
|
||||
<button class="detail-tab active" data-tab="tasks" onclick="switchDetailTab('tasks')">
|
||||
<span class="tab-icon"><i data-lucide="list-checks" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Tasks</span>
|
||||
<span class="tab-text">${t('tab.tasks')}</span>
|
||||
<span class="tab-count">${tasks.length}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="context" onclick="switchDetailTab('context')">
|
||||
<span class="tab-icon"><i data-lucide="package" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Context</span>
|
||||
<span class="tab-text">${t('tab.context')}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="summary" onclick="switchDetailTab('summary')">
|
||||
<span class="tab-icon"><i data-lucide="file-text" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Summary</span>
|
||||
<span class="tab-text">${t('tab.summary')}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="impl-plan" onclick="switchDetailTab('impl-plan')">
|
||||
<span class="tab-icon"><i data-lucide="ruler" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">IMPL Plan</span>
|
||||
<span class="tab-text">${t('tab.implPlan')}</span>
|
||||
</button>
|
||||
<button class="detail-tab" data-tab="conflict" onclick="switchDetailTab('conflict')"> <span class="tab-icon"><i data-lucide="scale" class="w-4 h-4"></i></span> <span class="tab-text">Conflict</span> </button>
|
||||
<button class="detail-tab" data-tab="conflict" onclick="switchDetailTab('conflict')"> <span class="tab-icon"><i data-lucide="scale" class="w-4 h-4"></i></span> <span class="tab-text">${t('tab.conflict')}</span> </button>
|
||||
${session.hasReview ? `
|
||||
<button class="detail-tab" data-tab="review" onclick="switchDetailTab('review')">
|
||||
<span class="tab-icon"><i data-lucide="search" class="w-4 h-4"></i></span>
|
||||
<span class="tab-text">Review</span>
|
||||
<span class="tab-text">${t('tab.review')}</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -178,33 +178,33 @@ function renderTasksTab(session, tasks, completed, inProgress, pending) {
|
||||
<!-- Combined Stats & Actions Bar -->
|
||||
<div class="task-toolbar">
|
||||
<div class="task-stats-bar">
|
||||
<span class="task-stat completed"><i data-lucide="check-circle" class="w-4 h-4 inline mr-1"></i>${completed} completed</span>
|
||||
<span class="task-stat in-progress"><i data-lucide="loader-2" class="w-4 h-4 inline mr-1"></i>${inProgress} in progress</span>
|
||||
<span class="task-stat pending"><i data-lucide="circle" class="w-4 h-4 inline mr-1"></i>${pending} pending</span>
|
||||
<span class="task-stat completed"><i data-lucide="check-circle" class="w-4 h-4 inline mr-1"></i>${completed} ${t('task.completed')}</span>
|
||||
<span class="task-stat in-progress"><i data-lucide="loader-2" class="w-4 h-4 inline mr-1"></i>${inProgress} ${t('task.inProgress')}</span>
|
||||
<span class="task-stat pending"><i data-lucide="circle" class="w-4 h-4 inline mr-1"></i>${pending} ${t('task.pending')}</span>
|
||||
</div>
|
||||
<div class="toolbar-divider"></div>
|
||||
<div class="task-bulk-actions">
|
||||
<span class="bulk-label">Quick Actions:</span>
|
||||
<button class="bulk-action-btn" onclick="bulkSetAllStatus('pending')" title="Set all tasks to pending">
|
||||
<span class="bulk-icon"><i data-lucide="circle" class="w-4 h-4"></i></span> All Pending
|
||||
<span class="bulk-label">${t('task.quickActions')}</span>
|
||||
<button class="bulk-action-btn" onclick="bulkSetAllStatus('pending')" title="${t('task.allPending')}">
|
||||
<span class="bulk-icon"><i data-lucide="circle" class="w-4 h-4"></i></span> ${t('task.allPending')}
|
||||
</button>
|
||||
<button class="bulk-action-btn" onclick="bulkSetAllStatus('in_progress')" title="Set all tasks to in progress">
|
||||
<span class="bulk-icon"><i data-lucide="loader-2" class="w-4 h-4"></i></span> All In Progress
|
||||
<button class="bulk-action-btn" onclick="bulkSetAllStatus('in_progress')" title="${t('task.allInProgress')}">
|
||||
<span class="bulk-icon"><i data-lucide="loader-2" class="w-4 h-4"></i></span> ${t('task.allInProgress')}
|
||||
</button>
|
||||
<button class="bulk-action-btn completed" onclick="bulkSetAllStatus('completed')" title="Set all tasks to completed">
|
||||
<span class="bulk-icon"><i data-lucide="check-circle" class="w-4 h-4"></i></span> All Completed
|
||||
<button class="bulk-action-btn completed" onclick="bulkSetAllStatus('completed')" title="${t('task.allCompleted')}">
|
||||
<span class="bulk-icon"><i data-lucide="check-circle" class="w-4 h-4"></i></span> ${t('task.allCompleted')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tasks-list" id="tasksListContent">
|
||||
${showLoading ? `
|
||||
<div class="tab-loading">Loading task details...</div>
|
||||
<div class="tab-loading">${t('common.loading')}</div>
|
||||
` : (tasks.length === 0 ? `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon"><i data-lucide="clipboard-list" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No Tasks</div>
|
||||
<div class="empty-text">This session has no tasks defined.</div>
|
||||
<div class="empty-title">${t('empty.noTasks')}</div>
|
||||
<div class="empty-text">${t('empty.noTasksText')}</div>
|
||||
</div>
|
||||
` : tasks.map(task => renderDetailTaskItem(task)).join(''))}
|
||||
</div>
|
||||
@@ -217,7 +217,7 @@ async function loadFullTaskDetails() {
|
||||
if (!session || !window.SERVER_MODE || !session.path) return;
|
||||
|
||||
const tasksContainer = document.getElementById('tasksListContent');
|
||||
tasksContainer.innerHTML = '<div class="tab-loading">Loading full task details...</div>';
|
||||
tasksContainer.innerHTML = `<div class="tab-loading">${t('common.loading')}</div>`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=tasks`);
|
||||
@@ -231,15 +231,15 @@ async function loadFullTaskDetails() {
|
||||
tasksContainer.innerHTML = `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon"><i data-lucide="clipboard-list" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No Task Files</div>
|
||||
<div class="empty-text">No IMPL-*.json files found in .task/</div>
|
||||
<div class="empty-title">${t('empty.noTaskFiles')}</div>
|
||||
<div class="empty-text">${t('empty.noTaskFilesText')}</div>
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
tasksContainer.innerHTML = `<div class="tab-error">Failed to load tasks: ${err.message}</div>`;
|
||||
tasksContainer.innerHTML = `<div class="tab-error">${t('context.loadError', { error: err.message })}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,9 +271,9 @@ function renderDetailTaskItem(task) {
|
||||
|
||||
function formatStatusLabel(status) {
|
||||
const labels = {
|
||||
'pending': '<i data-lucide="circle" class="w-3 h-3 inline mr-1"></i>Pending',
|
||||
'in_progress': '<i data-lucide="loader-2" class="w-3 h-3 inline mr-1"></i>In Progress',
|
||||
'completed': '<i data-lucide="check-circle" class="w-3 h-3 inline mr-1"></i>Completed'
|
||||
'pending': `<i data-lucide="circle" class="w-3 h-3 inline mr-1"></i>${t('task.status.pending')}`,
|
||||
'in_progress': `<i data-lucide="loader-2" class="w-3 h-3 inline mr-1"></i>${t('task.status.inProgress')}`,
|
||||
'completed': `<i data-lucide="check-circle" class="w-3 h-3 inline mr-1"></i>${t('task.status.completed')}`
|
||||
};
|
||||
return labels[status] || status;
|
||||
}
|
||||
@@ -562,7 +562,7 @@ function showRawSessionJson(sessionKey) {
|
||||
async function updateSingleTaskStatus(taskId, newStatus) {
|
||||
const session = sessionDataStore[currentSessionDetailKey];
|
||||
if (!session || !window.SERVER_MODE || !session.path) {
|
||||
showToast('Status update requires server mode', 'error');
|
||||
showToast(t('toast.statusUpdateRequires'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -582,14 +582,14 @@ async function updateSingleTaskStatus(taskId, newStatus) {
|
||||
// Update UI
|
||||
updateTaskItemUI(taskId, newStatus);
|
||||
updateTaskStatsBar();
|
||||
showToast(`Task ${taskId} status updated`, 'success');
|
||||
showToast(t('task.statusUpdated', { id: taskId }), 'success');
|
||||
} else {
|
||||
showToast(result.error || 'Failed to update status', 'error');
|
||||
showToast(result.error || t('toast.failedToUpdate'), 'error');
|
||||
// Revert select
|
||||
revertTaskSelect(taskId);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Error updating status: ' + error.message, 'error');
|
||||
showToast(t('toast.errorUpdating', { error: error.message }), 'error');
|
||||
revertTaskSelect(taskId);
|
||||
}
|
||||
}
|
||||
@@ -597,14 +597,15 @@ async function updateSingleTaskStatus(taskId, newStatus) {
|
||||
async function bulkSetAllStatus(newStatus) {
|
||||
const session = sessionDataStore[currentSessionDetailKey];
|
||||
if (!session || !window.SERVER_MODE || !session.path) {
|
||||
showToast('Bulk update requires server mode', 'error');
|
||||
showToast(t('toast.bulkUpdateRequires'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const taskIds = currentDrawerTasks.map(t => t.task_id || t.id);
|
||||
if (taskIds.length === 0) return;
|
||||
|
||||
if (!confirm(`Set all ${taskIds.length} tasks to "${formatStatusLabel(newStatus)}"?`)) {
|
||||
const statusLabel = t(`task.status.${newStatus === 'in_progress' ? 'inProgress' : newStatus}`);
|
||||
if (!confirm(t('task.setAllConfirm', { count: taskIds.length, status: statusLabel }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -624,12 +625,12 @@ async function bulkSetAllStatus(newStatus) {
|
||||
// Update all task UIs
|
||||
taskIds.forEach(id => updateTaskItemUI(id, newStatus));
|
||||
updateTaskStatsBar();
|
||||
showToast(`All ${taskIds.length} tasks updated`, 'success');
|
||||
showToast(t('task.tasksUpdated', { count: taskIds.length }), 'success');
|
||||
} else {
|
||||
showToast(result.error || 'Failed to bulk update', 'error');
|
||||
showToast(result.error || t('toast.failedToBulkUpdate'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Error in bulk update: ' + error.message, 'error');
|
||||
showToast(t('toast.errorInBulk', { error: error.message }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,7 +646,7 @@ async function bulkSetPendingToInProgress() {
|
||||
.map(t => t.task_id || t.id);
|
||||
|
||||
if (pendingTaskIds.length === 0) {
|
||||
showToast('No pending tasks to start', 'info');
|
||||
showToast(t('task.noPendingTasks'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -664,12 +665,12 @@ async function bulkSetPendingToInProgress() {
|
||||
if (result.success) {
|
||||
pendingTaskIds.forEach(id => updateTaskItemUI(id, 'in_progress'));
|
||||
updateTaskStatsBar();
|
||||
showToast(`${pendingTaskIds.length} tasks moved to In Progress`, 'success');
|
||||
showToast(t('task.movedToInProgress', { count: pendingTaskIds.length }), 'success');
|
||||
} else {
|
||||
showToast(result.error || 'Failed to update', 'error');
|
||||
showToast(result.error || t('toast.failedToUpdate'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Error: ' + error.message, 'error');
|
||||
showToast(t('toast.error', { error: error.message }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,7 +686,7 @@ async function bulkSetInProgressToCompleted() {
|
||||
.map(t => t.task_id || t.id);
|
||||
|
||||
if (inProgressTaskIds.length === 0) {
|
||||
showToast('No in-progress tasks to complete', 'info');
|
||||
showToast(t('task.noInProgressTasks'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -704,12 +705,12 @@ async function bulkSetInProgressToCompleted() {
|
||||
if (result.success) {
|
||||
inProgressTaskIds.forEach(id => updateTaskItemUI(id, 'completed'));
|
||||
updateTaskStatsBar();
|
||||
showToast(`${inProgressTaskIds.length} tasks completed`, 'success');
|
||||
showToast(t('task.tasksCompleted', { count: inProgressTaskIds.length }), 'success');
|
||||
} else {
|
||||
showToast(result.error || 'Failed to update', 'error');
|
||||
showToast(result.error || t('toast.failedToUpdate'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Error: ' + error.message, 'error');
|
||||
showToast(t('toast.error', { error: error.message }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,9 +744,9 @@ function updateTaskStatsBar() {
|
||||
const statsBar = document.querySelector('.task-stats-bar');
|
||||
if (statsBar) {
|
||||
statsBar.innerHTML = `
|
||||
<span class="task-stat completed"><i data-lucide="check-circle" class="w-4 h-4 inline mr-1"></i>${completed} completed</span>
|
||||
<span class="task-stat in-progress"><i data-lucide="loader-2" class="w-4 h-4 inline mr-1"></i>${inProgress} in progress</span>
|
||||
<span class="task-stat pending"><i data-lucide="circle" class="w-4 h-4 inline mr-1"></i>${pending} pending</span>
|
||||
<span class="task-stat completed"><i data-lucide="check-circle" class="w-4 h-4 inline mr-1"></i>${completed} ${t('task.completed')}</span>
|
||||
<span class="task-stat in-progress"><i data-lucide="loader-2" class="w-4 h-4 inline mr-1"></i>${inProgress} ${t('task.inProgress')}</span>
|
||||
<span class="task-stat pending"><i data-lucide="circle" class="w-4 h-4 inline mr-1"></i>${pending} ${t('task.pending')}</span>
|
||||
`;
|
||||
// Reinitialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
@@ -256,27 +256,27 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Path Selector -->
|
||||
<div class="flex items-center gap-2 relative">
|
||||
<label class="hidden sm:inline text-sm text-muted-foreground">Project:</label>
|
||||
<label class="hidden sm:inline text-sm text-muted-foreground" data-i18n="header.project">Project:</label>
|
||||
<div class="relative">
|
||||
<button class="flex items-center gap-2 px-3 py-1.5 bg-background border border-border rounded text-sm text-foreground hover:bg-hover max-w-[300px]" id="pathButton">
|
||||
<span class="truncate max-w-[240px]" id="currentPath">{{PROJECT_PATH}}</span>
|
||||
<span class="text-xs text-muted-foreground">▼</span>
|
||||
</button>
|
||||
<div class="path-menu hidden absolute top-full right-0 mt-1 bg-card border border-border rounded-lg shadow-lg min-w-[280px] z-50" id="pathMenu">
|
||||
<div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">Recent Projects</div>
|
||||
<div class="px-3 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide" data-i18n="header.recentProjects">Recent Projects</div>
|
||||
<div id="recentPaths" class="border-t border-border">
|
||||
<!-- Dynamic recent paths -->
|
||||
</div>
|
||||
<div class="p-2 border-t border-border">
|
||||
<button class="w-full flex items-center justify-center gap-2 px-3 py-2 bg-background border border-border rounded text-sm text-muted-foreground hover:bg-hover" id="browsePath">
|
||||
<i data-lucide="folder-open" class="w-4 h-4"></i>
|
||||
<span>Browse...</span>
|
||||
<span data-i18n="header.browse">Browse...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Refresh Button -->
|
||||
<button class="refresh-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded" id="refreshWorkspace" title="Refresh workspace">
|
||||
<button class="refresh-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded" id="refreshWorkspace" data-i18n-title="header.refreshWorkspace" title="Refresh workspace">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||||
<path d="M3 3v5h5"/>
|
||||
@@ -286,8 +286,13 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Language Toggle -->
|
||||
<button class="p-2 hover:bg-hover rounded flex items-center justify-center text-sm font-medium min-w-[40px]" id="langToggle" data-i18n-title="header.language" title="Language" onclick="switchLang(currentLang === 'zh' ? 'en' : 'zh')">
|
||||
EN
|
||||
</button>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<button class="p-2 hover:bg-hover rounded flex items-center justify-center" id="themeToggle" title="Toggle theme">
|
||||
<button class="p-2 hover:bg-hover rounded flex items-center justify-center" id="themeToggle" data-i18n-title="header.toggleTheme" title="Toggle theme">
|
||||
<i data-lucide="moon" class="w-5 h-5 theme-icon-dark"></i>
|
||||
<i data-lucide="sun" class="w-5 h-5 theme-icon-light hidden"></i>
|
||||
</button>
|
||||
@@ -306,25 +311,25 @@
|
||||
<div class="mb-2" id="projectOverviewNav">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<i data-lucide="layout-dashboard" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">Project</span>
|
||||
<span class="nav-section-title" data-i18n="nav.project">Project</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="project-overview" data-tooltip="Project Overview">
|
||||
<i data-lucide="bar-chart-3" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Overview</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.overview">Overview</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="explorer" data-tooltip="File Explorer">
|
||||
<i data-lucide="folder-tree" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Explorer</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.explorer">Explorer</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="cli-manager" data-tooltip="CLI Tools Status">
|
||||
<i data-lucide="terminal" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Status</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.status">Status</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeCliTools">0/3</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="cli-history" data-tooltip="CLI Execution History">
|
||||
<i data-lucide="history" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">History</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.history">History</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -333,22 +338,22 @@
|
||||
<div class="mb-2">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<i data-lucide="history" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">Sessions</span>
|
||||
<span class="nav-section-title" data-i18n="nav.sessions">Sessions</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors active" data-filter="all" data-tooltip="All Sessions">
|
||||
<i data-lucide="list" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">All</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.all">All</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeAll">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-filter="active" data-tooltip="Active Sessions">
|
||||
<i data-lucide="play-circle" class="nav-icon text-success"></i>
|
||||
<span class="nav-text flex-1">Active</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.active">Active</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-success-light text-success" id="badgeActive">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-filter="archived" data-tooltip="Archived Sessions">
|
||||
<i data-lucide="archive" class="nav-icon text-info"></i>
|
||||
<span class="nav-text flex-1">Archived</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.archived">Archived</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-info-light text-info" id="badgeArchived">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -358,17 +363,17 @@
|
||||
<div class="mb-2" id="liteTasksNav">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<i data-lucide="zap" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">Lite Tasks</span>
|
||||
<span class="nav-section-title" data-i18n="nav.liteTasks">Lite Tasks</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="lite-plan" data-tooltip="Lite Plan Sessions">
|
||||
<i data-lucide="file-edit" class="nav-icon text-indigo"></i>
|
||||
<span class="nav-text flex-1">Lite Plan</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.litePlan">Lite Plan</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-indigo-light text-indigo" id="badgeLitePlan">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="lite-fix" data-tooltip="Lite Fix Sessions">
|
||||
<i data-lucide="wrench" class="nav-icon text-orange"></i>
|
||||
<span class="nav-text flex-1">Lite Fix</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.liteFix">Lite Fix</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-orange-light text-orange" id="badgeLiteFix">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -378,12 +383,12 @@
|
||||
<div class="mb-2" id="mcpServersNav">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<i data-lucide="plug" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">MCP Servers</span>
|
||||
<span class="nav-section-title" data-i18n="nav.mcpServers">MCP Servers</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="mcp-manager" data-tooltip="MCP Server Management">
|
||||
<i data-lucide="settings" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Manage</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.manage">Manage</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeMcpServers">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -393,12 +398,12 @@
|
||||
<div class="mb-2" id="hooksNav">
|
||||
<div class="flex items-center px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
<i data-lucide="webhook" class="nav-section-icon mr-2"></i>
|
||||
<span class="nav-section-title">Hooks</span>
|
||||
<span class="nav-section-title" data-i18n="nav.hooks">Hooks</span>
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="hook-manager" data-tooltip="Hook Management">
|
||||
<i data-lucide="cable" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1">Manage</span>
|
||||
<span class="nav-text flex-1" data-i18n="nav.manage">Manage</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeHooks">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -410,7 +415,7 @@
|
||||
<div class="p-3 border-t border-border">
|
||||
<button class="flex items-center justify-center gap-2 w-full px-3 py-2 border border-border rounded text-sm text-muted-foreground hover:bg-hover transition-colors" id="sidebarToggle">
|
||||
<i data-lucide="panel-left-close" class="toggle-icon w-4 h-4 transition-transform duration-300"></i>
|
||||
<span class="toggle-text">Collapse</span>
|
||||
<span class="toggle-text" data-i18n="nav.collapse">Collapse</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -424,22 +429,22 @@
|
||||
<div class="bg-card border border-border rounded-lg p-4 text-center hover:shadow-md transition-all duration-200 min-w-[140px]">
|
||||
<div class="flex justify-center mb-1 text-primary"><i data-lucide="layers" class="w-5 h-5"></i></div>
|
||||
<div class="text-2xl font-bold text-foreground" id="statTotalSessions">0</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Total Sessions</div>
|
||||
<div class="text-xs text-muted-foreground mt-1" data-i18n="stats.totalSessions">Total Sessions</div>
|
||||
</div>
|
||||
<div class="bg-card border border-border rounded-lg p-4 text-center hover:shadow-md transition-all duration-200 min-w-[140px]">
|
||||
<div class="flex justify-center mb-1 text-success"><i data-lucide="activity" class="w-5 h-5"></i></div>
|
||||
<div class="text-2xl font-bold text-foreground" id="statActiveSessions">0</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Active Sessions</div>
|
||||
<div class="text-xs text-muted-foreground mt-1" data-i18n="stats.activeSessions">Active Sessions</div>
|
||||
</div>
|
||||
<div class="bg-card border border-border rounded-lg p-4 text-center hover:shadow-md transition-all duration-200 min-w-[140px]">
|
||||
<div class="flex justify-center mb-1 text-primary"><i data-lucide="clipboard-list" class="w-5 h-5"></i></div>
|
||||
<div class="text-2xl font-bold text-foreground" id="statTotalTasks">0</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Total Tasks</div>
|
||||
<div class="text-xs text-muted-foreground mt-1" data-i18n="stats.totalTasks">Total Tasks</div>
|
||||
</div>
|
||||
<div class="bg-card border border-border rounded-lg p-4 text-center hover:shadow-md transition-all duration-200 min-w-[140px]">
|
||||
<div class="flex justify-center mb-1 text-success"><i data-lucide="check-circle-2" class="w-5 h-5"></i></div>
|
||||
<div class="text-2xl font-bold text-foreground" id="statCompletedTasks">0</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">Completed Tasks</div>
|
||||
<div class="text-xs text-muted-foreground mt-1" data-i18n="stats.completedTasks">Completed Tasks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -451,7 +456,7 @@
|
||||
<div class="carousel-empty flex items-center justify-center h-full text-muted-foreground">
|
||||
<div class="text-center">
|
||||
<div class="flex justify-center mb-2"><i data-lucide="target" class="w-8 h-8"></i></div>
|
||||
<p class="text-sm">No active sessions</p>
|
||||
<p class="text-sm" data-i18n="carousel.noActiveSessions">No active sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -459,7 +464,7 @@
|
||||
<!-- Bottom: Dots Indicator & Controls -->
|
||||
<div class="carousel-footer flex items-center justify-center gap-3 py-2 border-t border-border bg-muted/20">
|
||||
<!-- Previous Button -->
|
||||
<button class="carousel-btn p-1 rounded hover:bg-hover text-muted-foreground hover:text-foreground" id="carouselPrev" title="Previous">
|
||||
<button class="carousel-btn p-1 rounded hover:bg-hover text-muted-foreground hover:text-foreground" id="carouselPrev" data-i18n-title="carousel.previous" title="Previous">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
|
||||
</button>
|
||||
|
||||
@@ -469,12 +474,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Next Button -->
|
||||
<button class="carousel-btn p-1 rounded hover:bg-hover text-muted-foreground hover:text-foreground" id="carouselNext" title="Next">
|
||||
<button class="carousel-btn p-1 rounded hover:bg-hover text-muted-foreground hover:text-foreground" id="carouselNext" data-i18n-title="carousel.next" title="Next">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- Pause Button -->
|
||||
<button class="carousel-btn p-1 rounded hover:bg-hover text-muted-foreground hover:text-foreground ml-1" id="carouselPause" title="Pause auto-play">
|
||||
<button class="carousel-btn p-1 rounded hover:bg-hover text-muted-foreground hover:text-foreground ml-1" id="carouselPause" data-i18n-title="carousel.pause" title="Pause auto-play">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" id="carouselPauseIcon"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -483,10 +488,10 @@
|
||||
|
||||
<!-- Content Header -->
|
||||
<div class="flex items-center justify-between flex-wrap gap-3 mb-5">
|
||||
<h2 class="text-xl font-semibold text-foreground" id="contentTitle">All Sessions</h2>
|
||||
<h2 class="text-xl font-semibold text-foreground" id="contentTitle" data-i18n="title.allSessions">All Sessions</h2>
|
||||
<div class="relative">
|
||||
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground"></i>
|
||||
<input type="text" placeholder="Search..." id="searchInput"
|
||||
<input type="text" data-i18n-placeholder="search.placeholder" placeholder="Search..." id="searchInput"
|
||||
class="pl-9 pr-4 py-2 w-60 border border-border rounded-lg bg-background text-foreground text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</div>
|
||||
</div>
|
||||
@@ -500,14 +505,14 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="flex items-center justify-between px-5 h-10 bg-card border-t border-border text-xs text-muted-foreground">
|
||||
<div>Generated: <span id="generatedAt">-</span></div>
|
||||
<div>CCW Dashboard v1.0</div>
|
||||
<div><span data-i18n="footer.generated">Generated:</span> <span id="generatedAt">-</span></div>
|
||||
<div data-i18n="footer.version">CCW Dashboard v1.0</div>
|
||||
</footer>
|
||||
|
||||
<!-- Task Detail Drawer -->
|
||||
<div class="task-detail-drawer fixed top-0 right-0 w-1/2 max-w-full h-full bg-card border-l border-border shadow-lg z-50 flex flex-col" id="taskDetailDrawer">
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground" id="drawerTaskTitle">Task Details</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground" id="drawerTaskTitle" data-i18n="tab.tasks">Task Details</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeTaskDrawer()">×</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-5" id="drawerContent">
|
||||
@@ -522,11 +527,11 @@
|
||||
<div class="markdown-modal-backdrop absolute inset-0 bg-black/60" onclick="closeMarkdownModal()"></div>
|
||||
<div class="markdown-modal-content relative bg-card border border-border rounded-lg shadow-2xl w-[90vw] max-w-4xl h-[85vh] flex flex-col">
|
||||
<div class="markdown-modal-header flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground" id="markdownModalTitle">Content Preview</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground" id="markdownModalTitle" data-i18n="modal.contentPreview">Content Preview</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex bg-muted rounded-lg p-0.5">
|
||||
<button id="mdTabRaw" class="md-tab-btn px-3 py-1 text-sm rounded-md transition-colors" onclick="switchMarkdownTab('raw')">Raw</button>
|
||||
<button id="mdTabPreview" class="md-tab-btn px-3 py-1 text-sm rounded-md transition-colors active" onclick="switchMarkdownTab('preview')">Preview</button>
|
||||
<button id="mdTabRaw" class="md-tab-btn px-3 py-1 text-sm rounded-md transition-colors" onclick="switchMarkdownTab('raw')" data-i18n="modal.raw">Raw</button>
|
||||
<button id="mdTabPreview" class="md-tab-btn px-3 py-1 text-sm rounded-md transition-colors active" onclick="switchMarkdownTab('preview')" data-i18n="modal.preview">Preview</button>
|
||||
</div>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeMarkdownModal()">×</button>
|
||||
</div>
|
||||
@@ -544,10 +549,10 @@
|
||||
<div class="mcp-modal-content relative bg-card border border-border rounded-lg shadow-2xl w-[90vw] max-w-lg flex flex-col">
|
||||
<div class="mcp-modal-header flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">Create MCP Server</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground" data-i18n="mcp.createTitle">Create MCP Server</h3>
|
||||
<div class="flex bg-muted rounded-lg p-0.5">
|
||||
<button id="mcpTabForm" class="mcp-tab-btn px-3 py-1 text-sm rounded-md transition-colors active" onclick="switchMcpCreateTab('form')">Form</button>
|
||||
<button id="mcpTabJson" class="mcp-tab-btn px-3 py-1 text-sm rounded-md transition-colors" onclick="switchMcpCreateTab('json')">JSON</button>
|
||||
<button id="mcpTabForm" class="mcp-tab-btn px-3 py-1 text-sm rounded-md transition-colors active" onclick="switchMcpCreateTab('form')" data-i18n="mcp.form">Form</button>
|
||||
<button id="mcpTabJson" class="mcp-tab-btn px-3 py-1 text-sm rounded-md transition-colors" onclick="switchMcpCreateTab('json')" data-i18n="mcp.json">JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeMcpCreateModal()">×</button>
|
||||
@@ -555,13 +560,13 @@
|
||||
<!-- Form Mode -->
|
||||
<div id="mcpFormMode" class="mcp-modal-body p-4 space-y-4">
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-foreground mb-1">Server Name <span class="text-destructive">*</span></label>
|
||||
<input type="text" id="mcpServerName" placeholder="e.g., my-mcp-server"
|
||||
<label class="block text-sm font-medium text-foreground mb-1"><span data-i18n="mcp.serverName">Server Name</span> <span class="text-destructive">*</span></label>
|
||||
<input type="text" id="mcpServerName" data-i18n-placeholder="mcp.serverNamePlaceholder" placeholder="e.g., my-mcp-server"
|
||||
class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-foreground mb-1">Command <span class="text-destructive">*</span></label>
|
||||
<input type="text" id="mcpServerCommand" placeholder="e.g., npx, uvx, node, python"
|
||||
<label class="block text-sm font-medium text-foreground mb-1"><span data-i18n="mcp.command">Command</span> <span class="text-destructive">*</span></label>
|
||||
<input type="text" id="mcpServerCommand" data-i18n-placeholder="mcp.commandPlaceholder" placeholder="e.g., npx, uvx, node, python"
|
||||
class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -570,7 +575,7 @@
|
||||
class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm font-mono focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 resize-none"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-foreground mb-1">Environment Variables (KEY=VALUE per line)</label>
|
||||
<label class="block text-sm font-medium text-foreground mb-1" data-i18n="mcp.envVars">Environment Variables (KEY=VALUE per line)</label>
|
||||
<textarea id="mcpServerEnv" placeholder="e.g., API_KEY=your-api-key DEBUG=true" rows="3"
|
||||
class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm font-mono focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 resize-none"></textarea>
|
||||
</div>
|
||||
@@ -578,7 +583,7 @@
|
||||
<!-- JSON Mode -->
|
||||
<div id="mcpJsonMode" class="mcp-modal-body p-4 space-y-4 hidden">
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-foreground mb-1">Paste MCP Server JSON Configuration</label>
|
||||
<label class="block text-sm font-medium text-foreground mb-1" data-i18n="mcp.pasteJson">Paste MCP Server JSON Configuration</label>
|
||||
<textarea id="mcpServerJson" placeholder='{
|
||||
"servers": {
|
||||
"my-server": {
|
||||
@@ -599,8 +604,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mcp-modal-footer flex justify-end gap-2 px-4 py-3 border-t border-border">
|
||||
<button class="px-4 py-2 text-sm bg-muted text-foreground rounded-lg hover:bg-hover transition-colors" onclick="closeMcpCreateModal()">Cancel</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity" onclick="submitMcpCreate()">Create</button>
|
||||
<button class="px-4 py-2 text-sm bg-muted text-foreground rounded-lg hover:bg-hover transition-colors" onclick="closeMcpCreateModal()" data-i18n="common.cancel">Cancel</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity" onclick="submitMcpCreate()" data-i18n="common.create">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -610,18 +615,18 @@
|
||||
<div class="hook-modal-backdrop absolute inset-0 bg-black/60" onclick="closeHookCreateModal()"></div>
|
||||
<div class="hook-modal-content relative bg-card border border-border rounded-lg shadow-2xl w-[90vw] max-w-lg flex flex-col max-h-[90vh]">
|
||||
<div class="hook-modal-header flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground" id="hookModalTitle">Create Hook</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground" id="hookModalTitle" data-i18n="hook.createTitle">Create Hook</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded" onclick="closeHookCreateModal()">×</button>
|
||||
</div>
|
||||
<div class="hook-modal-body p-4 space-y-4 overflow-y-auto">
|
||||
<div class="form-group">
|
||||
<label class="block text-sm font-medium text-foreground mb-1">Hook Event <span class="text-destructive">*</span></label>
|
||||
<label class="block text-sm font-medium text-foreground mb-1"><span data-i18n="hook.event">Hook Event</span> <span class="text-destructive">*</span></label>
|
||||
<select id="hookEvent" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20">
|
||||
<option value="">Select an event...</option>
|
||||
<option value="PreToolUse">PreToolUse - Before a tool is executed</option>
|
||||
<option value="PostToolUse">PostToolUse - After a tool completes</option>
|
||||
<option value="Notification">Notification - On notifications</option>
|
||||
<option value="Stop">Stop - When agent stops</option>
|
||||
<option value="" data-i18n="hook.selectEvent">Select an event...</option>
|
||||
<option value="PreToolUse" data-i18n="hook.preToolUse">PreToolUse - Before a tool is executed</option>
|
||||
<option value="PostToolUse" data-i18n="hook.postToolUse">PostToolUse - After a tool completes</option>
|
||||
<option value="Notification" data-i18n="hook.notification">Notification - On notifications</option>
|
||||
<option value="Stop" data-i18n="hook.stop">Stop - When agent stops</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
||||
Reference in New Issue
Block a user