diff --git a/.claude/commands/issue/manage.md b/.claude/commands/issue/manage.md new file mode 100644 index 00000000..eb5794e3 --- /dev/null +++ b/.claude/commands/issue/manage.md @@ -0,0 +1,865 @@ +--- +name: manage +description: Interactive issue management (CRUD) via ccw cli endpoints with menu-driven interface +argument-hint: "[issue-id] [--action list|view|edit|delete|bulk]" +allowed-tools: TodoWrite(*), Bash(*), Read(*), Write(*), AskUserQuestion(*), Task(*) +--- + +# Issue Manage Command (/issue:manage) + +## Overview + +Interactive menu-driven interface for issue management using `ccw issue` CLI endpoints: +- **List**: Browse and filter issues +- **View**: Detailed issue inspection +- **Edit**: Modify issue fields +- **Delete**: Remove issues +- **Bulk**: Batch operations on multiple issues + +## CLI Endpoints Reference + +```bash +# Core endpoints (ccw issue) +ccw issue list # List all issues +ccw issue list --json # Get issue details +ccw issue status # Detailed status +ccw issue init --title "..." # Create issue +ccw issue task --title "..." # Add task + +# Queue management +ccw issue queue # List queue +ccw issue queue add # Add to queue +ccw issue next # Get next task +ccw issue done # Complete task +``` + +## Usage + +```bash +# Interactive mode (menu-driven) +/issue:manage + +# Direct to specific issue +/issue:manage GH-123 + +# Direct action +/issue:manage --action list +/issue:manage GH-123 --action edit +``` + +## Implementation + +### Phase 1: Entry Point + +```javascript +const issueId = parseIssueId(userInput); +const action = flags.action; + +// Show main menu if no action specified +if (!action) { + await showMainMenu(issueId); +} else { + await executeAction(action, issueId); +} +``` + +### Phase 2: Main Menu + +```javascript +async function showMainMenu(preselectedIssue = null) { + // Fetch current issues summary + const issuesResult = Bash('ccw issue list --json 2>/dev/null || echo "[]"'); + const issues = JSON.parse(issuesResult) || []; + + const queueResult = Bash('ccw issue status --json 2>/dev/null'); + const queueStatus = JSON.parse(queueResult || '{}'); + + console.log(` +## Issue Management Dashboard + +**Total Issues**: ${issues.length} +**Queue Status**: ${queueStatus.queue?.total_tasks || 0} tasks (${queueStatus.queue?.pending_count || 0} pending) + +### Quick Stats +- Registered: ${issues.filter(i => i.status === 'registered').length} +- Planned: ${issues.filter(i => i.status === 'planned').length} +- Executing: ${issues.filter(i => i.status === 'executing').length} +- Completed: ${issues.filter(i => i.status === 'completed').length} +`); + + const answer = AskUserQuestion({ + questions: [{ + question: 'What would you like to do?', + header: 'Action', + multiSelect: false, + options: [ + { label: 'List Issues', description: 'Browse all issues with filters' }, + { label: 'View Issue', description: 'Detailed view of specific issue' }, + { label: 'Create Issue', description: 'Add new issue from text or GitHub' }, + { label: 'Edit Issue', description: 'Modify issue fields' }, + { label: 'Delete Issue', description: 'Remove issue(s)' }, + { label: 'Bulk Operations', description: 'Batch actions on multiple issues' } + ] + }] + }); + + const selected = parseAnswer(answer); + + switch (selected) { + case 'List Issues': + await listIssuesInteractive(); + break; + case 'View Issue': + await viewIssueInteractive(preselectedIssue); + break; + case 'Create Issue': + await createIssueInteractive(); + break; + case 'Edit Issue': + await editIssueInteractive(preselectedIssue); + break; + case 'Delete Issue': + await deleteIssueInteractive(preselectedIssue); + break; + case 'Bulk Operations': + await bulkOperationsInteractive(); + break; + } +} +``` + +### Phase 3: List Issues + +```javascript +async function listIssuesInteractive() { + // Ask for filter + const filterAnswer = AskUserQuestion({ + questions: [{ + question: 'Filter issues by status?', + header: 'Filter', + multiSelect: true, + options: [ + { label: 'All', description: 'Show all issues' }, + { label: 'Registered', description: 'New, unplanned issues' }, + { label: 'Planned', description: 'Issues with bound solutions' }, + { label: 'Queued', description: 'In execution queue' }, + { label: 'Executing', description: 'Currently being worked on' }, + { label: 'Completed', description: 'Finished issues' }, + { label: 'Failed', description: 'Failed issues' } + ] + }] + }); + + const filters = parseMultiAnswer(filterAnswer); + + // Fetch and filter issues + const result = Bash('ccw issue list --json'); + let issues = JSON.parse(result) || []; + + if (!filters.includes('All')) { + const statusMap = { + 'Registered': 'registered', + 'Planned': 'planned', + 'Queued': 'queued', + 'Executing': 'executing', + 'Completed': 'completed', + 'Failed': 'failed' + }; + const allowedStatuses = filters.map(f => statusMap[f]).filter(Boolean); + issues = issues.filter(i => allowedStatuses.includes(i.status)); + } + + if (issues.length === 0) { + console.log('No issues found matching filters.'); + return showMainMenu(); + } + + // Display issues table + console.log(` +## Issues (${issues.length}) + +| ID | Status | Priority | Title | +|----|--------|----------|-------| +${issues.map(i => `| ${i.id} | ${i.status} | P${i.priority} | ${i.title.substring(0, 40)} |`).join('\n')} +`); + + // Ask for action on issue + const actionAnswer = AskUserQuestion({ + questions: [{ + question: 'Select an issue to view/edit, or return to menu:', + header: 'Select', + multiSelect: false, + options: [ + ...issues.slice(0, 10).map(i => ({ + label: i.id, + description: i.title.substring(0, 50) + })), + { label: 'Back to Menu', description: 'Return to main menu' } + ] + }] + }); + + const selected = parseAnswer(actionAnswer); + + if (selected === 'Back to Menu') { + return showMainMenu(); + } + + // View selected issue + await viewIssueInteractive(selected); +} +``` + +### Phase 4: View Issue + +```javascript +async function viewIssueInteractive(issueId) { + if (!issueId) { + // Ask for issue ID + const issues = JSON.parse(Bash('ccw issue list --json') || '[]'); + + const idAnswer = AskUserQuestion({ + questions: [{ + question: 'Select issue to view:', + header: 'Issue', + multiSelect: false, + options: issues.slice(0, 10).map(i => ({ + label: i.id, + description: `${i.status} - ${i.title.substring(0, 40)}` + })) + }] + }); + + issueId = parseAnswer(idAnswer); + } + + // Fetch detailed status + const result = Bash(`ccw issue status ${issueId} --json`); + const data = JSON.parse(result); + + const issue = data.issue; + const solutions = data.solutions || []; + const bound = data.bound; + + console.log(` +## Issue: ${issue.id} + +**Title**: ${issue.title} +**Status**: ${issue.status} +**Priority**: P${issue.priority} +**Created**: ${issue.created_at} +**Updated**: ${issue.updated_at} + +### Context +${issue.context || 'No context provided'} + +### Solutions (${solutions.length}) +${solutions.length === 0 ? 'No solutions registered' : + solutions.map(s => `- ${s.is_bound ? '◉' : '○'} ${s.id}: ${s.tasks?.length || 0} tasks`).join('\n')} + +${bound ? `### Bound Solution: ${bound.id}\n**Tasks**: ${bound.tasks?.length || 0}` : ''} +`); + + // Show tasks if bound solution exists + if (bound?.tasks?.length > 0) { + console.log(` +### Tasks +| ID | Action | Scope | Title | +|----|--------|-------|-------| +${bound.tasks.map(t => `| ${t.id} | ${t.action} | ${t.scope?.substring(0, 20) || '-'} | ${t.title.substring(0, 30)} |`).join('\n')} +`); + } + + // Action menu + const actionAnswer = AskUserQuestion({ + questions: [{ + question: 'What would you like to do?', + header: 'Action', + multiSelect: false, + options: [ + { label: 'Edit Issue', description: 'Modify issue fields' }, + { label: 'Plan Issue', description: 'Generate solution (/issue:plan)' }, + { label: 'Add to Queue', description: 'Queue bound solution tasks' }, + { label: 'View Queue', description: 'See queue status' }, + { label: 'Delete Issue', description: 'Remove this issue' }, + { label: 'Back to Menu', description: 'Return to main menu' } + ] + }] + }); + + const action = parseAnswer(actionAnswer); + + switch (action) { + case 'Edit Issue': + await editIssueInteractive(issueId); + break; + case 'Plan Issue': + console.log(`Running: /issue:plan ${issueId}`); + // Invoke plan skill + break; + case 'Add to Queue': + Bash(`ccw issue queue add ${issueId}`); + console.log(`✓ Added ${issueId} tasks to queue`); + break; + case 'View Queue': + const queueOutput = Bash('ccw issue queue'); + console.log(queueOutput); + break; + case 'Delete Issue': + await deleteIssueInteractive(issueId); + break; + default: + return showMainMenu(); + } +} +``` + +### Phase 5: Edit Issue + +```javascript +async function editIssueInteractive(issueId) { + if (!issueId) { + const issues = JSON.parse(Bash('ccw issue list --json') || '[]'); + const idAnswer = AskUserQuestion({ + questions: [{ + question: 'Select issue to edit:', + header: 'Issue', + multiSelect: false, + options: issues.slice(0, 10).map(i => ({ + label: i.id, + description: `${i.status} - ${i.title.substring(0, 40)}` + })) + }] + }); + issueId = parseAnswer(idAnswer); + } + + // Get current issue data + const result = Bash(`ccw issue list ${issueId} --json`); + const issueData = JSON.parse(result); + const issue = issueData.issue || issueData; + + // Ask which field to edit + const fieldAnswer = AskUserQuestion({ + questions: [{ + question: 'Which field to edit?', + header: 'Field', + multiSelect: false, + options: [ + { label: 'Title', description: `Current: ${issue.title?.substring(0, 40)}` }, + { label: 'Priority', description: `Current: P${issue.priority}` }, + { label: 'Status', description: `Current: ${issue.status}` }, + { label: 'Context', description: 'Edit problem description' }, + { label: 'Labels', description: `Current: ${issue.labels?.join(', ') || 'none'}` }, + { label: 'Back', description: 'Return without changes' } + ] + }] + }); + + const field = parseAnswer(fieldAnswer); + + if (field === 'Back') { + return viewIssueInteractive(issueId); + } + + let updatePayload = {}; + + switch (field) { + case 'Title': + const titleAnswer = AskUserQuestion({ + questions: [{ + question: 'Enter new title (or select current to keep):', + header: 'Title', + multiSelect: false, + options: [ + { label: issue.title.substring(0, 50), description: 'Keep current title' } + ] + }] + }); + const newTitle = parseAnswer(titleAnswer); + if (newTitle && newTitle !== issue.title.substring(0, 50)) { + updatePayload.title = newTitle; + } + break; + + case 'Priority': + const priorityAnswer = AskUserQuestion({ + questions: [{ + question: 'Select priority:', + header: 'Priority', + multiSelect: false, + options: [ + { label: 'P1 - Critical', description: 'Production blocking' }, + { label: 'P2 - High', description: 'Major functionality' }, + { label: 'P3 - Medium', description: 'Normal priority (default)' }, + { label: 'P4 - Low', description: 'Minor issues' }, + { label: 'P5 - Trivial', description: 'Nice to have' } + ] + }] + }); + const priorityStr = parseAnswer(priorityAnswer); + updatePayload.priority = parseInt(priorityStr.charAt(1)); + break; + + case 'Status': + const statusAnswer = AskUserQuestion({ + questions: [{ + question: 'Select status:', + header: 'Status', + multiSelect: false, + options: [ + { label: 'registered', description: 'New issue, not yet planned' }, + { label: 'planning', description: 'Solution being generated' }, + { label: 'planned', description: 'Solution bound, ready for queue' }, + { label: 'queued', description: 'In execution queue' }, + { label: 'executing', description: 'Currently being worked on' }, + { label: 'completed', description: 'All tasks finished' }, + { label: 'failed', description: 'Execution failed' }, + { label: 'paused', description: 'Temporarily on hold' } + ] + }] + }); + updatePayload.status = parseAnswer(statusAnswer); + break; + + case 'Context': + console.log(`Current context:\n${issue.context || '(empty)'}\n`); + const contextAnswer = AskUserQuestion({ + questions: [{ + question: 'Enter new context (problem description):', + header: 'Context', + multiSelect: false, + options: [ + { label: 'Keep current', description: 'No changes' } + ] + }] + }); + const newContext = parseAnswer(contextAnswer); + if (newContext && newContext !== 'Keep current') { + updatePayload.context = newContext; + } + break; + + case 'Labels': + const labelsAnswer = AskUserQuestion({ + questions: [{ + question: 'Enter labels (comma-separated):', + header: 'Labels', + multiSelect: false, + options: [ + { label: issue.labels?.join(',') || '', description: 'Keep current labels' } + ] + }] + }); + const labelsStr = parseAnswer(labelsAnswer); + if (labelsStr) { + updatePayload.labels = labelsStr.split(',').map(l => l.trim()); + } + break; + } + + // Apply update if any + if (Object.keys(updatePayload).length > 0) { + // Read, update, write issues.jsonl + const issuesPath = '.workflow/issues/issues.jsonl'; + const allIssues = Bash(`cat "${issuesPath}"`) + .split('\n') + .filter(line => line.trim()) + .map(line => JSON.parse(line)); + + const idx = allIssues.findIndex(i => i.id === issueId); + if (idx !== -1) { + allIssues[idx] = { + ...allIssues[idx], + ...updatePayload, + updated_at: new Date().toISOString() + }; + + Write(issuesPath, allIssues.map(i => JSON.stringify(i)).join('\n')); + console.log(`✓ Updated ${issueId}: ${Object.keys(updatePayload).join(', ')}`); + } + } + + // Continue editing or return + const continueAnswer = AskUserQuestion({ + questions: [{ + question: 'Continue editing?', + header: 'Continue', + multiSelect: false, + options: [ + { label: 'Edit Another Field', description: 'Continue editing this issue' }, + { label: 'View Issue', description: 'See updated issue' }, + { label: 'Back to Menu', description: 'Return to main menu' } + ] + }] + }); + + const cont = parseAnswer(continueAnswer); + if (cont === 'Edit Another Field') { + await editIssueInteractive(issueId); + } else if (cont === 'View Issue') { + await viewIssueInteractive(issueId); + } else { + return showMainMenu(); + } +} +``` + +### Phase 6: Delete Issue + +```javascript +async function deleteIssueInteractive(issueId) { + if (!issueId) { + const issues = JSON.parse(Bash('ccw issue list --json') || '[]'); + const idAnswer = AskUserQuestion({ + questions: [{ + question: 'Select issue to delete:', + header: 'Delete', + multiSelect: false, + options: issues.slice(0, 10).map(i => ({ + label: i.id, + description: `${i.status} - ${i.title.substring(0, 40)}` + })) + }] + }); + issueId = parseAnswer(idAnswer); + } + + // Confirm deletion + const confirmAnswer = AskUserQuestion({ + questions: [{ + question: `Delete issue ${issueId}? This will also remove associated solutions.`, + header: 'Confirm', + multiSelect: false, + options: [ + { label: 'Delete', description: 'Permanently remove issue and solutions' }, + { label: 'Cancel', description: 'Keep issue' } + ] + }] + }); + + if (parseAnswer(confirmAnswer) !== 'Delete') { + console.log('Deletion cancelled.'); + return showMainMenu(); + } + + // Remove from issues.jsonl + const issuesPath = '.workflow/issues/issues.jsonl'; + const allIssues = Bash(`cat "${issuesPath}"`) + .split('\n') + .filter(line => line.trim()) + .map(line => JSON.parse(line)); + + const filtered = allIssues.filter(i => i.id !== issueId); + Write(issuesPath, filtered.map(i => JSON.stringify(i)).join('\n')); + + // Remove solutions file if exists + const solPath = `.workflow/issues/solutions/${issueId}.jsonl`; + Bash(`rm -f "${solPath}" 2>/dev/null || true`); + + // Remove from queue if present + const queuePath = '.workflow/issues/queue.json'; + if (Bash(`test -f "${queuePath}" && echo exists`) === 'exists') { + const queue = JSON.parse(Bash(`cat "${queuePath}"`)); + queue.queue = queue.queue.filter(q => q.issue_id !== issueId); + Write(queuePath, JSON.stringify(queue, null, 2)); + } + + console.log(`✓ Deleted issue ${issueId}`); + return showMainMenu(); +} +``` + +### Phase 7: Bulk Operations + +```javascript +async function bulkOperationsInteractive() { + const bulkAnswer = AskUserQuestion({ + questions: [{ + question: 'Select bulk operation:', + header: 'Bulk', + multiSelect: false, + options: [ + { label: 'Update Status', description: 'Change status of multiple issues' }, + { label: 'Update Priority', description: 'Change priority of multiple issues' }, + { label: 'Add Labels', description: 'Add labels to multiple issues' }, + { label: 'Delete Multiple', description: 'Remove multiple issues' }, + { label: 'Queue All Planned', description: 'Add all planned issues to queue' }, + { label: 'Retry All Failed', description: 'Reset all failed tasks to pending' }, + { label: 'Back', description: 'Return to main menu' } + ] + }] + }); + + const operation = parseAnswer(bulkAnswer); + + if (operation === 'Back') { + return showMainMenu(); + } + + // Get issues for selection + const allIssues = JSON.parse(Bash('ccw issue list --json') || '[]'); + + if (operation === 'Queue All Planned') { + const planned = allIssues.filter(i => i.status === 'planned' && i.bound_solution_id); + for (const issue of planned) { + Bash(`ccw issue queue add ${issue.id}`); + console.log(`✓ Queued ${issue.id}`); + } + console.log(`\n✓ Queued ${planned.length} issues`); + return showMainMenu(); + } + + if (operation === 'Retry All Failed') { + Bash('ccw issue retry'); + console.log('✓ Reset all failed tasks to pending'); + return showMainMenu(); + } + + // Multi-select issues + const selectAnswer = AskUserQuestion({ + questions: [{ + question: 'Select issues (multi-select):', + header: 'Select', + multiSelect: true, + options: allIssues.slice(0, 15).map(i => ({ + label: i.id, + description: `${i.status} - ${i.title.substring(0, 30)}` + })) + }] + }); + + const selectedIds = parseMultiAnswer(selectAnswer); + + if (selectedIds.length === 0) { + console.log('No issues selected.'); + return showMainMenu(); + } + + // Execute bulk operation + const issuesPath = '.workflow/issues/issues.jsonl'; + let issues = Bash(`cat "${issuesPath}"`) + .split('\n') + .filter(line => line.trim()) + .map(line => JSON.parse(line)); + + switch (operation) { + case 'Update Status': + const statusAnswer = AskUserQuestion({ + questions: [{ + question: 'Select new status:', + header: 'Status', + multiSelect: false, + options: [ + { label: 'registered', description: 'Reset to registered' }, + { label: 'paused', description: 'Pause issues' }, + { label: 'completed', description: 'Mark completed' } + ] + }] + }); + const newStatus = parseAnswer(statusAnswer); + issues = issues.map(i => + selectedIds.includes(i.id) + ? { ...i, status: newStatus, updated_at: new Date().toISOString() } + : i + ); + break; + + case 'Update Priority': + const prioAnswer = AskUserQuestion({ + questions: [{ + question: 'Select new priority:', + header: 'Priority', + multiSelect: false, + options: [ + { label: 'P1', description: 'Critical' }, + { label: 'P2', description: 'High' }, + { label: 'P3', description: 'Medium' }, + { label: 'P4', description: 'Low' }, + { label: 'P5', description: 'Trivial' } + ] + }] + }); + const newPrio = parseInt(parseAnswer(prioAnswer).charAt(1)); + issues = issues.map(i => + selectedIds.includes(i.id) + ? { ...i, priority: newPrio, updated_at: new Date().toISOString() } + : i + ); + break; + + case 'Add Labels': + const labelAnswer = AskUserQuestion({ + questions: [{ + question: 'Enter labels to add (comma-separated):', + header: 'Labels', + multiSelect: false, + options: [ + { label: 'bug', description: 'Bug fix' }, + { label: 'feature', description: 'New feature' }, + { label: 'urgent', description: 'Urgent priority' } + ] + }] + }); + const newLabels = parseAnswer(labelAnswer).split(',').map(l => l.trim()); + issues = issues.map(i => + selectedIds.includes(i.id) + ? { + ...i, + labels: [...new Set([...(i.labels || []), ...newLabels])], + updated_at: new Date().toISOString() + } + : i + ); + break; + + case 'Delete Multiple': + const confirmDelete = AskUserQuestion({ + questions: [{ + question: `Delete ${selectedIds.length} issues permanently?`, + header: 'Confirm', + multiSelect: false, + options: [ + { label: 'Delete All', description: 'Remove selected issues' }, + { label: 'Cancel', description: 'Keep issues' } + ] + }] + }); + if (parseAnswer(confirmDelete) === 'Delete All') { + issues = issues.filter(i => !selectedIds.includes(i.id)); + // Clean up solutions + for (const id of selectedIds) { + Bash(`rm -f ".workflow/issues/solutions/${id}.jsonl" 2>/dev/null || true`); + } + } else { + console.log('Deletion cancelled.'); + return showMainMenu(); + } + break; + } + + Write(issuesPath, issues.map(i => JSON.stringify(i)).join('\n')); + console.log(`✓ Updated ${selectedIds.length} issues`); + return showMainMenu(); +} +``` + +### Phase 8: Create Issue (Redirect) + +```javascript +async function createIssueInteractive() { + const typeAnswer = AskUserQuestion({ + questions: [{ + question: 'Create issue from:', + header: 'Source', + multiSelect: false, + options: [ + { label: 'GitHub URL', description: 'Import from GitHub issue' }, + { label: 'Text Description', description: 'Enter problem description' }, + { label: 'Quick Create', description: 'Just title and priority' } + ] + }] + }); + + const type = parseAnswer(typeAnswer); + + if (type === 'GitHub URL' || type === 'Text Description') { + console.log('Use /issue:new for structured issue creation'); + console.log('Example: /issue:new https://github.com/org/repo/issues/123'); + return showMainMenu(); + } + + // Quick create + const titleAnswer = AskUserQuestion({ + questions: [{ + question: 'Enter issue title:', + header: 'Title', + multiSelect: false, + options: [ + { label: 'Authentication Bug', description: 'Example title' } + ] + }] + }); + + const title = parseAnswer(titleAnswer); + + const prioAnswer = AskUserQuestion({ + questions: [{ + question: 'Select priority:', + header: 'Priority', + multiSelect: false, + options: [ + { label: 'P3 - Medium (Recommended)', description: 'Normal priority' }, + { label: 'P1 - Critical', description: 'Production blocking' }, + { label: 'P2 - High', description: 'Major functionality' } + ] + }] + }); + + const priority = parseInt(parseAnswer(prioAnswer).charAt(1)); + + // Generate ID and create + const id = `ISS-${Date.now()}`; + Bash(`ccw issue init ${id} --title "${title}" --priority ${priority}`); + + console.log(`✓ Created issue ${id}`); + await viewIssueInteractive(id); +} +``` + +## Helper Functions + +```javascript +function parseAnswer(answer) { + // Extract selected option from AskUserQuestion response + if (typeof answer === 'string') return answer; + if (answer.answers) { + const values = Object.values(answer.answers); + return values[0] || ''; + } + return ''; +} + +function parseMultiAnswer(answer) { + // Extract multiple selections + if (typeof answer === 'string') return answer.split(',').map(s => s.trim()); + if (answer.answers) { + const values = Object.values(answer.answers); + return values.flatMap(v => v.split(',').map(s => s.trim())); + } + return []; +} + +function parseFlags(input) { + const flags = {}; + const matches = input.matchAll(/--(\w+)\s+([^\s-]+)/g); + for (const match of matches) { + flags[match[1]] = match[2]; + } + return flags; +} + +function parseIssueId(input) { + const match = input.match(/^([A-Z]+-\d+|ISS-\d+|GH-\d+)/i); + return match ? match[1] : null; +} +``` + +## Error Handling + +| Error | Resolution | +|-------|------------| +| No issues found | Suggest creating with /issue:new | +| Issue not found | Show available issues, ask for correction | +| Invalid selection | Show error, re-prompt | +| Write failure | Check permissions, show error | +| Queue operation fails | Show ccw issue error, suggest fix | + +## Related Commands + +- `/issue:new` - Create structured issue +- `/issue:plan` - Plan solution for issue +- `/issue:queue` - Form execution queue +- `/issue:execute` - Execute queued tasks +- `ccw issue list` - CLI list command +- `ccw issue status` - CLI status command diff --git a/.claude/commands/issue/new.md b/.claude/commands/issue/new.md new file mode 100644 index 00000000..e781da30 --- /dev/null +++ b/.claude/commands/issue/new.md @@ -0,0 +1,366 @@ +--- +name: new +description: Create structured issue from GitHub URL or text description, extracting key elements into issues.jsonl +argument-hint: " [--priority 1-5] [--labels label1,label2]" +allowed-tools: TodoWrite(*), Bash(*), Read(*), Write(*), WebFetch(*), AskUserQuestion(*) +--- + +# Issue New Command (/issue:new) + +## Overview + +Creates a new structured issue from either: +1. **GitHub Issue URL** - Fetches and parses issue content via `gh` CLI +2. **Text Description** - Parses natural language into structured fields + +Outputs a well-formed issue entry to `.workflow/issues/issues.jsonl`. + +## Issue Structure + +```typescript +interface Issue { + id: string; // GH-123 or ISS-YYYYMMDD-HHMMSS + title: string; // Issue title (clear, concise) + status: 'registered'; // Initial status + priority: number; // 1 (critical) to 5 (low) + context: string; // Problem description + source: 'github' | 'text'; // Input source type + source_url?: string; // GitHub URL if applicable + labels?: string[]; // Categorization labels + + // Structured extraction + problem_statement: string; // What is the problem? + expected_behavior?: string; // What should happen? + actual_behavior?: string; // What actually happens? + affected_components?: string[];// Files/modules affected + reproduction_steps?: string[]; // Steps to reproduce + + // Metadata + bound_solution_id: null; + solution_count: 0; + created_at: string; + updated_at: string; +} +``` + +## Usage + +```bash +# From GitHub URL +/issue:new https://github.com/owner/repo/issues/123 + +# From text description +/issue:new "Login fails when password contains special characters. Expected: successful login. Actual: 500 error. Affects src/auth/*" + +# With options +/issue:new --priority 2 --labels "bug,auth" +``` + +## Implementation + +### Phase 1: Input Detection + +```javascript +const input = userInput.trim(); +const flags = parseFlags(userInput); // --priority, --labels + +// Detect input type +const isGitHubUrl = input.match(/github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/); +const isGitHubShort = input.match(/^#(\d+)$/); // #123 format + +let issueData = {}; + +if (isGitHubUrl || isGitHubShort) { + // GitHub issue - fetch via gh CLI + issueData = await fetchGitHubIssue(input); +} else { + // Text description - parse structure + issueData = await parseTextDescription(input); +} +``` + +### Phase 2: GitHub Issue Fetching + +```javascript +async function fetchGitHubIssue(urlOrNumber) { + let issueRef; + + if (urlOrNumber.startsWith('http')) { + // Extract owner/repo/number from URL + const match = urlOrNumber.match(/github\.com\/([\w-]+)\/([\w-]+)\/issues\/(\d+)/); + if (!match) throw new Error('Invalid GitHub URL'); + issueRef = `${match[1]}/${match[2]}#${match[3]}`; + } else { + // #123 format - use current repo + issueRef = urlOrNumber.replace('#', ''); + } + + // Fetch via gh CLI + const result = Bash(`gh issue view ${issueRef} --json number,title,body,labels,state,url`); + const ghIssue = JSON.parse(result); + + // Parse body for structure + const parsed = parseIssueBody(ghIssue.body); + + return { + id: `GH-${ghIssue.number}`, + title: ghIssue.title, + source: 'github', + source_url: ghIssue.url, + labels: ghIssue.labels.map(l => l.name), + context: ghIssue.body, + ...parsed + }; +} + +function parseIssueBody(body) { + // Extract structured sections from markdown body + const sections = {}; + + // Problem/Description + const problemMatch = body.match(/##?\s*(problem|description|issue)[:\s]*([\s\S]*?)(?=##|$)/i); + if (problemMatch) sections.problem_statement = problemMatch[2].trim(); + + // Expected behavior + const expectedMatch = body.match(/##?\s*(expected|should)[:\s]*([\s\S]*?)(?=##|$)/i); + if (expectedMatch) sections.expected_behavior = expectedMatch[2].trim(); + + // Actual behavior + const actualMatch = body.match(/##?\s*(actual|current)[:\s]*([\s\S]*?)(?=##|$)/i); + if (actualMatch) sections.actual_behavior = actualMatch[2].trim(); + + // Steps to reproduce + const stepsMatch = body.match(/##?\s*(steps|reproduce)[:\s]*([\s\S]*?)(?=##|$)/i); + if (stepsMatch) { + const stepsText = stepsMatch[2].trim(); + sections.reproduction_steps = stepsText + .split('\n') + .filter(line => line.match(/^\s*[\d\-\*]/)) + .map(line => line.replace(/^\s*[\d\.\-\*]\s*/, '').trim()); + } + + // Affected components (from file references) + const fileMatches = body.match(/`[^`]*\.(ts|js|tsx|jsx|py|go|rs)[^`]*`/g); + if (fileMatches) { + sections.affected_components = [...new Set(fileMatches.map(f => f.replace(/`/g, '')))]; + } + + // Fallback: use entire body as problem statement + if (!sections.problem_statement) { + sections.problem_statement = body.substring(0, 500); + } + + return sections; +} +``` + +### Phase 3: Text Description Parsing + +```javascript +async function parseTextDescription(text) { + // Generate unique ID + const id = `ISS-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`; + + // Extract structured elements using patterns + const result = { + id, + source: 'text', + title: '', + problem_statement: '', + expected_behavior: null, + actual_behavior: null, + affected_components: [], + reproduction_steps: [] + }; + + // Pattern: "Title. Description. Expected: X. Actual: Y. Affects: files" + const sentences = text.split(/\.(?=\s|$)/); + + // First sentence as title + result.title = sentences[0]?.trim() || 'Untitled Issue'; + + // Look for keywords + for (const sentence of sentences) { + const s = sentence.trim(); + + if (s.match(/^expected:?\s*/i)) { + result.expected_behavior = s.replace(/^expected:?\s*/i, ''); + } else if (s.match(/^actual:?\s*/i)) { + result.actual_behavior = s.replace(/^actual:?\s*/i, ''); + } else if (s.match(/^affects?:?\s*/i)) { + const components = s.replace(/^affects?:?\s*/i, '').split(/[,\s]+/); + result.affected_components = components.filter(c => c.includes('/') || c.includes('.')); + } else if (s.match(/^steps?:?\s*/i)) { + result.reproduction_steps = s.replace(/^steps?:?\s*/i, '').split(/[,;]/); + } else if (!result.problem_statement && s.length > 10) { + result.problem_statement = s; + } + } + + // Fallback problem statement + if (!result.problem_statement) { + result.problem_statement = text.substring(0, 300); + } + + return result; +} +``` + +### Phase 4: User Confirmation + +```javascript +// Show parsed data and ask for confirmation +console.log(` +## Parsed Issue + +**ID**: ${issueData.id} +**Title**: ${issueData.title} +**Source**: ${issueData.source}${issueData.source_url ? ` (${issueData.source_url})` : ''} + +### Problem Statement +${issueData.problem_statement} + +${issueData.expected_behavior ? `### Expected Behavior\n${issueData.expected_behavior}\n` : ''} +${issueData.actual_behavior ? `### Actual Behavior\n${issueData.actual_behavior}\n` : ''} +${issueData.affected_components?.length ? `### Affected Components\n${issueData.affected_components.map(c => `- ${c}`).join('\n')}\n` : ''} +${issueData.reproduction_steps?.length ? `### Reproduction Steps\n${issueData.reproduction_steps.map((s, i) => `${i+1}. ${s}`).join('\n')}\n` : ''} +`); + +// Ask user to confirm or edit +const answer = AskUserQuestion({ + questions: [{ + question: 'Create this issue?', + header: 'Confirm', + multiSelect: false, + options: [ + { label: 'Create', description: 'Save issue to issues.jsonl' }, + { label: 'Edit Title', description: 'Modify the issue title' }, + { label: 'Edit Priority', description: 'Change priority (1-5)' }, + { label: 'Cancel', description: 'Discard and exit' } + ] + }] +}); + +if (answer.includes('Cancel')) { + console.log('Issue creation cancelled.'); + return; +} + +if (answer.includes('Edit Title')) { + const titleAnswer = AskUserQuestion({ + questions: [{ + question: 'Enter new title:', + header: 'Title', + multiSelect: false, + options: [ + { label: issueData.title.substring(0, 40), description: 'Keep current' } + ] + }] + }); + // Handle custom input via "Other" + if (titleAnswer.customText) { + issueData.title = titleAnswer.customText; + } +} +``` + +### Phase 5: Write to JSONL + +```javascript +// Construct final issue object +const priority = flags.priority ? parseInt(flags.priority) : 3; +const labels = flags.labels ? flags.labels.split(',').map(l => l.trim()) : []; + +const newIssue = { + id: issueData.id, + title: issueData.title, + status: 'registered', + priority, + context: issueData.problem_statement, + source: issueData.source, + source_url: issueData.source_url || null, + labels: [...(issueData.labels || []), ...labels], + + // Structured fields + problem_statement: issueData.problem_statement, + expected_behavior: issueData.expected_behavior || null, + actual_behavior: issueData.actual_behavior || null, + affected_components: issueData.affected_components || [], + reproduction_steps: issueData.reproduction_steps || [], + + // Metadata + bound_solution_id: null, + solution_count: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() +}; + +// Ensure directory exists +Bash('mkdir -p .workflow/issues'); + +// Append to issues.jsonl +const issuesPath = '.workflow/issues/issues.jsonl'; +Bash(`echo '${JSON.stringify(newIssue)}' >> "${issuesPath}"`); + +console.log(` +## Issue Created + +**ID**: ${newIssue.id} +**Title**: ${newIssue.title} +**Priority**: ${newIssue.priority} +**Labels**: ${newIssue.labels.join(', ') || 'none'} +**Source**: ${newIssue.source} + +### Next Steps +1. Plan solution: \`/issue:plan ${newIssue.id}\` +2. View details: \`ccw issue status ${newIssue.id}\` +3. Manage issues: \`/issue:manage\` +`); +``` + +## Examples + +### GitHub Issue + +```bash +/issue:new https://github.com/myorg/myrepo/issues/42 --priority 2 + +# Output: +## Issue Created +**ID**: GH-42 +**Title**: Fix memory leak in WebSocket handler +**Priority**: 2 +**Labels**: bug, performance +**Source**: github (https://github.com/myorg/myrepo/issues/42) +``` + +### Text Description + +```bash +/issue:new "API rate limiting not working. Expected: 429 after 100 requests. Actual: No limit. Affects src/middleware/rate-limit.ts" + +# Output: +## Issue Created +**ID**: ISS-20251227-142530 +**Title**: API rate limiting not working +**Priority**: 3 +**Labels**: none +**Source**: text +``` + +## Error Handling + +| Error | Resolution | +|-------|------------| +| Invalid GitHub URL | Show format hint, ask for correction | +| gh CLI not available | Fall back to WebFetch for public issues | +| Empty description | Prompt user for required fields | +| Duplicate issue ID | Auto-increment or suggest merge | +| Parse failure | Show raw input, ask for manual structuring | + +## Related Commands + +- `/issue:plan` - Plan solution for issue +- `/issue:manage` - Interactive issue management +- `ccw issue list` - List all issues +- `ccw issue status ` - View issue details diff --git a/ccw/src/commands/issue.ts b/ccw/src/commands/issue.ts index 4fa57607..e60548ab 100644 --- a/ccw/src/commands/issue.ts +++ b/ccw/src/commands/issue.ts @@ -663,9 +663,11 @@ async function queueAction(subAction: string | undefined, issueId: string | unde for (const item of queue.queue) { const statusColor = { 'pending': chalk.gray, + 'ready': chalk.cyan, 'executing': chalk.yellow, 'completed': chalk.green, - 'failed': chalk.red + 'failed': chalk.red, + 'blocked': chalk.magenta }[item.status] || chalk.white; console.log( diff --git a/ccw/src/core/dashboard-generator-patch.ts b/ccw/src/core/dashboard-generator-patch.ts index 7062ba02..1158ca03 100644 --- a/ccw/src/core/dashboard-generator-patch.ts +++ b/ccw/src/core/dashboard-generator-patch.ts @@ -21,6 +21,7 @@ const MODULE_FILES = [ 'dashboard-js/components/tabs-other.js', 'dashboard-js/components/carousel.js', 'dashboard-js/components/notifications.js', + 'dashboard-js/components/cli-stream-viewer.js', 'dashboard-js/components/global-notifications.js', 'dashboard-js/components/cli-status.js', 'dashboard-js/components/cli-history.js', diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index 3fb47788..afcb2a0a 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -88,7 +88,8 @@ const MODULE_CSS_FILES = [ '29-help.css', '30-core-memory.css', '31-api-settings.css', - '32-issue-manager.css' + '32-issue-manager.css', + '33-cli-stream-viewer.css' ]; // Modular JS files in dependency order @@ -109,6 +110,7 @@ const MODULE_FILES = [ 'components/flowchart.js', 'components/carousel.js', 'components/notifications.js', + 'components/cli-stream-viewer.js', 'components/global-notifications.js', 'components/task-queue-sidebar.js', 'components/cli-status.js', diff --git a/ccw/src/templates/dashboard-css/32-issue-manager.css b/ccw/src/templates/dashboard-css/32-issue-manager.css index 5ad1725c..e2d632cd 100644 --- a/ccw/src/templates/dashboard-css/32-issue-manager.css +++ b/ccw/src/templates/dashboard-css/32-issue-manager.css @@ -16,6 +16,11 @@ color: hsl(var(--muted-foreground)); } +/* Issue Header */ +.issue-header { + margin-bottom: 1.5rem; +} + /* View Toggle (Issues/Queue) */ .issue-view-toggle { display: inline-flex; @@ -60,17 +65,59 @@ width: 100%; } -.issues-empty-state { +.issues-empty-state, +.issue-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; - min-height: 160px; + min-height: 200px; + text-align: center; + color: hsl(var(--muted-foreground)); +} + +.issue-empty svg { + margin-bottom: 1rem; + opacity: 0.5; +} + +/* Issue Filters */ +.issue-filters { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.issue-filter-btn { + padding: 0.375rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + color: hsl(var(--muted-foreground)); + background: hsl(var(--muted)); + border: 1px solid transparent; + cursor: pointer; + transition: all 0.15s ease; +} + +.issue-filter-btn:hover { + background: hsl(var(--muted) / 0.8); + color: hsl(var(--foreground)); +} + +.issue-filter-btn.active { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); } /* Issue Card */ .issue-card { position: relative; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + padding: 1rem; transition: all 0.2s ease; cursor: pointer; } @@ -78,6 +125,7 @@ .issue-card:hover { border-color: hsl(var(--primary)); transform: translateY(-2px); + box-shadow: 0 4px 12px hsl(var(--foreground) / 0.08); } .issue-card-header { @@ -93,14 +141,16 @@ color: hsl(var(--muted-foreground)); } -.issue-card-title { +.issue-card-title, +.issue-title { font-weight: 600; font-size: 0.9375rem; line-height: 1.4; margin-top: 0.25rem; } -.issue-card-meta { +.issue-card-meta, +.issue-meta { display: flex; align-items: center; gap: 0.5rem; @@ -141,6 +191,11 @@ color: hsl(var(--muted-foreground)); } +.issue-status.planning { + background: hsl(38 92% 50% / 0.15); + color: hsl(38 92% 50%); +} + .issue-status.planned { background: hsl(217 91% 60% / 0.15); color: hsl(217 91% 60%); @@ -210,13 +265,30 @@ gap: 1.5rem; } -.queue-empty-state { +.queue-empty-state, +.queue-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px; text-align: center; + color: hsl(var(--muted-foreground)); +} + +.queue-empty svg { + margin-bottom: 1rem; + opacity: 0.5; +} + +/* Issue ID */ +.issue-id { + font-family: var(--font-mono); + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + background: hsl(var(--muted)); + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; } /* Execution Group */ @@ -977,3 +1049,713 @@ font-size: 0.75rem; font-weight: 500; } + +/* ========================================== + TOOLBAR & SEARCH + ========================================== */ + +.issue-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + padding: 0.75rem; + background: hsl(var(--muted) / 0.3); + border-radius: 0.5rem; + border: 1px solid hsl(var(--border)); +} + +.issue-search { + position: relative; + display: flex; + align-items: center; + flex: 1; + min-width: 200px; + max-width: 320px; +} + +.issue-search > i:first-child { + position: absolute; + left: 0.75rem; + color: hsl(var(--muted-foreground)); + pointer-events: none; +} + +.issue-search input { + width: 100%; + padding: 0.5rem 2rem 0.5rem 2.25rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + background: hsl(var(--background)); + font-size: 0.875rem; + color: hsl(var(--foreground)); + transition: border-color 0.15s ease; +} + +.issue-search input:focus { + outline: none; + border-color: hsl(var(--primary)); +} + +.issue-search input::placeholder { + color: hsl(var(--muted-foreground)); +} + +.issue-search-clear { + position: absolute; + right: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + border: none; + border-radius: 9999px; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + cursor: pointer; + transition: all 0.15s ease; +} + +.issue-search-clear:hover { + background: hsl(var(--destructive) / 0.1); + color: hsl(var(--destructive)); +} + +/* ========================================== + CREATE BUTTON + ========================================== */ + +.issue-create-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 1rem; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.issue-create-btn:hover { + background: hsl(var(--primary) / 0.9); + transform: translateY(-1px); +} + +.issue-create-btn:active { + transform: translateY(0); +} + +/* ========================================== + ISSUE STATS + ========================================== */ + +.issue-stats { + padding: 0.5rem 0; +} + +/* ========================================== + EMPTY STATE (CENTERED) + ========================================== */ + +.issue-empty-container { + grid-column: 1 / -1; + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; +} + +.issue-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 2rem; +} + +.issue-empty > i, +.issue-empty > svg { + color: hsl(var(--muted-foreground)); + opacity: 0.5; + margin-bottom: 1rem; +} + +.issue-empty-title { + font-size: 1.125rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 0.5rem; +} + +.issue-empty-hint { + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + margin-bottom: 1.5rem; +} + +.issue-empty-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.625rem 1.25rem; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.issue-empty-btn:hover { + background: hsl(var(--primary) / 0.9); +} + +/* ========================================== + MODAL STYLES + ========================================== */ + +.issue-modal { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; +} + +.issue-modal.hidden { + display: none; +} + +.issue-modal-backdrop { + position: absolute; + inset: 0; + background: hsl(var(--foreground) / 0.5); + animation: fadeIn 0.15s ease-out; +} + +.issue-modal-content { + position: relative; + width: 90%; + max-width: 480px; + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-radius: 0.75rem; + box-shadow: 0 20px 40px hsl(var(--foreground) / 0.15); + animation: modalSlideIn 0.2s ease-out; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.issue-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + border-bottom: 1px solid hsl(var(--border)); +} + +.issue-modal-header h3 { + font-size: 1.125rem; + font-weight: 600; + color: hsl(var(--foreground)); +} + +.issue-modal-body { + padding: 1.25rem; + max-height: 60vh; + overflow-y: auto; +} + +.issue-modal-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.75rem; + padding: 1rem 1.25rem; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.3); +} + +/* ========================================== + FORM STYLES + ========================================== */ + +.form-group { + margin-bottom: 1rem; +} + +.form-group:last-child { + margin-bottom: 0; +} + +.form-group label { + display: block; + font-size: 0.8125rem; + font-weight: 500; + color: hsl(var(--foreground)); + margin-bottom: 0.375rem; +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 0.625rem 0.75rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + background: hsl(var(--background)); + font-size: 0.875rem; + color: hsl(var(--foreground)); + transition: border-color 0.15s ease; +} + +.form-group input:focus, +.form-group textarea:focus, +.form-group select:focus { + outline: none; + border-color: hsl(var(--primary)); +} + +.form-group input::placeholder, +.form-group textarea::placeholder { + color: hsl(var(--muted-foreground)); +} + +.form-group textarea { + resize: vertical; + min-height: 80px; +} + +.form-group select { + cursor: pointer; +} + +/* ========================================== + BUTTON STYLES + ========================================== */ + +.btn-primary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + padding: 0.5rem 1rem; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn-primary:hover { + background: hsl(var(--primary) / 0.9); +} + +.btn-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + padding: 0.5rem 1rem; + background: hsl(var(--muted)); + color: hsl(var(--foreground)); + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn-secondary:hover { + background: hsl(var(--muted) / 0.8); +} + +.btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + padding: 0; + background: transparent; + color: hsl(var(--muted-foreground)); + border: none; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn-icon:hover { + background: hsl(var(--muted)); + color: hsl(var(--foreground)); +} + +/* ========================================== + DETAIL PANEL ENHANCEMENTS + ========================================== */ + +.issue-detail-content { + padding: 1.25rem; + overflow-y: auto; + flex: 1; +} + +.detail-section { + margin-bottom: 1.5rem; +} + +.detail-label { + display: block; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: hsl(var(--muted-foreground)); + margin-bottom: 0.5rem; +} + +.detail-editable { + display: flex; + align-items: flex-start; + gap: 0.5rem; +} + +.detail-value { + flex: 1; + font-size: 1rem; + font-weight: 500; + color: hsl(var(--foreground)); +} + +.detail-context { + position: relative; +} + +.detail-pre { + font-family: inherit; + font-size: 0.875rem; + line-height: 1.6; + color: hsl(var(--foreground)); + white-space: pre-wrap; + word-break: break-word; + padding: 0.75rem; + background: hsl(var(--muted) / 0.3); + border-radius: 0.375rem; +} + +.btn-edit { + display: flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + padding: 0; + background: transparent; + color: hsl(var(--muted-foreground)); + border: none; + border-radius: 0.25rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn-edit:hover { + background: hsl(var(--muted)); + color: hsl(var(--primary)); +} + +/* Solutions List */ +.solutions-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.solution-item { + padding: 0.75rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.solution-item:hover { + border-color: hsl(var(--primary) / 0.5); +} + +.solution-item.bound { + border-color: hsl(var(--success)); + background: hsl(var(--success) / 0.05); +} + +.solution-header { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.solution-id { + color: hsl(var(--muted-foreground)); +} + +.solution-bound-badge { + display: inline-flex; + padding: 0.125rem 0.375rem; + background: hsl(var(--success) / 0.15); + color: hsl(var(--success)); + font-size: 0.6875rem; + font-weight: 500; + border-radius: 0.25rem; +} + +.solution-tasks { + margin-left: auto; + color: hsl(var(--muted-foreground)); +} + +.solution-tasks-list { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid hsl(var(--border) / 0.5); +} + +/* Tasks List */ +.tasks-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.task-item-detail { + padding: 0.75rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; +} + +.task-title-detail { + font-size: 0.875rem; + color: hsl(var(--foreground)); + margin-top: 0.375rem; +} + +/* Edit Mode */ +.edit-input, +.edit-textarea { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid hsl(var(--primary)); + border-radius: 0.375rem; + background: hsl(var(--background)); + font-size: 0.875rem; + color: hsl(var(--foreground)); +} + +.edit-input:focus, +.edit-textarea:focus { + outline: none; +} + +.edit-textarea { + resize: vertical; + min-height: 100px; +} + +.edit-actions { + display: flex; + gap: 0.25rem; + margin-top: 0.5rem; +} + +.btn-save, +.btn-cancel { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + padding: 0; + border: none; + border-radius: 0.25rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.btn-save { + background: hsl(var(--success) / 0.15); + color: hsl(var(--success)); +} + +.btn-save:hover { + background: hsl(var(--success) / 0.25); +} + +.btn-cancel { + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +.btn-cancel:hover { + background: hsl(var(--destructive) / 0.1); + color: hsl(var(--destructive)); +} + +/* ========================================== + QUEUE ENHANCEMENTS + ========================================== */ + +.queue-info { + padding: 0.5rem 0; +} + +.queue-items { + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.queue-items.parallel { + flex-direction: row; + flex-wrap: wrap; +} + +.queue-items.parallel .queue-item { + flex: 1; + min-width: 200px; +} + +.queue-group-type { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.875rem; + font-weight: 600; +} + +.queue-group-type.parallel { + color: hsl(142 71% 45%); +} + +.queue-group-type.sequential { + color: hsl(262 83% 58%); +} + +/* Queue Item Status Colors */ +.queue-item.ready { + border-color: hsl(199 89% 48%); +} + +.queue-item.executing { + border-color: hsl(45 93% 47%); + background: hsl(45 93% 47% / 0.05); +} + +.queue-item.completed { + border-color: hsl(var(--success)); + background: hsl(var(--success) / 0.05); +} + +.queue-item.failed { + border-color: hsl(var(--destructive)); + background: hsl(var(--destructive) / 0.05); +} + +.queue-item.blocked { + border-color: hsl(262 83% 58%); + opacity: 0.7; +} + +/* Priority indicator */ +.issue-priority { + display: inline-flex; + align-items: center; + gap: 0.125rem; +} + +/* Conflicts list */ +.conflicts-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.conflict-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.5rem 0.75rem; + background: hsl(45 93% 47% / 0.1); + border: 1px solid hsl(45 93% 47% / 0.3); + border-radius: 0.375rem; +} + +.conflict-file { + color: hsl(var(--primary)); +} + +.conflict-tasks { + flex: 1; +} + +.conflict-status { + font-size: 0.6875rem; + font-weight: 500; + padding: 0.125rem 0.375rem; + border-radius: 0.25rem; +} + +.conflict-status.pending { + background: hsl(45 93% 47% / 0.15); + color: hsl(45 93% 47%); +} + +.conflict-status.resolved { + background: hsl(var(--success) / 0.15); + color: hsl(var(--success)); +} + +/* ========================================== + RESPONSIVE TOOLBAR + ========================================== */ + +@media (max-width: 640px) { + .issue-toolbar { + flex-direction: column; + align-items: stretch; + } + + .issue-search { + max-width: none; + } + + .issue-filters { + justify-content: flex-start; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } +} diff --git a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js index d460a32a..d12036a9 100644 --- a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +++ b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js @@ -212,7 +212,7 @@ function renderStreamTabs() { ${exec.mode} `; @@ -238,8 +238,8 @@ function renderStreamContent(executionId) { contentContainer.innerHTML = `
-
${t('cliStream.noStreams')}
-
${t('cliStream.noStreamsHint')}
+
${_streamT('cliStream.noStreams')}
+
${_streamT('cliStream.noStreamsHint')}
`; if (typeof lucide !== 'undefined') lucide.createIcons(); @@ -279,10 +279,10 @@ function renderStreamStatus(executionId) { : formatDuration(Date.now() - exec.startTime); const statusLabel = exec.status === 'running' - ? t('cliStream.running') + ? _streamT('cliStream.running') : exec.status === 'completed' - ? t('cliStream.completed') - : t('cliStream.error'); + ? _streamT('cliStream.completed') + : _streamT('cliStream.error'); statusContainer.innerHTML = `
@@ -296,15 +296,15 @@ function renderStreamStatus(executionId) {
- ${exec.output.length} ${t('cliStream.lines') || 'lines'} + ${exec.output.length} ${_streamT('cliStream.lines') || 'lines'}
`; @@ -428,10 +428,15 @@ function escapeHtml(text) { return div.innerHTML; } -// Translation helper with fallback -function t(key) { - if (typeof window.t === 'function') { - return window.t(key); +// Translation helper with fallback (uses global t from i18n.js) +function _streamT(key) { + // First try global t() from i18n.js + if (typeof t === 'function' && t !== _streamT) { + try { + return t(key); + } catch (e) { + // Fall through to fallbacks + } } // Fallback values const fallbacks = { diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index d786b500..4f1ac3eb 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -1729,6 +1729,50 @@ const i18n = { 'nav.issues': 'Issues', 'nav.issueManager': 'Manager', 'title.issueManager': 'Issue Manager', + // issues.* keys (used by issue-manager.js) + 'issues.title': 'Issue Manager', + 'issues.description': 'Manage issues, solutions, and execution queue', + 'issues.viewIssues': 'Issues', + 'issues.viewQueue': 'Queue', + 'issues.filterStatus': 'Status', + 'issues.filterAll': 'All', + 'issues.noIssues': 'No issues found', + 'issues.createHint': 'Click "Create" to add your first issue', + 'issues.priority': 'Priority', + 'issues.tasks': 'tasks', + 'issues.solutions': 'solutions', + 'issues.boundSolution': 'Bound', + 'issues.queueEmpty': 'Queue is empty', + 'issues.reorderHint': 'Drag items within a group to reorder', + 'issues.parallelGroup': 'Parallel', + 'issues.sequentialGroup': 'Sequential', + 'issues.dependsOn': 'Depends on', + // Create & Search + 'issues.create': 'Create', + 'issues.createTitle': 'Create New Issue', + 'issues.issueId': 'Issue ID', + 'issues.issueTitle': 'Title', + 'issues.issueContext': 'Context', + 'issues.issuePriority': 'Priority', + 'issues.titlePlaceholder': 'Brief description of the issue', + 'issues.contextPlaceholder': 'Detailed description, requirements, etc.', + 'issues.priorityLowest': 'Lowest', + 'issues.priorityLow': 'Low', + 'issues.priorityMedium': 'Medium', + 'issues.priorityHigh': 'High', + 'issues.priorityCritical': 'Critical', + 'issues.searchPlaceholder': 'Search issues...', + 'issues.showing': 'Showing', + 'issues.of': 'of', + 'issues.issues': 'issues', + 'issues.tryDifferentFilter': 'Try adjusting your search or filters', + 'issues.createFirst': 'Create First Issue', + 'issues.idRequired': 'Issue ID is required', + 'issues.titleRequired': 'Title is required', + 'issues.created': 'Issue created successfully', + 'issues.confirmDelete': 'Are you sure you want to delete this issue?', + 'issues.deleted': 'Issue deleted', + // issue.* keys (legacy) 'issue.viewIssues': 'Issues', 'issue.viewQueue': 'Queue', 'issue.filterAll': 'All', @@ -3508,6 +3552,50 @@ const i18n = { 'nav.issues': '议题', 'nav.issueManager': '管理器', 'title.issueManager': '议题管理器', + // issues.* keys (used by issue-manager.js) + 'issues.title': '议题管理器', + 'issues.description': '管理议题、解决方案和执行队列', + 'issues.viewIssues': '议题', + 'issues.viewQueue': '队列', + 'issues.filterStatus': '状态', + 'issues.filterAll': '全部', + 'issues.noIssues': '暂无议题', + 'issues.createHint': '点击"创建"添加您的第一个议题', + 'issues.priority': '优先级', + 'issues.tasks': '任务', + 'issues.solutions': '解决方案', + 'issues.boundSolution': '已绑定', + 'issues.queueEmpty': '队列为空', + 'issues.reorderHint': '在组内拖拽项目以重新排序', + 'issues.parallelGroup': '并行', + 'issues.sequentialGroup': '顺序', + 'issues.dependsOn': '依赖于', + // Create & Search + 'issues.create': '创建', + 'issues.createTitle': '创建新议题', + 'issues.issueId': '议题ID', + 'issues.issueTitle': '标题', + 'issues.issueContext': '上下文', + 'issues.issuePriority': '优先级', + 'issues.titlePlaceholder': '简要描述议题', + 'issues.contextPlaceholder': '详细描述、需求等', + 'issues.priorityLowest': '最低', + 'issues.priorityLow': '低', + 'issues.priorityMedium': '中', + 'issues.priorityHigh': '高', + 'issues.priorityCritical': '紧急', + 'issues.searchPlaceholder': '搜索议题...', + 'issues.showing': '显示', + 'issues.of': '共', + 'issues.issues': '条议题', + 'issues.tryDifferentFilter': '尝试调整搜索或筛选条件', + 'issues.createFirst': '创建第一个议题', + 'issues.idRequired': '议题ID为必填', + 'issues.titleRequired': '标题为必填', + 'issues.created': '议题创建成功', + 'issues.confirmDelete': '确定要删除此议题吗?', + 'issues.deleted': '议题已删除', + // issue.* keys (legacy) 'issue.viewIssues': '议题', 'issue.viewQueue': '队列', 'issue.filterAll': '全部', diff --git a/ccw/src/templates/dashboard-js/views/issue-manager.js b/ccw/src/templates/dashboard-js/views/issue-manager.js index 298461b7..bacb1072 100644 --- a/ccw/src/templates/dashboard-js/views/issue-manager.js +++ b/ccw/src/templates/dashboard-js/views/issue-manager.js @@ -10,6 +10,7 @@ var issueData = { selectedIssue: null, selectedSolution: null, statusFilter: 'all', + searchQuery: '', viewMode: 'issues' // 'issues' | 'queue' }; var issueLoading = false; @@ -91,10 +92,20 @@ function renderIssueView() { if (!container) return; const issues = issueData.issues || []; - const filteredIssues = issueData.statusFilter === 'all' + // Apply both status and search filters + let filteredIssues = issueData.statusFilter === 'all' ? issues : issues.filter(i => i.status === issueData.statusFilter); + if (issueData.searchQuery) { + const query = issueData.searchQuery.toLowerCase(); + filteredIssues = filteredIssues.filter(i => + i.id.toLowerCase().includes(query) || + (i.title && i.title.toLowerCase().includes(query)) || + (i.context && i.context.toLowerCase().includes(query)) + ); + } + container.innerHTML = `
@@ -110,16 +121,24 @@ function renderIssueView() {
- -
- - + + +
+ + +
@@ -128,6 +147,47 @@ function renderIssueView() { + + + `; @@ -147,11 +207,26 @@ function switchIssueView(mode) { // ========== Issue List Section ========== function renderIssueListSection(issues) { const statuses = ['all', 'registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed']; + const totalIssues = issueData.issues?.length || 0; return ` - -
-
+ +
+ + +
${t('issues.filterStatus') || 'Status'}: ${statuses.map(status => `
+ +
+ + ${t('issues.showing') || 'Showing'} ${issues.length} ${t('issues.of') || 'of'} ${totalIssues} ${t('issues.issues') || 'issues'} + +
+
${issues.length === 0 ? ` -
- -

${t('issues.noIssues') || 'No issues found'}

-

${t('issues.createHint') || 'Create issues using: ccw issue init '}

+
+
+ +

${t('issues.noIssues') || 'No issues found'}

+

${issueData.searchQuery || issueData.statusFilter !== 'all' + ? (t('issues.tryDifferentFilter') || 'Try adjusting your search or filters') + : (t('issues.createHint') || 'Click "Create" to add your first issue')}

+ ${!issueData.searchQuery && issueData.statusFilter === 'all' ? ` + + ` : ''} +
` : issues.map(issue => renderIssueCard(issue)).join('')}
@@ -702,3 +794,122 @@ async function updateTaskStatus(issueId, taskId, status) { showNotification('Failed to update task status', 'error'); } } + +// ========== Search Functions ========== +function handleIssueSearch(value) { + issueData.searchQuery = value; + renderIssueView(); +} + +function clearIssueSearch() { + issueData.searchQuery = ''; + renderIssueView(); +} + +// ========== Create Issue Modal ========== +function showCreateIssueModal() { + const modal = document.getElementById('createIssueModal'); + if (modal) { + modal.classList.remove('hidden'); + lucide.createIcons(); + // Focus on first input + setTimeout(() => { + document.getElementById('newIssueId')?.focus(); + }, 100); + } +} + +function hideCreateIssueModal() { + const modal = document.getElementById('createIssueModal'); + if (modal) { + modal.classList.add('hidden'); + // Clear form + const idInput = document.getElementById('newIssueId'); + const titleInput = document.getElementById('newIssueTitle'); + const contextInput = document.getElementById('newIssueContext'); + const prioritySelect = document.getElementById('newIssuePriority'); + if (idInput) idInput.value = ''; + if (titleInput) titleInput.value = ''; + if (contextInput) contextInput.value = ''; + if (prioritySelect) prioritySelect.value = '3'; + } +} + +async function createIssue() { + const idInput = document.getElementById('newIssueId'); + const titleInput = document.getElementById('newIssueTitle'); + const contextInput = document.getElementById('newIssueContext'); + const prioritySelect = document.getElementById('newIssuePriority'); + + const issueId = idInput?.value?.trim(); + const title = titleInput?.value?.trim(); + const context = contextInput?.value?.trim(); + const priority = parseInt(prioritySelect?.value || '3'); + + if (!issueId) { + showNotification(t('issues.idRequired') || 'Issue ID is required', 'error'); + idInput?.focus(); + return; + } + + if (!title) { + showNotification(t('issues.titleRequired') || 'Title is required', 'error'); + titleInput?.focus(); + return; + } + + try { + const response = await fetch('/api/issues?path=' + encodeURIComponent(projectPath), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: issueId, + title: title, + context: context, + priority: priority, + source: 'dashboard' + }) + }); + + const result = await response.json(); + + if (!response.ok || result.error) { + showNotification(result.error || 'Failed to create issue', 'error'); + return; + } + + showNotification(t('issues.created') || 'Issue created successfully', 'success'); + hideCreateIssueModal(); + + // Reload data and refresh view + await loadIssueData(); + renderIssueView(); + } catch (err) { + console.error('Failed to create issue:', err); + showNotification('Failed to create issue', 'error'); + } +} + +// ========== Delete Issue ========== +async function deleteIssue(issueId) { + if (!confirm(t('issues.confirmDelete') || 'Are you sure you want to delete this issue?')) { + return; + } + + try { + const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('Failed to delete'); + + showNotification(t('issues.deleted') || 'Issue deleted', 'success'); + closeIssueDetail(); + + // Reload data and refresh view + await loadIssueData(); + renderIssueView(); + } catch (err) { + showNotification('Failed to delete issue', 'error'); + } +}