feat(issue-manager): update queue status display logic in renderQueueCard function

This commit is contained in:
catlog22
2026-01-22 22:33:44 +08:00
parent 2819f3597f
commit f5b6bb97bc
3 changed files with 171 additions and 37 deletions

View File

@@ -125,12 +125,33 @@ export async function installCommand(options: InstallOptions): Promise<void> {
console.log(''); console.log('');
info(`Found ${availableDirs.length} directories to install: ${availableDirs.join(', ')}`); info(`Found ${availableDirs.length} directories to install: ${availableDirs.join(', ')}`);
// Show what will be installed including .codex/prompts // Show what will be installed including .codex subdirectories
if (availableDirs.includes('.codex')) { if (availableDirs.includes('.codex')) {
const promptsPath = join(sourceDir, '.codex', 'prompts'); const codexPath = join(sourceDir, '.codex');
// Show prompts info
const promptsPath = join(codexPath, 'prompts');
if (existsSync(promptsPath)) { if (existsSync(promptsPath)) {
const promptFiles = readdirSync(promptsPath, { recursive: true }); const promptFiles = readdirSync(promptsPath, { recursive: true }).filter(f =>
info(` └─ .codex/prompts: ${promptFiles.length} files (workflow execute, lite-execute)`); statSync(join(promptsPath, f.toString())).isFile()
);
info(` └─ .codex/prompts: ${promptFiles.length} files`);
}
// Show agents info
const agentsPath = join(codexPath, 'agents');
if (existsSync(agentsPath)) {
const agentFiles = readdirSync(agentsPath).filter(f => f.endsWith('.md'));
info(` └─ .codex/agents: ${agentFiles.length} agent definitions`);
}
// Show skills info
const skillsPath = join(codexPath, 'skills');
if (existsSync(skillsPath)) {
const skillDirs = readdirSync(skillsPath).filter(f =>
statSync(join(skillsPath, f)).isDirectory()
);
info(` └─ .codex/skills: ${skillDirs.length} skills`);
} }
} }
@@ -272,11 +293,33 @@ export async function installCommand(options: InstallOptions): Promise<void> {
summaryLines.push(''); summaryLines.push('');
summaryLines.push(chalk.gray(`Manifest: ${basename(manifestPath)}`)); summaryLines.push(chalk.gray(`Manifest: ${basename(manifestPath)}`));
// Add codex prompts info if installed // Add codex components info if installed
if (availableDirs.includes('.codex')) { if (availableDirs.includes('.codex')) {
const codexPath = join(installPath, '.codex');
summaryLines.push(''); summaryLines.push('');
summaryLines.push(chalk.cyan('Codex Prompts: ✓ Installed')); summaryLines.push(chalk.cyan('Codex Components:'));
summaryLines.push(chalk.gray(` Path: ${join(installPath, '.codex', 'prompts')}`));
// Prompts
const promptsPath = join(codexPath, 'prompts');
if (existsSync(promptsPath)) {
summaryLines.push(chalk.gray(` ✓ prompts: ${promptsPath}`));
}
// Agents
const agentsPath = join(codexPath, 'agents');
if (existsSync(agentsPath)) {
const agentCount = readdirSync(agentsPath).filter(f => f.endsWith('.md')).length;
summaryLines.push(chalk.gray(` ✓ agents: ${agentCount} definitions`));
}
// Skills
const skillsPath = join(codexPath, 'skills');
if (existsSync(skillsPath)) {
const skillCount = readdirSync(skillsPath).filter(f =>
statSync(join(skillsPath, f)).isDirectory()
).length;
summaryLines.push(chalk.gray(` ✓ skills: ${skillCount} skills`));
}
} }
summaryBox({ summaryBox({

View File

@@ -339,41 +339,58 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
const issuesDir = join(projectPath, '.workflow', 'issues'); const issuesDir = join(projectPath, '.workflow', 'issues');
// ===== Queue Routes (top-level /api/queue) ===== // ===== Helper: Normalize queue path (supports both /api/queue/* and /api/issues/queue/*) =====
const normalizeQueuePath = (path: string): string | null => {
if (path.startsWith('/api/issues/queue')) {
return path.replace('/api/issues/queue', '/api/queue');
}
if (path.startsWith('/api/queue')) {
return path;
}
return null;
};
// GET /api/queue - Get execution queue const normalizedPath = normalizeQueuePath(pathname);
if (pathname === '/api/queue' && req.method === 'GET') {
// ===== Queue Routes (supports both /api/queue/* and /api/issues/queue/*) =====
// GET /api/queue or /api/issues/queue - Get execution queue
if ((normalizedPath === '/api/queue') && req.method === 'GET') {
const queue = groupQueueByExecutionGroup(readQueue(issuesDir)); const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(queue)); res.end(JSON.stringify(queue));
return true; return true;
} }
// GET /api/queue/history - Get queue history (all queues from index) // GET /api/queue/history or /api/issues/queue/history - Get queue history (all queues from index)
if (pathname === '/api/queue/history' && req.method === 'GET') { if (normalizedPath === '/api/queue/history' && req.method === 'GET') {
const queuesDir = join(issuesDir, 'queues'); const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json'); const indexPath = join(queuesDir, 'index.json');
if (!existsSync(indexPath)) { if (!existsSync(indexPath)) {
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ queues: [], active_queue_id: null })); res.end(JSON.stringify({ queues: [], active_queue_id: null, active_queue_ids: [] }));
return true; return true;
} }
try { try {
const index = JSON.parse(readFileSync(indexPath, 'utf8')); const index = JSON.parse(readFileSync(indexPath, 'utf8'));
// Ensure active_queue_ids is always returned for multi-queue support
if (!index.active_queue_ids) {
index.active_queue_ids = index.active_queue_id ? [index.active_queue_id] : [];
}
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(index)); res.end(JSON.stringify(index));
} catch { } catch {
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ queues: [], active_queue_id: null })); res.end(JSON.stringify({ queues: [], active_queue_id: null, active_queue_ids: [] }));
} }
return true; return true;
} }
// GET /api/queue/:id - Get specific queue by ID // GET /api/queue/:id or /api/issues/queue/:id - Get specific queue by ID
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/); const queueDetailMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)$/);
const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge']; const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge', 'activate'];
if (queueDetailMatch && req.method === 'GET' && !reservedQueuePaths.includes(queueDetailMatch[1])) { if (queueDetailMatch && req.method === 'GET' && !reservedQueuePaths.includes(queueDetailMatch[1])) {
const queueId = queueDetailMatch[1]; const queueId = queueDetailMatch[1];
const queuesDir = join(issuesDir, 'queues'); const queuesDir = join(issuesDir, 'queues');
@@ -396,8 +413,55 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true; return true;
} }
// POST /api/queue/switch - Switch active queue // POST /api/queue/activate or /api/issues/queue/activate - Activate one or more queues (multi-queue support)
if (pathname === '/api/queue/switch' && req.method === 'POST') { if (normalizedPath === '/api/queue/activate' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { queueId, queueIds } = body;
// Support both single queueId and array queueIds
const idsToActivate: string[] = queueIds
? (Array.isArray(queueIds) ? queueIds : [queueIds])
: (queueId ? [queueId] : []);
if (idsToActivate.length === 0) {
return { error: 'queueId or queueIds required' };
}
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
// Validate all queue IDs exist
for (const id of idsToActivate) {
const queueFilePath = join(queuesDir, `${id}.json`);
if (!existsSync(queueFilePath)) {
return { error: `Queue ${id} not found` };
}
}
try {
const index = existsSync(indexPath)
? JSON.parse(readFileSync(indexPath, 'utf8'))
: { active_queue_id: null, active_queue_ids: [], queues: [] };
index.active_queue_ids = idsToActivate;
index.active_queue_id = idsToActivate[0] || null; // Backward compat
writeFileSync(indexPath, JSON.stringify(index, null, 2));
return {
success: true,
active_queue_ids: idsToActivate,
active_queue_id: idsToActivate[0] || null // Backward compat
};
} catch (err) {
return { error: 'Failed to activate queue(s)' };
}
});
return true;
}
// POST /api/queue/switch or /api/issues/queue/switch - Switch active queue (legacy, single queue)
if (normalizedPath === '/api/queue/switch' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => { handlePostRequest(req, res, async (body: any) => {
const { queueId } = body; const { queueId } = body;
if (!queueId) return { error: 'queueId required' }; if (!queueId) return { error: 'queueId required' };
@@ -413,12 +477,18 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
try { try {
const index = existsSync(indexPath) const index = existsSync(indexPath)
? JSON.parse(readFileSync(indexPath, 'utf8')) ? JSON.parse(readFileSync(indexPath, 'utf8'))
: { active_queue_id: null, queues: [] }; : { active_queue_id: null, active_queue_ids: [], queues: [] };
index.active_queue_id = queueId; index.active_queue_id = queueId;
index.active_queue_ids = [queueId]; // Also update multi-queue array
writeFileSync(indexPath, JSON.stringify(index, null, 2)); writeFileSync(indexPath, JSON.stringify(index, null, 2));
return { success: true, active_queue_id: queueId }; return {
success: true,
active_queue_id: queueId,
active_queue_ids: [queueId]
};
} catch (err) { } catch (err) {
return { error: 'Failed to switch queue' }; return { error: 'Failed to switch queue' };
} }
@@ -426,22 +496,43 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true; return true;
} }
// POST /api/queue/deactivate - Deactivate current queue (set active to null) // POST /api/queue/deactivate or /api/issues/queue/deactivate - Deactivate queue(s)
if (pathname === '/api/queue/deactivate' && req.method === 'POST') { if (normalizedPath === '/api/queue/deactivate' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => { handlePostRequest(req, res, async (body: any) => {
const { queueId } = body; // Optional: specific queue to deactivate
const queuesDir = join(issuesDir, 'queues'); const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json'); const indexPath = join(queuesDir, 'index.json');
try { try {
const index = existsSync(indexPath) const index = existsSync(indexPath)
? JSON.parse(readFileSync(indexPath, 'utf8')) ? JSON.parse(readFileSync(indexPath, 'utf8'))
: { active_queue_id: null, queues: [] }; : { active_queue_id: null, active_queue_ids: [], queues: [] };
const currentActiveIds = index.active_queue_ids || (index.active_queue_id ? [index.active_queue_id] : []);
let deactivatedIds: string[] = [];
let remainingIds: string[] = [];
if (queueId) {
// Deactivate specific queue
deactivatedIds = currentActiveIds.includes(queueId) ? [queueId] : [];
remainingIds = currentActiveIds.filter((id: string) => id !== queueId);
} else {
// Deactivate all
deactivatedIds = [...currentActiveIds];
remainingIds = [];
}
index.active_queue_ids = remainingIds;
index.active_queue_id = remainingIds[0] || null; // Backward compat
const previousActiveId = index.active_queue_id;
index.active_queue_id = null;
writeFileSync(indexPath, JSON.stringify(index, null, 2)); writeFileSync(indexPath, JSON.stringify(index, null, 2));
return { success: true, previous_active_id: previousActiveId }; return {
success: true,
deactivated_queue_ids: deactivatedIds,
active_queue_ids: remainingIds,
active_queue_id: remainingIds[0] || null // Backward compat
};
} catch (err) { } catch (err) {
return { error: 'Failed to deactivate queue' }; return { error: 'Failed to deactivate queue' };
} }
@@ -449,8 +540,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true; return true;
} }
// POST /api/queue/reorder - Reorder queue items (supports both solutions and tasks) // POST /api/queue/reorder or /api/issues/queue/reorder - Reorder queue items (supports both solutions and tasks)
if (pathname === '/api/queue/reorder' && req.method === 'POST') { if (normalizedPath === '/api/queue/reorder' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => { handlePostRequest(req, res, async (body: any) => {
const { groupId, newOrder } = body; const { groupId, newOrder } = body;
if (!groupId || !Array.isArray(newOrder)) { if (!groupId || !Array.isArray(newOrder)) {
@@ -501,8 +592,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true; return true;
} }
// DELETE /api/queue/:queueId/item/:itemId - Delete item from queue // DELETE /api/queue/:queueId/item/:itemId or /api/issues/queue/:queueId/item/:itemId
const queueItemDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/); const queueItemDeleteMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/);
if (queueItemDeleteMatch && req.method === 'DELETE') { if (queueItemDeleteMatch && req.method === 'DELETE') {
const queueId = queueItemDeleteMatch[1]; const queueId = queueItemDeleteMatch[1];
const itemId = decodeURIComponent(queueItemDeleteMatch[2]); const itemId = decodeURIComponent(queueItemDeleteMatch[2]);
@@ -576,8 +667,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true; return true;
} }
// DELETE /api/queue/:queueId - Delete entire queue // DELETE /api/queue/:queueId or /api/issues/queue/:queueId - Delete entire queue
const queueDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)$/); const queueDeleteMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)$/);
if (queueDeleteMatch && req.method === 'DELETE') { if (queueDeleteMatch && req.method === 'DELETE') {
const queueId = queueDeleteMatch[1]; const queueId = queueDeleteMatch[1];
const queuesDir = join(issuesDir, 'queues'); const queuesDir = join(issuesDir, 'queues');
@@ -618,8 +709,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true; return true;
} }
// POST /api/queue/merge - Merge source queue into target queue // POST /api/queue/merge or /api/issues/queue/merge - Merge source queue into target queue
if (pathname === '/api/queue/merge' && req.method === 'POST') { if (normalizedPath === '/api/queue/merge' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => { handlePostRequest(req, res, async (body: any) => {
const { sourceQueueId, targetQueueId } = body; const { sourceQueueId, targetQueueId } = body;
if (!sourceQueueId || !targetQueueId) { if (!sourceQueueId || !targetQueueId) {
@@ -757,7 +848,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
} }
// POST /api/queue/split - Split items from source queue into a new queue // POST /api/queue/split - Split items from source queue into a new queue
if (pathname === '/api/queue/split' && req.method === 'POST') { if (normalizedPath === '/api/queue/split' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => { handlePostRequest(req, res, async (body: any) => {
const { sourceQueueId, itemIds } = body; const { sourceQueueId, itemIds } = body;
if (!sourceQueueId || !itemIds || !Array.isArray(itemIds) || itemIds.length === 0) { if (!sourceQueueId || !itemIds || !Array.isArray(itemIds) || itemIds.length === 0) {

View File

@@ -711,7 +711,7 @@ function renderQueueCard(queue, isActive) {
<span class="queue-card-id font-mono">${safeQueueId}</span> <span class="queue-card-id font-mono">${safeQueueId}</span>
<div class="queue-card-badges"> <div class="queue-card-badges">
${isActive ? '<span class="queue-active-badge">Active</span>' : ''} ${isActive ? '<span class="queue-active-badge">Active</span>' : ''}
<span class="queue-status-badge ${statusClass}">${queue.status || 'unknown'}</span> ${!isActive || queue.status !== 'active' ? `<span class="queue-status-badge ${statusClass}">${queue.status || 'unknown'}</span>` : ''}
</div> </div>
</div> </div>