mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat(issue-manager): update queue status display logic in renderQueueCard function
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user