feat: Enhance issue and solution management with new UI components and functionality

- Added internationalization support for new issue and solution-related strings in i18n.js.
- Implemented a solution detail modal in issue-manager.js to display solution information and bind/unbind actions.
- Enhanced the skill loading function to combine project and user skills in hook-manager.js.
- Improved queue rendering logic to handle empty states and display queue statistics in issue-manager.js.
- Introduced command modals for queue operations, allowing users to generate execution queues via CLI commands.
- Added functionality to auto-generate issue IDs and regenerate them in the create issue modal.
- Implemented detailed rendering of solution tasks, including acceptance criteria and modification points.
This commit is contained in:
catlog22
2025-12-27 11:27:45 +08:00
parent 8f310339df
commit 4da06864f8
11 changed files with 2490 additions and 169 deletions

View File

@@ -199,7 +199,7 @@ async function ripgrepFallback(issue, projectRoot) {
## Phase 3: Solution Planning
### Task Decomposition
### Task Decomposition (Closed-Loop)
```javascript
function decomposeTasks(issue, exploration) {
@@ -217,15 +217,104 @@ function decomposeTasks(issue, exploration) {
action: inferAction(group),
description: group.description,
modification_points: group.points,
// Phase 1: Implementation
implementation: generateImplementationSteps(group, exploration),
// Phase 2: Test
test: generateTestRequirements(group, exploration, issue.lifecycle_requirements),
// Phase 3: Regression
regression: generateRegressionChecks(group, issue.lifecycle_requirements),
// Phase 4: Acceptance
acceptance: generateAcceptanceCriteria(group),
// Phase 5: Commit
commit: generateCommitSpec(group, issue),
depends_on: inferDependencies(group, tasks),
estimated_minutes: estimateTime(group)
estimated_minutes: estimateTime(group),
executor: inferExecutor(group)
})
}
return tasks
}
function generateTestRequirements(group, exploration, lifecycle) {
const test = {
unit: [],
integration: [],
commands: [],
coverage_target: 80
}
// Generate unit test requirements based on action
if (group.action === 'Create' || group.action === 'Implement') {
test.unit.push(`Test ${group.title} happy path`)
test.unit.push(`Test ${group.title} error cases`)
}
// Generate test commands based on project patterns
if (exploration.test_patterns?.includes('jest')) {
test.commands.push(`npm test -- --grep '${group.scope}'`)
} else if (exploration.test_patterns?.includes('vitest')) {
test.commands.push(`npx vitest run ${group.scope}`)
} else {
test.commands.push(`npm test`)
}
// Add integration tests if needed
if (lifecycle?.test_strategy === 'integration' || lifecycle?.test_strategy === 'e2e') {
test.integration.push(`Integration test for ${group.title}`)
}
return test
}
function generateRegressionChecks(group, lifecycle) {
const regression = []
switch (lifecycle?.regression_scope) {
case 'full':
regression.push('npm test')
regression.push('npm run test:integration')
break
case 'related':
regression.push(`npm test -- --grep '${group.scope}'`)
regression.push(`npm test -- --changed`)
break
case 'affected':
default:
regression.push(`npm test -- --findRelatedTests ${group.points[0]?.file}`)
break
}
return regression
}
function generateCommitSpec(group, issue) {
const typeMap = {
'Create': 'feat',
'Implement': 'feat',
'Update': 'feat',
'Fix': 'fix',
'Refactor': 'refactor',
'Test': 'test',
'Configure': 'chore',
'Delete': 'chore'
}
const scope = group.scope.split('/').pop()?.replace(/\..*$/, '') || 'core'
return {
type: typeMap[group.action] || 'feat',
scope: scope,
message_template: `${typeMap[group.action] || 'feat'}(${scope}): ${group.title.toLowerCase()}\n\n${group.description || ''}`,
breaking: false
}
}
```
### Action Type Inference
@@ -347,11 +436,15 @@ function generateImplementationSteps(group, exploration) {
}
```
### Acceptance Criteria Generation
### Acceptance Criteria Generation (Closed-Loop)
```javascript
function generateAcceptanceCriteria(task) {
const criteria = []
const acceptance = {
criteria: [],
verification: [],
manual_checks: []
}
// Action-specific criteria
const actionCriteria = {
@@ -363,14 +456,41 @@ function generateAcceptanceCriteria(task) {
'Configure': [`Configuration applied correctly`]
}
criteria.push(...(actionCriteria[task.action] || []))
acceptance.criteria.push(...(actionCriteria[task.action] || []))
// Add quantified criteria
if (task.modification_points.length > 0) {
criteria.push(`${task.modification_points.length} file(s) modified correctly`)
acceptance.criteria.push(`${task.modification_points.length} file(s) modified correctly`)
}
return criteria.slice(0, 4) // Max 4 criteria
// Generate verification steps for each criterion
for (const criterion of acceptance.criteria) {
acceptance.verification.push(generateVerificationStep(criterion, task))
}
// Limit to reasonable counts
acceptance.criteria = acceptance.criteria.slice(0, 4)
acceptance.verification = acceptance.verification.slice(0, 4)
return acceptance
}
function generateVerificationStep(criterion, task) {
// Generate executable verification for criterion
if (criterion.includes('file created')) {
return `ls -la ${task.modification_points[0]?.file} && head -20 ${task.modification_points[0]?.file}`
}
if (criterion.includes('test')) {
return `npm test -- --grep '${task.scope}'`
}
if (criterion.includes('export')) {
return `node -e "console.log(require('./${task.modification_points[0]?.file}'))"`
}
if (criterion.includes('API') || criterion.includes('endpoint')) {
return `curl -X GET http://localhost:3000/${task.scope} -v`
}
// Default: describe manual check
return `Manually verify: ${criterion}`
}
```
@@ -413,20 +533,61 @@ function validateSolution(solution) {
function validateTask(task) {
const errors = []
// Basic fields
if (!/^T\d+$/.test(task.id)) errors.push('Invalid task ID format')
if (!task.title?.trim()) errors.push('Missing title')
if (!task.scope?.trim()) errors.push('Missing scope')
if (!['Create', 'Update', 'Implement', 'Refactor', 'Configure', 'Test', 'Fix', 'Delete'].includes(task.action)) {
errors.push('Invalid action type')
}
// Phase 1: Implementation
if (!task.implementation || task.implementation.length < 2) {
errors.push('Need 2+ implementation steps')
}
if (!task.acceptance || task.acceptance.length < 1) {
errors.push('Need 1+ acceptance criteria')
// Phase 2: Test
if (!task.test) {
errors.push('Missing test phase')
} else {
if (!task.test.commands || task.test.commands.length < 1) {
errors.push('Need 1+ test commands')
}
}
if (task.acceptance?.some(a => /works correctly|good performance|properly/i.test(a))) {
errors.push('Vague acceptance criteria')
// Phase 3: Regression
if (!task.regression || task.regression.length < 1) {
errors.push('Need 1+ regression checks')
}
// Phase 4: Acceptance
if (!task.acceptance) {
errors.push('Missing acceptance phase')
} else {
if (!task.acceptance.criteria || task.acceptance.criteria.length < 1) {
errors.push('Need 1+ acceptance criteria')
}
if (!task.acceptance.verification || task.acceptance.verification.length < 1) {
errors.push('Need 1+ verification steps')
}
if (task.acceptance.criteria?.some(a => /works correctly|good performance|properly/i.test(a))) {
errors.push('Vague acceptance criteria')
}
}
// Phase 5: Commit
if (!task.commit) {
errors.push('Missing commit phase')
} else {
if (!['feat', 'fix', 'refactor', 'test', 'docs', 'chore'].includes(task.commit.type)) {
errors.push('Invalid commit type')
}
if (!task.commit.scope?.trim()) {
errors.push('Missing commit scope')
}
if (!task.commit.message_template?.trim()) {
errors.push('Missing commit message template')
}
}
return errors
@@ -500,7 +661,9 @@ function generateOutput(solutions, conflicts) {
}
```
### Solution Schema
### Solution Schema (Closed-Loop Tasks)
Each task MUST include ALL 5 lifecycle phases:
```json
{
@@ -517,10 +680,62 @@ function generateOutput(solutions, conflicts) {
"modification_points": [
{ "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" }
],
"implementation": ["Step 1", "Step 2", "..."],
"acceptance": ["Criterion 1", "Criterion 2"],
"implementation": [
"Create auth.ts file in src/middleware/",
"Implement JWT token extraction from Authorization header",
"Add token validation using jsonwebtoken library",
"Handle error cases (missing, invalid, expired tokens)",
"Export middleware function"
],
"test": {
"unit": [
"Test valid token passes through",
"Test invalid token returns 401",
"Test expired token returns 401",
"Test missing token returns 401"
],
"integration": [
"Protected route returns 401 without token",
"Protected route returns 200 with valid token"
],
"commands": [
"npm test -- --grep 'auth middleware'",
"npm run test:coverage -- src/middleware/auth.ts"
],
"coverage_target": 80
},
"regression": [
"npm test -- --grep 'existing routes'",
"npm run test:integration"
],
"acceptance": {
"criteria": [
"Middleware validates JWT tokens successfully",
"Returns 401 with appropriate error for invalid tokens",
"Passes decoded user payload to request context"
],
"verification": [
"curl -H 'Authorization: Bearer <valid>' /api/protected → 200",
"curl /api/protected → 401 {error: 'No token'}",
"curl -H 'Authorization: Bearer invalid' /api/protected → 401"
],
"manual_checks": []
},
"commit": {
"type": "feat",
"scope": "auth",
"message_template": "feat(auth): add JWT validation middleware\n\n- Implement token extraction and validation\n- Add error handling for invalid/expired tokens\n- Export middleware for route protection",
"breaking": false
},
"depends_on": [],
"estimated_minutes": 30
"estimated_minutes": 30,
"executor": "codex"
}
],
"exploration_context": {
@@ -622,6 +837,14 @@ Before outputting solution:
6. Include file:line references in modification_points where possible
7. Detect and report cross-issue file conflicts in batch mode
8. Include exploration_context with patterns and relevant_files
9. **Generate ALL 5 lifecycle phases for each task**:
- `implementation`: 2-7 concrete steps
- `test`: unit tests, commands, coverage target
- `regression`: regression check commands
- `acceptance`: criteria + verification steps
- `commit`: type, scope, message template
10. Infer test commands from project's test framework
11. Generate commit message following conventional commits
**NEVER**:
1. Execute implementation (return plan only)
@@ -632,3 +855,5 @@ Before outputting solution:
6. Assume file exists without verification
7. Generate more than 10 tasks per issue
8. Skip ACE search (unless fallback triggered)
9. **Omit any of the 5 lifecycle phases** (test, regression, acceptance, commit)
10. Skip verification steps in acceptance criteria

View File

@@ -148,15 +148,15 @@ TodoWrite({
});
```
### Phase 3: Codex Coordination (Single Task Mode)
### Phase 3: Codex Coordination (Single Task Mode - Full Lifecycle)
```javascript
// Execute tasks - single codex instance per task
// Execute tasks - single codex instance per task with full lifecycle
async function executeTask(queueItem) {
const codexPrompt = `
## Single Task Execution
## Single Task Execution - CLOSED-LOOP LIFECYCLE
You are executing ONE task from the issue queue. Follow these steps exactly:
You are executing ONE task from the issue queue. Each task has 5 phases that MUST ALL complete successfully.
### Step 1: Fetch Task
Run this command to get your task:
@@ -164,35 +164,71 @@ Run this command to get your task:
ccw issue next
\`\`\`
This returns JSON with:
- queue_id: Queue item ID
- task: Task definition with implementation steps
- context: Exploration context
- execution_hints: Executor and time estimate
This returns JSON with full lifecycle definition:
- task.implementation: Implementation steps
- task.test: Test requirements and commands
- task.regression: Regression check commands
- task.acceptance: Acceptance criteria and verification
- task.commit: Commit specification
### Step 2: Execute Task
Read the returned task object and:
### Step 2: Execute Full Lifecycle
**Phase 1: IMPLEMENT**
1. Follow task.implementation steps in order
2. Meet all task.acceptance criteria
3. Use provided context.relevant_files for reference
2. Modify files specified in modification_points
3. Use context.relevant_files for reference
4. Use context.patterns for code style
**Phase 2: TEST**
1. Run test commands from task.test.commands
2. Ensure all unit tests pass (task.test.unit)
3. Run integration tests if specified (task.test.integration)
4. Verify coverage meets task.test.coverage_target if specified
5. If tests fail → fix code and re-run, do NOT proceed until tests pass
**Phase 3: REGRESSION**
1. Run all commands in task.regression
2. Ensure no existing tests are broken
3. If regression fails → fix and re-run
**Phase 4: ACCEPTANCE**
1. Verify each criterion in task.acceptance.criteria
2. Execute verification steps in task.acceptance.verification
3. Complete any manual_checks if specified
4. All criteria MUST pass before proceeding
**Phase 5: COMMIT**
1. Stage all modified files
2. Use task.commit.message_template as commit message
3. Commit with: git commit -m "$(cat <<'EOF'\n<message>\nEOF\n)"
4. If commit_strategy is 'per-task', commit now
5. If commit_strategy is 'atomic' or 'squash', stage but don't commit
### Step 3: Report Completion
When done, run:
When ALL phases complete successfully:
\`\`\`bash
ccw issue complete <queue_id> --result '{"files_modified": ["path1", "path2"], "summary": "What was done"}'
ccw issue complete <queue_id> --result '{
"files_modified": ["path1", "path2"],
"tests_passed": true,
"regression_passed": true,
"acceptance_passed": true,
"committed": true,
"commit_hash": "<hash>",
"summary": "What was done"
}'
\`\`\`
If task fails, run:
If any phase fails and cannot be fixed:
\`\`\`bash
ccw issue fail <queue_id> --reason "Why it failed"
ccw issue fail <queue_id> --reason "Phase X failed: <details>"
\`\`\`
### Rules
- NEVER read task files directly - use ccw issue next
- Execute the FULL task before marking complete
- Do NOT loop - execute ONE task only
- Report accurate files_modified in result
- NEVER skip any lifecycle phase
- Tests MUST pass before proceeding to acceptance
- Regression MUST pass before commit
- ALL acceptance criteria MUST be verified
- Report accurate lifecycle status in result
### Start Now
Begin by running: ccw issue next

View File

@@ -15,7 +15,7 @@ Creates a new structured issue from either:
Outputs a well-formed issue entry to `.workflow/issues/issues.jsonl`.
## Issue Structure
## Issue Structure (Closed-Loop)
```typescript
interface Issue {
@@ -27,14 +27,22 @@ interface Issue {
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
// Closed-loop requirements (guide plan generation)
lifecycle_requirements: {
test_strategy: 'unit' | 'integration' | 'e2e' | 'manual' | 'auto';
regression_scope: 'affected' | 'related' | 'full'; // Which tests to run
acceptance_type: 'automated' | 'manual' | 'both'; // How to verify
commit_strategy: 'per-task' | 'squash' | 'atomic'; // Commit granularity
};
// Metadata
bound_solution_id: null;
solution_count: 0;
@@ -43,6 +51,52 @@ interface Issue {
}
```
## Task Lifecycle (Each Task is Closed-Loop)
When `/issue:plan` generates tasks, each task MUST include:
```typescript
interface SolutionTask {
id: string;
title: string;
scope: string;
action: string;
// Phase 1: Implementation
implementation: string[]; // Step-by-step implementation
modification_points: { file: string; target: string; change: string }[];
// Phase 2: Testing
test: {
unit?: string[]; // Unit test requirements
integration?: string[]; // Integration test requirements
commands?: string[]; // Test commands to run
coverage_target?: number; // Minimum coverage %
};
// Phase 3: Regression
regression: string[]; // Regression check commands/points
// Phase 4: Acceptance
acceptance: {
criteria: string[]; // Testable acceptance criteria
verification: string[]; // How to verify each criterion
manual_checks?: string[]; // Manual verification if needed
};
// Phase 5: Commit
commit: {
type: 'feat' | 'fix' | 'refactor' | 'test' | 'docs' | 'chore';
scope: string; // e.g., "auth", "api"
message_template: string; // Commit message template
breaking?: boolean;
};
depends_on: string[];
executor: 'codex' | 'gemini' | 'agent' | 'auto';
}
```
## Usage
```bash
@@ -206,7 +260,58 @@ async function parseTextDescription(text) {
}
```
### Phase 4: User Confirmation
### Phase 4: Lifecycle Configuration
```javascript
// Ask for lifecycle requirements (or use smart defaults)
const lifecycleAnswer = AskUserQuestion({
questions: [
{
question: 'Test strategy for this issue?',
header: 'Test',
multiSelect: false,
options: [
{ label: 'auto', description: 'Auto-detect based on affected files (Recommended)' },
{ label: 'unit', description: 'Unit tests only' },
{ label: 'integration', description: 'Integration tests' },
{ label: 'e2e', description: 'End-to-end tests' },
{ label: 'manual', description: 'Manual testing only' }
]
},
{
question: 'Regression scope?',
header: 'Regression',
multiSelect: false,
options: [
{ label: 'affected', description: 'Only affected module tests (Recommended)' },
{ label: 'related', description: 'Affected + dependent modules' },
{ label: 'full', description: 'Full test suite' }
]
},
{
question: 'Commit strategy?',
header: 'Commit',
multiSelect: false,
options: [
{ label: 'per-task', description: 'One commit per task (Recommended)' },
{ label: 'atomic', description: 'Single commit for entire issue' },
{ label: 'squash', description: 'Squash at the end' }
]
}
]
});
const lifecycle = {
test_strategy: lifecycleAnswer.test || 'auto',
regression_scope: lifecycleAnswer.regression || 'affected',
acceptance_type: 'automated',
commit_strategy: lifecycleAnswer.commit || 'per-task'
};
issueData.lifecycle_requirements = lifecycle;
```
### Phase 5: User Confirmation
```javascript
// Show parsed data and ask for confirmation
@@ -224,6 +329,11 @@ ${issueData.expected_behavior ? `### Expected Behavior\n${issueData.expected_beh
${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` : ''}
### Lifecycle Configuration
- **Test Strategy**: ${lifecycle.test_strategy}
- **Regression Scope**: ${lifecycle.regression_scope}
- **Commit Strategy**: ${lifecycle.commit_strategy}
`);
// Ask user to confirm or edit
@@ -264,7 +374,7 @@ if (answer.includes('Edit Title')) {
}
```
### Phase 5: Write to JSONL
### Phase 6: Write to JSONL
```javascript
// Construct final issue object
@@ -280,14 +390,22 @@ const newIssue = {
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 || [],
// Closed-loop lifecycle requirements
lifecycle_requirements: issueData.lifecycle_requirements || {
test_strategy: 'auto',
regression_scope: 'affected',
acceptance_type: 'automated',
commit_strategy: 'per-task'
},
// Metadata
bound_solution_id: null,
solution_count: 0,

View File

@@ -133,28 +133,59 @@ TodoWrite({
for (const [batchIndex, batch] of batches.entries()) {
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
// Build issue prompt for agent
// Build issue prompt for agent with lifecycle requirements
const issuePrompt = `
## Issues to Plan
## Issues to Plan (Closed-Loop Tasks Required)
${batch.map((issue, i) => `
### Issue ${i + 1}: ${issue.id}
**Title**: ${issue.title}
**Context**: ${issue.context || 'No context provided'}
**Affected Components**: ${issue.affected_components?.join(', ') || 'Not specified'}
**Lifecycle Requirements**:
- Test Strategy: ${issue.lifecycle_requirements?.test_strategy || 'auto'}
- Regression Scope: ${issue.lifecycle_requirements?.regression_scope || 'affected'}
- Commit Strategy: ${issue.lifecycle_requirements?.commit_strategy || 'per-task'}
`).join('\n')}
## Project Root
${process.cwd()}
## Requirements
## Requirements - CLOSED-LOOP TASKS
Each task MUST include ALL lifecycle phases:
### 1. Implementation
- implementation: string[] (2-7 concrete steps)
- modification_points: { file, target, change }[]
### 2. Test
- test.unit: string[] (unit test requirements)
- test.integration: string[] (integration test requirements if needed)
- test.commands: string[] (actual test commands to run)
- test.coverage_target: number (minimum coverage %)
### 3. Regression
- regression: string[] (commands to run for regression check)
- Based on issue's regression_scope setting
### 4. Acceptance
- acceptance.criteria: string[] (testable acceptance criteria)
- acceptance.verification: string[] (how to verify each criterion)
- acceptance.manual_checks: string[] (manual checks if needed)
### 5. Commit
- commit.type: feat|fix|refactor|test|docs|chore
- commit.scope: string (module name)
- commit.message_template: string (full commit message)
- commit.breaking: boolean
## Additional Requirements
1. Use ACE semantic search (mcp__ace-tool__search_context) for exploration
2. Generate complete solution with task breakdown
3. Each task must have:
- implementation steps (2-7 steps)
- acceptance criteria (1-4 testable criteria)
- modification_points (exact file locations)
- depends_on (task dependencies)
4. Detect file conflicts if multiple issues
2. Detect file conflicts if multiple issues
3. Generate executable test commands based on project's test framework
4. Infer commit scope from affected files
`;
// Launch issue-plan-agent (combines explore + plan)
@@ -281,7 +312,7 @@ ${issues.map(i => {
`);
```
## Solution Format
## Solution Format (Closed-Loop Tasks)
Each solution line in `solutions/{issue-id}.jsonl`:
@@ -299,18 +330,56 @@ Each solution line in `solutions/{issue-id}.jsonl`:
"modification_points": [
{ "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" }
],
"implementation": [
"Create auth.ts file",
"Implement JWT validation",
"Add error handling",
"Export middleware"
"Create auth.ts file in src/middleware/",
"Implement JWT token validation using jsonwebtoken",
"Add error handling for invalid/expired tokens",
"Export middleware function"
],
"acceptance": [
"Middleware validates JWT tokens",
"Returns 401 for invalid tokens"
"test": {
"unit": [
"Test valid token passes through",
"Test invalid token returns 401",
"Test expired token returns 401",
"Test missing token returns 401"
],
"commands": [
"npm test -- --grep 'auth middleware'",
"npm run test:coverage -- src/middleware/auth.ts"
],
"coverage_target": 80
},
"regression": [
"npm test -- --grep 'protected routes'",
"npm run test:integration -- auth"
],
"acceptance": {
"criteria": [
"Middleware validates JWT tokens successfully",
"Returns 401 for invalid or missing tokens",
"Passes decoded token to request context"
],
"verification": [
"curl -H 'Authorization: Bearer valid_token' /api/protected → 200",
"curl /api/protected → 401",
"curl -H 'Authorization: Bearer invalid' /api/protected → 401"
]
},
"commit": {
"type": "feat",
"scope": "auth",
"message_template": "feat(auth): add JWT validation middleware\n\n- Implement token validation\n- Add error handling for invalid tokens\n- Export for route protection",
"breaking": false
},
"depends_on": [],
"estimated_minutes": 30
"estimated_minutes": 30,
"executor": "codex"
}
],
"exploration_context": {

View File

@@ -20,30 +20,67 @@ Queue formation command using **issue-queue-agent** that analyzes all bound solu
- Parallel/Sequential group assignment
- Output global queue.json
## Storage Structure (Flat JSONL)
## Storage Structure (Queue History)
```
.workflow/issues/
├── issues.jsonl # All issues (one per line)
├── queue.json # Execution queue (output)
├── queues/ # Queue history directory
│ ├── index.json # Queue index (active + history)
│ ├── {queue-id}.json # Individual queue files
│ └── ...
└── solutions/
├── {issue-id}.jsonl # Solutions for issue
└── ...
```
### Queue Index Schema
```json
{
"active_queue_id": "QUE-20251227-143000",
"queues": [
{
"id": "QUE-20251227-143000",
"status": "active",
"issue_ids": ["GH-123", "GH-124"],
"total_tasks": 8,
"completed_tasks": 3,
"created_at": "2025-12-27T14:30:00Z"
},
{
"id": "QUE-20251226-100000",
"status": "completed",
"issue_ids": ["GH-120"],
"total_tasks": 5,
"completed_tasks": 5,
"created_at": "2025-12-26T10:00:00Z",
"completed_at": "2025-12-26T12:30:00Z"
}
]
}
```
## Usage
```bash
/issue:queue [FLAGS]
# Examples
/issue:queue # Form queue from all bound solutions
/issue:queue --rebuild # Rebuild queue (clear and regenerate)
/issue:queue --issue GH-123 # Add only specific issue to queue
/issue:queue # Form NEW queue from all bound solutions
/issue:queue --issue GH-123 # Form queue for specific issue only
/issue:queue --append GH-124 # Append to active queue
/issue:queue --list # List all queues (history)
/issue:queue --switch QUE-xxx # Switch active queue
/issue:queue --archive # Archive completed active queue
# Flags
--rebuild Clear existing queue and regenerate
--issue <id> Add only specific issue's tasks
--issue <id> Form queue for specific issue only
--append <id> Append issue to active queue (don't create new)
--list List all queues with status
--switch <queue-id> Switch active queue
--archive Archive current queue (mark completed)
--clear <queue-id> Delete a queue from history
```
## Execution Process
@@ -215,10 +252,15 @@ ${(queueOutput.execution_groups || []).map(g => {
## Queue Schema
Output `queue.json`:
Output `queues/{queue-id}.json`:
```json
{
"id": "QUE-20251227-143000",
"name": "Auth Feature Queue",
"status": "active",
"issue_ids": ["GH-123", "GH-124"],
"queue": [
{
"queue_id": "Q-001",
@@ -233,6 +275,7 @@ Output `queue.json`:
"queued_at": "2025-12-26T10:00:00Z"
}
],
"conflicts": [
{
"type": "file_conflict",
@@ -244,24 +287,32 @@ Output `queue.json`:
"resolved": true
}
],
"execution_groups": [
{ "id": "P1", "type": "parallel", "task_count": 3, "tasks": ["GH-123:T1", "GH-124:T1", "GH-125:T1"] },
{ "id": "S2", "type": "sequential", "task_count": 2, "tasks": ["GH-123:T2", "GH-124:T2"] }
],
"_metadata": {
"version": "2.0",
"storage": "jsonl",
"total_tasks": 5,
"total_conflicts": 1,
"resolved_conflicts": 1,
"parallel_groups": 1,
"sequential_groups": 1,
"timestamp": "2025-12-26T10:00:00Z",
"pending_count": 3,
"completed_count": 2,
"failed_count": 0,
"created_at": "2025-12-26T10:00:00Z",
"updated_at": "2025-12-26T11:00:00Z",
"source": "issue-queue-agent"
}
}
```
### Queue ID Format
```
QUE-YYYYMMDD-HHMMSS
例如: QUE-20251227-143052
```
## Semantic Priority Rules
| Factor | Priority Boost |

View File

@@ -36,6 +36,26 @@ interface Issue {
completed_at?: string;
}
interface TaskTest {
unit?: string[]; // Unit test requirements
integration?: string[]; // Integration test requirements
commands?: string[]; // Test commands to run
coverage_target?: number; // Minimum coverage % (optional)
}
interface TaskAcceptance {
criteria: string[]; // Acceptance criteria (testable)
verification: string[]; // How to verify each criterion
manual_checks?: string[]; // Manual verification steps if needed
}
interface TaskCommit {
type: 'feat' | 'fix' | 'refactor' | 'test' | 'docs' | 'chore';
scope: string; // Commit scope (e.g., "auth", "api")
message_template: string; // Commit message template
breaking?: boolean; // Breaking change flag
}
interface SolutionTask {
id: string;
title: string;
@@ -43,11 +63,26 @@ interface SolutionTask {
action: string;
description?: string;
modification_points?: { file: string; target: string; change: string }[];
implementation: string[];
acceptance: string[];
// Lifecycle phases (closed-loop)
implementation: string[]; // Implementation steps
test: TaskTest; // Test requirements
regression: string[]; // Regression check points
acceptance: TaskAcceptance; // Acceptance criteria & verification
commit: TaskCommit; // Commit specification
depends_on: string[];
estimated_minutes?: number;
executor: 'codex' | 'gemini' | 'agent' | 'auto';
// Lifecycle status tracking
lifecycle_status?: {
implemented: boolean;
tested: boolean;
regression_passed: boolean;
accepted: boolean;
committed: boolean;
};
status?: string;
priority?: number;
}
@@ -83,8 +118,13 @@ interface QueueItem {
}
interface Queue {
id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS
name?: string; // Optional queue name
status: 'active' | 'completed' | 'archived' | 'failed';
issue_ids: string[]; // Issues in this queue
queue: QueueItem[];
conflicts: any[];
execution_groups?: any[];
_metadata: {
version: string;
total_tasks: number;
@@ -92,10 +132,24 @@ interface Queue {
executing_count: number;
completed_count: number;
failed_count: number;
last_updated: string;
created_at: string;
updated_at: string;
};
}
interface QueueIndex {
active_queue_id: string | null;
queues: {
id: string;
status: string;
issue_ids: string[];
total_tasks: number;
completed_tasks: number;
created_at: string;
completed_at?: string;
}[];
}
interface IssueOptions {
status?: string;
title?: string;
@@ -208,40 +262,121 @@ function generateSolutionId(): string {
return `SOL-${ts}`;
}
// ============ Queue JSON ============
// ============ Queue Management (Multi-Queue) ============
function readQueue(): Queue {
const path = join(getIssuesDir(), 'queue.json');
function getQueuesDir(): string {
return join(getIssuesDir(), 'queues');
}
function ensureQueuesDir(): void {
const dir = getQueuesDir();
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
}
function readQueueIndex(): QueueIndex {
const path = join(getQueuesDir(), 'index.json');
if (!existsSync(path)) {
return {
queue: [],
conflicts: [],
_metadata: {
version: '2.0',
total_tasks: 0,
pending_count: 0,
executing_count: 0,
completed_count: 0,
failed_count: 0,
last_updated: new Date().toISOString()
}
};
return { active_queue_id: null, queues: [] };
}
return JSON.parse(readFileSync(path, 'utf-8'));
}
function writeQueueIndex(index: QueueIndex): void {
ensureQueuesDir();
writeFileSync(join(getQueuesDir(), 'index.json'), JSON.stringify(index, null, 2), 'utf-8');
}
function generateQueueFileId(): string {
const now = new Date();
const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
return `QUE-${ts}`;
}
function readQueue(queueId?: string): Queue | null {
const index = readQueueIndex();
const targetId = queueId || index.active_queue_id;
if (!targetId) return null;
const path = join(getQueuesDir(), `${targetId}.json`);
if (!existsSync(path)) return null;
return JSON.parse(readFileSync(path, 'utf-8'));
}
function readActiveQueue(): Queue {
const queue = readQueue();
if (queue) return queue;
// Return empty queue structure if no active queue
return createEmptyQueue();
}
function createEmptyQueue(): Queue {
return {
id: generateQueueFileId(),
status: 'active',
issue_ids: [],
queue: [],
conflicts: [],
_metadata: {
version: '2.0',
total_tasks: 0,
pending_count: 0,
executing_count: 0,
completed_count: 0,
failed_count: 0,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}
};
}
function writeQueue(queue: Queue): void {
ensureIssuesDir();
ensureQueuesDir();
// Update metadata counts
queue._metadata.total_tasks = queue.queue.length;
queue._metadata.pending_count = queue.queue.filter(q => q.status === 'pending').length;
queue._metadata.executing_count = queue.queue.filter(q => q.status === 'executing').length;
queue._metadata.completed_count = queue.queue.filter(q => q.status === 'completed').length;
queue._metadata.failed_count = queue.queue.filter(q => q.status === 'failed').length;
queue._metadata.last_updated = new Date().toISOString();
writeFileSync(join(getIssuesDir(), 'queue.json'), JSON.stringify(queue, null, 2), 'utf-8');
queue._metadata.updated_at = new Date().toISOString();
// Write queue file
const path = join(getQueuesDir(), `${queue.id}.json`);
writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8');
// Update index
const index = readQueueIndex();
const existingIdx = index.queues.findIndex(q => q.id === queue.id);
const indexEntry = {
id: queue.id,
status: queue.status,
issue_ids: queue.issue_ids,
total_tasks: queue._metadata.total_tasks,
completed_tasks: queue._metadata.completed_count,
created_at: queue._metadata.created_at,
completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined
};
if (existingIdx >= 0) {
index.queues[existingIdx] = indexEntry;
} else {
index.queues.unshift(indexEntry);
}
if (queue.status === 'active') {
index.active_queue_id = queue.id;
}
writeQueueIndex(index);
}
function generateQueueId(queue: Queue): string {
function generateQueueItemId(queue: Queue): string {
const maxNum = queue.queue.reduce((max, q) => {
const match = q.queue_id.match(/^Q-(\d+)$/);
return match ? Math.max(max, parseInt(match[1])) : max;
@@ -379,17 +514,19 @@ async function listAction(issueId: string | undefined, options: IssueOptions): P
async function statusAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
if (!issueId) {
// Show queue status
const queue = readQueue();
const queue = readActiveQueue();
const issues = readIssues();
const index = readQueueIndex();
if (options.json) {
console.log(JSON.stringify({ queue: queue._metadata, issues: issues.length }, null, 2));
console.log(JSON.stringify({ queue: queue._metadata, issues: issues.length, queues: index.queues.length }, null, 2));
return;
}
console.log(chalk.bold.cyan('\nSystem Status\n'));
console.log(`Issues: ${issues.length}`);
console.log(`Queue: ${queue._metadata.total_tasks} tasks`);
console.log(`Queues: ${index.queues.length} (Active: ${index.active_queue_id || 'none'})`);
console.log(`Active Queue: ${queue._metadata.total_tasks} tasks`);
console.log(` Pending: ${queue._metadata.pending_count}`);
console.log(` Executing: ${queue._metadata.executing_count}`);
console.log(` Completed: ${queue._metadata.completed_count}`);
@@ -497,7 +634,20 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
action: 'Implement',
description: options.description || options.title,
implementation: [],
acceptance: ['Task completed successfully'],
test: {
unit: [],
commands: ['npm test']
},
regression: ['npm test'],
acceptance: {
criteria: ['Task completed successfully'],
verification: ['Manual verification']
},
commit: {
type: 'feat',
scope: 'core',
message_template: `feat(core): ${options.title}`
},
depends_on: [],
executor: (options.executor as any) || 'auto'
};
@@ -590,13 +740,90 @@ async function bindAction(issueId: string | undefined, solutionId: string | unde
}
/**
* queue - Queue management (list / add)
* queue - Queue management (list / add / history)
*/
async function queueAction(subAction: string | undefined, issueId: string | undefined, options: IssueOptions): Promise<void> {
const queue = readQueue();
// List all queues (history)
if (subAction === 'list' || subAction === 'history') {
const index = readQueueIndex();
if (options.json) {
console.log(JSON.stringify(index, null, 2));
return;
}
console.log(chalk.bold.cyan('\nQueue History\n'));
console.log(chalk.gray(`Active: ${index.active_queue_id || 'none'}`));
console.log();
if (index.queues.length === 0) {
console.log(chalk.yellow('No queues found'));
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
return;
}
console.log(chalk.gray('ID'.padEnd(22) + 'Status'.padEnd(12) + 'Tasks'.padEnd(10) + 'Issues'));
console.log(chalk.gray('-'.repeat(70)));
for (const q of index.queues) {
const statusColor = {
'active': chalk.green,
'completed': chalk.cyan,
'archived': chalk.gray,
'failed': chalk.red
}[q.status] || chalk.white;
const marker = q.id === index.active_queue_id ? '→ ' : ' ';
console.log(
marker +
q.id.padEnd(20) +
statusColor(q.status.padEnd(12)) +
`${q.completed_tasks}/${q.total_tasks}`.padEnd(10) +
q.issue_ids.join(', ')
);
}
return;
}
// Switch active queue
if (subAction === 'switch' && issueId) {
const queueId = issueId; // issueId is actually queue ID here
const targetQueue = readQueue(queueId);
if (!targetQueue) {
console.error(chalk.red(`Queue "${queueId}" not found`));
process.exit(1);
}
const index = readQueueIndex();
index.active_queue_id = queueId;
writeQueueIndex(index);
console.log(chalk.green(`✓ Switched to queue ${queueId}`));
return;
}
// Archive current queue
if (subAction === 'archive') {
const queue = readActiveQueue();
if (!queue.id || queue.queue.length === 0) {
console.log(chalk.yellow('No active queue to archive'));
return;
}
queue.status = 'archived';
writeQueue(queue);
const index = readQueueIndex();
index.active_queue_id = null;
writeQueueIndex(index);
console.log(chalk.green(`✓ Archived queue ${queue.id}`));
return;
}
// Add issue tasks to queue
if (subAction === 'add' && issueId) {
// Add issue tasks to queue
const issue = findIssue(issueId);
if (!issue) {
console.error(chalk.red(`Issue "${issueId}" not found`));
@@ -610,13 +837,27 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
process.exit(1);
}
// Get or create active queue (create new if current is completed/archived)
let queue = readActiveQueue();
const isNewQueue = queue.queue.length === 0 || queue.status !== 'active';
if (queue.status !== 'active') {
// Create new queue if current is not active
queue = createEmptyQueue();
}
// Add issue to queue's issue list
if (!queue.issue_ids.includes(issueId)) {
queue.issue_ids.push(issueId);
}
let added = 0;
for (const task of solution.tasks) {
const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id);
if (exists) continue;
queue.queue.push({
queue_id: generateQueueId(queue),
queue_id: generateQueueItemId(queue),
issue_id: issueId,
solution_id: solution.id,
task_id: task.id,
@@ -637,26 +878,35 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
writeQueue(queue);
updateIssue(issueId, { status: 'queued', queued_at: new Date().toISOString() });
console.log(chalk.green(`✓ Added ${added} tasks to queue from ${solution.id}`));
if (isNewQueue) {
console.log(chalk.green(`✓ Created queue ${queue.id}`));
}
console.log(chalk.green(`✓ Added ${added} tasks from ${solution.id}`));
return;
}
// List queue
// Show current queue
const queue = readActiveQueue();
if (options.json) {
console.log(JSON.stringify(queue, null, 2));
return;
}
console.log(chalk.bold.cyan('\nExecution Queue\n'));
console.log(chalk.gray(`Total: ${queue._metadata.total_tasks} | Pending: ${queue._metadata.pending_count} | Executing: ${queue._metadata.executing_count} | Completed: ${queue._metadata.completed_count}`));
console.log();
console.log(chalk.bold.cyan('\nActive Queue\n'));
if (queue.queue.length === 0) {
console.log(chalk.yellow('Queue is empty'));
console.log(chalk.gray('Add tasks: ccw issue queue add <issue-id>'));
if (!queue.id || queue.queue.length === 0) {
console.log(chalk.yellow('No active queue'));
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
console.log(chalk.gray('Or list history: ccw issue queue list'));
return;
}
console.log(chalk.gray(`Queue: ${queue.id}`));
console.log(chalk.gray(`Issues: ${queue.issue_ids.join(', ')}`));
console.log(chalk.gray(`Total: ${queue._metadata.total_tasks} | Pending: ${queue._metadata.pending_count} | Executing: ${queue._metadata.executing_count} | Completed: ${queue._metadata.completed_count}`));
console.log();
console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
console.log(chalk.gray('-'.repeat(60)));
@@ -684,7 +934,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
* next - Get next ready task for execution (JSON output)
*/
async function nextAction(options: IssueOptions): Promise<void> {
const queue = readQueue();
const queue = readActiveQueue();
// Find ready tasks
const readyTasks = queue.queue.filter(item => {
@@ -749,7 +999,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
process.exit(1);
}
const queue = readQueue();
const queue = readActiveQueue();
const idx = queue.queue.findIndex(q => q.queue_id === queueId);
if (idx === -1) {
@@ -771,31 +1021,49 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
}
}
writeQueue(queue);
// Check if all issue tasks are complete
const issueId = queue.queue[idx].issue_id;
const issueTasks = queue.queue.filter(q => q.issue_id === issueId);
const allComplete = issueTasks.every(q => q.status === 'completed');
const anyFailed = issueTasks.some(q => q.status === 'failed');
const allIssueComplete = issueTasks.every(q => q.status === 'completed');
const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
if (allComplete) {
if (allIssueComplete) {
updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
console.log(chalk.green(`${queueId} completed`));
console.log(chalk.green(`✓ Issue ${issueId} completed (all tasks done)`));
} else if (anyFailed) {
} else if (anyIssueFailed) {
updateIssue(issueId, { status: 'failed' });
console.log(chalk.red(`${queueId} failed`));
} else {
console.log(isFail ? chalk.red(`${queueId} failed`) : chalk.green(`${queueId} completed`));
}
// Check if entire queue is complete
const allQueueComplete = queue.queue.every(q => q.status === 'completed');
const anyQueueFailed = queue.queue.some(q => q.status === 'failed');
if (allQueueComplete) {
queue.status = 'completed';
console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`));
} else if (anyQueueFailed && queue.queue.every(q => q.status === 'completed' || q.status === 'failed')) {
queue.status = 'failed';
console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
}
writeQueue(queue);
}
/**
* retry - Retry failed tasks
*/
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
const queue = readQueue();
const queue = readActiveQueue();
if (!queue.id || queue.queue.length === 0) {
console.log(chalk.yellow('No active queue'));
return;
}
let updated = 0;
for (const item of queue.queue) {
@@ -815,6 +1083,11 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
return;
}
// Reset queue status if it was failed
if (queue.status === 'failed') {
queue.status = 'active';
}
writeQueue(queue);
if (issueId) {
@@ -873,7 +1146,7 @@ export async function issueCommand(
await doneAction(argsArray[0], { ...options, fail: true });
break;
default:
console.log(chalk.bold.cyan('\nCCW Issue Management (v2.0 - Unified JSONL)\n'));
console.log(chalk.bold.cyan('\nCCW Issue Management (v3.0 - Multi-Queue + Lifecycle)\n'));
console.log(chalk.bold('Core Commands:'));
console.log(chalk.gray(' init <issue-id> Initialize new issue'));
console.log(chalk.gray(' list [issue-id] List issues or tasks'));
@@ -882,8 +1155,11 @@ export async function issueCommand(
console.log(chalk.gray(' bind <issue-id> [sol-id] Bind solution (--solution <path> to register)'));
console.log();
console.log(chalk.bold('Queue Commands:'));
console.log(chalk.gray(' queue [list] Show execution queue'));
console.log(chalk.gray(' queue add <issue-id> Add bound solution tasks to queue'));
console.log(chalk.gray(' queue Show active queue'));
console.log(chalk.gray(' queue list List all queues (history)'));
console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
console.log(chalk.gray(' queue archive Archive current queue'));
console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
console.log();
console.log(chalk.bold('Execution Endpoints:'));
@@ -902,6 +1178,7 @@ export async function issueCommand(
console.log(chalk.bold('Storage:'));
console.log(chalk.gray(' .workflow/issues/issues.jsonl All issues'));
console.log(chalk.gray(' .workflow/issues/solutions/*.jsonl Solutions per issue'));
console.log(chalk.gray(' .workflow/issues/queue.json Execution queue'));
console.log(chalk.gray(' .workflow/issues/queues/ Queue files (multi-queue)'));
console.log(chalk.gray(' .workflow/issues/queues/index.json Queue index'));
}
}

View File

@@ -72,21 +72,68 @@ function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[
}
function readQueue(issuesDir: string) {
const queuePath = join(issuesDir, 'queue.json');
if (!existsSync(queuePath)) {
return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
// Try new multi-queue structure first
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
if (existsSync(indexPath)) {
try {
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
const activeQueueId = index.active_queue_id;
if (activeQueueId) {
const queueFilePath = join(queuesDir, `${activeQueueId}.json`);
if (existsSync(queueFilePath)) {
return JSON.parse(readFileSync(queueFilePath, 'utf8'));
}
}
} catch {
// Fall through to legacy check
}
}
try {
return JSON.parse(readFileSync(queuePath, 'utf8'));
} catch {
return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
// Fallback to legacy queue.json
const legacyQueuePath = join(issuesDir, 'queue.json');
if (existsSync(legacyQueuePath)) {
try {
return JSON.parse(readFileSync(legacyQueuePath, 'utf8'));
} catch {
// Return empty queue
}
}
return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
}
function writeQueue(issuesDir: string, queue: any) {
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
queue._metadata = { ...queue._metadata, last_updated: new Date().toISOString(), total_tasks: queue.queue?.length || 0 };
writeFileSync(join(issuesDir, 'queue.json'), JSON.stringify(queue, null, 2));
queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.queue?.length || 0 };
// Check if using new multi-queue structure
const queuesDir = join(issuesDir, 'queues');
const indexPath = join(queuesDir, 'index.json');
if (existsSync(indexPath) && queue.id) {
// Write to new structure
const queueFilePath = join(queuesDir, `${queue.id}.json`);
writeFileSync(queueFilePath, JSON.stringify(queue, null, 2));
// Update index metadata
try {
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
const queueEntry = index.queues?.find((q: any) => q.id === queue.id);
if (queueEntry) {
queueEntry.total_tasks = queue.queue?.length || 0;
queueEntry.completed_tasks = queue.queue?.filter((i: any) => i.status === 'completed').length || 0;
writeFileSync(indexPath, JSON.stringify(index, null, 2));
}
} catch {
// Ignore index update errors
}
} else {
// Fallback to legacy queue.json
writeFileSync(join(issuesDir, 'queue.json'), JSON.stringify(queue, null, 2));
}
}
function getIssueDetail(issuesDir: string, issueId: string) {

View File

@@ -276,9 +276,105 @@
color: hsl(var(--muted-foreground));
}
.queue-empty svg {
.queue-empty svg,
.queue-empty > i {
margin-bottom: 1rem;
opacity: 0.5;
color: hsl(var(--muted-foreground));
}
.queue-empty-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.queue-empty-title {
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
}
.queue-empty-hint {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
margin-bottom: 1.5rem;
}
.queue-create-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;
}
.queue-create-btn:hover {
background: hsl(var(--primary) / 0.9);
transform: translateY(-1px);
}
/* Queue Toolbar */
.queue-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
}
.queue-stats {
display: flex;
align-items: center;
gap: 0.5rem;
}
.queue-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Command Box */
.command-option {
margin-bottom: 0.75rem;
}
.command-box {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: hsl(var(--muted) / 0.5);
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
}
.command-text {
flex: 1;
font-family: var(--font-mono);
font-size: 0.875rem;
color: hsl(var(--foreground));
word-break: break-all;
}
.command-info {
padding: 0.75rem;
background: hsl(var(--primary) / 0.05);
border-radius: 0.375rem;
border-left: 3px solid hsl(var(--primary));
}
/* Issue ID */
@@ -1349,6 +1445,20 @@
cursor: pointer;
}
/* Input with action button */
.input-with-action {
display: flex;
gap: 0.5rem;
}
.input-with-action input {
flex: 1;
}
.input-with-action .btn-icon {
flex-shrink: 0;
}
/* ==========================================
BUTTON STYLES
========================================== */
@@ -1759,3 +1869,676 @@
-webkit-overflow-scrolling: touch;
}
}
/* ==========================================
SOLUTION DETAIL MODAL
========================================== */
.solution-modal {
position: fixed;
inset: 0;
z-index: 1100;
display: flex;
align-items: center;
justify-content: center;
}
.solution-modal.hidden {
display: none;
}
.solution-modal-backdrop {
position: absolute;
inset: 0;
background: hsl(var(--foreground) / 0.6);
animation: fadeIn 0.15s ease-out;
}
.solution-modal-content {
position: relative;
width: 90%;
max-width: 720px;
max-height: 85vh;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 0.75rem;
box-shadow: 0 25px 50px hsl(var(--foreground) / 0.2);
display: flex;
flex-direction: column;
animation: modalSlideIn 0.2s ease-out;
}
.solution-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid hsl(var(--border));
flex-shrink: 0;
}
.solution-modal-title h3 {
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-top: 0.25rem;
}
.solution-modal-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.solution-modal-body {
flex: 1;
overflow-y: auto;
padding: 1.25rem;
}
/* Solution Overview Stats */
.solution-overview {
display: flex;
gap: 1.5rem;
padding: 1rem;
background: hsl(var(--muted) / 0.3);
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.solution-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.solution-stat-value {
font-size: 1.5rem;
font-weight: 700;
color: hsl(var(--foreground));
}
.solution-stat-label {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Solution Detail Section */
.solution-detail-section {
margin-bottom: 1.5rem;
}
.solution-detail-section:last-child {
margin-bottom: 0;
}
.solution-detail-section-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.75rem;
}
/* Solution Tasks Detail */
.solution-tasks-detail {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.solution-task-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
overflow: hidden;
}
.solution-task-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: hsl(var(--muted) / 0.3);
cursor: pointer;
transition: background 0.15s ease;
}
.solution-task-header:hover {
background: hsl(var(--muted) / 0.5);
}
.solution-task-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.solution-task-index {
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
min-width: 1.5rem;
}
.solution-task-id {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
.task-expand-icon {
transition: transform 0.2s ease;
color: hsl(var(--muted-foreground));
}
.solution-task-title {
padding: 0.75rem 1rem;
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
border-top: 1px solid hsl(var(--border) / 0.5);
}
.solution-task-details {
padding: 0.75rem 1rem;
border-top: 1px solid hsl(var(--border));
background: hsl(var(--muted) / 0.15);
}
.solution-task-scope {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
padding: 0.5rem;
background: hsl(var(--primary) / 0.1);
border-radius: 0.375rem;
}
.solution-task-scope-label {
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
}
.solution-task-subtitle {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
color: hsl(var(--muted-foreground));
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.solution-task-mod-points,
.solution-task-impl-steps,
.solution-task-acceptance,
.solution-task-deps {
margin-bottom: 0.75rem;
}
.solution-task-list,
.solution-impl-list,
.solution-acceptance-list {
margin: 0;
padding-left: 1.25rem;
font-size: 0.8125rem;
color: hsl(var(--foreground));
}
.solution-task-list li,
.solution-impl-list li,
.solution-acceptance-list li {
margin: 0.25rem 0;
}
.solution-mod-point {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.mod-point-file {
color: hsl(var(--primary));
font-size: 0.8125rem;
}
.mod-point-change {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin-left: 0.5rem;
}
.solution-deps-list {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.solution-dep-tag {
padding: 0.125rem 0.5rem;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
border-radius: 0.25rem;
font-size: 0.75rem;
}
/* ==========================================
LIFECYCLE PHASE BADGES
========================================== */
.phase-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 600;
margin-right: 0.25rem;
}
.phase-badge.phase-1 {
background: hsl(217 91% 60% / 0.2);
color: hsl(217 91% 60%);
}
.phase-badge.phase-2 {
background: hsl(262 83% 58% / 0.2);
color: hsl(262 83% 58%);
}
.phase-badge.phase-3 {
background: hsl(25 95% 53% / 0.2);
color: hsl(25 95% 53%);
}
.phase-badge.phase-4 {
background: hsl(142 71% 45% / 0.2);
color: hsl(142 71% 45%);
}
.phase-badge.phase-5 {
background: hsl(199 89% 48% / 0.2);
color: hsl(199 89% 48%);
}
/* ==========================================
QUEUE STATS GRID
========================================== */
.queue-stats-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.75rem;
}
@media (max-width: 768px) {
.queue-stats-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 480px) {
.queue-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.queue-stat-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.75rem 1rem;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
text-align: center;
}
.queue-stat-card .queue-stat-value {
font-size: 1.5rem;
font-weight: 700;
color: hsl(var(--foreground));
line-height: 1.2;
}
.queue-stat-card .queue-stat-label {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.025em;
margin-top: 0.25rem;
}
.queue-stat-card.pending {
border-color: hsl(var(--muted-foreground) / 0.3);
}
.queue-stat-card.pending .queue-stat-value {
color: hsl(var(--muted-foreground));
}
.queue-stat-card.executing {
border-color: hsl(45 93% 47% / 0.5);
background: hsl(45 93% 47% / 0.05);
}
.queue-stat-card.executing .queue-stat-value {
color: hsl(45 93% 47%);
}
.queue-stat-card.completed {
border-color: hsl(var(--success) / 0.5);
background: hsl(var(--success) / 0.05);
}
.queue-stat-card.completed .queue-stat-value {
color: hsl(var(--success));
}
.queue-stat-card.failed {
border-color: hsl(var(--destructive) / 0.5);
background: hsl(var(--destructive) / 0.05);
}
.queue-stat-card.failed .queue-stat-value {
color: hsl(var(--destructive));
}
/* ==========================================
QUEUE INFO CARDS
========================================== */
.queue-info-card {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.queue-info-label {
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.05em;
}
.queue-info-value {
font-size: 0.875rem;
color: hsl(var(--foreground));
}
/* Queue Status Badge */
.queue-status-badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 500;
text-transform: uppercase;
}
.queue-status-badge.active {
background: hsl(217 91% 60% / 0.15);
color: hsl(217 91% 60%);
}
.queue-status-badge.completed {
background: hsl(var(--success) / 0.15);
color: hsl(var(--success));
}
.queue-status-badge.failed {
background: hsl(var(--destructive) / 0.15);
color: hsl(var(--destructive));
}
.queue-status-badge.archived {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
/* ==========================================
SOLUTION TASK SECTIONS
========================================== */
.solution-task-section {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid hsl(var(--border) / 0.3);
}
.solution-task-section:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
/* ==========================================
TEST SECTION STYLES
========================================== */
.test-subsection,
.acceptance-subsection {
margin-bottom: 0.5rem;
}
.test-subsection:last-child,
.acceptance-subsection:last-child {
margin-bottom: 0;
}
.test-label,
.acceptance-label {
display: block;
font-size: 0.6875rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.025em;
margin-bottom: 0.25rem;
}
.test-list {
margin: 0;
padding-left: 1.25rem;
font-size: 0.8125rem;
color: hsl(var(--foreground));
}
.test-list li {
margin: 0.125rem 0;
}
.test-commands,
.verification-commands {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.test-command,
.verification-command {
display: block;
padding: 0.375rem 0.625rem;
background: hsl(var(--muted) / 0.5);
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;
font-family: var(--font-mono);
font-size: 0.75rem;
color: hsl(var(--foreground));
word-break: break-all;
}
.coverage-target {
font-size: 0.6875rem;
font-weight: 400;
color: hsl(var(--muted-foreground));
margin-left: 0.25rem;
}
/* ==========================================
COMMIT INFO STYLES
========================================== */
.commit-info {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
padding: 0.625rem;
}
.commit-type {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.commit-type-badge {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: lowercase;
}
.commit-type-badge.feat {
background: hsl(142 71% 45% / 0.15);
color: hsl(142 71% 45%);
}
.commit-type-badge.fix {
background: hsl(0 84% 60% / 0.15);
color: hsl(0 84% 60%);
}
.commit-type-badge.refactor {
background: hsl(262 83% 58% / 0.15);
color: hsl(262 83% 58%);
}
.commit-type-badge.test {
background: hsl(199 89% 48% / 0.15);
color: hsl(199 89% 48%);
}
.commit-type-badge.docs {
background: hsl(45 93% 47% / 0.15);
color: hsl(45 93% 47%);
}
.commit-type-badge.chore {
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
}
.commit-scope {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
}
.commit-breaking {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.375rem;
background: hsl(var(--destructive) / 0.15);
color: hsl(var(--destructive));
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 0.05em;
}
.commit-message {
margin: 0;
padding: 0.5rem;
background: hsl(var(--background));
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;
font-family: var(--font-mono);
font-size: 0.75rem;
color: hsl(var(--foreground));
white-space: pre-wrap;
word-break: break-word;
}
/* Modification Point Target */
.mod-point-target {
font-size: 0.75rem;
color: hsl(var(--primary));
font-family: var(--font-mono);
}
/* JSON Toggle */
.solution-json-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.75rem 1rem;
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s ease;
}
.solution-json-toggle:hover {
background: hsl(var(--muted) / 0.5);
color: hsl(var(--foreground));
}
.solution-json-content {
margin-top: 0.5rem;
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
overflow: hidden;
}
.solution-json-pre {
margin: 0;
padding: 1rem;
background: hsl(var(--muted) / 0.3);
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.5;
color: hsl(var(--foreground));
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
}
/* Responsive Solution Modal */
@media (max-width: 640px) {
.solution-modal-content {
max-height: 95vh;
margin: 0.5rem;
}
.solution-overview {
flex-wrap: wrap;
justify-content: center;
}
.solution-stat {
min-width: 80px;
}
}

View File

@@ -1772,6 +1772,45 @@ const i18n = {
'issues.created': 'Issue created successfully',
'issues.confirmDelete': 'Are you sure you want to delete this issue?',
'issues.deleted': 'Issue deleted',
'issues.idAutoGenerated': 'Auto-generated',
'issues.regenerateId': 'Regenerate ID',
// Solution detail
'issues.solutionDetail': 'Solution Details',
'issues.bind': 'Bind',
'issues.unbind': 'Unbind',
'issues.bound': 'Bound',
'issues.totalTasks': 'Total Tasks',
'issues.bindStatus': 'Bind Status',
'issues.createdAt': 'Created',
'issues.taskList': 'Task List',
'issues.noTasks': 'No tasks in this solution',
'issues.noSolutions': 'No solutions',
'issues.viewJson': 'View Raw JSON',
'issues.scope': 'Scope',
'issues.modificationPoints': 'Modification Points',
'issues.implementationSteps': 'Implementation Steps',
'issues.acceptanceCriteria': 'Acceptance Criteria',
'issues.dependencies': 'Dependencies',
'issues.solutionBound': 'Solution bound successfully',
'issues.solutionUnbound': 'Solution unbound',
// Queue operations
'issues.queueEmptyHint': 'Generate execution queue from bound solutions',
'issues.createQueue': 'Create Queue',
'issues.regenerate': 'Regenerate',
'issues.regenerateQueue': 'Regenerate Queue',
'issues.refreshQueue': 'Refresh',
'issues.executionGroups': 'groups',
'issues.totalItems': 'items',
'issues.queueRefreshed': 'Queue refreshed',
'issues.confirmCreateQueue': 'This will execute /issue:queue command via Claude Code CLI to generate execution queue from bound solutions.\n\nContinue?',
'issues.creatingQueue': 'Creating execution queue...',
'issues.queueExecutionStarted': 'Queue generation started',
'issues.queueCreated': 'Queue created successfully',
'issues.queueCreationFailed': 'Queue creation failed',
'issues.queueCommandHint': 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:',
'issues.queueCommandInfo': 'After running the command, click "Refresh" to see the updated queue.',
'issues.alternative': 'Alternative',
'issues.refreshAfter': 'Refresh Queue',
// issue.* keys (legacy)
'issue.viewIssues': 'Issues',
'issue.viewQueue': 'Queue',
@@ -3595,6 +3634,45 @@ const i18n = {
'issues.created': '议题创建成功',
'issues.confirmDelete': '确定要删除此议题吗?',
'issues.deleted': '议题已删除',
'issues.idAutoGenerated': '自动生成',
'issues.regenerateId': '重新生成ID',
// Solution detail
'issues.solutionDetail': '解决方案详情',
'issues.bind': '绑定',
'issues.unbind': '解绑',
'issues.bound': '已绑定',
'issues.totalTasks': '任务总数',
'issues.bindStatus': '绑定状态',
'issues.createdAt': '创建时间',
'issues.taskList': '任务列表',
'issues.noTasks': '此解决方案无任务',
'issues.noSolutions': '暂无解决方案',
'issues.viewJson': '查看原始JSON',
'issues.scope': '作用域',
'issues.modificationPoints': '修改点',
'issues.implementationSteps': '实现步骤',
'issues.acceptanceCriteria': '验收标准',
'issues.dependencies': '依赖项',
'issues.solutionBound': '解决方案已绑定',
'issues.solutionUnbound': '解决方案已解绑',
// Queue operations
'issues.queueEmptyHint': '从绑定的解决方案生成执行队列',
'issues.createQueue': '创建队列',
'issues.regenerate': '重新生成',
'issues.regenerateQueue': '重新生成队列',
'issues.refreshQueue': '刷新',
'issues.executionGroups': '个执行组',
'issues.totalItems': '个任务',
'issues.queueRefreshed': '队列已刷新',
'issues.confirmCreateQueue': '这将通过 Claude Code CLI 执行 /issue:queue 命令,从绑定的解决方案生成执行队列。\n\n是否继续',
'issues.creatingQueue': '正在创建执行队列...',
'issues.queueExecutionStarted': '队列生成已启动',
'issues.queueCreated': '队列创建成功',
'issues.queueCreationFailed': '队列创建失败',
'issues.queueCommandHint': '在终端中运行以下命令之一,从绑定的解决方案生成执行队列:',
'issues.queueCommandInfo': '运行命令后,点击"刷新"查看更新后的队列。',
'issues.alternative': '或者',
'issues.refreshAfter': '刷新队列',
// issue.* keys (legacy)
'issue.viewIssues': '议题',
'issue.viewQueue': '队列',

View File

@@ -168,16 +168,22 @@ async function loadAvailableSkills() {
if (!response.ok) throw new Error('Failed to load skills');
const data = await response.json();
// Combine project and user skills (API returns { projectSkills: [], userSkills: [] })
const allSkills = [
...(data.projectSkills || []).map(s => ({ ...s, scope: 'project' })),
...(data.userSkills || []).map(s => ({ ...s, scope: 'user' }))
];
const container = document.getElementById('skill-discovery-skill-context');
if (container && data.skills) {
if (data.skills.length === 0) {
if (container) {
if (allSkills.length === 0) {
container.innerHTML = `
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
<span class="text-muted-foreground ml-2">${t('hook.wizard.noSkillsFound').split('.')[0]}</span>
`;
} else {
const skillBadges = data.skills.map(skill => `
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description)}">${escapeHtml(skill.name)}</span>
const skillBadges = allSkills.map(skill => `
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description || '')}">${escapeHtml(skill.name)}</span>
`).join('');
container.innerHTML = `
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
@@ -187,7 +193,7 @@ async function loadAvailableSkills() {
}
// Store skills for wizard use
window.availableSkills = data.skills || [];
window.availableSkills = allSkills;
} catch (err) {
console.error('Failed to load skills:', err);
const container = document.getElementById('skill-discovery-skill-context');

View File

@@ -9,6 +9,7 @@ var issueData = {
queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} },
selectedIssue: null,
selectedSolution: null,
selectedSolutionIssueId: null,
statusFilter: 'all',
searchQuery: '',
viewMode: 'issues' // 'issues' | 'queue'
@@ -148,6 +149,31 @@ function renderIssueView() {
<!-- Detail Panel -->
<div id="issueDetailPanel" class="issue-detail-panel hidden"></div>
<!-- Solution Detail Modal -->
<div id="solutionDetailModal" class="solution-modal hidden">
<div class="solution-modal-backdrop" onclick="closeSolutionDetail()"></div>
<div class="solution-modal-content">
<div class="solution-modal-header">
<div class="solution-modal-title">
<span id="solutionDetailId" class="font-mono text-sm text-muted-foreground"></span>
<h3 id="solutionDetailTitle">${t('issues.solutionDetail') || 'Solution Details'}</h3>
</div>
<div class="solution-modal-actions">
<button id="solutionBindBtn" class="btn-secondary" onclick="toggleSolutionBind()">
<i data-lucide="link" class="w-4 h-4"></i>
<span>${t('issues.bind') || 'Bind'}</span>
</button>
<button class="btn-icon" onclick="closeSolutionDetail()">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
</div>
<div class="solution-modal-body" id="solutionDetailBody">
<!-- Content will be rendered dynamically -->
</div>
</div>
</div>
<!-- Create Issue Modal -->
<div id="createIssueModal" class="issue-modal hidden">
<div class="issue-modal-backdrop" onclick="hideCreateIssueModal()"></div>
@@ -161,7 +187,12 @@ function renderIssueView() {
<div class="issue-modal-body">
<div class="form-group">
<label>${t('issues.issueId') || 'Issue ID'}</label>
<input type="text" id="newIssueId" placeholder="e.g., GH-123 or TASK-001" />
<div class="input-with-action">
<input type="text" id="newIssueId" placeholder="${t('issues.idAutoGenerated') || 'Auto-generated'}" />
<button type="button" class="btn-icon" onclick="regenerateIssueId()" title="${t('issues.regenerateId') || 'Regenerate ID'}">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
</button>
</div>
</div>
<div class="form-group">
<label>${t('issues.issueTitle') || 'Title'}</label>
@@ -329,20 +360,129 @@ function filterIssuesByStatus(status) {
// ========== Queue Section ==========
function renderQueueSection() {
const queue = issueData.queue;
const groups = queue.execution_groups || [];
const groupedItems = queue.grouped_items || {};
const queueItems = queue.queue || [];
const metadata = queue._metadata || {};
if (groups.length === 0 && (!queue.queue || queue.queue.length === 0)) {
// Check if queue is empty
if (queueItems.length === 0) {
return `
<div class="queue-empty">
<i data-lucide="git-branch" class="w-12 h-12 text-muted-foreground mb-4"></i>
<p class="text-muted-foreground">${t('issues.queueEmpty') || 'Queue is empty'}</p>
<p class="text-sm text-muted-foreground mt-2">Run /issue:queue to form execution queue</p>
<div class="queue-empty-container">
<div class="queue-empty">
<i data-lucide="git-branch" class="w-16 h-16"></i>
<p class="queue-empty-title">${t('issues.queueEmpty') || 'Queue is empty'}</p>
<p class="queue-empty-hint">${t('issues.queueEmptyHint') || 'Generate execution queue from bound solutions'}</p>
<button class="queue-create-btn" onclick="createExecutionQueue()">
<i data-lucide="play" class="w-4 h-4"></i>
<span>${t('issues.createQueue') || 'Create Queue'}</span>
</button>
</div>
</div>
`;
}
// Group items by execution_group or treat all as single group
const groups = queue.execution_groups || [];
let groupedItems = queue.grouped_items || {};
// If no execution_groups, create a default grouping from queue items
if (groups.length === 0 && queueItems.length > 0) {
const groupMap = {};
queueItems.forEach(item => {
const groupId = item.execution_group || 'default';
if (!groupMap[groupId]) {
groupMap[groupId] = [];
}
groupMap[groupId].push(item);
});
// Create synthetic groups
const syntheticGroups = Object.keys(groupMap).map(groupId => ({
id: groupId,
type: 'sequential',
task_count: groupMap[groupId].length
}));
return `
<!-- Queue Header -->
<div class="queue-toolbar mb-4">
<div class="queue-stats">
<div class="queue-info-card">
<span class="queue-info-label">${t('issues.queueId') || 'Queue ID'}</span>
<span class="queue-info-value font-mono text-sm">${queue.id || 'N/A'}</span>
</div>
<div class="queue-info-card">
<span class="queue-info-label">${t('issues.status') || 'Status'}</span>
<span class="queue-status-badge ${queue.status || ''}">${queue.status || 'unknown'}</span>
</div>
<div class="queue-info-card">
<span class="queue-info-label">${t('issues.issues') || 'Issues'}</span>
<span class="queue-info-value">${(queue.issue_ids || []).join(', ') || 'N/A'}</span>
</div>
</div>
<div class="queue-actions">
<button class="btn-secondary" onclick="refreshQueue()" title="${t('issues.refreshQueue') || 'Refresh'}">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
</button>
<button class="btn-secondary" onclick="createExecutionQueue()" title="${t('issues.regenerateQueue') || 'Regenerate Queue'}">
<i data-lucide="rotate-cw" class="w-4 h-4"></i>
<span>${t('issues.regenerate') || 'Regenerate'}</span>
</button>
</div>
</div>
<!-- Queue Stats -->
<div class="queue-stats-grid mb-4">
<div class="queue-stat-card">
<span class="queue-stat-value">${metadata.total_tasks || queueItems.length}</span>
<span class="queue-stat-label">${t('issues.totalTasks') || 'Total'}</span>
</div>
<div class="queue-stat-card pending">
<span class="queue-stat-value">${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length}</span>
<span class="queue-stat-label">${t('issues.pending') || 'Pending'}</span>
</div>
<div class="queue-stat-card executing">
<span class="queue-stat-value">${metadata.executing_count || queueItems.filter(i => i.status === 'executing').length}</span>
<span class="queue-stat-label">${t('issues.executing') || 'Executing'}</span>
</div>
<div class="queue-stat-card completed">
<span class="queue-stat-value">${metadata.completed_count || queueItems.filter(i => i.status === 'completed').length}</span>
<span class="queue-stat-label">${t('issues.completed') || 'Completed'}</span>
</div>
<div class="queue-stat-card failed">
<span class="queue-stat-value">${metadata.failed_count || queueItems.filter(i => i.status === 'failed').length}</span>
<span class="queue-stat-label">${t('issues.failed') || 'Failed'}</span>
</div>
</div>
<!-- Queue Items -->
<div class="queue-timeline">
${syntheticGroups.map(group => renderQueueGroup(group, groupMap[group.id] || [])).join('')}
</div>
${queue.conflicts && queue.conflicts.length > 0 ? renderConflictsSection(queue.conflicts) : ''}
`;
}
return `
<!-- Queue Toolbar -->
<div class="queue-toolbar mb-4">
<div class="queue-stats">
<span class="text-sm text-muted-foreground">
${groups.length} ${t('issues.executionGroups') || 'groups'} ·
${queueItems.length} ${t('issues.totalItems') || 'items'}
</span>
</div>
<div class="queue-actions">
<button class="btn-secondary" onclick="refreshQueue()" title="${t('issues.refreshQueue') || 'Refresh'}">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
</button>
<button class="btn-secondary" onclick="createExecutionQueue()" title="${t('issues.regenerateQueue') || 'Regenerate Queue'}">
<i data-lucide="rotate-cw" class="w-4 h-4"></i>
<span>${t('issues.regenerate') || 'Regenerate'}</span>
</button>
</div>
</div>
<div class="queue-info mb-4">
<p class="text-sm text-muted-foreground">
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
@@ -605,24 +745,16 @@ function renderIssueDetailPanel(issue) {
<div class="detail-section">
<label class="detail-label">${t('issues.solutions') || 'Solutions'} (${issue.solutions?.length || 0})</label>
<div class="solutions-list">
${(issue.solutions || []).map(sol => `
<div class="solution-item ${sol.is_bound ? 'bound' : ''}" onclick="toggleSolutionExpand('${sol.id}')">
${(issue.solutions || []).length > 0 ? (issue.solutions || []).map(sol => `
<div class="solution-item ${sol.is_bound ? 'bound' : ''}" onclick="openSolutionDetail('${issue.id}', '${sol.id}')">
<div class="solution-header">
<span class="solution-id font-mono text-xs">${sol.id}</span>
${sol.is_bound ? '<span class="solution-bound-badge">Bound</span>' : ''}
<span class="solution-tasks text-xs">${sol.tasks?.length || 0} tasks</span>
</div>
<div class="solution-tasks-list hidden" id="solution-${sol.id}">
${(sol.tasks || []).map(task => `
<div class="task-item">
<span class="task-id font-mono">${task.id}</span>
<span class="task-action ${task.action?.toLowerCase() || ''}">${task.action || 'Unknown'}</span>
<span class="task-title">${task.title || ''}</span>
</div>
`).join('')}
${sol.is_bound ? '<span class="solution-bound-badge">' + (t('issues.bound') || 'Bound') + '</span>' : ''}
<span class="solution-tasks text-xs">${sol.tasks?.length || 0} ${t('issues.tasks') || 'tasks'}</span>
<i data-lucide="chevron-right" class="w-4 h-4 ml-auto text-muted-foreground"></i>
</div>
</div>
`).join('') || '<p class="text-sm text-muted-foreground">No solutions</p>'}
`).join('') : '<p class="text-sm text-muted-foreground">' + (t('issues.noSolutions') || 'No solutions') + '</p>'}
</div>
</div>
@@ -630,7 +762,7 @@ function renderIssueDetailPanel(issue) {
<div class="detail-section">
<label class="detail-label">${t('issues.tasks') || 'Tasks'} (${issue.tasks?.length || 0})</label>
<div class="tasks-list">
${(issue.tasks || []).map(task => `
${(issue.tasks || []).length > 0 ? (issue.tasks || []).map(task => `
<div class="task-item-detail">
<div class="flex items-center justify-between">
<span class="font-mono text-sm">${task.id}</span>
@@ -642,7 +774,7 @@ function renderIssueDetailPanel(issue) {
</div>
<p class="task-title-detail">${task.title || task.description || ''}</p>
</div>
`).join('') || '<p class="text-sm text-muted-foreground">No tasks</p>'}
`).join('') : '<p class="text-sm text-muted-foreground">' + (t('issues.noTasks') || 'No tasks') + '</p>'}
</div>
</div>
</div>
@@ -666,6 +798,353 @@ function toggleSolutionExpand(solId) {
}
}
// ========== Solution Detail Modal ==========
function openSolutionDetail(issueId, solutionId) {
const issue = issueData.selectedIssue || issueData.issues.find(i => i.id === issueId);
if (!issue) return;
const solution = issue.solutions?.find(s => s.id === solutionId);
if (!solution) return;
issueData.selectedSolution = solution;
issueData.selectedSolutionIssueId = issueId;
const modal = document.getElementById('solutionDetailModal');
if (modal) {
modal.classList.remove('hidden');
renderSolutionDetail(solution);
lucide.createIcons();
}
}
function closeSolutionDetail() {
const modal = document.getElementById('solutionDetailModal');
if (modal) {
modal.classList.add('hidden');
}
issueData.selectedSolution = null;
issueData.selectedSolutionIssueId = null;
}
function renderSolutionDetail(solution) {
const idEl = document.getElementById('solutionDetailId');
const bodyEl = document.getElementById('solutionDetailBody');
const bindBtn = document.getElementById('solutionBindBtn');
if (idEl) {
idEl.textContent = solution.id;
}
// Update bind button state
if (bindBtn) {
if (solution.is_bound) {
bindBtn.innerHTML = `<i data-lucide="unlink" class="w-4 h-4"></i><span>${t('issues.unbind') || 'Unbind'}</span>`;
bindBtn.classList.remove('btn-secondary');
bindBtn.classList.add('btn-primary');
} else {
bindBtn.innerHTML = `<i data-lucide="link" class="w-4 h-4"></i><span>${t('issues.bind') || 'Bind'}</span>`;
bindBtn.classList.remove('btn-primary');
bindBtn.classList.add('btn-secondary');
}
}
if (!bodyEl) return;
const tasks = solution.tasks || [];
bodyEl.innerHTML = `
<!-- Solution Overview -->
<div class="solution-detail-section">
<div class="solution-overview">
<div class="solution-stat">
<span class="solution-stat-value">${tasks.length}</span>
<span class="solution-stat-label">${t('issues.totalTasks') || 'Total Tasks'}</span>
</div>
<div class="solution-stat">
<span class="solution-stat-value">${solution.is_bound ? '✓' : '—'}</span>
<span class="solution-stat-label">${t('issues.bindStatus') || 'Bind Status'}</span>
</div>
<div class="solution-stat">
<span class="solution-stat-value">${solution.created_at ? new Date(solution.created_at).toLocaleDateString() : '—'}</span>
<span class="solution-stat-label">${t('issues.createdAt') || 'Created'}</span>
</div>
</div>
</div>
<!-- Tasks List -->
<div class="solution-detail-section">
<h4 class="solution-detail-section-title">
<i data-lucide="list-checks" class="w-4 h-4"></i>
${t('issues.taskList') || 'Task List'}
</h4>
<div class="solution-tasks-detail">
${tasks.length === 0 ? `
<p class="text-sm text-muted-foreground text-center py-4">${t('issues.noTasks') || 'No tasks in this solution'}</p>
` : tasks.map((task, index) => renderSolutionTask(task, index)).join('')}
</div>
</div>
<!-- Raw JSON (collapsible) -->
<div class="solution-detail-section">
<button class="solution-json-toggle" onclick="toggleSolutionJson()">
<i data-lucide="code" class="w-4 h-4"></i>
<span>${t('issues.viewJson') || 'View Raw JSON'}</span>
<i data-lucide="chevron-down" class="w-4 h-4 ml-auto"></i>
</button>
<div id="solutionJsonContent" class="solution-json-content hidden">
<pre class="solution-json-pre">${escapeHtml(JSON.stringify(solution, null, 2))}</pre>
</div>
</div>
`;
lucide.createIcons();
}
function renderSolutionTask(task, index) {
const actionClass = (task.action || 'unknown').toLowerCase();
const modPoints = task.modification_points || [];
// Support both old and new field names
const implSteps = task.implementation || task.implementation_steps || [];
const acceptance = task.acceptance || task.acceptance_criteria || [];
const testInfo = task.test || {};
const regression = task.regression || [];
const commitInfo = task.commit || {};
const dependsOn = task.depends_on || task.dependencies || [];
// Handle acceptance as object or array
const acceptanceCriteria = Array.isArray(acceptance) ? acceptance : (acceptance.criteria || []);
const acceptanceVerification = acceptance.verification || [];
return `
<div class="solution-task-card">
<div class="solution-task-header" onclick="toggleTaskExpand(${index})">
<div class="solution-task-info">
<span class="solution-task-index">#${index + 1}</span>
<span class="solution-task-id font-mono">${task.id || ''}</span>
<span class="task-action-badge ${actionClass}">${task.action || 'Unknown'}</span>
</div>
<i data-lucide="chevron-down" class="w-4 h-4 task-expand-icon" id="taskExpandIcon${index}"></i>
</div>
<div class="solution-task-title">${task.title || task.description || 'No title'}</div>
<div class="solution-task-details hidden" id="taskDetails${index}">
${task.scope ? `
<div class="solution-task-scope">
<span class="solution-task-scope-label">${t('issues.scope') || 'Scope'}:</span>
<span class="font-mono text-sm">${task.scope}</span>
</div>
` : ''}
<!-- Phase 1: Implementation -->
${implSteps.length > 0 ? `
<div class="solution-task-section">
<h5 class="solution-task-subtitle">
<i data-lucide="code" class="w-3.5 h-3.5"></i>
<span class="phase-badge phase-1">1</span>
${t('issues.implementation') || 'Implementation'}
</h5>
<ol class="solution-impl-list">
${implSteps.map(step => `<li>${typeof step === 'string' ? step : step.description || JSON.stringify(step)}</li>`).join('')}
</ol>
</div>
` : ''}
${modPoints.length > 0 ? `
<div class="solution-task-section">
<h5 class="solution-task-subtitle">
<i data-lucide="file-edit" class="w-3.5 h-3.5"></i>
${t('issues.modificationPoints') || 'Modification Points'}
</h5>
<ul class="solution-task-list">
${modPoints.map(mp => `
<li class="solution-mod-point">
<span class="mod-point-file font-mono">${mp.file || mp}</span>
${mp.target ? `<span class="mod-point-target">→ ${mp.target}</span>` : ''}
${mp.change ? `<span class="mod-point-change">${mp.change}</span>` : ''}
</li>
`).join('')}
</ul>
</div>
` : ''}
<!-- Phase 2: Test -->
${(testInfo.unit?.length > 0 || testInfo.commands?.length > 0) ? `
<div class="solution-task-section">
<h5 class="solution-task-subtitle">
<i data-lucide="flask-conical" class="w-3.5 h-3.5"></i>
<span class="phase-badge phase-2">2</span>
${t('issues.test') || 'Test'}
${testInfo.coverage_target ? `<span class="coverage-target">(${testInfo.coverage_target}% coverage)</span>` : ''}
</h5>
${testInfo.unit?.length > 0 ? `
<div class="test-subsection">
<span class="test-label">${t('issues.unitTests') || 'Unit Tests'}:</span>
<ul class="test-list">
${testInfo.unit.map(t => `<li>${t}</li>`).join('')}
</ul>
</div>
` : ''}
${testInfo.integration?.length > 0 ? `
<div class="test-subsection">
<span class="test-label">${t('issues.integrationTests') || 'Integration'}:</span>
<ul class="test-list">
${testInfo.integration.map(t => `<li>${t}</li>`).join('')}
</ul>
</div>
` : ''}
${testInfo.commands?.length > 0 ? `
<div class="test-subsection">
<span class="test-label">${t('issues.commands') || 'Commands'}:</span>
<div class="test-commands">
${testInfo.commands.map(cmd => `<code class="test-command">${cmd}</code>`).join('')}
</div>
</div>
` : ''}
</div>
` : ''}
<!-- Phase 3: Regression -->
${regression.length > 0 ? `
<div class="solution-task-section">
<h5 class="solution-task-subtitle">
<i data-lucide="rotate-ccw" class="w-3.5 h-3.5"></i>
<span class="phase-badge phase-3">3</span>
${t('issues.regression') || 'Regression'}
</h5>
<div class="test-commands">
${regression.map(cmd => `<code class="test-command">${cmd}</code>`).join('')}
</div>
</div>
` : ''}
<!-- Phase 4: Acceptance -->
${acceptanceCriteria.length > 0 ? `
<div class="solution-task-section">
<h5 class="solution-task-subtitle">
<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>
<span class="phase-badge phase-4">4</span>
${t('issues.acceptance') || 'Acceptance'}
</h5>
<div class="acceptance-subsection">
<span class="acceptance-label">${t('issues.criteria') || 'Criteria'}:</span>
<ul class="solution-acceptance-list">
${acceptanceCriteria.map(ac => `<li>${typeof ac === 'string' ? ac : ac.description || JSON.stringify(ac)}</li>`).join('')}
</ul>
</div>
${acceptanceVerification.length > 0 ? `
<div class="acceptance-subsection">
<span class="acceptance-label">${t('issues.verification') || 'Verification'}:</span>
<div class="verification-commands">
${acceptanceVerification.map(v => `<code class="verification-command">${v}</code>`).join('')}
</div>
</div>
` : ''}
</div>
` : ''}
<!-- Phase 5: Commit -->
${commitInfo.type ? `
<div class="solution-task-section">
<h5 class="solution-task-subtitle">
<i data-lucide="git-commit" class="w-3.5 h-3.5"></i>
<span class="phase-badge phase-5">5</span>
${t('issues.commit') || 'Commit'}
</h5>
<div class="commit-info">
<div class="commit-type">
<span class="commit-type-badge ${commitInfo.type}">${commitInfo.type}</span>
<span class="commit-scope">(${commitInfo.scope || 'core'})</span>
${commitInfo.breaking ? '<span class="commit-breaking">BREAKING</span>' : ''}
</div>
${commitInfo.message_template ? `
<pre class="commit-message">${commitInfo.message_template}</pre>
` : ''}
</div>
</div>
` : ''}
<!-- Dependencies -->
${dependsOn.length > 0 ? `
<div class="solution-task-section">
<h5 class="solution-task-subtitle">
<i data-lucide="git-branch" class="w-3.5 h-3.5"></i>
${t('issues.dependencies') || 'Dependencies'}
</h5>
<div class="solution-deps-list">
${dependsOn.map(dep => `<span class="solution-dep-tag font-mono">${dep}</span>`).join('')}
</div>
</div>
` : ''}
</div>
</div>
`;
}
function toggleTaskExpand(index) {
const details = document.getElementById('taskDetails' + index);
const icon = document.getElementById('taskExpandIcon' + index);
if (details) {
details.classList.toggle('hidden');
}
if (icon) {
icon.style.transform = details?.classList.contains('hidden') ? '' : 'rotate(180deg)';
}
}
function toggleSolutionJson() {
const content = document.getElementById('solutionJsonContent');
if (content) {
content.classList.toggle('hidden');
}
}
async function toggleSolutionBind() {
const solution = issueData.selectedSolution;
const issueId = issueData.selectedSolutionIssueId;
if (!solution || !issueId) return;
const action = solution.is_bound ? 'unbind' : 'bind';
try {
const response = await fetch('/api/issues/' + encodeURIComponent(issueId) + '?path=' + encodeURIComponent(projectPath), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bound_solution_id: action === 'bind' ? solution.id : null
})
});
if (!response.ok) throw new Error('Failed to ' + action);
showNotification(action === 'bind' ? (t('issues.solutionBound') || 'Solution bound') : (t('issues.solutionUnbound') || 'Solution unbound'), 'success');
// Refresh data
await loadIssueData();
const detail = await loadIssueDetail(issueId);
if (detail) {
issueData.selectedIssue = detail;
// Update solution reference
const updatedSolution = detail.solutions?.find(s => s.id === solution.id);
if (updatedSolution) {
issueData.selectedSolution = updatedSolution;
renderSolutionDetail(updatedSolution);
}
renderIssueDetailPanel(detail);
}
} catch (err) {
console.error('Failed to ' + action + ' solution:', err);
showNotification('Failed to ' + action + ' solution', 'error');
}
}
// Helper: escape HTML
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function openQueueItemDetail(queueId) {
const item = issueData.queue.queue?.find(q => q.queue_id === queueId);
if (item) {
@@ -807,18 +1286,58 @@ function clearIssueSearch() {
}
// ========== Create Issue Modal ==========
function generateIssueId() {
// Generate unique ID: ISSUE-YYYYMMDD-XXX format
const now = new Date();
const dateStr = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0');
// Find existing IDs with same date prefix
const prefix = 'ISSUE-' + dateStr + '-';
const existingIds = (issueData.issues || [])
.map(i => i.id)
.filter(id => id.startsWith(prefix));
// Get next sequence number
let maxSeq = 0;
existingIds.forEach(id => {
const seqStr = id.replace(prefix, '');
const seq = parseInt(seqStr, 10);
if (!isNaN(seq) && seq > maxSeq) {
maxSeq = seq;
}
});
return prefix + String(maxSeq + 1).padStart(3, '0');
}
function showCreateIssueModal() {
const modal = document.getElementById('createIssueModal');
if (modal) {
modal.classList.remove('hidden');
// Auto-generate issue ID
const idInput = document.getElementById('newIssueId');
if (idInput) {
idInput.value = generateIssueId();
}
lucide.createIcons();
// Focus on first input
// Focus on title input instead of ID
setTimeout(() => {
document.getElementById('newIssueId')?.focus();
document.getElementById('newIssueTitle')?.focus();
}, 100);
}
}
function regenerateIssueId() {
const idInput = document.getElementById('newIssueId');
if (idInput) {
idInput.value = generateIssueId();
}
}
function hideCreateIssueModal() {
const modal = document.getElementById('createIssueModal');
if (modal) {
@@ -913,3 +1432,115 @@ async function deleteIssue(issueId) {
showNotification('Failed to delete issue', 'error');
}
}
// ========== Queue Operations ==========
async function refreshQueue() {
try {
await loadQueueData();
renderIssueView();
showNotification(t('issues.queueRefreshed') || 'Queue refreshed', 'success');
} catch (err) {
showNotification('Failed to refresh queue', 'error');
}
}
function createExecutionQueue() {
showQueueCommandModal();
}
function showQueueCommandModal() {
// Create modal if not exists
let modal = document.getElementById('queueCommandModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'queueCommandModal';
modal.className = 'issue-modal';
document.body.appendChild(modal);
}
const command = 'claude /issue:queue';
const altCommand = 'ccw issue queue';
modal.innerHTML = `
<div class="issue-modal-backdrop" onclick="hideQueueCommandModal()"></div>
<div class="issue-modal-content" style="max-width: 560px;">
<div class="issue-modal-header">
<h3>${t('issues.createQueue') || 'Create Execution Queue'}</h3>
<button class="btn-icon" onclick="hideQueueCommandModal()">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="issue-modal-body">
<p class="text-sm text-muted-foreground mb-4">
${t('issues.queueCommandHint') || 'Run one of the following commands in your terminal to generate the execution queue from bound solutions:'}
</p>
<div class="command-option mb-3">
<label class="text-xs font-medium text-muted-foreground mb-1 block">
<i data-lucide="terminal" class="w-3 h-3 inline mr-1"></i>
Claude Code CLI
</label>
<div class="command-box">
<code class="command-text">${command}</code>
<button class="btn-icon" onclick="copyCommand('${command}')" title="${t('common.copy') || 'Copy'}">
<i data-lucide="copy" class="w-4 h-4"></i>
</button>
</div>
</div>
<div class="command-option">
<label class="text-xs font-medium text-muted-foreground mb-1 block">
<i data-lucide="terminal" class="w-3 h-3 inline mr-1"></i>
CCW CLI (${t('issues.alternative') || 'Alternative'})
</label>
<div class="command-box">
<code class="command-text">${altCommand}</code>
<button class="btn-icon" onclick="copyCommand('${altCommand}')" title="${t('common.copy') || 'Copy'}">
<i data-lucide="copy" class="w-4 h-4"></i>
</button>
</div>
</div>
<div class="command-info mt-4">
<p class="text-xs text-muted-foreground">
<i data-lucide="info" class="w-3 h-3 inline mr-1"></i>
${t('issues.queueCommandInfo') || 'After running the command, click "Refresh" to see the updated queue.'}
</p>
</div>
</div>
<div class="issue-modal-footer">
<button class="btn-secondary" onclick="hideQueueCommandModal()">${t('common.close') || 'Close'}</button>
<button class="btn-primary" onclick="hideQueueCommandModal(); refreshQueue();">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
${t('issues.refreshAfter') || 'Refresh Queue'}
</button>
</div>
</div>
`;
modal.classList.remove('hidden');
lucide.createIcons();
}
function hideQueueCommandModal() {
const modal = document.getElementById('queueCommandModal');
if (modal) {
modal.classList.add('hidden');
}
}
function copyCommand(command) {
navigator.clipboard.writeText(command).then(() => {
showNotification(t('common.copied') || 'Copied to clipboard', 'success');
}).catch(err => {
console.error('Failed to copy:', err);
// Fallback: select text
const textArea = document.createElement('textarea');
textArea.value = command;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification(t('common.copied') || 'Copied to clipboard', 'success');
});
}