mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat(cli): 添加 --rule 选项支持模板自动发现
重构 ccw cli 模板系统: - 新增 template-discovery.ts 模块,支持扁平化模板自动发现 - 添加 --rule <template> 选项,自动加载 protocol 和 template - 模板目录从嵌套结构 (prompts/category/file.txt) 迁移到扁平结构 (prompts/category-function.txt) - 更新所有 agent/command 文件,使用 $PROTO $TMPL 环境变量替代 $(cat ...) 模式 - 支持模糊匹配:--rule 02-review-architecture 可匹配 analysis-review-architecture.txt 其他更新: - Dashboard: 添加 Claude Manager 和 Issue Manager 页面 - Codex-lens: 增强 chain_search 和 clustering 模块 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -906,3 +906,182 @@
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* Batch Delete Modal
|
||||
* ======================================== */
|
||||
.batch-delete-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.warning-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
border: 1px solid hsl(var(--destructive) / 0.3);
|
||||
border-radius: 0.5rem;
|
||||
color: hsl(var(--destructive));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.warning-banner i {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.delete-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.file-list-container h4 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: hsl(var(--muted) / 0.2);
|
||||
}
|
||||
|
||||
.delete-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.delete-file-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.delete-file-item:hover {
|
||||
background: hsl(var(--hover));
|
||||
}
|
||||
|
||||
.delete-file-item i {
|
||||
flex-shrink: 0;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-info .file-name {
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-info .file-path {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-family: 'Courier New', monospace;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.level-badge.project {
|
||||
background: hsl(142, 76%, 36%, 0.15);
|
||||
color: hsl(142, 76%, 36%);
|
||||
border: 1px solid hsl(142, 76%, 36%, 0.3);
|
||||
}
|
||||
|
||||
.level-badge.module {
|
||||
background: hsl(221, 83%, 53%, 0.15);
|
||||
color: hsl(221, 83%, 53%);
|
||||
border: 1px solid hsl(221, 83%, 53%, 0.3);
|
||||
}
|
||||
|
||||
.confirmation-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Remove file button in batch delete list */
|
||||
.remove-file-btn {
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.delete-file-item:hover .remove-file-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.remove-file-btn:hover {
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
/* Empty list message */
|
||||
.empty-list-message {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -3300,3 +3300,93 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
SPLIT QUEUE MODAL STYLES
|
||||
========================================== */
|
||||
|
||||
.split-queue-modal-content {
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.split-queue-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.split-queue-issues {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.split-queue-issue-group {
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.split-queue-issue-group:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.split-queue-issue-header {
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid hsl(var(--border) / 0.5);
|
||||
}
|
||||
|
||||
.split-queue-issue-header label {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.split-queue-issue-header input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.split-queue-solutions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.split-queue-solutions label {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.split-queue-solutions label:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
.split-queue-solutions input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Checkbox styles */
|
||||
.split-queue-modal-content input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.split-queue-modal-content input[type="checkbox"]:hover {
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.split-queue-modal-content input[type="checkbox"]:checked {
|
||||
background-color: hsl(var(--primary));
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
@@ -1704,6 +1704,19 @@ const i18n = {
|
||||
'claude.deleteFile': 'Delete File',
|
||||
'claude.deleteConfirm': 'Are you sure you want to delete {file}?',
|
||||
'claude.deleteWarning': 'This action cannot be undone.',
|
||||
'claude.batchDeleteProject': 'Delete Project Files',
|
||||
'claude.batchDeleteTitle': 'Delete Project Workspace Files',
|
||||
'claude.batchDeleteWarning': 'This will delete all CLAUDE.md files in the project workspace (excluding user-level files)',
|
||||
'claude.noProjectFiles': 'No project workspace files to delete',
|
||||
'claude.filesToDelete': 'Files to delete:',
|
||||
'claude.totalSize': 'Total size:',
|
||||
'claude.fileList': 'File List',
|
||||
'claude.confirmDelete': 'Confirm Delete',
|
||||
'claude.deletingFiles': 'Deleting {count} files...',
|
||||
'claude.batchDeleteSuccess': 'Successfully deleted {deleted} of {total} files',
|
||||
'claude.batchDeleteError': 'Failed to delete files',
|
||||
'claude.removeFromList': 'Remove from list',
|
||||
'claude.noFilesInList': 'No files in the list',
|
||||
'claude.copyContent': 'Copy Content',
|
||||
'claude.contentCopied': 'Content copied to clipboard',
|
||||
'claude.copyError': 'Failed to copy content',
|
||||
@@ -4013,6 +4026,19 @@ const i18n = {
|
||||
'claude.deleteFile': '删除文件',
|
||||
'claude.deleteConfirm': '确定要删除 {file} 吗?',
|
||||
'claude.deleteWarning': '此操作无法撤销。',
|
||||
'claude.batchDeleteProject': '删除项目文件',
|
||||
'claude.batchDeleteTitle': '删除项目工作空间文件',
|
||||
'claude.batchDeleteWarning': '此操作将删除项目工作空间内的所有 CLAUDE.md 文件(不包括用户级文件)',
|
||||
'claude.noProjectFiles': '没有可删除的项目工作空间文件',
|
||||
'claude.filesToDelete': '待删除文件数:',
|
||||
'claude.totalSize': '总大小:',
|
||||
'claude.fileList': '文件清单',
|
||||
'claude.confirmDelete': '确认删除',
|
||||
'claude.deletingFiles': '正在删除 {count} 个文件...',
|
||||
'claude.batchDeleteSuccess': '成功删除 {deleted}/{total} 个文件',
|
||||
'claude.batchDeleteError': '删除文件失败',
|
||||
'claude.removeFromList': '从清单中移除',
|
||||
'claude.noFilesInList': '清单中没有文件',
|
||||
'claude.copyContent': '复制内容',
|
||||
'claude.contentCopied': '内容已复制到剪贴板',
|
||||
'claude.copyError': '复制内容失败',
|
||||
|
||||
@@ -24,6 +24,7 @@ var searchQuery = '';
|
||||
var freshnessData = {}; // { [filePath]: FreshnessResult }
|
||||
var freshnessSummary = null;
|
||||
var searchKeyboardHandlerAdded = false;
|
||||
var pendingDeleteFiles = []; // Files pending for batch delete
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderClaudeManager() {
|
||||
@@ -64,6 +65,9 @@ async function renderClaudeManager() {
|
||||
'<button class="btn btn-sm btn-secondary" onclick="refreshClaudeFiles()">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> ' + t('common.refresh') +
|
||||
'</button>' +
|
||||
'<button class="btn btn-sm btn-danger" onclick="showBatchDeleteDialog()">' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4"></i> ' + t('claude.batchDeleteProject') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="claude-manager-columns">' +
|
||||
@@ -959,3 +963,167 @@ window.initClaudeManager = function() {
|
||||
|
||||
// Make destroyClaudeManager accessible globally as well
|
||||
window.destroyClaudeManager = destroyClaudeManager;
|
||||
|
||||
// ========== Batch Delete Functions ==========
|
||||
/**
|
||||
* Show batch delete confirmation dialog for project workspace files
|
||||
*/
|
||||
function showBatchDeleteDialog() {
|
||||
// Get project workspace files (project + modules, exclude user)
|
||||
var projectFiles = [];
|
||||
|
||||
if (claudeFilesData.project.main) {
|
||||
projectFiles.push(claudeFilesData.project.main);
|
||||
}
|
||||
|
||||
projectFiles.push(...claudeFilesData.modules);
|
||||
|
||||
if (projectFiles.length === 0) {
|
||||
showRefreshToast(t('claude.noProjectFiles') || 'No project workspace files to delete', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize pending delete files list
|
||||
pendingDeleteFiles = [...projectFiles];
|
||||
|
||||
// Render the modal with current pending files
|
||||
renderBatchDeleteModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render or re-render the batch delete modal content
|
||||
*/
|
||||
function renderBatchDeleteModal() {
|
||||
// Build file list HTML with remove buttons
|
||||
var fileListHTML = pendingDeleteFiles.map(function(file, index) {
|
||||
var levelBadge = file.level === 'project'
|
||||
? '<span class="level-badge project">' + t('claudeManager.projectLevel') + '</span>'
|
||||
: '<span class="level-badge module">' + t('claudeManager.moduleLevel') + '</span>';
|
||||
|
||||
return '<div class="delete-file-item" data-file-index="' + index + '">' +
|
||||
'<i data-lucide="file-text" class="w-4 h-4"></i>' +
|
||||
'<div class="file-info">' +
|
||||
'<span class="file-name">' + escapeHtml(file.name) + '</span>' +
|
||||
'<span class="file-path">' + escapeHtml(file.relativePath) + '</span>' +
|
||||
'</div>' +
|
||||
levelBadge +
|
||||
'<button class="btn btn-sm btn-ghost remove-file-btn" onclick="removeFromDeleteList(' + index + ')" title="' + (t('claude.removeFromList') || 'Remove from list') + '">' +
|
||||
'<i data-lucide="x" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
var totalSize = pendingDeleteFiles.reduce(function(sum, f) { return sum + f.size; }, 0);
|
||||
|
||||
var modalContent = '<div class="batch-delete-modal">' +
|
||||
'<div class="warning-banner">' +
|
||||
'<i data-lucide="alert-triangle" class="w-5 h-5"></i>' +
|
||||
'<span>' + (t('claude.batchDeleteWarning') || 'This will delete all CLAUDE.md files in the project workspace') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="delete-summary" id="delete-summary">' +
|
||||
'<div class="summary-item">' +
|
||||
'<span class="summary-label">' + t('claude.filesToDelete') + '</span>' +
|
||||
'<span class="summary-value" id="files-to-delete-count">' + pendingDeleteFiles.length + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="summary-item">' +
|
||||
'<span class="summary-label">' + t('claude.totalSize') + '</span>' +
|
||||
'<span class="summary-value" id="total-size-value">' + formatFileSize(totalSize) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="file-list-container">' +
|
||||
'<h4>' + t('claude.fileList') + '</h4>' +
|
||||
'<div class="file-list" id="pending-file-list">' + (fileListHTML || '<div class="empty-list-message">' + (t('claude.noFilesInList') || 'No files in the list') + '</div>') + '</div>' +
|
||||
'</div>' +
|
||||
'<div class="confirmation-actions">' +
|
||||
'<button class="btn btn-secondary" onclick="closeModal()">' +
|
||||
'<i data-lucide="x" class="w-4 h-4"></i> ' + t('common.cancel') +
|
||||
'</button>' +
|
||||
'<button class="btn btn-danger" onclick="confirmBatchDeleteProject()"' + (pendingDeleteFiles.length === 0 ? ' disabled' : '') + '>' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4"></i> ' + t('claude.confirmDelete') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
showModal(
|
||||
t('claude.batchDeleteTitle') || 'Delete Project Workspace Files',
|
||||
modalContent,
|
||||
{ size: 'large' }
|
||||
);
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file from the pending delete list
|
||||
*/
|
||||
function removeFromDeleteList(index) {
|
||||
if (index >= 0 && index < pendingDeleteFiles.length) {
|
||||
pendingDeleteFiles.splice(index, 1);
|
||||
renderBatchDeleteModal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute batch delete for project workspace files
|
||||
*/
|
||||
async function confirmBatchDeleteProject() {
|
||||
// Collect file paths from pending delete list
|
||||
var filePaths = pendingDeleteFiles.map(function(file) {
|
||||
return file.path;
|
||||
});
|
||||
|
||||
if (filePaths.length === 0) return;
|
||||
|
||||
closeModal();
|
||||
|
||||
// Show progress
|
||||
showRefreshToast(
|
||||
(t('claude.deletingFiles') || 'Deleting {count} files...').replace('{count}', filePaths.length),
|
||||
'info'
|
||||
);
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/batch-delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
paths: filePaths,
|
||||
confirm: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Batch delete failed');
|
||||
|
||||
var result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
var message = (t('claude.batchDeleteSuccess') || 'Successfully deleted {deleted} of {total} files')
|
||||
.replace('{deleted}', result.deleted)
|
||||
.replace('{total}', result.total);
|
||||
|
||||
showRefreshToast(message, 'success');
|
||||
addGlobalNotification('success', message, null, 'CLAUDE.md');
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
console.warn('Some files failed to delete:', result.errors);
|
||||
}
|
||||
|
||||
// Clear selection if deleted file was selected
|
||||
if (selectedFile && filePaths.includes(selectedFile.path)) {
|
||||
selectedFile = null;
|
||||
}
|
||||
|
||||
// Refresh file tree
|
||||
await refreshClaudeFiles();
|
||||
} else {
|
||||
throw new Error(result.error || 'Unknown error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in batch delete:', error);
|
||||
showRefreshToast(
|
||||
t('claude.batchDeleteError') || 'Failed to delete files',
|
||||
'error'
|
||||
);
|
||||
addGlobalNotification('error', t('claude.batchDeleteError') || 'Failed to delete files', null, 'CLAUDE.md');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,6 +562,11 @@ function renderQueueCard(queue, isActive) {
|
||||
<i data-lucide="git-merge" class="w-3 h-3"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
${queue.status !== 'merged' && issueCount > 1 ? `
|
||||
<button class="btn-sm" onclick="showSplitQueueModal('${safeQueueId}')" title="Split queue into multiple queues">
|
||||
<i data-lucide="git-branch" class="w-3 h-3"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
<button class="btn-sm btn-danger" onclick="confirmDeleteQueue('${safeQueueId}')" title="${t('issues.deleteQueue') || 'Delete queue'}">
|
||||
<i data-lucide="trash-2" class="w-3 h-3"></i>
|
||||
</button>
|
||||
@@ -989,6 +994,188 @@ async function executeQueueMerge(sourceQueueId) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Queue Split Modal ==========
|
||||
async function showSplitQueueModal(queueId) {
|
||||
let modal = document.getElementById('splitQueueModal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'splitQueueModal';
|
||||
modal.className = 'issue-modal';
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Fetch queue details
|
||||
let queue;
|
||||
try {
|
||||
const response = await fetch('/api/queue/' + encodeURIComponent(queueId) + '?path=' + encodeURIComponent(projectPath));
|
||||
queue = await response.json();
|
||||
if (queue.error) throw new Error(queue.error);
|
||||
} catch (err) {
|
||||
showNotification('Failed to load queue details', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const safeQueueId = escapeHtml(queueId || '');
|
||||
const items = queue.solutions || queue.tasks || [];
|
||||
const isSolutionLevel = !!queue.solutions;
|
||||
|
||||
// Group items by issue
|
||||
const issueGroups = {};
|
||||
items.forEach(item => {
|
||||
const issueId = item.issue_id || 'unknown';
|
||||
if (!issueGroups[issueId]) {
|
||||
issueGroups[issueId] = [];
|
||||
}
|
||||
issueGroups[issueId].push(item);
|
||||
});
|
||||
|
||||
const issueIds = Object.keys(issueGroups);
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="issue-modal-content split-queue-modal-content">
|
||||
<div class="issue-modal-header">
|
||||
<h3><i data-lucide="git-branch" class="w-5 h-5"></i> Split Queue: ${safeQueueId}</h3>
|
||||
<button class="issue-modal-close" onclick="hideSplitQueueModal()">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="issue-modal-body">
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
Select issues and their solutions to split into a new queue. The remaining items will stay in the current queue.
|
||||
</p>
|
||||
|
||||
${issueIds.length === 0 ? `
|
||||
<p class="text-center text-muted-foreground py-4">No items to split</p>
|
||||
` : `
|
||||
<div class="split-queue-controls mb-3">
|
||||
<button class="btn-sm btn-secondary" onclick="selectAllIssues()">
|
||||
<i data-lucide="check-square" class="w-3 h-3"></i> Select All
|
||||
</button>
|
||||
<button class="btn-sm btn-secondary" onclick="deselectAllIssues()">
|
||||
<i data-lucide="square" class="w-3 h-3"></i> Deselect All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="split-queue-issues">
|
||||
${issueIds.map(issueId => {
|
||||
const issueItems = issueGroups[issueId];
|
||||
const safeIssueId = escapeHtml(issueId);
|
||||
return `
|
||||
<div class="split-queue-issue-group" data-issue-id="${safeIssueId}">
|
||||
<div class="split-queue-issue-header">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox"
|
||||
class="issue-checkbox"
|
||||
data-issue-id="${safeIssueId}"
|
||||
onchange="toggleIssueSelection('${safeIssueId}')">
|
||||
<span class="font-medium">${safeIssueId}</span>
|
||||
<span class="text-xs text-muted-foreground">(${issueItems.length} ${isSolutionLevel ? 'solution' : 'task'}${issueItems.length > 1 ? 's' : ''})</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="split-queue-solutions ml-6">
|
||||
${issueItems.map(item => {
|
||||
const itemId = item.item_id || item.solution_id || item.task_id || '';
|
||||
const safeItemId = escapeHtml(itemId);
|
||||
const displayName = isSolutionLevel
|
||||
? (item.solution_id || itemId)
|
||||
: (item.task_id || itemId);
|
||||
return `
|
||||
<label class="flex items-center gap-2 py-1">
|
||||
<input type="checkbox"
|
||||
class="solution-checkbox"
|
||||
data-issue-id="${safeIssueId}"
|
||||
data-item-id="${safeItemId}"
|
||||
value="${safeItemId}">
|
||||
<span class="text-sm font-mono">${escapeHtml(displayName)}</span>
|
||||
${item.task_count ? `<span class="text-xs text-muted-foreground">(${item.task_count} tasks)</span>` : ''}
|
||||
</label>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div class="issue-modal-footer">
|
||||
<button class="btn-secondary" onclick="hideSplitQueueModal()">Cancel</button>
|
||||
${issueIds.length > 0 ? `
|
||||
<button class="btn-primary" onclick="executeQueueSplit('${safeQueueId}')">
|
||||
<i data-lucide="git-branch" class="w-4 h-4"></i>
|
||||
<span>Split Queue</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function hideSplitQueueModal() {
|
||||
const modal = document.getElementById('splitQueueModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleIssueSelection(issueId) {
|
||||
const issueCheckbox = document.querySelector(`.issue-checkbox[data-issue-id="${issueId}"]`);
|
||||
const solutionCheckboxes = document.querySelectorAll(`.solution-checkbox[data-issue-id="${issueId}"]`);
|
||||
|
||||
if (issueCheckbox && solutionCheckboxes) {
|
||||
solutionCheckboxes.forEach(cb => {
|
||||
cb.checked = issueCheckbox.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function selectAllIssues() {
|
||||
const allCheckboxes = document.querySelectorAll('.split-queue-modal-content input[type="checkbox"]');
|
||||
allCheckboxes.forEach(cb => cb.checked = true);
|
||||
}
|
||||
|
||||
function deselectAllIssues() {
|
||||
const allCheckboxes = document.querySelectorAll('.split-queue-modal-content input[type="checkbox"]');
|
||||
allCheckboxes.forEach(cb => cb.checked = false);
|
||||
}
|
||||
|
||||
async function executeQueueSplit(sourceQueueId) {
|
||||
const selectedCheckboxes = document.querySelectorAll('.solution-checkbox:checked');
|
||||
const selectedItemIds = Array.from(selectedCheckboxes).map(cb => cb.value);
|
||||
|
||||
if (selectedItemIds.length === 0) {
|
||||
showNotification('Please select at least one item to split', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/queue/split?path=' + encodeURIComponent(projectPath), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sourceQueueId, itemIds: selectedItemIds })
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showNotification(`Split ${result.splitItemCount} items into new queue ${result.newQueueId}`, 'success');
|
||||
hideSplitQueueModal();
|
||||
queueData.expandedQueueId = null;
|
||||
await Promise.all([loadQueueData(), loadAllQueues()]);
|
||||
renderIssueView();
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to split queue', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to split queue:', err);
|
||||
showNotification('Failed to split queue', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Legacy Queue Render (for backward compatibility) ==========
|
||||
function renderLegacyQueueSection() {
|
||||
const queue = issueData.queue;
|
||||
|
||||
@@ -1024,7 +1024,9 @@ async function loadAndRenderMultiCliSummaryTab(session, contentArea) {
|
||||
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
contentArea.innerHTML = renderMultiCliSummaryContent(data.summary, session);
|
||||
// Support both summaries (from .summaries/) and summary (from plan.json)
|
||||
const summaryText = data.summary || (data.summaries?.length ? data.summaries[0].content : null);
|
||||
contentArea.innerHTML = renderMultiCliSummaryContent(summaryText, session);
|
||||
initCollapsibleSections(contentArea);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
@@ -3135,16 +3137,38 @@ async function loadAndRenderLiteSummaryTab(session, contentArea) {
|
||||
const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
contentArea.innerHTML = renderSummaryContent(data.summaries);
|
||||
return;
|
||||
// Prioritize .summaries/ directory content
|
||||
if (data.summaries?.length) {
|
||||
contentArea.innerHTML = renderSummaryContent(data.summaries);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
// Fallback to plan.json summary field
|
||||
if (data.summary) {
|
||||
contentArea.innerHTML = renderSummaryContent([{ name: 'Summary', content: data.summary }]);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
|
||||
// Fallback: try to get summary from session object (plan.summary or synthesis.convergence.summary)
|
||||
const plan = session.plan || {};
|
||||
const synthesis = session.latestSynthesis || session.discussionTopic || {};
|
||||
const summaryText = plan.summary || synthesis.convergence?.summary;
|
||||
|
||||
if (summaryText) {
|
||||
contentArea.innerHTML = renderSummaryContent([{ name: 'Summary', content: summaryText }]);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
// No summary available
|
||||
contentArea.innerHTML = `
|
||||
<div class="tab-empty-state">
|
||||
<div class="empty-icon"><i data-lucide="file-text" class="w-12 h-12"></i></div>
|
||||
<div class="empty-title">No Summaries</div>
|
||||
<div class="empty-text">No summaries found in .summaries/</div>
|
||||
<div class="empty-title">${t('empty.noSummary') || 'No Summary'}</div>
|
||||
<div class="empty-text">${t('empty.noSummaryText') || 'No summary available for this session.'}</div>
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
Reference in New Issue
Block a user