refactor: Update issue queue structure and commands

- Changed queue structure from 'queue' to 'tasks' in various files for clarity.
- Updated CLI commands to reflect new task ID usage instead of queue ID.
- Enhanced queue management with new delete functionality for historical queues.
- Improved metadata handling and task execution tracking.
- Updated dashboard and issue manager views to accommodate new task structure.
- Bumped version to 6.3.8 in package.json and package-lock.json.
This commit is contained in:
catlog22
2025-12-27 22:04:15 +08:00
parent 2e493277a1
commit b58589ddad
13 changed files with 394 additions and 336 deletions

View File

@@ -277,6 +277,7 @@ export function run(argv: string[]): void {
.option('--priority <n>', 'Task priority (1-5)')
.option('--format <fmt>', 'Output format: json, markdown')
.option('--json', 'Output as JSON')
.option('--ids', 'List only IDs (one per line, for scripting)')
.option('--force', 'Force operation')
// New options for solution/queue management
.option('--solution <path>', 'Solution JSON file path')

View File

@@ -5,7 +5,7 @@
*/
import chalk from 'chalk';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
import { join, resolve } from 'path';
// Handle EPIPE errors gracefully
@@ -29,6 +29,18 @@ interface Issue {
source?: string;
source_url?: string;
labels?: string[];
// Agent workflow fields
affected_components?: string[];
lifecycle_requirements?: {
test_strategy?: 'unit' | 'integration' | 'e2e' | 'auto';
regression_scope?: 'full' | 'related' | 'affected';
commit_strategy?: 'per-task' | 'atomic' | 'squash';
};
problem_statement?: string;
expected_behavior?: string;
actual_behavior?: string;
reproduction_steps?: string[];
// Timestamps
created_at: string;
updated_at: string;
planned_at?: string;
@@ -100,17 +112,17 @@ interface Solution {
}
interface QueueItem {
queue_id: string;
item_id: string; // Task item ID in queue: T-1, T-2, ... (formerly queue_id)
issue_id: string;
solution_id: string;
task_id: string;
title?: string;
status: 'pending' | 'ready' | 'executing' | 'completed' | 'failed' | 'blocked';
execution_order: number;
execution_group: string;
depends_on: string[];
semantic_priority: number;
assigned_executor: 'codex' | 'gemini' | 'agent';
queued_at: string;
started_at?: string;
completed_at?: string;
result?: Record<string, any>;
@@ -118,11 +130,11 @@ interface QueueItem {
}
interface Queue {
id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS
id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS (derived from filename)
name?: string; // Optional queue name
status: 'active' | 'completed' | 'archived' | 'failed';
issue_ids: string[]; // Issues in this queue
queue: QueueItem[];
tasks: QueueItem[]; // Task items (formerly 'queue')
conflicts: any[];
execution_groups?: any[];
_metadata: {
@@ -132,13 +144,12 @@ interface Queue {
executing_count: number;
completed_count: number;
failed_count: number;
created_at: string;
updated_at: string;
};
}
interface QueueIndex {
active_queue_id: string | null;
active_item_id: string | null;
queues: {
id: string;
status: string;
@@ -162,6 +173,7 @@ interface IssueOptions {
json?: boolean;
force?: boolean;
fail?: boolean;
ids?: boolean; // List only IDs (one per line)
}
const ISSUES_DIR = '.workflow/issues';
@@ -278,7 +290,7 @@ function ensureQueuesDir(): void {
function readQueueIndex(): QueueIndex {
const path = join(getQueuesDir(), 'index.json');
if (!existsSync(path)) {
return { active_queue_id: null, queues: [] };
return { active_item_id: null, queues: [] };
}
return JSON.parse(readFileSync(path, 'utf-8'));
}
@@ -319,16 +331,15 @@ function createEmptyQueue(): Queue {
id: generateQueueFileId(),
status: 'active',
issue_ids: [],
queue: [],
tasks: [],
conflicts: [],
_metadata: {
version: '2.0',
version: '2.1',
total_tasks: 0,
pending_count: 0,
executing_count: 0,
completed_count: 0,
failed_count: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
};
@@ -338,11 +349,11 @@ function writeQueue(queue: Queue): void {
ensureQueuesDir();
// Update metadata counts
queue._metadata.total_tasks = queue.queue.length;
queue._metadata.pending_count = queue.queue.filter(q => q.status === 'pending').length;
queue._metadata.executing_count = queue.queue.filter(q => q.status === 'executing').length;
queue._metadata.completed_count = queue.queue.filter(q => q.status === 'completed').length;
queue._metadata.failed_count = queue.queue.filter(q => q.status === 'failed').length;
queue._metadata.total_tasks = queue.tasks.length;
queue._metadata.pending_count = queue.tasks.filter(q => q.status === 'pending').length;
queue._metadata.executing_count = queue.tasks.filter(q => q.status === 'executing').length;
queue._metadata.completed_count = queue.tasks.filter(q => q.status === 'completed').length;
queue._metadata.failed_count = queue.tasks.filter(q => q.status === 'failed').length;
queue._metadata.updated_at = new Date().toISOString();
// Write queue file
@@ -359,7 +370,7 @@ function writeQueue(queue: Queue): void {
issue_ids: queue.issue_ids,
total_tasks: queue._metadata.total_tasks,
completed_tasks: queue._metadata.completed_count,
created_at: queue._metadata.created_at,
created_at: queue.id.replace('QUE-', '').replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, '$1-$2-$3T$4:$5:$6Z'), // Derive from ID
completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined
};
@@ -377,11 +388,11 @@ function writeQueue(queue: Queue): void {
}
function generateQueueItemId(queue: Queue): string {
const maxNum = queue.queue.reduce((max, q) => {
const match = q.queue_id.match(/^Q-(\d+)$/);
const maxNum = queue.tasks.reduce((max, q) => {
const match = q.item_id.match(/^T-(\d+)$/);
return match ? Math.max(max, parseInt(match[1])) : max;
}, 0);
return `Q-${String(maxNum + 1).padStart(3, '0')}`;
return `T-${maxNum + 1}`;
}
// ============ Commands ============
@@ -429,7 +440,19 @@ async function initAction(issueId: string | undefined, options: IssueOptions): P
async function listAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
if (!issueId) {
// List all issues
const issues = readIssues();
let issues = readIssues();
// Filter by status if specified
if (options.status) {
const statuses = options.status.split(',').map(s => s.trim());
issues = issues.filter(i => statuses.includes(i.status));
}
// IDs only mode (one per line, for scripting)
if (options.ids) {
issues.forEach(i => console.log(i.id));
return;
}
if (options.json) {
console.log(JSON.stringify(issues, null, 2));
@@ -519,7 +542,8 @@ async function statusAction(issueId: string | undefined, options: IssueOptions):
const index = readQueueIndex();
if (options.json) {
console.log(JSON.stringify({ queue: queue._metadata, issues: issues.length, queues: index.queues.length }, null, 2));
// Return full queue for programmatic access
console.log(JSON.stringify(queue, null, 2));
return;
}
@@ -806,7 +830,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
// Archive current queue
if (subAction === 'archive') {
const queue = readActiveQueue();
if (!queue.id || queue.queue.length === 0) {
if (!queue.id || queue.tasks.length === 0) {
console.log(chalk.yellow('No active queue to archive'));
return;
}
@@ -822,6 +846,31 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
return;
}
// Delete queue from history
if ((subAction === 'clear' || subAction === 'delete') && issueId) {
const queueId = issueId; // issueId is actually queue ID here
const queuePath = join(getQueuesDir(), `${queueId}.json`);
if (!existsSync(queuePath)) {
console.error(chalk.red(`Queue "${queueId}" not found`));
process.exit(1);
}
// Remove from index
const index = readQueueIndex();
index.queues = index.queues.filter(q => q.id !== queueId);
if (index.active_queue_id === queueId) {
index.active_queue_id = null;
}
writeQueueIndex(index);
// Delete queue file
unlinkSync(queuePath);
console.log(chalk.green(`✓ Deleted queue ${queueId}`));
return;
}
// Add issue tasks to queue
if (subAction === 'add' && issueId) {
const issue = findIssue(issueId);
@@ -839,7 +888,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
// Get or create active queue (create new if current is completed/archived)
let queue = readActiveQueue();
const isNewQueue = queue.queue.length === 0 || queue.status !== 'active';
const isNewQueue = queue.tasks.length === 0 || queue.status !== 'active';
if (queue.status !== 'active') {
// Create new queue if current is not active
@@ -853,24 +902,23 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
let added = 0;
for (const task of solution.tasks) {
const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id);
const exists = queue.tasks.some(q => q.issue_id === issueId && q.task_id === task.id);
if (exists) continue;
queue.queue.push({
queue_id: generateQueueItemId(queue),
queue.tasks.push({
item_id: generateQueueItemId(queue),
issue_id: issueId,
solution_id: solution.id,
task_id: task.id,
status: 'pending',
execution_order: queue.queue.length + 1,
execution_order: queue.tasks.length + 1,
execution_group: 'P1',
depends_on: task.depends_on.map(dep => {
const depItem = queue.queue.find(q => q.task_id === dep && q.issue_id === issueId);
return depItem?.queue_id || dep;
const depItem = queue.tasks.find(q => q.task_id === dep && q.issue_id === issueId);
return depItem?.item_id || dep;
}),
semantic_priority: 0.5,
assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any,
queued_at: new Date().toISOString()
assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any
});
added++;
}
@@ -895,7 +943,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
console.log(chalk.bold.cyan('\nActive Queue\n'));
if (!queue.id || queue.queue.length === 0) {
if (!queue.id || queue.tasks.length === 0) {
console.log(chalk.yellow('No active queue'));
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
console.log(chalk.gray('Or list history: ccw issue queue list'));
@@ -910,7 +958,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
console.log(chalk.gray('-'.repeat(60)));
for (const item of queue.queue) {
for (const item of queue.tasks) {
const statusColor = {
'pending': chalk.gray,
'ready': chalk.cyan,
@@ -921,7 +969,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
}[item.status] || chalk.white;
console.log(
item.queue_id.padEnd(10) +
item.item_id.padEnd(10) +
item.issue_id.substring(0, 13).padEnd(15) +
item.task_id.padEnd(8) +
statusColor(item.status.padEnd(12)) +
@@ -937,13 +985,13 @@ async function nextAction(options: IssueOptions): Promise<void> {
const queue = readActiveQueue();
// Priority 1: Resume executing tasks (interrupted/crashed)
const executingTasks = queue.queue.filter(item => item.status === 'executing');
const executingTasks = queue.tasks.filter(item => item.status === 'executing');
// Priority 2: Find pending tasks with satisfied dependencies
const pendingTasks = queue.queue.filter(item => {
const pendingTasks = queue.tasks.filter(item => {
if (item.status !== 'pending') return false;
return item.depends_on.every(depId => {
const dep = queue.queue.find(q => q.queue_id === depId);
const dep = queue.tasks.find(q => q.item_id === depId);
return !dep || dep.status === 'completed';
});
});
@@ -976,25 +1024,25 @@ async function nextAction(options: IssueOptions): Promise<void> {
// Only update status if not already executing (new task)
if (!isResume) {
const idx = queue.queue.findIndex(q => q.queue_id === nextItem.queue_id);
queue.queue[idx].status = 'executing';
queue.queue[idx].started_at = new Date().toISOString();
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();
writeQueue(queue);
updateIssue(nextItem.issue_id, { status: 'executing' });
}
// Calculate queue stats for context
const stats = {
total: queue.queue.length,
completed: queue.queue.filter(q => q.status === 'completed').length,
failed: queue.queue.filter(q => q.status === 'failed').length,
total: queue.tasks.length,
completed: queue.tasks.filter(q => q.status === 'completed').length,
failed: queue.tasks.filter(q => q.status === 'failed').length,
executing: executingTasks.length,
pending: pendingTasks.length
};
const remaining = stats.pending + stats.executing;
console.log(JSON.stringify({
queue_id: nextItem.queue_id,
item_id: nextItem.item_id,
issue_id: nextItem.issue_id,
solution_id: nextItem.solution_id,
task: taskDef,
@@ -1025,7 +1073,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
}
const queue = readActiveQueue();
const idx = queue.queue.findIndex(q => q.queue_id === queueId);
const idx = queue.tasks.findIndex(q => q.item_id === queueId);
if (idx === -1) {
console.error(chalk.red(`Queue item "${queueId}" not found`));
@@ -1033,22 +1081,22 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
}
const isFail = options.fail;
queue.queue[idx].status = isFail ? 'failed' : 'completed';
queue.queue[idx].completed_at = new Date().toISOString();
queue.tasks[idx].status = isFail ? 'failed' : 'completed';
queue.tasks[idx].completed_at = new Date().toISOString();
if (isFail) {
queue.queue[idx].failure_reason = options.reason || 'Unknown failure';
queue.tasks[idx].failure_reason = options.reason || 'Unknown failure';
} else if (options.result) {
try {
queue.queue[idx].result = JSON.parse(options.result);
queue.tasks[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.queue[idx].issue_id;
const issueTasks = queue.queue.filter(q => q.issue_id === issueId);
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');
@@ -1064,13 +1112,13 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
}
// Check if entire queue is complete
const allQueueComplete = queue.queue.every(q => q.status === 'completed');
const anyQueueFailed = queue.queue.some(q => q.status === 'failed');
const allQueueComplete = queue.tasks.every(q => q.status === 'completed');
const anyQueueFailed = queue.tasks.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.queue.every(q => q.status === 'completed' || q.status === 'failed')) {
} else if (anyQueueFailed && queue.tasks.every(q => q.status === 'completed' || q.status === 'failed')) {
queue.status = 'failed';
console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
}
@@ -1079,24 +1127,20 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
}
/**
* retry - Retry failed tasks, or reset stuck executing tasks (--force)
* retry - Reset failed tasks to pending for re-execution
*/
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
const queue = readActiveQueue();
if (!queue.id || queue.queue.length === 0) {
if (!queue.id || queue.tasks.length === 0) {
console.log(chalk.yellow('No active queue'));
return;
}
let updated = 0;
// Check for stuck executing tasks (started > 30 min ago with no completion)
const stuckThreshold = 30 * 60 * 1000; // 30 minutes
const now = Date.now();
for (const item of queue.queue) {
// Retry failed tasks
for (const item of queue.tasks) {
// Retry failed tasks only
if (item.status === 'failed') {
if (!issueId || item.issue_id === issueId) {
item.status = 'pending';
@@ -1106,23 +1150,11 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
updated++;
}
}
// Reset stuck executing tasks (optional: use --force or --reset-stuck)
else if (item.status === 'executing' && options.force) {
const startedAt = item.started_at ? new Date(item.started_at).getTime() : 0;
if (now - startedAt > stuckThreshold) {
if (!issueId || item.issue_id === issueId) {
console.log(chalk.yellow(`Resetting stuck task: ${item.queue_id} (started ${Math.round((now - startedAt) / 60000)} min ago)`));
item.status = 'pending';
item.started_at = undefined;
updated++;
}
}
}
}
if (updated === 0) {
console.log(chalk.yellow('No failed/stuck tasks to retry'));
console.log(chalk.gray('Use --force to reset stuck executing tasks (>30 min)'));
console.log(chalk.yellow('No failed tasks to retry'));
console.log(chalk.gray('Note: Interrupted (executing) tasks are auto-resumed by "ccw issue next"'));
return;
}
@@ -1203,7 +1235,8 @@ export async function issueCommand(
console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
console.log(chalk.gray(' queue archive Archive current queue'));
console.log(chalk.gray(' retry [issue-id] [--force] Retry failed/stuck tasks'));
console.log(chalk.gray(' queue delete <queue-id> Delete queue from history'));
console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
console.log();
console.log(chalk.bold('Execution Endpoints:'));
console.log(chalk.gray(' next Get next ready task (JSON)'));
@@ -1212,6 +1245,8 @@ export async function issueCommand(
console.log();
console.log(chalk.bold('Options:'));
console.log(chalk.gray(' --title <title> Issue/task title'));
console.log(chalk.gray(' --status <status> Filter by status (comma-separated)'));
console.log(chalk.gray(' --ids List only IDs (one per line)'));
console.log(chalk.gray(' --solution <path> Solution JSON file'));
console.log(chalk.gray(' --result <json> Execution result'));
console.log(chalk.gray(' --reason <text> Failure reason'));

View File

@@ -5,7 +5,9 @@
* Storage Structure:
* .workflow/issues/
* ├── issues.jsonl # All issues (one per line)
* ├── queue.json # Execution queue
* ├── queues/ # Queue history directory
* │ ├── index.json # Queue index (active + history)
* │ └── {queue-id}.json # Individual queue files
* └── solutions/
* ├── {issue-id}.jsonl # Solutions for issue (one per line)
* └── ...
@@ -102,12 +104,12 @@ function readQueue(issuesDir: string) {
}
}
return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
return { tasks: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
}
function writeQueue(issuesDir: string, queue: any) {
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.queue?.length || 0 };
queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.tasks?.length || 0 };
// Check if using new multi-queue structure
const queuesDir = join(issuesDir, 'queues');
@@ -123,8 +125,8 @@ function writeQueue(issuesDir: string, queue: any) {
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
const queueEntry = index.queues?.find((q: any) => q.id === queue.id);
if (queueEntry) {
queueEntry.total_tasks = queue.queue?.length || 0;
queueEntry.completed_tasks = queue.queue?.filter((i: any) => i.status === 'completed').length || 0;
queueEntry.total_tasks = queue.tasks?.length || 0;
queueEntry.completed_tasks = queue.tasks?.filter((i: any) => i.status === 'completed').length || 0;
writeFileSync(indexPath, JSON.stringify(index, null, 2));
}
} catch {
@@ -151,15 +153,29 @@ function getIssueDetail(issuesDir: string, issueId: string) {
}
function enrichIssues(issues: any[], issuesDir: string) {
return issues.map(issue => ({
...issue,
solution_count: readSolutionsJsonl(issuesDir, issue.id).length
}));
return issues.map(issue => {
const solutions = readSolutionsJsonl(issuesDir, issue.id);
let taskCount = 0;
// Get task count from bound solution
if (issue.bound_solution_id) {
const boundSol = solutions.find(s => s.id === issue.bound_solution_id);
if (boundSol?.tasks) {
taskCount = boundSol.tasks.length;
}
}
return {
...issue,
solution_count: solutions.length,
task_count: taskCount
};
});
}
function groupQueueByExecutionGroup(queue: any) {
const groups: { [key: string]: any[] } = {};
for (const item of queue.queue || []) {
for (const item of queue.tasks || []) {
const groupId = item.execution_group || 'ungrouped';
if (!groups[groupId]) groups[groupId] = [];
groups[groupId].push(item);
@@ -171,7 +187,7 @@ function groupQueueByExecutionGroup(queue: any) {
id,
type: id.startsWith('P') ? 'parallel' : id.startsWith('S') ? 'sequential' : 'unknown',
task_count: items.length,
tasks: items.map(i => i.queue_id)
tasks: items.map(i => i.item_id)
})).sort((a, b) => {
const aFirst = groups[a.id]?.[0]?.execution_order || 0;
const bFirst = groups[b.id]?.[0]?.execution_order || 0;
@@ -229,20 +245,20 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
}
const queue = readQueue(issuesDir);
const groupItems = queue.queue.filter((item: any) => item.execution_group === groupId);
const otherItems = queue.queue.filter((item: any) => item.execution_group !== groupId);
const groupItems = queue.tasks.filter((item: any) => item.execution_group === groupId);
const otherItems = queue.tasks.filter((item: any) => item.execution_group !== groupId);
if (groupItems.length === 0) return { error: `No items in group ${groupId}` };
const groupQueueIds = new Set(groupItems.map((i: any) => i.queue_id));
if (groupQueueIds.size !== new Set(newOrder).size) {
const groupItemIds = new Set(groupItems.map((i: any) => i.item_id));
if (groupItemIds.size !== new Set(newOrder).size) {
return { error: 'newOrder must contain all group items' };
}
for (const id of newOrder) {
if (!groupQueueIds.has(id)) return { error: `Invalid queue_id: ${id}` };
if (!groupItemIds.has(id)) return { error: `Invalid item_id: ${id}` };
}
const itemMap = new Map(groupItems.map((i: any) => [i.queue_id, i]));
const itemMap = new Map(groupItems.map((i: any) => [i.item_id, i]));
const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx }));
const newQueue = [...otherItems, ...reorderedItems].sort((a, b) => {
const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999');
@@ -255,7 +271,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
});
newQueue.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; });
queue.queue = newQueue;
queue.tasks = newQueue;
writeQueue(issuesDir, queue);
return { success: true, groupId, reordered: newOrder.length };

View File

@@ -6,7 +6,7 @@
// ========== Issue State ==========
var issueData = {
issues: [],
queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} },
queue: { tasks: [], 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 = { queue: [], conflicts: [], execution_groups: [], grouped_items: {} };
issueData.queue = { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} };
}
}
@@ -360,7 +360,7 @@ function filterIssuesByStatus(status) {
// ========== Queue Section ==========
function renderQueueSection() {
const queue = issueData.queue;
const queueItems = queue.queue || [];
const queueItems = queue.tasks || [];
const metadata = queue._metadata || {};
// Check if queue is empty
@@ -530,10 +530,10 @@ function renderQueueItem(item, index, total) {
return `
<div class="queue-item ${statusColors[item.status] || ''}"
draggable="true"
data-queue-id="${item.queue_id}"
data-item-id="${item.item_id}"
data-group-id="${item.execution_group}"
onclick="openQueueItemDetail('${item.queue_id}')">
<span class="queue-item-id font-mono text-xs">${item.queue_id}</span>
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>
<span class="queue-item-priority" style="opacity: ${item.semantic_priority || 0.5}">
@@ -586,12 +586,12 @@ function handleIssueDragStart(e) {
const item = e.target.closest('.queue-item');
if (!item) return;
issueDragState.dragging = item.dataset.queueId;
issueDragState.dragging = item.dataset.itemId;
issueDragState.groupId = item.dataset.groupId;
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', item.dataset.queueId);
e.dataTransfer.setData('text/plain', item.dataset.itemId);
}
function handleIssueDragEnd(e) {
@@ -610,7 +610,7 @@ function handleIssueDragOver(e) {
e.preventDefault();
const target = e.target.closest('.queue-item');
if (!target || target.dataset.queueId === issueDragState.dragging) return;
if (!target || target.dataset.itemId === issueDragState.dragging) return;
// Only allow drag within same group
if (target.dataset.groupId !== issueDragState.groupId) {
@@ -635,7 +635,7 @@ function handleIssueDrop(e) {
// Get new order
const items = Array.from(container.querySelectorAll('.queue-item'));
const draggedItem = items.find(i => i.dataset.queueId === issueDragState.dragging);
const draggedItem = items.find(i => i.dataset.itemId === issueDragState.dragging);
const targetIndex = items.indexOf(target);
const draggedIndex = items.indexOf(draggedItem);
@@ -649,7 +649,7 @@ function handleIssueDrop(e) {
}
// Get new order and save
const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.queueId);
const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.itemId);
saveQueueOrder(issueDragState.groupId, newOrder);
}
@@ -767,7 +767,7 @@ function renderIssueDetailPanel(issue) {
<div class="flex items-center justify-between">
<span class="font-mono text-sm">${task.id}</span>
<select class="task-status-select" onchange="updateTaskStatus('${issue.id}', '${task.id}', this.value)">
${['pending', 'ready', 'in_progress', 'completed', 'failed', 'paused', 'skipped'].map(s =>
${['pending', 'ready', 'executing', 'completed', 'failed', 'blocked', 'paused', 'skipped'].map(s =>
`<option value="${s}" ${task.status === s ? 'selected' : ''}>${s}</option>`
).join('')}
</select>
@@ -1145,8 +1145,8 @@ function escapeHtml(text) {
return div.innerHTML;
}
function openQueueItemDetail(queueId) {
const item = issueData.queue.queue?.find(q => q.queue_id === queueId);
function openQueueItemDetail(itemId) {
const item = issueData.queue.tasks?.find(q => q.item_id === itemId);
if (item) {
openIssueDetail(item.issue_id);
}