mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
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:
@@ -199,7 +199,7 @@ async function ripgrepFallback(issue, projectRoot) {
|
|||||||
|
|
||||||
## Phase 3: Solution Planning
|
## Phase 3: Solution Planning
|
||||||
|
|
||||||
### Task Decomposition
|
### Task Decomposition (Closed-Loop)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function decomposeTasks(issue, exploration) {
|
function decomposeTasks(issue, exploration) {
|
||||||
@@ -217,15 +217,104 @@ function decomposeTasks(issue, exploration) {
|
|||||||
action: inferAction(group),
|
action: inferAction(group),
|
||||||
description: group.description,
|
description: group.description,
|
||||||
modification_points: group.points,
|
modification_points: group.points,
|
||||||
|
|
||||||
|
// Phase 1: Implementation
|
||||||
implementation: generateImplementationSteps(group, exploration),
|
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),
|
acceptance: generateAcceptanceCriteria(group),
|
||||||
|
|
||||||
|
// Phase 5: Commit
|
||||||
|
commit: generateCommitSpec(group, issue),
|
||||||
|
|
||||||
depends_on: inferDependencies(group, tasks),
|
depends_on: inferDependencies(group, tasks),
|
||||||
estimated_minutes: estimateTime(group)
|
estimated_minutes: estimateTime(group),
|
||||||
|
executor: inferExecutor(group)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks
|
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
|
### Action Type Inference
|
||||||
@@ -347,11 +436,15 @@ function generateImplementationSteps(group, exploration) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Acceptance Criteria Generation
|
### Acceptance Criteria Generation (Closed-Loop)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
function generateAcceptanceCriteria(task) {
|
function generateAcceptanceCriteria(task) {
|
||||||
const criteria = []
|
const acceptance = {
|
||||||
|
criteria: [],
|
||||||
|
verification: [],
|
||||||
|
manual_checks: []
|
||||||
|
}
|
||||||
|
|
||||||
// Action-specific criteria
|
// Action-specific criteria
|
||||||
const actionCriteria = {
|
const actionCriteria = {
|
||||||
@@ -363,14 +456,41 @@ function generateAcceptanceCriteria(task) {
|
|||||||
'Configure': [`Configuration applied correctly`]
|
'Configure': [`Configuration applied correctly`]
|
||||||
}
|
}
|
||||||
|
|
||||||
criteria.push(...(actionCriteria[task.action] || []))
|
acceptance.criteria.push(...(actionCriteria[task.action] || []))
|
||||||
|
|
||||||
// Add quantified criteria
|
// Add quantified criteria
|
||||||
if (task.modification_points.length > 0) {
|
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) {
|
function validateTask(task) {
|
||||||
const errors = []
|
const errors = []
|
||||||
|
|
||||||
|
// Basic fields
|
||||||
if (!/^T\d+$/.test(task.id)) errors.push('Invalid task ID format')
|
if (!/^T\d+$/.test(task.id)) errors.push('Invalid task ID format')
|
||||||
if (!task.title?.trim()) errors.push('Missing title')
|
if (!task.title?.trim()) errors.push('Missing title')
|
||||||
if (!task.scope?.trim()) errors.push('Missing scope')
|
if (!task.scope?.trim()) errors.push('Missing scope')
|
||||||
if (!['Create', 'Update', 'Implement', 'Refactor', 'Configure', 'Test', 'Fix', 'Delete'].includes(task.action)) {
|
if (!['Create', 'Update', 'Implement', 'Refactor', 'Configure', 'Test', 'Fix', 'Delete'].includes(task.action)) {
|
||||||
errors.push('Invalid action type')
|
errors.push('Invalid action type')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 1: Implementation
|
||||||
if (!task.implementation || task.implementation.length < 2) {
|
if (!task.implementation || task.implementation.length < 2) {
|
||||||
errors.push('Need 2+ implementation steps')
|
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
|
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
|
```json
|
||||||
{
|
{
|
||||||
@@ -517,10 +680,62 @@ function generateOutput(solutions, conflicts) {
|
|||||||
"modification_points": [
|
"modification_points": [
|
||||||
{ "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" }
|
{ "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": [],
|
"depends_on": [],
|
||||||
"estimated_minutes": 30
|
"estimated_minutes": 30,
|
||||||
|
"executor": "codex"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"exploration_context": {
|
"exploration_context": {
|
||||||
@@ -622,6 +837,14 @@ Before outputting solution:
|
|||||||
6. Include file:line references in modification_points where possible
|
6. Include file:line references in modification_points where possible
|
||||||
7. Detect and report cross-issue file conflicts in batch mode
|
7. Detect and report cross-issue file conflicts in batch mode
|
||||||
8. Include exploration_context with patterns and relevant_files
|
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**:
|
**NEVER**:
|
||||||
1. Execute implementation (return plan only)
|
1. Execute implementation (return plan only)
|
||||||
@@ -632,3 +855,5 @@ Before outputting solution:
|
|||||||
6. Assume file exists without verification
|
6. Assume file exists without verification
|
||||||
7. Generate more than 10 tasks per issue
|
7. Generate more than 10 tasks per issue
|
||||||
8. Skip ACE search (unless fallback triggered)
|
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
|
||||||
|
|||||||
@@ -148,15 +148,15 @@ TodoWrite({
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Phase 3: Codex Coordination (Single Task Mode)
|
### Phase 3: Codex Coordination (Single Task Mode - Full Lifecycle)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Execute tasks - single codex instance per task
|
// Execute tasks - single codex instance per task with full lifecycle
|
||||||
async function executeTask(queueItem) {
|
async function executeTask(queueItem) {
|
||||||
const codexPrompt = `
|
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
|
### Step 1: Fetch Task
|
||||||
Run this command to get your task:
|
Run this command to get your task:
|
||||||
@@ -164,35 +164,71 @@ Run this command to get your task:
|
|||||||
ccw issue next
|
ccw issue next
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
This returns JSON with:
|
This returns JSON with full lifecycle definition:
|
||||||
- queue_id: Queue item ID
|
- task.implementation: Implementation steps
|
||||||
- task: Task definition with implementation steps
|
- task.test: Test requirements and commands
|
||||||
- context: Exploration context
|
- task.regression: Regression check commands
|
||||||
- execution_hints: Executor and time estimate
|
- task.acceptance: Acceptance criteria and verification
|
||||||
|
- task.commit: Commit specification
|
||||||
|
|
||||||
### Step 2: Execute Task
|
### Step 2: Execute Full Lifecycle
|
||||||
Read the returned task object and:
|
|
||||||
|
**Phase 1: IMPLEMENT**
|
||||||
1. Follow task.implementation steps in order
|
1. Follow task.implementation steps in order
|
||||||
2. Meet all task.acceptance criteria
|
2. Modify files specified in modification_points
|
||||||
3. Use provided context.relevant_files for reference
|
3. Use context.relevant_files for reference
|
||||||
4. Use context.patterns for code style
|
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
|
### Step 3: Report Completion
|
||||||
When done, run:
|
When ALL phases complete successfully:
|
||||||
\`\`\`bash
|
\`\`\`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
|
\`\`\`bash
|
||||||
ccw issue fail <queue_id> --reason "Why it failed"
|
ccw issue fail <queue_id> --reason "Phase X failed: <details>"
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### Rules
|
### Rules
|
||||||
- NEVER read task files directly - use ccw issue next
|
- NEVER skip any lifecycle phase
|
||||||
- Execute the FULL task before marking complete
|
- Tests MUST pass before proceeding to acceptance
|
||||||
- Do NOT loop - execute ONE task only
|
- Regression MUST pass before commit
|
||||||
- Report accurate files_modified in result
|
- ALL acceptance criteria MUST be verified
|
||||||
|
- Report accurate lifecycle status in result
|
||||||
|
|
||||||
### Start Now
|
### Start Now
|
||||||
Begin by running: ccw issue next
|
Begin by running: ccw issue next
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Creates a new structured issue from either:
|
|||||||
|
|
||||||
Outputs a well-formed issue entry to `.workflow/issues/issues.jsonl`.
|
Outputs a well-formed issue entry to `.workflow/issues/issues.jsonl`.
|
||||||
|
|
||||||
## Issue Structure
|
## Issue Structure (Closed-Loop)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface Issue {
|
interface Issue {
|
||||||
@@ -35,6 +35,14 @@ interface Issue {
|
|||||||
affected_components?: string[];// Files/modules affected
|
affected_components?: string[];// Files/modules affected
|
||||||
reproduction_steps?: string[]; // Steps to reproduce
|
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
|
// Metadata
|
||||||
bound_solution_id: null;
|
bound_solution_id: null;
|
||||||
solution_count: 0;
|
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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```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
|
```javascript
|
||||||
// Show parsed data and ask for confirmation
|
// 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.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.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` : ''}
|
${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
|
// 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
|
```javascript
|
||||||
// Construct final issue object
|
// Construct final issue object
|
||||||
@@ -288,6 +398,14 @@ const newIssue = {
|
|||||||
affected_components: issueData.affected_components || [],
|
affected_components: issueData.affected_components || [],
|
||||||
reproduction_steps: issueData.reproduction_steps || [],
|
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
|
// Metadata
|
||||||
bound_solution_id: null,
|
bound_solution_id: null,
|
||||||
solution_count: 0,
|
solution_count: 0,
|
||||||
|
|||||||
@@ -133,28 +133,59 @@ TodoWrite({
|
|||||||
for (const [batchIndex, batch] of batches.entries()) {
|
for (const [batchIndex, batch] of batches.entries()) {
|
||||||
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
|
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
|
||||||
|
|
||||||
// Build issue prompt for agent
|
// Build issue prompt for agent with lifecycle requirements
|
||||||
const issuePrompt = `
|
const issuePrompt = `
|
||||||
## Issues to Plan
|
## Issues to Plan (Closed-Loop Tasks Required)
|
||||||
|
|
||||||
${batch.map((issue, i) => `
|
${batch.map((issue, i) => `
|
||||||
### Issue ${i + 1}: ${issue.id}
|
### Issue ${i + 1}: ${issue.id}
|
||||||
**Title**: ${issue.title}
|
**Title**: ${issue.title}
|
||||||
**Context**: ${issue.context || 'No context provided'}
|
**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')}
|
`).join('\n')}
|
||||||
|
|
||||||
## Project Root
|
## Project Root
|
||||||
${process.cwd()}
|
${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
|
1. Use ACE semantic search (mcp__ace-tool__search_context) for exploration
|
||||||
2. Generate complete solution with task breakdown
|
2. Detect file conflicts if multiple issues
|
||||||
3. Each task must have:
|
3. Generate executable test commands based on project's test framework
|
||||||
- implementation steps (2-7 steps)
|
4. Infer commit scope from affected files
|
||||||
- acceptance criteria (1-4 testable criteria)
|
|
||||||
- modification_points (exact file locations)
|
|
||||||
- depends_on (task dependencies)
|
|
||||||
4. Detect file conflicts if multiple issues
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Launch issue-plan-agent (combines explore + plan)
|
// 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`:
|
Each solution line in `solutions/{issue-id}.jsonl`:
|
||||||
|
|
||||||
@@ -299,18 +330,56 @@ Each solution line in `solutions/{issue-id}.jsonl`:
|
|||||||
"modification_points": [
|
"modification_points": [
|
||||||
{ "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" }
|
{ "file": "src/middleware/auth.ts", "target": "new file", "change": "Create middleware" }
|
||||||
],
|
],
|
||||||
|
|
||||||
"implementation": [
|
"implementation": [
|
||||||
"Create auth.ts file",
|
"Create auth.ts file in src/middleware/",
|
||||||
"Implement JWT validation",
|
"Implement JWT token validation using jsonwebtoken",
|
||||||
"Add error handling",
|
"Add error handling for invalid/expired tokens",
|
||||||
"Export middleware"
|
"Export middleware function"
|
||||||
],
|
],
|
||||||
"acceptance": [
|
|
||||||
"Middleware validates JWT tokens",
|
"test": {
|
||||||
"Returns 401 for invalid tokens"
|
"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": [],
|
"depends_on": [],
|
||||||
"estimated_minutes": 30
|
"estimated_minutes": 30,
|
||||||
|
"executor": "codex"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"exploration_context": {
|
"exploration_context": {
|
||||||
|
|||||||
@@ -20,30 +20,67 @@ Queue formation command using **issue-queue-agent** that analyzes all bound solu
|
|||||||
- Parallel/Sequential group assignment
|
- Parallel/Sequential group assignment
|
||||||
- Output global queue.json
|
- Output global queue.json
|
||||||
|
|
||||||
## Storage Structure (Flat JSONL)
|
## Storage Structure (Queue History)
|
||||||
|
|
||||||
```
|
```
|
||||||
.workflow/issues/
|
.workflow/issues/
|
||||||
├── issues.jsonl # All issues (one per line)
|
├── 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/
|
└── solutions/
|
||||||
├── {issue-id}.jsonl # Solutions for issue
|
├── {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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/issue:queue [FLAGS]
|
/issue:queue [FLAGS]
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
/issue:queue # Form queue from all bound solutions
|
/issue:queue # Form NEW queue from all bound solutions
|
||||||
/issue:queue --rebuild # Rebuild queue (clear and regenerate)
|
/issue:queue --issue GH-123 # Form queue for specific issue only
|
||||||
/issue:queue --issue GH-123 # Add only specific issue to queue
|
/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
|
# Flags
|
||||||
--rebuild Clear existing queue and regenerate
|
--issue <id> Form queue for specific issue only
|
||||||
--issue <id> Add only specific issue's tasks
|
--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
|
## Execution Process
|
||||||
@@ -215,10 +252,15 @@ ${(queueOutput.execution_groups || []).map(g => {
|
|||||||
|
|
||||||
## Queue Schema
|
## Queue Schema
|
||||||
|
|
||||||
Output `queue.json`:
|
Output `queues/{queue-id}.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"id": "QUE-20251227-143000",
|
||||||
|
"name": "Auth Feature Queue",
|
||||||
|
"status": "active",
|
||||||
|
"issue_ids": ["GH-123", "GH-124"],
|
||||||
|
|
||||||
"queue": [
|
"queue": [
|
||||||
{
|
{
|
||||||
"queue_id": "Q-001",
|
"queue_id": "Q-001",
|
||||||
@@ -233,6 +275,7 @@ Output `queue.json`:
|
|||||||
"queued_at": "2025-12-26T10:00:00Z"
|
"queued_at": "2025-12-26T10:00:00Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"conflicts": [
|
"conflicts": [
|
||||||
{
|
{
|
||||||
"type": "file_conflict",
|
"type": "file_conflict",
|
||||||
@@ -244,24 +287,32 @@ Output `queue.json`:
|
|||||||
"resolved": true
|
"resolved": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"execution_groups": [
|
"execution_groups": [
|
||||||
{ "id": "P1", "type": "parallel", "task_count": 3, "tasks": ["GH-123:T1", "GH-124:T1", "GH-125:T1"] },
|
{ "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"] }
|
{ "id": "S2", "type": "sequential", "task_count": 2, "tasks": ["GH-123:T2", "GH-124:T2"] }
|
||||||
],
|
],
|
||||||
|
|
||||||
"_metadata": {
|
"_metadata": {
|
||||||
"version": "2.0",
|
"version": "2.0",
|
||||||
"storage": "jsonl",
|
|
||||||
"total_tasks": 5,
|
"total_tasks": 5,
|
||||||
"total_conflicts": 1,
|
"pending_count": 3,
|
||||||
"resolved_conflicts": 1,
|
"completed_count": 2,
|
||||||
"parallel_groups": 1,
|
"failed_count": 0,
|
||||||
"sequential_groups": 1,
|
"created_at": "2025-12-26T10:00:00Z",
|
||||||
"timestamp": "2025-12-26T10:00:00Z",
|
"updated_at": "2025-12-26T11:00:00Z",
|
||||||
"source": "issue-queue-agent"
|
"source": "issue-queue-agent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Queue ID Format
|
||||||
|
|
||||||
|
```
|
||||||
|
QUE-YYYYMMDD-HHMMSS
|
||||||
|
例如: QUE-20251227-143052
|
||||||
|
```
|
||||||
|
|
||||||
## Semantic Priority Rules
|
## Semantic Priority Rules
|
||||||
|
|
||||||
| Factor | Priority Boost |
|
| Factor | Priority Boost |
|
||||||
|
|||||||
@@ -36,6 +36,26 @@ interface Issue {
|
|||||||
completed_at?: string;
|
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 {
|
interface SolutionTask {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -43,11 +63,26 @@ interface SolutionTask {
|
|||||||
action: string;
|
action: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
modification_points?: { file: string; target: string; change: 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[];
|
depends_on: string[];
|
||||||
estimated_minutes?: number;
|
estimated_minutes?: number;
|
||||||
executor: 'codex' | 'gemini' | 'agent' | 'auto';
|
executor: 'codex' | 'gemini' | 'agent' | 'auto';
|
||||||
|
|
||||||
|
// Lifecycle status tracking
|
||||||
|
lifecycle_status?: {
|
||||||
|
implemented: boolean;
|
||||||
|
tested: boolean;
|
||||||
|
regression_passed: boolean;
|
||||||
|
accepted: boolean;
|
||||||
|
committed: boolean;
|
||||||
|
};
|
||||||
status?: string;
|
status?: string;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
}
|
}
|
||||||
@@ -83,8 +118,13 @@ interface QueueItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Queue {
|
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[];
|
queue: QueueItem[];
|
||||||
conflicts: any[];
|
conflicts: any[];
|
||||||
|
execution_groups?: any[];
|
||||||
_metadata: {
|
_metadata: {
|
||||||
version: string;
|
version: string;
|
||||||
total_tasks: number;
|
total_tasks: number;
|
||||||
@@ -92,10 +132,24 @@ interface Queue {
|
|||||||
executing_count: number;
|
executing_count: number;
|
||||||
completed_count: number;
|
completed_count: number;
|
||||||
failed_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 {
|
interface IssueOptions {
|
||||||
status?: string;
|
status?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -208,40 +262,121 @@ function generateSolutionId(): string {
|
|||||||
return `SOL-${ts}`;
|
return `SOL-${ts}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Queue JSON ============
|
// ============ Queue Management (Multi-Queue) ============
|
||||||
|
|
||||||
function readQueue(): Queue {
|
function getQueuesDir(): string {
|
||||||
const path = join(getIssuesDir(), 'queue.json');
|
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)) {
|
if (!existsSync(path)) {
|
||||||
return {
|
return { active_queue_id: null, queues: [] };
|
||||||
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 JSON.parse(readFileSync(path, 'utf-8'));
|
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 {
|
function writeQueue(queue: Queue): void {
|
||||||
ensureIssuesDir();
|
ensureQueuesDir();
|
||||||
|
|
||||||
|
// Update metadata counts
|
||||||
queue._metadata.total_tasks = queue.queue.length;
|
queue._metadata.total_tasks = queue.queue.length;
|
||||||
queue._metadata.pending_count = queue.queue.filter(q => q.status === 'pending').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.executing_count = queue.queue.filter(q => q.status === 'executing').length;
|
||||||
queue._metadata.completed_count = queue.queue.filter(q => q.status === 'completed').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.failed_count = queue.queue.filter(q => q.status === 'failed').length;
|
||||||
queue._metadata.last_updated = new Date().toISOString();
|
queue._metadata.updated_at = new Date().toISOString();
|
||||||
writeFileSync(join(getIssuesDir(), 'queue.json'), JSON.stringify(queue, null, 2), 'utf-8');
|
|
||||||
|
// 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 maxNum = queue.queue.reduce((max, q) => {
|
||||||
const match = q.queue_id.match(/^Q-(\d+)$/);
|
const match = q.queue_id.match(/^Q-(\d+)$/);
|
||||||
return match ? Math.max(max, parseInt(match[1])) : max;
|
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> {
|
async function statusAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
|
||||||
if (!issueId) {
|
if (!issueId) {
|
||||||
// Show queue status
|
// Show queue status
|
||||||
const queue = readQueue();
|
const queue = readActiveQueue();
|
||||||
const issues = readIssues();
|
const issues = readIssues();
|
||||||
|
const index = readQueueIndex();
|
||||||
|
|
||||||
if (options.json) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(chalk.bold.cyan('\nSystem Status\n'));
|
console.log(chalk.bold.cyan('\nSystem Status\n'));
|
||||||
console.log(`Issues: ${issues.length}`);
|
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(` Pending: ${queue._metadata.pending_count}`);
|
||||||
console.log(` Executing: ${queue._metadata.executing_count}`);
|
console.log(` Executing: ${queue._metadata.executing_count}`);
|
||||||
console.log(` Completed: ${queue._metadata.completed_count}`);
|
console.log(` Completed: ${queue._metadata.completed_count}`);
|
||||||
@@ -497,7 +634,20 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
|
|||||||
action: 'Implement',
|
action: 'Implement',
|
||||||
description: options.description || options.title,
|
description: options.description || options.title,
|
||||||
implementation: [],
|
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: [],
|
depends_on: [],
|
||||||
executor: (options.executor as any) || 'auto'
|
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> {
|
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) {
|
if (subAction === 'add' && issueId) {
|
||||||
// Add issue tasks to queue
|
|
||||||
const issue = findIssue(issueId);
|
const issue = findIssue(issueId);
|
||||||
if (!issue) {
|
if (!issue) {
|
||||||
console.error(chalk.red(`Issue "${issueId}" not found`));
|
console.error(chalk.red(`Issue "${issueId}" not found`));
|
||||||
@@ -610,13 +837,27 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|||||||
process.exit(1);
|
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;
|
let added = 0;
|
||||||
for (const task of solution.tasks) {
|
for (const task of solution.tasks) {
|
||||||
const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id);
|
const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id);
|
||||||
if (exists) continue;
|
if (exists) continue;
|
||||||
|
|
||||||
queue.queue.push({
|
queue.queue.push({
|
||||||
queue_id: generateQueueId(queue),
|
queue_id: generateQueueItemId(queue),
|
||||||
issue_id: issueId,
|
issue_id: issueId,
|
||||||
solution_id: solution.id,
|
solution_id: solution.id,
|
||||||
task_id: task.id,
|
task_id: task.id,
|
||||||
@@ -637,26 +878,35 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|||||||
writeQueue(queue);
|
writeQueue(queue);
|
||||||
updateIssue(issueId, { status: 'queued', queued_at: new Date().toISOString() });
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List queue
|
// Show current queue
|
||||||
|
const queue = readActiveQueue();
|
||||||
|
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
console.log(JSON.stringify(queue, null, 2));
|
console.log(JSON.stringify(queue, null, 2));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(chalk.bold.cyan('\nExecution Queue\n'));
|
console.log(chalk.bold.cyan('\nActive 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();
|
|
||||||
|
|
||||||
if (queue.queue.length === 0) {
|
if (!queue.id || queue.queue.length === 0) {
|
||||||
console.log(chalk.yellow('Queue is empty'));
|
console.log(chalk.yellow('No active queue'));
|
||||||
console.log(chalk.gray('Add tasks: ccw issue queue add <issue-id>'));
|
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
|
||||||
|
console.log(chalk.gray('Or list history: ccw issue queue list'));
|
||||||
return;
|
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('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
|
||||||
console.log(chalk.gray('-'.repeat(60)));
|
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)
|
* next - Get next ready task for execution (JSON output)
|
||||||
*/
|
*/
|
||||||
async function nextAction(options: IssueOptions): Promise<void> {
|
async function nextAction(options: IssueOptions): Promise<void> {
|
||||||
const queue = readQueue();
|
const queue = readActiveQueue();
|
||||||
|
|
||||||
// Find ready tasks
|
// Find ready tasks
|
||||||
const readyTasks = queue.queue.filter(item => {
|
const readyTasks = queue.queue.filter(item => {
|
||||||
@@ -749,7 +999,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue = readQueue();
|
const queue = readActiveQueue();
|
||||||
const idx = queue.queue.findIndex(q => q.queue_id === queueId);
|
const idx = queue.queue.findIndex(q => q.queue_id === queueId);
|
||||||
|
|
||||||
if (idx === -1) {
|
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
|
// Check if all issue tasks are complete
|
||||||
const issueId = queue.queue[idx].issue_id;
|
const issueId = queue.queue[idx].issue_id;
|
||||||
const issueTasks = queue.queue.filter(q => q.issue_id === issueId);
|
const issueTasks = queue.queue.filter(q => q.issue_id === issueId);
|
||||||
const allComplete = issueTasks.every(q => q.status === 'completed');
|
const allIssueComplete = issueTasks.every(q => q.status === 'completed');
|
||||||
const anyFailed = issueTasks.some(q => q.status === 'failed');
|
const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
|
||||||
|
|
||||||
if (allComplete) {
|
if (allIssueComplete) {
|
||||||
updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
|
updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
|
||||||
console.log(chalk.green(`✓ ${queueId} completed`));
|
console.log(chalk.green(`✓ ${queueId} completed`));
|
||||||
console.log(chalk.green(`✓ Issue ${issueId} completed (all tasks done)`));
|
console.log(chalk.green(`✓ Issue ${issueId} completed (all tasks done)`));
|
||||||
} else if (anyFailed) {
|
} else if (anyIssueFailed) {
|
||||||
updateIssue(issueId, { status: 'failed' });
|
updateIssue(issueId, { status: 'failed' });
|
||||||
console.log(chalk.red(`✗ ${queueId} failed`));
|
console.log(chalk.red(`✗ ${queueId} failed`));
|
||||||
} else {
|
} else {
|
||||||
console.log(isFail ? chalk.red(`✗ ${queueId} failed`) : chalk.green(`✓ ${queueId} completed`));
|
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
|
* retry - Retry failed tasks
|
||||||
*/
|
*/
|
||||||
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
|
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;
|
let updated = 0;
|
||||||
|
|
||||||
for (const item of queue.queue) {
|
for (const item of queue.queue) {
|
||||||
@@ -815,6 +1083,11 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset queue status if it was failed
|
||||||
|
if (queue.status === 'failed') {
|
||||||
|
queue.status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
writeQueue(queue);
|
writeQueue(queue);
|
||||||
|
|
||||||
if (issueId) {
|
if (issueId) {
|
||||||
@@ -873,7 +1146,7 @@ export async function issueCommand(
|
|||||||
await doneAction(argsArray[0], { ...options, fail: true });
|
await doneAction(argsArray[0], { ...options, fail: true });
|
||||||
break;
|
break;
|
||||||
default:
|
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.bold('Core Commands:'));
|
||||||
console.log(chalk.gray(' init <issue-id> Initialize new issue'));
|
console.log(chalk.gray(' init <issue-id> Initialize new issue'));
|
||||||
console.log(chalk.gray(' list [issue-id] List issues or tasks'));
|
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(chalk.gray(' bind <issue-id> [sol-id] Bind solution (--solution <path> to register)'));
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.bold('Queue Commands:'));
|
console.log(chalk.bold('Queue Commands:'));
|
||||||
console.log(chalk.gray(' queue [list] Show execution queue'));
|
console.log(chalk.gray(' queue Show active queue'));
|
||||||
console.log(chalk.gray(' queue add <issue-id> Add bound solution tasks to 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(chalk.gray(' retry [issue-id] Retry failed tasks'));
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.bold('Execution Endpoints:'));
|
console.log(chalk.bold('Execution Endpoints:'));
|
||||||
@@ -902,6 +1178,7 @@ export async function issueCommand(
|
|||||||
console.log(chalk.bold('Storage:'));
|
console.log(chalk.bold('Storage:'));
|
||||||
console.log(chalk.gray(' .workflow/issues/issues.jsonl All issues'));
|
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/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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,21 +72,68 @@ function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readQueue(issuesDir: string) {
|
function readQueue(issuesDir: string) {
|
||||||
const queuePath = join(issuesDir, 'queue.json');
|
// Try new multi-queue structure first
|
||||||
if (!existsSync(queuePath)) {
|
const queuesDir = join(issuesDir, 'queues');
|
||||||
return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
|
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'));
|
// Fallback to legacy queue.json
|
||||||
} catch {
|
const legacyQueuePath = join(issuesDir, 'queue.json');
|
||||||
return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
|
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) {
|
function writeQueue(issuesDir: string, queue: any) {
|
||||||
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
|
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
|
||||||
queue._metadata = { ...queue._metadata, last_updated: new Date().toISOString(), total_tasks: queue.queue?.length || 0 };
|
queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.queue?.length || 0 };
|
||||||
writeFileSync(join(issuesDir, 'queue.json'), JSON.stringify(queue, null, 2));
|
|
||||||
|
// 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) {
|
function getIssueDetail(issuesDir: string, issueId: string) {
|
||||||
|
|||||||
@@ -276,9 +276,105 @@
|
|||||||
color: hsl(var(--muted-foreground));
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-empty svg {
|
.queue-empty svg,
|
||||||
|
.queue-empty > i {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
opacity: 0.5;
|
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 */
|
/* Issue ID */
|
||||||
@@ -1349,6 +1445,20 @@
|
|||||||
cursor: pointer;
|
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
|
BUTTON STYLES
|
||||||
========================================== */
|
========================================== */
|
||||||
@@ -1759,3 +1869,676 @@
|
|||||||
-webkit-overflow-scrolling: touch;
|
-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1772,6 +1772,45 @@ const i18n = {
|
|||||||
'issues.created': 'Issue created successfully',
|
'issues.created': 'Issue created successfully',
|
||||||
'issues.confirmDelete': 'Are you sure you want to delete this issue?',
|
'issues.confirmDelete': 'Are you sure you want to delete this issue?',
|
||||||
'issues.deleted': 'Issue deleted',
|
'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.* keys (legacy)
|
||||||
'issue.viewIssues': 'Issues',
|
'issue.viewIssues': 'Issues',
|
||||||
'issue.viewQueue': 'Queue',
|
'issue.viewQueue': 'Queue',
|
||||||
@@ -3595,6 +3634,45 @@ const i18n = {
|
|||||||
'issues.created': '议题创建成功',
|
'issues.created': '议题创建成功',
|
||||||
'issues.confirmDelete': '确定要删除此议题吗?',
|
'issues.confirmDelete': '确定要删除此议题吗?',
|
||||||
'issues.deleted': '议题已删除',
|
'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.* keys (legacy)
|
||||||
'issue.viewIssues': '议题',
|
'issue.viewIssues': '议题',
|
||||||
'issue.viewQueue': '队列',
|
'issue.viewQueue': '队列',
|
||||||
|
|||||||
@@ -168,16 +168,22 @@ async function loadAvailableSkills() {
|
|||||||
if (!response.ok) throw new Error('Failed to load skills');
|
if (!response.ok) throw new Error('Failed to load skills');
|
||||||
const data = await response.json();
|
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');
|
const container = document.getElementById('skill-discovery-skill-context');
|
||||||
if (container && data.skills) {
|
if (container) {
|
||||||
if (data.skills.length === 0) {
|
if (allSkills.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
<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>
|
<span class="text-muted-foreground ml-2">${t('hook.wizard.noSkillsFound').split('.')[0]}</span>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
const skillBadges = data.skills.map(skill => `
|
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>
|
<span class="px-2 py-0.5 bg-emerald-500/10 text-emerald-500 rounded" title="${escapeHtml(skill.description || '')}">${escapeHtml(skill.name)}</span>
|
||||||
`).join('');
|
`).join('');
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<span class="font-mono bg-muted px-1.5 py-0.5 rounded">${t('hook.wizard.availableSkills')}</span>
|
<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
|
// Store skills for wizard use
|
||||||
window.availableSkills = data.skills || [];
|
window.availableSkills = allSkills;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load skills:', err);
|
console.error('Failed to load skills:', err);
|
||||||
const container = document.getElementById('skill-discovery-skill-context');
|
const container = document.getElementById('skill-discovery-skill-context');
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ var issueData = {
|
|||||||
queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} },
|
queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} },
|
||||||
selectedIssue: null,
|
selectedIssue: null,
|
||||||
selectedSolution: null,
|
selectedSolution: null,
|
||||||
|
selectedSolutionIssueId: null,
|
||||||
statusFilter: 'all',
|
statusFilter: 'all',
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
viewMode: 'issues' // 'issues' | 'queue'
|
viewMode: 'issues' // 'issues' | 'queue'
|
||||||
@@ -148,6 +149,31 @@ function renderIssueView() {
|
|||||||
<!-- Detail Panel -->
|
<!-- Detail Panel -->
|
||||||
<div id="issueDetailPanel" class="issue-detail-panel hidden"></div>
|
<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 -->
|
<!-- Create Issue Modal -->
|
||||||
<div id="createIssueModal" class="issue-modal hidden">
|
<div id="createIssueModal" class="issue-modal hidden">
|
||||||
<div class="issue-modal-backdrop" onclick="hideCreateIssueModal()"></div>
|
<div class="issue-modal-backdrop" onclick="hideCreateIssueModal()"></div>
|
||||||
@@ -161,7 +187,12 @@ function renderIssueView() {
|
|||||||
<div class="issue-modal-body">
|
<div class="issue-modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>${t('issues.issueId') || 'Issue ID'}</label>
|
<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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>${t('issues.issueTitle') || 'Title'}</label>
|
<label>${t('issues.issueTitle') || 'Title'}</label>
|
||||||
@@ -329,20 +360,129 @@ function filterIssuesByStatus(status) {
|
|||||||
// ========== Queue Section ==========
|
// ========== Queue Section ==========
|
||||||
function renderQueueSection() {
|
function renderQueueSection() {
|
||||||
const queue = issueData.queue;
|
const queue = issueData.queue;
|
||||||
const groups = queue.execution_groups || [];
|
const queueItems = queue.queue || [];
|
||||||
const groupedItems = queue.grouped_items || {};
|
const metadata = queue._metadata || {};
|
||||||
|
|
||||||
if (groups.length === 0 && (!queue.queue || queue.queue.length === 0)) {
|
// Check if queue is empty
|
||||||
|
if (queueItems.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="queue-empty">
|
<div class="queue-empty-container">
|
||||||
<i data-lucide="git-branch" class="w-12 h-12 text-muted-foreground mb-4"></i>
|
<div class="queue-empty">
|
||||||
<p class="text-muted-foreground">${t('issues.queueEmpty') || 'Queue is empty'}</p>
|
<i data-lucide="git-branch" class="w-16 h-16"></i>
|
||||||
<p class="text-sm text-muted-foreground mt-2">Run /issue:queue to form execution queue</p>
|
<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>
|
</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 `
|
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">
|
<div class="queue-info mb-4">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
<i data-lucide="info" class="w-4 h-4 inline mr-1"></i>
|
||||||
@@ -605,24 +745,16 @@ function renderIssueDetailPanel(issue) {
|
|||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<label class="detail-label">${t('issues.solutions') || 'Solutions'} (${issue.solutions?.length || 0})</label>
|
<label class="detail-label">${t('issues.solutions') || 'Solutions'} (${issue.solutions?.length || 0})</label>
|
||||||
<div class="solutions-list">
|
<div class="solutions-list">
|
||||||
${(issue.solutions || []).map(sol => `
|
${(issue.solutions || []).length > 0 ? (issue.solutions || []).map(sol => `
|
||||||
<div class="solution-item ${sol.is_bound ? 'bound' : ''}" onclick="toggleSolutionExpand('${sol.id}')">
|
<div class="solution-item ${sol.is_bound ? 'bound' : ''}" onclick="openSolutionDetail('${issue.id}', '${sol.id}')">
|
||||||
<div class="solution-header">
|
<div class="solution-header">
|
||||||
<span class="solution-id font-mono text-xs">${sol.id}</span>
|
<span class="solution-id font-mono text-xs">${sol.id}</span>
|
||||||
${sol.is_bound ? '<span class="solution-bound-badge">Bound</span>' : ''}
|
${sol.is_bound ? '<span class="solution-bound-badge">' + (t('issues.bound') || 'Bound') + '</span>' : ''}
|
||||||
<span class="solution-tasks text-xs">${sol.tasks?.length || 0} tasks</span>
|
<span class="solution-tasks text-xs">${sol.tasks?.length || 0} ${t('issues.tasks') || 'tasks'}</span>
|
||||||
</div>
|
<i data-lucide="chevron-right" class="w-4 h-4 ml-auto text-muted-foreground"></i>
|
||||||
<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('')}
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -630,7 +762,7 @@ function renderIssueDetailPanel(issue) {
|
|||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<label class="detail-label">${t('issues.tasks') || 'Tasks'} (${issue.tasks?.length || 0})</label>
|
<label class="detail-label">${t('issues.tasks') || 'Tasks'} (${issue.tasks?.length || 0})</label>
|
||||||
<div class="tasks-list">
|
<div class="tasks-list">
|
||||||
${(issue.tasks || []).map(task => `
|
${(issue.tasks || []).length > 0 ? (issue.tasks || []).map(task => `
|
||||||
<div class="task-item-detail">
|
<div class="task-item-detail">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-mono text-sm">${task.id}</span>
|
<span class="font-mono text-sm">${task.id}</span>
|
||||||
@@ -642,7 +774,7 @@ function renderIssueDetailPanel(issue) {
|
|||||||
</div>
|
</div>
|
||||||
<p class="task-title-detail">${task.title || task.description || ''}</p>
|
<p class="task-title-detail">${task.title || task.description || ''}</p>
|
||||||
</div>
|
</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>
|
</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) {
|
function openQueueItemDetail(queueId) {
|
||||||
const item = issueData.queue.queue?.find(q => q.queue_id === queueId);
|
const item = issueData.queue.queue?.find(q => q.queue_id === queueId);
|
||||||
if (item) {
|
if (item) {
|
||||||
@@ -807,18 +1286,58 @@ function clearIssueSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== Create Issue Modal ==========
|
// ========== 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() {
|
function showCreateIssueModal() {
|
||||||
const modal = document.getElementById('createIssueModal');
|
const modal = document.getElementById('createIssueModal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Auto-generate issue ID
|
||||||
|
const idInput = document.getElementById('newIssueId');
|
||||||
|
if (idInput) {
|
||||||
|
idInput.value = generateIssueId();
|
||||||
|
}
|
||||||
|
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
// Focus on first input
|
// Focus on title input instead of ID
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('newIssueId')?.focus();
|
document.getElementById('newIssueTitle')?.focus();
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function regenerateIssueId() {
|
||||||
|
const idInput = document.getElementById('newIssueId');
|
||||||
|
if (idInput) {
|
||||||
|
idInput.value = generateIssueId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hideCreateIssueModal() {
|
function hideCreateIssueModal() {
|
||||||
const modal = document.getElementById('createIssueModal');
|
const modal = document.getElementById('createIssueModal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -913,3 +1432,115 @@ async function deleteIssue(issueId) {
|
|||||||
showNotification('Failed to delete issue', 'error');
|
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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user