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('');
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')) {
const promptsPath = join(sourceDir, '.codex', 'prompts');
const codexPath = join(sourceDir, '.codex');
// Show prompts info
const promptsPath = join(codexPath, 'prompts');
if (existsSync(promptsPath)) {
const promptFiles = readdirSync(promptsPath, { recursive: true });
info(` └─ .codex/prompts: ${promptFiles.length} files (workflow execute, lite-execute)`);
const promptFiles = readdirSync(promptsPath, { recursive: true }).filter(f =>
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(chalk.gray(`Manifest: ${basename(manifestPath)}`));
// Add codex prompts info if installed
// Add codex components info if installed
if (availableDirs.includes('.codex')) {
const codexPath = join(installPath, '.codex');
summaryLines.push('');
summaryLines.push(chalk.cyan('Codex Prompts: ✓ Installed'));
summaryLines.push(chalk.gray(` Path: ${join(installPath, '.codex', 'prompts')}`));
summaryLines.push(chalk.cyan('Codex Components:'));
// 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({

View File

@@ -339,41 +339,58 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
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
if (pathname === '/api/queue' && req.method === 'GET') {
const normalizedPath = normalizeQueuePath(pathname);
// ===== 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));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(queue));
return true;
}
// GET /api/queue/history - Get queue history (all queues from index)
if (pathname === '/api/queue/history' && req.method === 'GET') {
// GET /api/queue/history or /api/issues/queue/history - Get queue history (all queues from index)
if (normalizedPath === '/api/queue/history' && req.method === 'GET') {
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
if (!existsSync(indexPath)) {
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;
}
try {
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.end(JSON.stringify(index));
} catch {
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;
}
// GET /api/queue/:id - Get specific queue by ID
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge'];
// GET /api/queue/:id or /api/issues/queue/:id - Get specific queue by ID
const queueDetailMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)$/);
const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge', 'activate'];
if (queueDetailMatch && req.method === 'GET' && !reservedQueuePaths.includes(queueDetailMatch[1])) {
const queueId = queueDetailMatch[1];
const queuesDir = join(issuesDir, 'queues');
@@ -396,8 +413,55 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// POST /api/queue/switch - Switch active queue
if (pathname === '/api/queue/switch' && req.method === 'POST') {
// POST /api/queue/activate or /api/issues/queue/activate - Activate one or more queues (multi-queue support)
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) => {
const { queueId } = body;
if (!queueId) return { error: 'queueId required' };
@@ -413,12 +477,18 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
try {
const index = existsSync(indexPath)
? 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_ids = [queueId]; // Also update multi-queue array
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) {
return { error: 'Failed to switch queue' };
}
@@ -426,22 +496,43 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// POST /api/queue/deactivate - Deactivate current queue (set active to null)
if (pathname === '/api/queue/deactivate' && req.method === 'POST') {
// POST /api/queue/deactivate or /api/issues/queue/deactivate - Deactivate queue(s)
if (normalizedPath === '/api/queue/deactivate' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { queueId } = body; // Optional: specific queue to deactivate
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
try {
const index = existsSync(indexPath)
? 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));
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) {
return { error: 'Failed to deactivate queue' };
}
@@ -449,8 +540,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// POST /api/queue/reorder - Reorder queue items (supports both solutions and tasks)
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
// POST /api/queue/reorder or /api/issues/queue/reorder - Reorder queue items (supports both solutions and tasks)
if (normalizedPath === '/api/queue/reorder' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { groupId, newOrder } = body;
if (!groupId || !Array.isArray(newOrder)) {
@@ -501,8 +592,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// DELETE /api/queue/:queueId/item/:itemId - Delete item from queue
const queueItemDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/);
// DELETE /api/queue/:queueId/item/:itemId or /api/issues/queue/:queueId/item/:itemId
const queueItemDeleteMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/);
if (queueItemDeleteMatch && req.method === 'DELETE') {
const queueId = queueItemDeleteMatch[1];
const itemId = decodeURIComponent(queueItemDeleteMatch[2]);
@@ -576,8 +667,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// DELETE /api/queue/:queueId - Delete entire queue
const queueDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
// DELETE /api/queue/:queueId or /api/issues/queue/:queueId - Delete entire queue
const queueDeleteMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)$/);
if (queueDeleteMatch && req.method === 'DELETE') {
const queueId = queueDeleteMatch[1];
const queuesDir = join(issuesDir, 'queues');
@@ -618,8 +709,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// POST /api/queue/merge - Merge source queue into target queue
if (pathname === '/api/queue/merge' && req.method === 'POST') {
// POST /api/queue/merge or /api/issues/queue/merge - Merge source queue into target queue
if (normalizedPath === '/api/queue/merge' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { sourceQueueId, targetQueueId } = body;
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
if (pathname === '/api/queue/split' && req.method === 'POST') {
if (normalizedPath === '/api/queue/split' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { sourceQueueId, itemIds } = body;
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>
<div class="queue-card-badges">
${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>