feat: Enhance issue management to support solution-level queues

- Added support for solution-level queues in the issue management system.
- Updated interfaces to include solution-specific properties such as `approach`, `task_count`, and `files_touched`.
- Modified queue handling to differentiate between task-level and solution-level items.
- Adjusted rendering logic in the dashboard to display solutions and their associated tasks correctly.
- Enhanced queue statistics and conflict resolution to accommodate the new solution structure.
- Updated actions (next, done, retry) to handle both tasks and solutions seamlessly.
This commit is contained in:
catlog22
2025-12-28 13:21:34 +08:00
parent 4c6b28030f
commit 2eaefb61ab
6 changed files with 828 additions and 491 deletions

View File

@@ -102,6 +102,7 @@ interface SolutionTask {
interface Solution {
id: string;
description?: string;
approach?: string; // Solution approach description
tasks: SolutionTask[];
exploration_context?: Record<string, any>;
analysis?: { risk?: string; impact?: string; complexity?: string };
@@ -112,10 +113,10 @@ interface Solution {
}
interface QueueItem {
item_id: string; // Task item ID in queue: T-1, T-2, ... (formerly queue_id)
item_id: string; // Item ID in queue: T-1, T-2, ... (task-level) or S-1, S-2, ... (solution-level)
issue_id: string;
solution_id: string;
task_id: string;
task_id?: string; // Only for task-level queues
title?: string;
status: 'pending' | 'ready' | 'executing' | 'completed' | 'failed' | 'blocked';
execution_order: number;
@@ -123,6 +124,8 @@ interface QueueItem {
depends_on: string[];
semantic_priority: number;
assigned_executor: 'codex' | 'gemini' | 'agent';
task_count?: number; // For solution-level queues
files_touched?: string[]; // For solution-level queues
started_at?: string;
completed_at?: string;
result?: Record<string, any>;
@@ -142,8 +145,10 @@ interface QueueConflict {
interface ExecutionGroup {
id: string; // Group ID: P1, S1, etc.
type: 'parallel' | 'sequential';
task_count: number;
tasks: string[]; // Item IDs in this group
task_count?: number; // For task-level queues
solution_count?: number; // For solution-level queues
tasks?: string[]; // Task IDs in this group (task-level)
solutions?: string[]; // Solution IDs in this group (solution-level)
}
interface Queue {
@@ -151,7 +156,8 @@ interface Queue {
name?: string; // Optional queue name
status: 'active' | 'completed' | 'archived' | 'failed';
issue_ids: string[]; // Issues in this queue
tasks: QueueItem[]; // Task items (formerly 'queue')
tasks: QueueItem[]; // Task items (task-level queue)
solutions?: QueueItem[]; // Solution items (solution-level queue)
conflicts: QueueConflict[];
execution_groups?: ExecutionGroup[];
_metadata: {
@@ -172,8 +178,10 @@ interface QueueIndex {
id: string;
status: string;
issue_ids: string[];
total_tasks: number;
completed_tasks: number;
total_tasks?: number; // For task-level queues
total_solutions?: number; // For solution-level queues
completed_tasks?: number; // For task-level queues
completed_solutions?: number; // For solution-level queues
created_at: string;
completed_at?: string;
}[];
@@ -845,92 +853,129 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
return;
}
// DAG - Return dependency graph for parallel execution planning
// DAG - Return dependency graph for parallel execution planning (solution-level)
if (subAction === 'dag') {
const queue = readActiveQueue();
if (!queue.id || queue.tasks.length === 0) {
// Support both old (tasks) and new (solutions) queue format
const items = queue.solutions || queue.tasks || [];
if (!queue.id || items.length === 0) {
console.log(JSON.stringify({ error: 'No active queue', nodes: [], edges: [], groups: [] }));
return;
}
// Build DAG nodes
const completedIds = new Set(queue.tasks.filter(t => t.status === 'completed').map(t => t.item_id));
const failedIds = new Set(queue.tasks.filter(t => t.status === 'failed').map(t => t.item_id));
// Build DAG nodes (solution-level)
const completedIds = new Set(items.filter(t => t.status === 'completed').map(t => t.item_id));
const failedIds = new Set(items.filter(t => t.status === 'failed').map(t => t.item_id));
const nodes = queue.tasks.map(task => ({
id: task.item_id,
issue_id: task.issue_id,
task_id: task.task_id,
status: task.status,
executor: task.assigned_executor,
priority: task.semantic_priority,
depends_on: task.depends_on,
const nodes = items.map(item => ({
id: item.item_id,
issue_id: item.issue_id,
solution_id: item.solution_id,
status: item.status,
executor: item.assigned_executor,
priority: item.semantic_priority,
depends_on: item.depends_on || [],
task_count: item.task_count || 1,
files_touched: item.files_touched || [],
// Calculate if ready (dependencies satisfied)
ready: task.status === 'pending' && task.depends_on.every(d => completedIds.has(d)),
blocked_by: task.depends_on.filter(d => !completedIds.has(d) && !failedIds.has(d))
ready: item.status === 'pending' && (item.depends_on || []).every(d => completedIds.has(d)),
blocked_by: (item.depends_on || []).filter(d => !completedIds.has(d) && !failedIds.has(d))
}));
// Build edges for visualization
const edges = queue.tasks.flatMap(task =>
task.depends_on.map(dep => ({ from: dep, to: task.item_id }))
const edges = items.flatMap(item =>
(item.depends_on || []).map(dep => ({ from: dep, to: item.item_id }))
);
// Group ready tasks by execution_group for parallel execution
const readyTasks = nodes.filter(n => n.ready || n.status === 'executing');
// Group ready items by execution_group
const readyItems = nodes.filter(n => n.ready || n.status === 'executing');
const groups: Record<string, string[]> = {};
for (const task of queue.tasks) {
if (readyTasks.some(r => r.id === task.item_id)) {
const group = task.execution_group || 'P1';
for (const item of items) {
if (readyItems.some(r => r.id === item.item_id)) {
const group = item.execution_group || 'P1';
if (!groups[group]) groups[group] = [];
groups[group].push(task.item_id);
groups[group].push(item.item_id);
}
}
// Calculate parallel batches (tasks with no dependencies on each other)
// Calculate parallel batches - prefer execution_groups from queue if available
const parallelBatches: string[][] = [];
const remainingReady = new Set(readyTasks.map(t => t.id));
const readyItemIds = new Set(readyItems.map(t => t.id));
while (remainingReady.size > 0) {
const batch: string[] = [];
const batchFiles = new Set<string>();
for (const taskId of remainingReady) {
const task = queue.tasks.find(t => t.item_id === taskId);
if (!task) continue;
// Check for file conflicts with already-batched tasks
const solution = findSolution(task.issue_id, task.solution_id);
const taskDef = solution?.tasks.find(t => t.id === task.task_id);
const taskFiles = taskDef?.modification_points?.map(mp => mp.file) || [];
const hasConflict = taskFiles.some(f => batchFiles.has(f));
if (!hasConflict) {
batch.push(taskId);
taskFiles.forEach(f => batchFiles.add(f));
// Check if queue has pre-assigned execution_groups
if (queue.execution_groups && queue.execution_groups.length > 0) {
// Use agent-assigned execution groups
for (const group of queue.execution_groups) {
const groupItems = (group.solutions || group.tasks || [])
.filter((id: string) => readyItemIds.has(id));
if (groupItems.length > 0) {
if (group.type === 'parallel') {
// All items in parallel group can run together
parallelBatches.push(groupItems);
} else {
// Sequential group: each item is its own batch
for (const itemId of groupItems) {
parallelBatches.push([itemId]);
}
}
}
}
} else {
// Fallback: calculate parallel batches from file conflicts
const remainingReady = new Set(readyItemIds);
if (batch.length === 0) {
// Fallback: take one at a time if all conflict
const first = Array.from(remainingReady)[0];
batch.push(first);
while (remainingReady.size > 0) {
const batch: string[] = [];
const batchFiles = new Set<string>();
for (const itemId of Array.from(remainingReady)) {
const item = items.find(t => t.item_id === itemId);
if (!item) continue;
// Get all files touched by this solution
let solutionFiles: string[] = item.files_touched || [];
// If not in queue item, fetch from solution definition
if (solutionFiles.length === 0) {
const solution = findSolution(item.issue_id, item.solution_id);
if (solution?.tasks) {
for (const task of solution.tasks) {
for (const mp of task.modification_points || []) {
solutionFiles.push(mp.file);
}
}
}
}
const hasConflict = solutionFiles.some(f => batchFiles.has(f));
if (!hasConflict) {
batch.push(itemId);
solutionFiles.forEach(f => batchFiles.add(f));
}
}
if (batch.length === 0) {
// Fallback: take one at a time if all conflict
const first = Array.from(remainingReady)[0];
batch.push(first);
}
parallelBatches.push(batch);
batch.forEach(id => remainingReady.delete(id));
}
parallelBatches.push(batch);
batch.forEach(id => remainingReady.delete(id));
}
console.log(JSON.stringify({
queue_id: queue.id,
total: nodes.length,
ready_count: readyTasks.length,
ready_count: readyItems.length,
completed_count: completedIds.size,
nodes,
edges,
groups: Object.entries(groups).map(([id, tasks]) => ({ id, tasks })),
groups: Object.entries(groups).map(([id, solutions]) => ({ id, solutions })),
parallel_batches: parallelBatches,
_summary: {
can_parallel: parallelBatches[0]?.length || 0,
@@ -1084,7 +1129,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
console.log(
item.item_id.padEnd(10) +
item.issue_id.substring(0, 13).padEnd(15) +
item.task_id.padEnd(8) +
(item.task_id || '-').padEnd(8) +
statusColor(item.status.padEnd(12)) +
item.assigned_executor
);
@@ -1097,91 +1142,107 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
*/
async function nextAction(itemId: string | undefined, options: IssueOptions): Promise<void> {
const queue = readActiveQueue();
let nextItem: typeof queue.tasks[0] | undefined;
// Support both old (tasks) and new (solutions) queue format
const items = queue.solutions || queue.tasks || [];
let nextItem: typeof items[0] | undefined;
let isResume = false;
// If specific item_id provided, fetch that task directly
// If specific item_id provided, fetch that item directly
if (itemId) {
nextItem = queue.tasks.find(t => t.item_id === itemId);
nextItem = items.find(t => t.item_id === itemId);
if (!nextItem) {
console.log(JSON.stringify({ status: 'error', message: `Task ${itemId} not found` }));
console.log(JSON.stringify({ status: 'error', message: `Item ${itemId} not found` }));
return;
}
if (nextItem.status === 'completed') {
console.log(JSON.stringify({ status: 'completed', message: `Task ${itemId} already completed` }));
console.log(JSON.stringify({ status: 'completed', message: `Item ${itemId} already completed` }));
return;
}
if (nextItem.status === 'failed') {
console.log(JSON.stringify({ status: 'failed', message: `Task ${itemId} failed, use retry to reset` }));
console.log(JSON.stringify({ status: 'failed', message: `Item ${itemId} failed, use retry to reset` }));
return;
}
isResume = nextItem.status === 'executing';
} else {
// Auto-select: Priority 1 - executing, Priority 2 - ready pending
const executingTasks = queue.tasks.filter(item => item.status === 'executing');
const pendingTasks = queue.tasks.filter(item => {
const executingItems = items.filter(item => item.status === 'executing');
const pendingItems = items.filter(item => {
if (item.status !== 'pending') return false;
return item.depends_on.every(depId => {
const dep = queue.tasks.find(q => q.item_id === depId);
return (item.depends_on || []).every(depId => {
const dep = items.find(q => q.item_id === depId);
return !dep || dep.status === 'completed';
});
});
const readyTasks = [...executingTasks, ...pendingTasks];
const readyItems = [...executingItems, ...pendingItems];
if (readyTasks.length === 0) {
if (readyItems.length === 0) {
console.log(JSON.stringify({
status: 'empty',
message: 'No ready tasks',
message: 'No ready items',
queue_status: queue._metadata
}, null, 2));
return;
}
readyTasks.sort((a, b) => a.execution_order - b.execution_order);
nextItem = readyTasks[0];
readyItems.sort((a, b) => a.execution_order - b.execution_order);
nextItem = readyItems[0];
isResume = nextItem.status === 'executing';
}
// Load task definition
// Load FULL solution with all tasks
const solution = findSolution(nextItem.issue_id, nextItem.solution_id);
const taskDef = solution?.tasks.find(t => t.id === nextItem.task_id);
if (!taskDef) {
console.log(JSON.stringify({ status: 'error', message: 'Task definition not found' }));
if (!solution) {
console.log(JSON.stringify({ status: 'error', message: 'Solution not found' }));
process.exit(1);
}
// Only update status if not already executing (new task)
// Only update status if not already executing
if (!isResume) {
const idx = queue.tasks.findIndex(q => q.item_id === nextItem.item_id);
queue.tasks[idx].status = 'executing';
queue.tasks[idx].started_at = new Date().toISOString();
const idx = items.findIndex(q => q.item_id === nextItem.item_id);
items[idx].status = 'executing';
items[idx].started_at = new Date().toISOString();
// Write back to correct array
if (queue.solutions) {
queue.solutions = items;
} else {
queue.tasks = items;
}
writeQueue(queue);
updateIssue(nextItem.issue_id, { status: 'executing' });
}
// Calculate queue stats for context
// Calculate queue stats
const stats = {
total: queue.tasks.length,
completed: queue.tasks.filter(q => q.status === 'completed').length,
failed: queue.tasks.filter(q => q.status === 'failed').length,
executing: queue.tasks.filter(q => q.status === 'executing').length,
pending: queue.tasks.filter(q => q.status === 'pending').length
total: items.length,
completed: items.filter(q => q.status === 'completed').length,
failed: items.filter(q => q.status === 'failed').length,
executing: items.filter(q => q.status === 'executing').length,
pending: items.filter(q => q.status === 'pending').length
};
const remaining = stats.pending + stats.executing;
// Calculate total estimated time for all tasks
const totalMinutes = solution.tasks?.reduce((sum, t) => sum + (t.estimated_minutes || 30), 0) || 30;
console.log(JSON.stringify({
item_id: nextItem.item_id,
issue_id: nextItem.issue_id,
solution_id: nextItem.solution_id,
task: taskDef,
context: solution?.exploration_context || {},
// Return full solution object with all tasks
solution: {
id: solution.id,
approach: solution.approach,
tasks: solution.tasks || [],
exploration_context: solution.exploration_context || {}
},
resumed: isResume,
resume_note: isResume ? `Resuming interrupted task (started: ${nextItem.started_at})` : undefined,
resume_note: isResume ? `Resuming interrupted item (started: ${nextItem.started_at})` : undefined,
execution_hints: {
executor: nextItem.assigned_executor,
estimated_minutes: taskDef.estimated_minutes || 30
task_count: solution.tasks?.length || 0,
estimated_minutes: totalMinutes
},
queue_progress: {
completed: stats.completed,
@@ -1203,33 +1264,43 @@ async function detailAction(itemId: string | undefined, options: IssueOptions):
}
const queue = readActiveQueue();
const queueItem = queue.tasks.find(t => t.item_id === itemId);
// Support both old (tasks) and new (solutions) queue format
const items = queue.solutions || queue.tasks || [];
const queueItem = items.find(t => t.item_id === itemId);
if (!queueItem) {
console.log(JSON.stringify({ status: 'error', message: `Task ${itemId} not found` }));
console.log(JSON.stringify({ status: 'error', message: `Item ${itemId} not found` }));
return;
}
// Load task definition from solution
// Load FULL solution with all tasks
const solution = findSolution(queueItem.issue_id, queueItem.solution_id);
const taskDef = solution?.tasks.find(t => t.id === queueItem.task_id);
if (!taskDef) {
console.log(JSON.stringify({ status: 'error', message: 'Task definition not found in solution' }));
if (!solution) {
console.log(JSON.stringify({ status: 'error', message: 'Solution not found' }));
return;
}
// Return full task info (READ-ONLY - no status update)
// Calculate total estimated time for all tasks
const totalMinutes = solution.tasks?.reduce((sum, t) => sum + (t.estimated_minutes || 30), 0) || 30;
// Return FULL SOLUTION with all tasks (READ-ONLY - no status update)
console.log(JSON.stringify({
item_id: queueItem.item_id,
issue_id: queueItem.issue_id,
solution_id: queueItem.solution_id,
status: queueItem.status,
task: taskDef,
context: solution?.exploration_context || {},
// Return full solution object with all tasks
solution: {
id: solution.id,
approach: solution.approach,
tasks: solution.tasks || [],
exploration_context: solution.exploration_context || {}
},
execution_hints: {
executor: queueItem.assigned_executor,
estimated_minutes: taskDef.estimated_minutes || 30
task_count: solution.tasks?.length || 0,
estimated_minutes: totalMinutes
}
}, null, 2));
}
@@ -1239,13 +1310,15 @@ async function detailAction(itemId: string | undefined, options: IssueOptions):
*/
async function doneAction(queueId: string | undefined, options: IssueOptions): Promise<void> {
if (!queueId) {
console.error(chalk.red('Queue ID is required'));
console.error(chalk.gray('Usage: ccw issue done <queue-id> [--fail] [--reason "..."]'));
console.error(chalk.red('Item ID is required'));
console.error(chalk.gray('Usage: ccw issue done <item-id> [--fail] [--reason "..."]'));
process.exit(1);
}
const queue = readActiveQueue();
const idx = queue.tasks.findIndex(q => q.item_id === queueId);
// Support both old (tasks) and new (solutions) queue format
const items = queue.solutions || queue.tasks || [];
const idx = items.findIndex(q => q.item_id === queueId);
if (idx === -1) {
console.error(chalk.red(`Queue item "${queueId}" not found`));
@@ -1253,66 +1326,69 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
}
const isFail = options.fail;
queue.tasks[idx].status = isFail ? 'failed' : 'completed';
queue.tasks[idx].completed_at = new Date().toISOString();
items[idx].status = isFail ? 'failed' : 'completed';
items[idx].completed_at = new Date().toISOString();
if (isFail) {
queue.tasks[idx].failure_reason = options.reason || 'Unknown failure';
items[idx].failure_reason = options.reason || 'Unknown failure';
} else if (options.result) {
try {
queue.tasks[idx].result = JSON.parse(options.result);
items[idx].result = JSON.parse(options.result);
} catch {
console.warn(chalk.yellow('Warning: Could not parse result JSON'));
}
}
// Check if all issue tasks are complete
const issueId = queue.tasks[idx].issue_id;
const issueTasks = queue.tasks.filter(q => q.issue_id === issueId);
const allIssueComplete = issueTasks.every(q => q.status === 'completed');
const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
// Update issue status (solution = issue in new model)
const issueId = items[idx].issue_id;
if (allIssueComplete) {
updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
console.log(chalk.green(`${queueId} completed`));
console.log(chalk.green(`✓ Issue ${issueId} completed (all tasks done)`));
} else if (anyIssueFailed) {
if (isFail) {
updateIssue(issueId, { status: 'failed' });
console.log(chalk.red(`${queueId} failed`));
} else {
console.log(isFail ? chalk.red(`${queueId} failed`) : chalk.green(`${queueId} completed`));
updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
console.log(chalk.green(`${queueId} completed`));
console.log(chalk.green(`✓ Issue ${issueId} completed`));
}
// Check if entire queue is complete
const allQueueComplete = queue.tasks.every(q => q.status === 'completed');
const anyQueueFailed = queue.tasks.some(q => q.status === 'failed');
const allQueueComplete = items.every(q => q.status === 'completed');
const anyQueueFailed = items.some(q => q.status === 'failed');
if (allQueueComplete) {
queue.status = 'completed';
console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`));
} else if (anyQueueFailed && queue.tasks.every(q => q.status === 'completed' || q.status === 'failed')) {
console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all solutions done)`));
} else if (anyQueueFailed && items.every(q => q.status === 'completed' || q.status === 'failed')) {
queue.status = 'failed';
console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed solutions`));
}
// Write back to queue (update the correct array)
if (queue.solutions) {
queue.solutions = items;
} else {
queue.tasks = items;
}
writeQueue(queue);
}
/**
* retry - Reset failed tasks to pending for re-execution
* retry - Reset failed items to pending for re-execution
*/
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
const queue = readActiveQueue();
// Support both old (tasks) and new (solutions) queue format
const items = queue.solutions || queue.tasks || [];
if (!queue.id || queue.tasks.length === 0) {
if (!queue.id || items.length === 0) {
console.log(chalk.yellow('No active queue'));
return;
}
let updated = 0;
for (const item of queue.tasks) {
// Retry failed tasks only
for (const item of items) {
// Retry failed items only
if (item.status === 'failed') {
if (!issueId || item.issue_id === issueId) {
item.status = 'pending';
@@ -1325,8 +1401,7 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
}
if (updated === 0) {
console.log(chalk.yellow('No failed tasks to retry'));
console.log(chalk.gray('Note: Interrupted (executing) tasks are auto-resumed by "ccw issue next"'));
console.log(chalk.yellow('No failed items to retry'));
return;
}
@@ -1335,13 +1410,19 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
queue.status = 'active';
}
// Write back to queue
if (queue.solutions) {
queue.solutions = items;
} else {
queue.tasks = items;
}
writeQueue(queue);
if (issueId) {
updateIssue(issueId, { status: 'queued' });
}
console.log(chalk.green(`✓ Reset ${updated} task(s) to pending`));
console.log(chalk.green(`✓ Reset ${updated} item(s) to pending`));
}
// ============ Main Entry ============

View File

@@ -6,7 +6,7 @@
// ========== Issue State ==========
var issueData = {
issues: [],
queue: { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} },
queue: { tasks: [], solutions: [], conflicts: [], execution_groups: [], grouped_items: {} },
selectedIssue: null,
selectedSolution: null,
selectedSolutionIssueId: null,
@@ -65,7 +65,7 @@ async function loadQueueData() {
issueData.queue = await response.json();
} catch (err) {
console.error('Failed to load queue:', err);
issueData.queue = { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} };
issueData.queue = { tasks: [], solutions: [], conflicts: [], execution_groups: [], grouped_items: {} };
}
}
@@ -360,7 +360,9 @@ function filterIssuesByStatus(status) {
// ========== Queue Section ==========
function renderQueueSection() {
const queue = issueData.queue;
const queueItems = queue.tasks || [];
// Support both solution-level and task-level queues
const queueItems = queue.solutions || queue.tasks || [];
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
const metadata = queue._metadata || {};
// Check if queue is empty
@@ -443,8 +445,8 @@ function renderQueueSection() {
<!-- Queue Stats -->
<div class="queue-stats-grid mb-4">
<div class="queue-stat-card">
<span class="queue-stat-value">${metadata.total_tasks || queueItems.length}</span>
<span class="queue-stat-label">${t('issues.totalTasks') || 'Total'}</span>
<span class="queue-stat-value">${isSolutionLevel ? (metadata.total_solutions || queueItems.length) : (metadata.total_tasks || queueItems.length)}</span>
<span class="queue-stat-label">${isSolutionLevel ? (t('issues.totalSolutions') || 'Solutions') : (t('issues.totalTasks') || 'Total')}</span>
</div>
<div class="queue-stat-card pending">
<span class="queue-stat-value">${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length}</span>
@@ -510,6 +512,9 @@ function renderQueueSection() {
function renderQueueGroup(group, items) {
const isParallel = group.type === 'parallel';
// Support both solution-level (solution_count) and task-level (task_count)
const itemCount = group.solution_count || group.task_count || items.length;
const itemLabel = group.solution_count ? 'solutions' : 'tasks';
return `
<div class="queue-group" data-group-id="${group.id}">
@@ -518,7 +523,7 @@ function renderQueueGroup(group, items) {
<i data-lucide="${isParallel ? 'git-merge' : 'arrow-right'}" class="w-4 h-4"></i>
${group.id} (${isParallel ? t('issues.parallelGroup') || 'Parallel' : t('issues.sequentialGroup') || 'Sequential'})
</div>
<span class="text-sm text-muted-foreground">${group.task_count} tasks</span>
<span class="text-sm text-muted-foreground">${itemCount} ${itemLabel}</span>
</div>
<div class="queue-items ${isParallel ? 'parallel' : 'sequential'}">
${items.map((item, idx) => renderQueueItem(item, idx, items.length)).join('')}
@@ -537,6 +542,9 @@ function renderQueueItem(item, index, total) {
blocked: 'blocked'
};
// Check if this is a solution-level item (has task_count) or task-level (has task_id)
const isSolutionItem = item.task_count !== undefined;
return `
<div class="queue-item ${statusColors[item.status] || ''}"
draggable="true"
@@ -545,7 +553,20 @@ function renderQueueItem(item, index, total) {
onclick="openQueueItemDetail('${item.item_id}')">
<span class="queue-item-id font-mono text-xs">${item.item_id}</span>
<span class="queue-item-issue text-xs text-muted-foreground">${item.issue_id}</span>
<span class="queue-item-task text-sm">${item.task_id}</span>
${isSolutionItem ? `
<span class="queue-item-solution text-sm" title="${item.solution_id || ''}">
<i data-lucide="package" class="w-3 h-3 inline mr-1"></i>
${item.task_count} ${t('issues.tasks') || 'tasks'}
</span>
${item.files_touched && item.files_touched.length > 0 ? `
<span class="queue-item-files text-xs text-muted-foreground" title="${item.files_touched.join(', ')}">
<i data-lucide="file" class="w-3 h-3"></i>
${item.files_touched.length}
</span>
` : ''}
` : `
<span class="queue-item-task text-sm">${item.task_id || '-'}</span>
`}
<span class="queue-item-priority" style="opacity: ${item.semantic_priority || 0.5}">
<i data-lucide="arrow-up" class="w-3 h-3"></i>
</span>
@@ -569,9 +590,12 @@ function renderConflictsSection(conflicts) {
${conflicts.map(c => `
<div class="conflict-item">
<span class="conflict-file font-mono text-xs">${c.file}</span>
<span class="conflict-tasks text-xs text-muted-foreground">${c.tasks.join(' → ')}</span>
<span class="conflict-status ${c.resolved ? 'resolved' : 'pending'}">
${c.resolved ? 'Resolved' : 'Pending'}
<span class="conflict-items text-xs text-muted-foreground">${(c.solutions || c.tasks || []).join(' → ')}</span>
${c.rationale ? `<span class="conflict-rationale text-xs text-muted-foreground" title="${c.rationale}">
<i data-lucide="info" class="w-3 h-3"></i>
</span>` : ''}
<span class="conflict-status ${c.resolved || c.resolution ? 'resolved' : 'pending'}">
${c.resolved || c.resolution ? 'Resolved' : 'Pending'}
</span>
</div>
`).join('')}
@@ -1156,7 +1180,9 @@ function escapeHtml(text) {
}
function openQueueItemDetail(itemId) {
const item = issueData.queue.tasks?.find(q => q.item_id === itemId);
// Support both solution-level and task-level queues
const items = issueData.queue.solutions || issueData.queue.tasks || [];
const item = items.find(q => q.item_id === itemId);
if (item) {
openIssueDetail(item.issue_id);
}
@@ -1600,7 +1626,7 @@ async function showQueueHistoryModal() {
</span>
<span class="text-xs text-muted-foreground">
<i data-lucide="check-circle" class="w-3 h-3 inline"></i>
${q.completed_tasks || 0}/${q.total_tasks || 0} tasks
${q.completed_solutions || q.completed_tasks || 0}/${q.total_solutions || q.total_tasks || 0} ${q.total_solutions ? 'solutions' : 'tasks'}
</span>
<span class="text-xs text-muted-foreground">
<i data-lucide="calendar" class="w-3 h-3 inline"></i>
@@ -1689,17 +1715,21 @@ async function viewQueueDetail(queueId) {
throw new Error(queue.error);
}
const tasks = queue.queue || [];
// Support both solution-level and task-level queues
const items = queue.solutions || queue.queue || queue.tasks || [];
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
const metadata = queue._metadata || {};
// Group by execution_group
const grouped = {};
tasks.forEach(task => {
const group = task.execution_group || 'ungrouped';
items.forEach(item => {
const group = item.execution_group || 'ungrouped';
if (!grouped[group]) grouped[group] = [];
grouped[group].push(task);
grouped[group].push(item);
});
const itemLabel = isSolutionLevel ? 'solutions' : 'tasks';
const detailHtml = `
<div class="queue-detail-view">
<div class="queue-detail-header mb-4">
@@ -1715,40 +1745,41 @@ async function viewQueueDetail(queueId) {
<div class="queue-detail-stats mb-4">
<div class="stat-item">
<span class="stat-value">${tasks.length}</span>
<span class="stat-label">Total</span>
<span class="stat-value">${items.length}</span>
<span class="stat-label">${isSolutionLevel ? 'Solutions' : 'Total'}</span>
</div>
<div class="stat-item completed">
<span class="stat-value">${tasks.filter(t => t.status === 'completed').length}</span>
<span class="stat-value">${items.filter(t => t.status === 'completed').length}</span>
<span class="stat-label">Completed</span>
</div>
<div class="stat-item pending">
<span class="stat-value">${tasks.filter(t => t.status === 'pending').length}</span>
<span class="stat-value">${items.filter(t => t.status === 'pending').length}</span>
<span class="stat-label">Pending</span>
</div>
<div class="stat-item failed">
<span class="stat-value">${tasks.filter(t => t.status === 'failed').length}</span>
<span class="stat-value">${items.filter(t => t.status === 'failed').length}</span>
<span class="stat-label">Failed</span>
</div>
</div>
<div class="queue-detail-groups">
${Object.entries(grouped).map(([groupId, items]) => `
${Object.entries(grouped).map(([groupId, groupItems]) => `
<div class="queue-group-section">
<div class="queue-group-header">
<i data-lucide="folder" class="w-4 h-4"></i>
<span>${groupId}</span>
<span class="text-xs text-muted-foreground">(${items.length} tasks)</span>
<span class="text-xs text-muted-foreground">(${groupItems.length} ${itemLabel})</span>
</div>
<div class="queue-group-items">
${items.map(item => `
${groupItems.map(item => `
<div class="queue-detail-item ${item.status || ''}">
<div class="item-main">
<span class="item-id font-mono text-xs">${item.queue_id || item.task_id || 'N/A'}</span>
<span class="item-title text-sm">${item.title || item.action || 'Untitled'}</span>
<span class="item-id font-mono text-xs">${item.item_id || item.queue_id || item.task_id || 'N/A'}</span>
<span class="item-title text-sm">${isSolutionLevel ? (item.task_count + ' tasks') : (item.title || item.action || 'Untitled')}</span>
</div>
<div class="item-meta">
<span class="item-issue text-xs">${item.issue_id || ''}</span>
${isSolutionLevel && item.files_touched ? `<span class="item-files text-xs">${item.files_touched.length} files</span>` : ''}
<span class="item-status ${item.status || ''}">${item.status || 'unknown'}</span>
</div>
</div>