mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-04 15:53:07 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d17bb02a4 | ||
|
|
5cab8ae8a5 | ||
|
|
ffe3b427ce | ||
|
|
8c953b287d | ||
|
|
b1e321267e | ||
|
|
d0275f14b2 | ||
|
|
ee4dc367d9 | ||
|
|
a63fb370aa | ||
|
|
da19a6ec89 | ||
|
|
bf84a157ea | ||
|
|
41f990ddd4 | ||
|
|
3463bc8e27 | ||
|
|
9ad755e225 | ||
|
|
8799a9c2fd | ||
|
|
1f859ae4b9 | ||
|
|
ecf4e4d848 | ||
|
|
8ceae6d6fd | ||
|
|
2fb93d20e0 |
@@ -9,6 +9,7 @@ keywords:
|
||||
- pattern
|
||||
readMode: required
|
||||
priority: high
|
||||
scope: project
|
||||
---
|
||||
|
||||
# Architecture Constraints
|
||||
|
||||
@@ -9,6 +9,7 @@ keywords:
|
||||
- convention
|
||||
readMode: required
|
||||
priority: high
|
||||
scope: project
|
||||
---
|
||||
|
||||
# Coding Conventions
|
||||
|
||||
287
.claude/commands/idaw/add.md
Normal file
287
.claude/commands/idaw/add.md
Normal file
@@ -0,0 +1,287 @@
|
||||
---
|
||||
name: add
|
||||
description: Add IDAW tasks - manual creation or import from ccw issue
|
||||
argument-hint: "[-y|--yes] [--from-issue <id>[,<id>,...]] \"description\" [--type <task_type>] [--priority <1-5>]"
|
||||
allowed-tools: AskUserQuestion(*), Read(*), Bash(*), Write(*), Glob(*)
|
||||
---
|
||||
|
||||
# IDAW Add Command (/idaw:add)
|
||||
|
||||
## Auto Mode
|
||||
|
||||
When `--yes` or `-y`: Skip clarification questions, create task with inferred details.
|
||||
|
||||
## IDAW Task Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "IDAW-001",
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"status": "pending",
|
||||
"priority": 2,
|
||||
"task_type": null,
|
||||
"skill_chain": null,
|
||||
"context": {
|
||||
"affected_files": [],
|
||||
"acceptance_criteria": [],
|
||||
"constraints": [],
|
||||
"references": []
|
||||
},
|
||||
"source": {
|
||||
"type": "manual|import-issue",
|
||||
"issue_id": null,
|
||||
"issue_snapshot": null
|
||||
},
|
||||
"execution": {
|
||||
"session_id": null,
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"skill_results": [],
|
||||
"git_commit": null,
|
||||
"error": null
|
||||
},
|
||||
"created_at": "ISO",
|
||||
"updated_at": "ISO"
|
||||
}
|
||||
```
|
||||
|
||||
**Valid task_type values**: `bugfix|bugfix-hotfix|feature|feature-complex|refactor|tdd|test|test-fix|review|docs`
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Parse Arguments
|
||||
|
||||
```javascript
|
||||
const args = $ARGUMENTS;
|
||||
const autoYes = /(-y|--yes)\b/.test(args);
|
||||
const fromIssue = args.match(/--from-issue\s+([\w,-]+)/)?.[1];
|
||||
const typeFlag = args.match(/--type\s+([\w-]+)/)?.[1];
|
||||
const priorityFlag = args.match(/--priority\s+(\d)/)?.[1];
|
||||
|
||||
// Extract description: content inside quotes (preferred), or fallback to stripping flags
|
||||
const quotedMatch = args.match(/(?:^|\s)["']([^"']+)["']/);
|
||||
const description = quotedMatch
|
||||
? quotedMatch[1].trim()
|
||||
: args.replace(/(-y|--yes|--from-issue\s+[\w,-]+|--type\s+[\w-]+|--priority\s+\d)/g, '').trim();
|
||||
```
|
||||
|
||||
### Phase 2: Route — Import or Manual
|
||||
|
||||
```
|
||||
--from-issue present?
|
||||
├─ YES → Import Mode (Phase 3A)
|
||||
└─ NO → Manual Mode (Phase 3B)
|
||||
```
|
||||
|
||||
### Phase 3A: Import Mode (from ccw issue)
|
||||
|
||||
```javascript
|
||||
const issueIds = fromIssue.split(',');
|
||||
|
||||
// Fetch all issues once (outside loop)
|
||||
let issues = [];
|
||||
try {
|
||||
const issueJson = Bash(`ccw issue list --json`);
|
||||
issues = JSON.parse(issueJson).issues || [];
|
||||
} catch (e) {
|
||||
console.log(`Error fetching CCW issues: ${e.message || e}`);
|
||||
console.log('Ensure ccw is installed and issues exist. Use /issue:new to create issues first.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const issueId of issueIds) {
|
||||
// 1. Find issue data
|
||||
const issue = issues.find(i => i.id === issueId.trim());
|
||||
if (!issue) {
|
||||
console.log(`Warning: Issue ${issueId} not found, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Check duplicate (same issue_id already imported)
|
||||
const existing = Glob('.workflow/.idaw/tasks/IDAW-*.json');
|
||||
for (const f of existing) {
|
||||
const data = JSON.parse(Read(f));
|
||||
if (data.source?.issue_id === issueId.trim()) {
|
||||
console.log(`Warning: Issue ${issueId} already imported as ${data.id}, skipping`);
|
||||
continue; // skip to next issue
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Generate next IDAW ID
|
||||
const nextId = generateNextId();
|
||||
|
||||
// 4. Map issue → IDAW task
|
||||
const task = {
|
||||
id: nextId,
|
||||
title: issue.title,
|
||||
description: issue.context || issue.title,
|
||||
status: 'pending',
|
||||
priority: parseInt(priorityFlag) || issue.priority || 3,
|
||||
task_type: typeFlag || inferTaskType(issue.title, issue.context || ''),
|
||||
skill_chain: null,
|
||||
context: {
|
||||
affected_files: issue.affected_components || [],
|
||||
acceptance_criteria: [],
|
||||
constraints: [],
|
||||
references: issue.source_url ? [issue.source_url] : []
|
||||
},
|
||||
source: {
|
||||
type: 'import-issue',
|
||||
issue_id: issue.id,
|
||||
issue_snapshot: {
|
||||
id: issue.id,
|
||||
title: issue.title,
|
||||
status: issue.status,
|
||||
context: issue.context,
|
||||
priority: issue.priority,
|
||||
created_at: issue.created_at
|
||||
}
|
||||
},
|
||||
execution: {
|
||||
session_id: null,
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
skill_results: [],
|
||||
git_commit: null,
|
||||
error: null
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 5. Write task file
|
||||
Bash('mkdir -p .workflow/.idaw/tasks');
|
||||
Write(`.workflow/.idaw/tasks/${nextId}.json`, JSON.stringify(task, null, 2));
|
||||
console.log(`Created ${nextId} from issue ${issueId}: ${issue.title}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3B: Manual Mode
|
||||
|
||||
```javascript
|
||||
// 1. Validate description
|
||||
if (!description && !autoYes) {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: 'Please provide a task description:',
|
||||
header: 'Task',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Provide description', description: 'What needs to be done?' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
// Use custom text from "Other"
|
||||
description = answer.customText || '';
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
console.log('Error: No description provided. Usage: /idaw:add "task description"');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Generate next IDAW ID
|
||||
const nextId = generateNextId();
|
||||
|
||||
// 3. Build title from first sentence
|
||||
const title = description.split(/[.\n]/)[0].substring(0, 80).trim();
|
||||
|
||||
// 4. Determine task_type
|
||||
const taskType = typeFlag || null; // null → inferred at run time
|
||||
|
||||
// 5. Create task
|
||||
const task = {
|
||||
id: nextId,
|
||||
title: title,
|
||||
description: description,
|
||||
status: 'pending',
|
||||
priority: parseInt(priorityFlag) || 3,
|
||||
task_type: taskType,
|
||||
skill_chain: null,
|
||||
context: {
|
||||
affected_files: [],
|
||||
acceptance_criteria: [],
|
||||
constraints: [],
|
||||
references: []
|
||||
},
|
||||
source: {
|
||||
type: 'manual',
|
||||
issue_id: null,
|
||||
issue_snapshot: null
|
||||
},
|
||||
execution: {
|
||||
session_id: null,
|
||||
started_at: null,
|
||||
completed_at: null,
|
||||
skill_results: [],
|
||||
git_commit: null,
|
||||
error: null
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
Bash('mkdir -p .workflow/.idaw/tasks');
|
||||
Write(`.workflow/.idaw/tasks/${nextId}.json`, JSON.stringify(task, null, 2));
|
||||
console.log(`Created ${nextId}: ${title}`);
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### ID Generation
|
||||
|
||||
```javascript
|
||||
function generateNextId() {
|
||||
const files = Glob('.workflow/.idaw/tasks/IDAW-*.json') || [];
|
||||
if (files.length === 0) return 'IDAW-001';
|
||||
|
||||
const maxNum = files
|
||||
.map(f => parseInt(f.match(/IDAW-(\d+)/)?.[1] || '0'))
|
||||
.reduce((max, n) => Math.max(max, n), 0);
|
||||
|
||||
return `IDAW-${String(maxNum + 1).padStart(3, '0')}`;
|
||||
}
|
||||
```
|
||||
|
||||
### Task Type Inference (deferred — used at run time if task_type is null)
|
||||
|
||||
```javascript
|
||||
function inferTaskType(title, description) {
|
||||
const text = `${title} ${description}`.toLowerCase();
|
||||
if (/urgent|production|critical/.test(text) && /fix|bug/.test(text)) return 'bugfix-hotfix';
|
||||
if (/refactor|重构|tech.*debt/.test(text)) return 'refactor';
|
||||
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
|
||||
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
|
||||
if (/generate test|写测试|add test/.test(text)) return 'test';
|
||||
if (/review|code review/.test(text)) return 'review';
|
||||
if (/docs|documentation|readme/.test(text)) return 'docs';
|
||||
if (/fix|bug|error|crash|fail/.test(text)) return 'bugfix';
|
||||
if (/complex|multi-module|architecture/.test(text)) return 'feature-complex';
|
||||
return 'feature';
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Manual creation
|
||||
/idaw:add "Fix login timeout bug" --type bugfix --priority 2
|
||||
/idaw:add "Add rate limiting to API endpoints" --priority 1
|
||||
/idaw:add "Refactor auth module to use strategy pattern"
|
||||
|
||||
# Import from ccw issue
|
||||
/idaw:add --from-issue ISS-20260128-001
|
||||
/idaw:add --from-issue ISS-20260128-001,ISS-20260128-002 --priority 1
|
||||
|
||||
# Auto mode (skip clarification)
|
||||
/idaw:add -y "Quick fix for typo in header"
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
Created IDAW-001: Fix login timeout bug
|
||||
Type: bugfix | Priority: 2 | Source: manual
|
||||
→ Next: /idaw:run or /idaw:status
|
||||
```
|
||||
442
.claude/commands/idaw/resume.md
Normal file
442
.claude/commands/idaw/resume.md
Normal file
@@ -0,0 +1,442 @@
|
||||
---
|
||||
name: resume
|
||||
description: Resume interrupted IDAW session from last checkpoint
|
||||
argument-hint: "[-y|--yes] [session-id]"
|
||||
allowed-tools: Skill(*), TodoWrite(*), AskUserQuestion(*), Read(*), Write(*), Bash(*), Glob(*)
|
||||
---
|
||||
|
||||
# IDAW Resume Command (/idaw:resume)
|
||||
|
||||
## Auto Mode
|
||||
|
||||
When `--yes` or `-y`: Auto-skip interrupted task, continue with remaining.
|
||||
|
||||
## Skill Chain Mapping
|
||||
|
||||
```javascript
|
||||
const SKILL_CHAIN_MAP = {
|
||||
'bugfix': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||
'bugfix-hotfix': ['workflow-lite-plan'],
|
||||
'feature': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||
'feature-complex': ['workflow-plan', 'workflow-execute', 'workflow-test-fix'],
|
||||
'refactor': ['workflow:refactor-cycle'],
|
||||
'tdd': ['workflow-tdd-plan', 'workflow-execute'],
|
||||
'test': ['workflow-test-fix'],
|
||||
'test-fix': ['workflow-test-fix'],
|
||||
'review': ['review-cycle'],
|
||||
'docs': ['workflow-lite-plan']
|
||||
};
|
||||
```
|
||||
|
||||
## Task Type Inference
|
||||
|
||||
```javascript
|
||||
function inferTaskType(title, description) {
|
||||
const text = `${title} ${description}`.toLowerCase();
|
||||
if (/urgent|production|critical/.test(text) && /fix|bug/.test(text)) return 'bugfix-hotfix';
|
||||
if (/refactor|重构|tech.*debt/.test(text)) return 'refactor';
|
||||
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
|
||||
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
|
||||
if (/generate test|写测试|add test/.test(text)) return 'test';
|
||||
if (/review|code review/.test(text)) return 'review';
|
||||
if (/docs|documentation|readme/.test(text)) return 'docs';
|
||||
if (/fix|bug|error|crash|fail/.test(text)) return 'bugfix';
|
||||
if (/complex|multi-module|architecture/.test(text)) return 'feature-complex';
|
||||
return 'feature';
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Find Resumable Session
|
||||
|
||||
```javascript
|
||||
const args = $ARGUMENTS;
|
||||
const autoYes = /(-y|--yes)/.test(args);
|
||||
const targetSessionId = args.replace(/(-y|--yes)/g, '').trim();
|
||||
|
||||
let session = null;
|
||||
let sessionDir = null;
|
||||
|
||||
if (targetSessionId) {
|
||||
// Load specific session
|
||||
sessionDir = `.workflow/.idaw/sessions/${targetSessionId}`;
|
||||
try {
|
||||
session = JSON.parse(Read(`${sessionDir}/session.json`));
|
||||
} catch {
|
||||
console.log(`Session "${targetSessionId}" not found.`);
|
||||
console.log('Use /idaw:status to list sessions, or /idaw:run to start a new one.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Find most recent running session
|
||||
const sessionFiles = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || [];
|
||||
|
||||
for (const f of sessionFiles) {
|
||||
try {
|
||||
const s = JSON.parse(Read(f));
|
||||
if (s.status === 'running') {
|
||||
session = s;
|
||||
sessionDir = f.replace(/\/session\.json$/, '').replace(/\\session\.json$/, '');
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
console.log('No running sessions found to resume.');
|
||||
console.log('Use /idaw:run to start a new execution.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Resuming session: ${session.session_id}`);
|
||||
```
|
||||
|
||||
### Phase 2: Handle Interrupted Task
|
||||
|
||||
```javascript
|
||||
// Find the task that was in_progress when interrupted
|
||||
let currentTaskId = session.current_task;
|
||||
let currentTask = null;
|
||||
|
||||
if (currentTaskId) {
|
||||
try {
|
||||
currentTask = JSON.parse(Read(`.workflow/.idaw/tasks/${currentTaskId}.json`));
|
||||
} catch {
|
||||
console.log(`Warning: Could not read task ${currentTaskId}`);
|
||||
currentTaskId = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTask && currentTask.status === 'in_progress') {
|
||||
if (autoYes) {
|
||||
// Auto: skip interrupted task
|
||||
currentTask.status = 'skipped';
|
||||
currentTask.execution.error = 'Skipped on resume (auto mode)';
|
||||
currentTask.execution.completed_at = new Date().toISOString();
|
||||
currentTask.updated_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2));
|
||||
session.skipped.push(currentTaskId);
|
||||
console.log(`Skipped interrupted task: ${currentTaskId}`);
|
||||
} else {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: `Task ${currentTaskId} was interrupted: "${currentTask.title}". How to proceed?`,
|
||||
header: 'Resume',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Retry', description: 'Reset to pending, re-execute from beginning' },
|
||||
{ label: 'Skip', description: 'Mark as skipped, move to next task' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
if (answer.answers?.Resume === 'Skip') {
|
||||
currentTask.status = 'skipped';
|
||||
currentTask.execution.error = 'Skipped on resume (user choice)';
|
||||
currentTask.execution.completed_at = new Date().toISOString();
|
||||
currentTask.updated_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2));
|
||||
session.skipped.push(currentTaskId);
|
||||
} else {
|
||||
// Retry: reset to pending
|
||||
currentTask.status = 'pending';
|
||||
currentTask.execution.started_at = null;
|
||||
currentTask.execution.completed_at = null;
|
||||
currentTask.execution.skill_results = [];
|
||||
currentTask.execution.error = null;
|
||||
currentTask.updated_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${currentTaskId}.json`, JSON.stringify(currentTask, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Build Remaining Task Queue
|
||||
|
||||
```javascript
|
||||
// Collect remaining tasks (pending, or the retried current task)
|
||||
const allTaskIds = session.tasks;
|
||||
const completedSet = new Set([...session.completed, ...session.failed, ...session.skipped]);
|
||||
|
||||
const remainingTasks = [];
|
||||
for (const taskId of allTaskIds) {
|
||||
if (completedSet.has(taskId)) continue;
|
||||
try {
|
||||
const task = JSON.parse(Read(`.workflow/.idaw/tasks/${taskId}.json`));
|
||||
if (task.status === 'pending') {
|
||||
remainingTasks.push(task);
|
||||
}
|
||||
} catch {
|
||||
console.log(`Warning: Could not read task ${taskId}, skipping`);
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingTasks.length === 0) {
|
||||
console.log('No remaining tasks to execute. Session complete.');
|
||||
session.status = 'completed';
|
||||
session.current_task = null;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort: priority ASC, then ID ASC
|
||||
remainingTasks.sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
|
||||
console.log(`Remaining tasks: ${remainingTasks.length}`);
|
||||
|
||||
// Append resume marker to progress.md
|
||||
const progressFile = `${sessionDir}/progress.md`;
|
||||
try {
|
||||
const currentProgress = Read(progressFile);
|
||||
Write(progressFile, currentProgress + `\n---\n**Resumed**: ${new Date().toISOString()}\n\n`);
|
||||
} catch {
|
||||
Write(progressFile, `# IDAW Progress — ${session.session_id}\n\n---\n**Resumed**: ${new Date().toISOString()}\n\n`);
|
||||
}
|
||||
|
||||
// Update TodoWrite
|
||||
TodoWrite({
|
||||
todos: remainingTasks.map((t, i) => ({
|
||||
content: `IDAW:[${i + 1}/${remainingTasks.length}] ${t.title}`,
|
||||
status: i === 0 ? 'in_progress' : 'pending',
|
||||
activeForm: `Executing ${t.title}`
|
||||
}))
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 4-6: Execute Remaining (reuse run.md main loop)
|
||||
|
||||
Execute remaining tasks using the same Phase 4-6 logic from `/idaw:run`:
|
||||
|
||||
```javascript
|
||||
// Phase 4: Main Loop — identical to run.md Phase 4
|
||||
for (let taskIdx = 0; taskIdx < remainingTasks.length; taskIdx++) {
|
||||
const task = remainingTasks[taskIdx];
|
||||
|
||||
// Resolve skill chain
|
||||
const resolvedType = task.task_type || inferTaskType(task.title, task.description);
|
||||
const chain = task.skill_chain || SKILL_CHAIN_MAP[resolvedType] || SKILL_CHAIN_MAP['feature'];
|
||||
|
||||
// Update task → in_progress
|
||||
task.status = 'in_progress';
|
||||
task.task_type = resolvedType;
|
||||
task.execution.started_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
|
||||
session.current_task = task.id;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
console.log(`\n--- [${taskIdx + 1}/${remainingTasks.length}] ${task.id}: ${task.title} ---`);
|
||||
console.log(`Chain: ${chain.join(' → ')}`);
|
||||
|
||||
// ━━━ Pre-Task CLI Context Analysis (for complex/bugfix tasks) ━━━
|
||||
if (['bugfix', 'bugfix-hotfix', 'feature-complex'].includes(resolvedType)) {
|
||||
console.log(` Pre-analysis: gathering context for ${resolvedType} task...`);
|
||||
const affectedFiles = (task.context?.affected_files || []).join(', ');
|
||||
const preAnalysisPrompt = `PURPOSE: Pre-analyze codebase context for IDAW task before execution.
|
||||
TASK: • Understand current state of: ${affectedFiles || 'files related to: ' + task.title} • Identify dependencies and risk areas • Note existing patterns to follow
|
||||
MODE: analysis
|
||||
CONTEXT: @**/*
|
||||
EXPECTED: Brief context summary (affected modules, dependencies, risk areas) in 3-5 bullet points
|
||||
CONSTRAINTS: Keep concise | Focus on execution-relevant context`;
|
||||
const preAnalysis = Bash(`ccw cli -p '${preAnalysisPrompt.replace(/'/g, "'\\''")}' --tool gemini --mode analysis 2>&1 || echo "Pre-analysis skipped"`);
|
||||
task.execution.skill_results.push({
|
||||
skill: 'cli-pre-analysis',
|
||||
status: 'completed',
|
||||
context_summary: preAnalysis?.substring(0, 500),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Execute skill chain
|
||||
let previousResult = null;
|
||||
let taskFailed = false;
|
||||
|
||||
for (let skillIdx = 0; skillIdx < chain.length; skillIdx++) {
|
||||
const skillName = chain[skillIdx];
|
||||
const skillArgs = assembleSkillArgs(skillName, task, previousResult, autoYes, skillIdx === 0);
|
||||
|
||||
console.log(` [${skillIdx + 1}/${chain.length}] ${skillName}`);
|
||||
|
||||
try {
|
||||
const result = Skill({ skill: skillName, args: skillArgs });
|
||||
previousResult = result;
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'completed',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
// ━━━ CLI-Assisted Error Recovery ━━━
|
||||
console.log(` Diagnosing failure: ${skillName}...`);
|
||||
const diagnosisPrompt = `PURPOSE: Diagnose why skill "${skillName}" failed during IDAW task execution.
|
||||
TASK: • Analyze error: ${String(error).substring(0, 300)} • Check affected files: ${(task.context?.affected_files || []).join(', ') || 'unknown'} • Identify root cause • Suggest fix strategy
|
||||
MODE: analysis
|
||||
CONTEXT: @**/* | Memory: IDAW task ${task.id}: ${task.title}
|
||||
EXPECTED: Root cause + actionable fix recommendation (1-2 sentences)
|
||||
CONSTRAINTS: Focus on actionable diagnosis`;
|
||||
const diagnosisResult = Bash(`ccw cli -p '${diagnosisPrompt.replace(/'/g, "'\\''")}' --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause 2>&1 || echo "CLI diagnosis unavailable"`);
|
||||
|
||||
task.execution.skill_results.push({
|
||||
skill: `cli-diagnosis:${skillName}`,
|
||||
status: 'completed',
|
||||
diagnosis: diagnosisResult?.substring(0, 500),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Retry with diagnosis context
|
||||
console.log(` Retry with diagnosis: ${skillName}`);
|
||||
try {
|
||||
const retryResult = Skill({ skill: skillName, args: skillArgs });
|
||||
previousResult = retryResult;
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'completed-retry-with-diagnosis',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (retryError) {
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'failed',
|
||||
error: String(retryError).substring(0, 200),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (autoYes) {
|
||||
taskFailed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: `${skillName} failed after CLI diagnosis + retry: ${String(retryError).substring(0, 100)}`,
|
||||
header: 'Error',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Skip task', description: 'Mark as failed, continue' },
|
||||
{ label: 'Abort', description: 'Stop run' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
if (answer.answers?.Error === 'Abort') {
|
||||
task.status = 'failed';
|
||||
task.execution.error = String(retryError).substring(0, 200);
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
session.failed.push(task.id);
|
||||
session.status = 'failed';
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
return;
|
||||
}
|
||||
taskFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Checkpoint
|
||||
if (taskFailed) {
|
||||
task.status = 'failed';
|
||||
task.execution.error = 'Skill chain failed after retry';
|
||||
task.execution.completed_at = new Date().toISOString();
|
||||
session.failed.push(task.id);
|
||||
} else {
|
||||
// Git commit
|
||||
const commitMsg = `feat(idaw): ${task.title} [${task.id}]`;
|
||||
const diffCheck = Bash('git diff --stat HEAD 2>/dev/null || echo ""');
|
||||
const untrackedCheck = Bash('git ls-files --others --exclude-standard 2>/dev/null || echo ""');
|
||||
|
||||
if (diffCheck?.trim() || untrackedCheck?.trim()) {
|
||||
Bash('git add -A');
|
||||
Bash(`git commit -m "$(cat <<'EOF'\n${commitMsg}\nEOF\n)"`);
|
||||
const commitHash = Bash('git rev-parse --short HEAD 2>/dev/null')?.trim();
|
||||
task.execution.git_commit = commitHash;
|
||||
} else {
|
||||
task.execution.git_commit = 'no-commit';
|
||||
}
|
||||
|
||||
task.status = 'completed';
|
||||
task.execution.completed_at = new Date().toISOString();
|
||||
session.completed.push(task.id);
|
||||
}
|
||||
|
||||
task.updated_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Append progress
|
||||
const chain_str = chain.join(' → ');
|
||||
const progressEntry = `## ${task.id} — ${task.title}\n- Status: ${task.status}\n- Chain: ${chain_str}\n- Commit: ${task.execution.git_commit || '-'}\n\n`;
|
||||
const currentProgress = Read(`${sessionDir}/progress.md`);
|
||||
Write(`${sessionDir}/progress.md`, currentProgress + progressEntry);
|
||||
}
|
||||
|
||||
// Phase 6: Report
|
||||
session.status = session.failed.length > 0 && session.completed.length === 0 ? 'failed' : 'completed';
|
||||
session.current_task = null;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
const summary = `\n---\n## Summary (Resumed)\n- Completed: ${session.completed.length}\n- Failed: ${session.failed.length}\n- Skipped: ${session.skipped.length}\n`;
|
||||
const finalProgress = Read(`${sessionDir}/progress.md`);
|
||||
Write(`${sessionDir}/progress.md`, finalProgress + summary);
|
||||
|
||||
console.log('\n=== IDAW Resume Complete ===');
|
||||
console.log(`Session: ${session.session_id}`);
|
||||
console.log(`Completed: ${session.completed.length} | Failed: ${session.failed.length} | Skipped: ${session.skipped.length}`);
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### assembleSkillArgs
|
||||
|
||||
```javascript
|
||||
function assembleSkillArgs(skillName, task, previousResult, autoYes, isFirst) {
|
||||
let args = '';
|
||||
|
||||
if (isFirst) {
|
||||
// Sanitize for shell safety
|
||||
const goal = `${task.title}\n${task.description}`
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/`/g, '\\`');
|
||||
args = `"${goal}"`;
|
||||
if (task.task_type === 'bugfix-hotfix') args += ' --hotfix';
|
||||
} else if (previousResult?.session_id) {
|
||||
args = `--session="${previousResult.session_id}"`;
|
||||
}
|
||||
|
||||
if (autoYes && !args.includes('-y') && !args.includes('--yes')) {
|
||||
args = args ? `${args} -y` : '-y';
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Resume most recent running session (interactive)
|
||||
/idaw:resume
|
||||
|
||||
# Resume specific session
|
||||
/idaw:resume IDA-auth-fix-20260301
|
||||
|
||||
# Resume with auto mode (skip interrupted, continue)
|
||||
/idaw:resume -y
|
||||
|
||||
# Resume specific session with auto mode
|
||||
/idaw:resume -y IDA-auth-fix-20260301
|
||||
```
|
||||
648
.claude/commands/idaw/run-coordinate.md
Normal file
648
.claude/commands/idaw/run-coordinate.md
Normal file
@@ -0,0 +1,648 @@
|
||||
---
|
||||
name: run-coordinate
|
||||
description: IDAW coordinator - execute task skill chains via external CLI with hook callbacks and git checkpoints
|
||||
argument-hint: "[-y|--yes] [--task <id>[,<id>,...]] [--dry-run] [--tool <tool>]"
|
||||
allowed-tools: Task(*), AskUserQuestion(*), Read(*), Write(*), Bash(*), Glob(*), Grep(*)
|
||||
---
|
||||
|
||||
# IDAW Run Coordinate Command (/idaw:run-coordinate)
|
||||
|
||||
Coordinator variant of `/idaw:run`: external CLI execution with background tasks and hook callbacks.
|
||||
|
||||
**Execution Model**: `ccw cli -p "..." --tool <tool> --mode write` in background → hook callback → next step.
|
||||
|
||||
**vs `/idaw:run`**: Direct `Skill()` calls (blocking, main process) vs `ccw cli` (background, external process).
|
||||
|
||||
## When to Use
|
||||
|
||||
| Scenario | Use |
|
||||
|----------|-----|
|
||||
| Standard IDAW execution (main process) | `/idaw:run` |
|
||||
| External CLI execution (background, hook-driven) | `/idaw:run-coordinate` |
|
||||
| Need `claude` or `gemini` as execution tool | `/idaw:run-coordinate --tool claude` |
|
||||
| Long-running tasks, avoid context window pressure | `/idaw:run-coordinate` |
|
||||
|
||||
## Skill Chain Mapping
|
||||
|
||||
```javascript
|
||||
const SKILL_CHAIN_MAP = {
|
||||
'bugfix': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||
'bugfix-hotfix': ['workflow-lite-plan'],
|
||||
'feature': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||
'feature-complex': ['workflow-plan', 'workflow-execute', 'workflow-test-fix'],
|
||||
'refactor': ['workflow:refactor-cycle'],
|
||||
'tdd': ['workflow-tdd-plan', 'workflow-execute'],
|
||||
'test': ['workflow-test-fix'],
|
||||
'test-fix': ['workflow-test-fix'],
|
||||
'review': ['review-cycle'],
|
||||
'docs': ['workflow-lite-plan']
|
||||
};
|
||||
```
|
||||
|
||||
## Task Type Inference
|
||||
|
||||
```javascript
|
||||
function inferTaskType(title, description) {
|
||||
const text = `${title} ${description}`.toLowerCase();
|
||||
if (/urgent|production|critical/.test(text) && /fix|bug/.test(text)) return 'bugfix-hotfix';
|
||||
if (/refactor|重构|tech.*debt/.test(text)) return 'refactor';
|
||||
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
|
||||
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
|
||||
if (/generate test|写测试|add test/.test(text)) return 'test';
|
||||
if (/review|code review/.test(text)) return 'review';
|
||||
if (/docs|documentation|readme/.test(text)) return 'docs';
|
||||
if (/fix|bug|error|crash|fail/.test(text)) return 'bugfix';
|
||||
if (/complex|multi-module|architecture/.test(text)) return 'feature-complex';
|
||||
return 'feature';
|
||||
}
|
||||
```
|
||||
|
||||
## 6-Phase Execution (Coordinator Model)
|
||||
|
||||
### Phase 1: Load Tasks
|
||||
|
||||
```javascript
|
||||
const args = $ARGUMENTS;
|
||||
const autoYes = /(-y|--yes)/.test(args);
|
||||
const dryRun = /--dry-run/.test(args);
|
||||
const taskFilter = args.match(/--task\s+([\w,-]+)/)?.[1]?.split(',') || null;
|
||||
const cliTool = args.match(/--tool\s+(\w+)/)?.[1] || 'claude';
|
||||
|
||||
// Load task files
|
||||
const taskFiles = Glob('.workflow/.idaw/tasks/IDAW-*.json') || [];
|
||||
|
||||
if (taskFiles.length === 0) {
|
||||
console.log('No IDAW tasks found. Use /idaw:add to create tasks.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and filter
|
||||
let tasks = taskFiles.map(f => JSON.parse(Read(f)));
|
||||
|
||||
if (taskFilter) {
|
||||
tasks = tasks.filter(t => taskFilter.includes(t.id));
|
||||
} else {
|
||||
tasks = tasks.filter(t => t.status === 'pending');
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
console.log('No pending tasks to execute. Use /idaw:add to add tasks or --task to specify IDs.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort: priority ASC (1=critical first), then ID ASC
|
||||
tasks.sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 2: Session Setup
|
||||
|
||||
```javascript
|
||||
// Generate session ID: IDA-{slug}-YYYYMMDD
|
||||
const slug = tasks[0].title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.substring(0, 20)
|
||||
.replace(/-$/, '');
|
||||
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
let sessionId = `IDA-${slug}-${dateStr}`;
|
||||
|
||||
// Check collision
|
||||
const existingSession = Glob(`.workflow/.idaw/sessions/${sessionId}/session.json`);
|
||||
if (existingSession?.length > 0) {
|
||||
sessionId = `${sessionId}-2`;
|
||||
}
|
||||
|
||||
const sessionDir = `.workflow/.idaw/sessions/${sessionId}`;
|
||||
Bash(`mkdir -p "${sessionDir}"`);
|
||||
|
||||
const session = {
|
||||
session_id: sessionId,
|
||||
mode: 'coordinate', // ★ Marks this as coordinator-mode session
|
||||
cli_tool: cliTool,
|
||||
status: 'running',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tasks: tasks.map(t => t.id),
|
||||
current_task: null,
|
||||
current_skill_index: 0,
|
||||
completed: [],
|
||||
failed: [],
|
||||
skipped: [],
|
||||
prompts_used: []
|
||||
};
|
||||
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Initialize progress.md
|
||||
const progressHeader = `# IDAW Progress — ${sessionId} (coordinate mode)\nStarted: ${session.created_at}\nCLI Tool: ${cliTool}\n\n`;
|
||||
Write(`${sessionDir}/progress.md`, progressHeader);
|
||||
```
|
||||
|
||||
### Phase 3: Startup Protocol
|
||||
|
||||
```javascript
|
||||
// Check for existing running sessions
|
||||
const runningSessions = Glob('.workflow/.idaw/sessions/IDA-*/session.json')
|
||||
?.map(f => { try { return JSON.parse(Read(f)); } catch { return null; } })
|
||||
.filter(s => s && s.status === 'running' && s.session_id !== sessionId) || [];
|
||||
|
||||
if (runningSessions.length > 0 && !autoYes) {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: `Found running session: ${runningSessions[0].session_id}. How to proceed?`,
|
||||
header: 'Conflict',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Resume existing', description: 'Use /idaw:resume instead' },
|
||||
{ label: 'Start fresh', description: 'Continue with new session' },
|
||||
{ label: 'Abort', description: 'Cancel this run' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
if (answer.answers?.Conflict === 'Resume existing') {
|
||||
console.log(`Use: /idaw:resume ${runningSessions[0].session_id}`);
|
||||
return;
|
||||
}
|
||||
if (answer.answers?.Conflict === 'Abort') return;
|
||||
}
|
||||
|
||||
// Check git status
|
||||
const gitStatus = Bash('git status --porcelain 2>/dev/null');
|
||||
if (gitStatus?.trim() && !autoYes) {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: 'Working tree has uncommitted changes. How to proceed?',
|
||||
header: 'Git',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Continue', description: 'Proceed with dirty tree' },
|
||||
{ label: 'Stash', description: 'git stash before running' },
|
||||
{ label: 'Abort', description: 'Stop and handle manually' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
if (answer.answers?.Git === 'Stash') Bash('git stash push -m "idaw-pre-run"');
|
||||
if (answer.answers?.Git === 'Abort') return;
|
||||
}
|
||||
|
||||
// Dry run
|
||||
if (dryRun) {
|
||||
console.log(`# Dry Run — ${sessionId} (coordinate mode, tool: ${cliTool})\n`);
|
||||
for (const task of tasks) {
|
||||
const taskType = task.task_type || inferTaskType(task.title, task.description);
|
||||
const chain = task.skill_chain || SKILL_CHAIN_MAP[taskType] || SKILL_CHAIN_MAP['feature'];
|
||||
console.log(`## ${task.id}: ${task.title}`);
|
||||
console.log(` Type: ${taskType} | Priority: ${task.priority}`);
|
||||
console.log(` Chain: ${chain.join(' → ')}`);
|
||||
console.log(` CLI: ccw cli --tool ${cliTool} --mode write\n`);
|
||||
}
|
||||
console.log(`Total: ${tasks.length} tasks`);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Launch First Task (then wait for hook)
|
||||
|
||||
```javascript
|
||||
// Start with the first task, first skill
|
||||
const firstTask = tasks[0];
|
||||
const resolvedType = firstTask.task_type || inferTaskType(firstTask.title, firstTask.description);
|
||||
const chain = firstTask.skill_chain || SKILL_CHAIN_MAP[resolvedType] || SKILL_CHAIN_MAP['feature'];
|
||||
|
||||
// Update task → in_progress
|
||||
firstTask.status = 'in_progress';
|
||||
firstTask.task_type = resolvedType;
|
||||
firstTask.execution.started_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${firstTask.id}.json`, JSON.stringify(firstTask, null, 2));
|
||||
|
||||
// Update session
|
||||
session.current_task = firstTask.id;
|
||||
session.current_skill_index = 0;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// ━━━ Pre-Task CLI Context Analysis (for complex/bugfix tasks) ━━━
|
||||
if (['bugfix', 'bugfix-hotfix', 'feature-complex'].includes(resolvedType)) {
|
||||
console.log(`Pre-analysis: gathering context for ${resolvedType} task...`);
|
||||
const affectedFiles = (firstTask.context?.affected_files || []).join(', ');
|
||||
const preAnalysisPrompt = `PURPOSE: Pre-analyze codebase context for IDAW task.
|
||||
TASK: • Understand current state of: ${affectedFiles || 'files related to: ' + firstTask.title} • Identify dependencies and risk areas
|
||||
MODE: analysis
|
||||
CONTEXT: @**/*
|
||||
EXPECTED: Brief context summary in 3-5 bullet points
|
||||
CONSTRAINTS: Keep concise`;
|
||||
Bash(`ccw cli -p '${preAnalysisPrompt.replace(/'/g, "'\\''")}' --tool gemini --mode analysis 2>&1 || echo "Pre-analysis skipped"`);
|
||||
}
|
||||
|
||||
// Assemble prompt for first skill
|
||||
const skillName = chain[0];
|
||||
const prompt = assembleCliPrompt(skillName, firstTask, null, autoYes);
|
||||
|
||||
session.prompts_used.push({
|
||||
task_id: firstTask.id,
|
||||
skill_index: 0,
|
||||
skill: skillName,
|
||||
prompt: prompt,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Launch via ccw cli in background
|
||||
console.log(`[1/${tasks.length}] ${firstTask.id}: ${firstTask.title}`);
|
||||
console.log(` Chain: ${chain.join(' → ')}`);
|
||||
console.log(` Launching: ${skillName} via ccw cli --tool ${cliTool}`);
|
||||
|
||||
Bash(
|
||||
`ccw cli -p "${escapeForShell(prompt)}" --tool ${cliTool} --mode write`,
|
||||
{ run_in_background: true }
|
||||
);
|
||||
|
||||
// ★ STOP HERE — wait for hook callback
|
||||
// Hook callback will trigger handleStepCompletion() below
|
||||
```
|
||||
|
||||
### Phase 5: Hook Callback Handler (per-step completion)
|
||||
|
||||
```javascript
|
||||
// Called by hook when background CLI completes
|
||||
async function handleStepCompletion(sessionId, cliOutput) {
|
||||
const sessionDir = `.workflow/.idaw/sessions/${sessionId}`;
|
||||
const session = JSON.parse(Read(`${sessionDir}/session.json`));
|
||||
|
||||
const taskId = session.current_task;
|
||||
const task = JSON.parse(Read(`.workflow/.idaw/tasks/${taskId}.json`));
|
||||
|
||||
const resolvedType = task.task_type || inferTaskType(task.title, task.description);
|
||||
const chain = task.skill_chain || SKILL_CHAIN_MAP[resolvedType] || SKILL_CHAIN_MAP['feature'];
|
||||
const skillIdx = session.current_skill_index;
|
||||
const skillName = chain[skillIdx];
|
||||
|
||||
// Parse CLI output for session ID
|
||||
const parsedOutput = parseCliOutput(cliOutput);
|
||||
|
||||
// Record skill result
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: parsedOutput.success ? 'completed' : 'failed',
|
||||
session_id: parsedOutput.sessionId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// ━━━ Handle failure with CLI diagnosis ━━━
|
||||
if (!parsedOutput.success) {
|
||||
console.log(` ${skillName} failed. Running CLI diagnosis...`);
|
||||
const diagnosisPrompt = `PURPOSE: Diagnose why skill "${skillName}" failed during IDAW task.
|
||||
TASK: • Analyze error output • Check affected files: ${(task.context?.affected_files || []).join(', ') || 'unknown'}
|
||||
MODE: analysis
|
||||
CONTEXT: @**/* | Memory: IDAW task ${task.id}: ${task.title}
|
||||
EXPECTED: Root cause + fix recommendation
|
||||
CONSTRAINTS: Actionable diagnosis`;
|
||||
Bash(`ccw cli -p '${diagnosisPrompt.replace(/'/g, "'\\''")}' --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause 2>&1 || true`);
|
||||
|
||||
task.execution.skill_results.push({
|
||||
skill: `cli-diagnosis:${skillName}`,
|
||||
status: 'completed',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Retry once
|
||||
console.log(` Retrying: ${skillName}`);
|
||||
const retryPrompt = assembleCliPrompt(skillName, task, parsedOutput, true);
|
||||
session.prompts_used.push({
|
||||
task_id: taskId,
|
||||
skill_index: skillIdx,
|
||||
skill: `${skillName}-retry`,
|
||||
prompt: retryPrompt,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
Write(`.workflow/.idaw/tasks/${taskId}.json`, JSON.stringify(task, null, 2));
|
||||
|
||||
Bash(
|
||||
`ccw cli -p "${escapeForShell(retryPrompt)}" --tool ${session.cli_tool} --mode write`,
|
||||
{ run_in_background: true }
|
||||
);
|
||||
return; // Wait for retry hook
|
||||
}
|
||||
|
||||
// ━━━ Skill succeeded — advance ━━━
|
||||
const nextSkillIdx = skillIdx + 1;
|
||||
|
||||
if (nextSkillIdx < chain.length) {
|
||||
// More skills in this task's chain → launch next skill
|
||||
session.current_skill_index = nextSkillIdx;
|
||||
session.updated_at = new Date().toISOString();
|
||||
|
||||
const nextSkill = chain[nextSkillIdx];
|
||||
const nextPrompt = assembleCliPrompt(nextSkill, task, parsedOutput, true);
|
||||
|
||||
session.prompts_used.push({
|
||||
task_id: taskId,
|
||||
skill_index: nextSkillIdx,
|
||||
skill: nextSkill,
|
||||
prompt: nextPrompt,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
Write(`.workflow/.idaw/tasks/${taskId}.json`, JSON.stringify(task, null, 2));
|
||||
|
||||
console.log(` Next skill: ${nextSkill}`);
|
||||
Bash(
|
||||
`ccw cli -p "${escapeForShell(nextPrompt)}" --tool ${session.cli_tool} --mode write`,
|
||||
{ run_in_background: true }
|
||||
);
|
||||
return; // Wait for next hook
|
||||
}
|
||||
|
||||
// ━━━ Task chain complete — git checkpoint ━━━
|
||||
const commitMsg = `feat(idaw): ${task.title} [${task.id}]`;
|
||||
const diffCheck = Bash('git diff --stat HEAD 2>/dev/null || echo ""');
|
||||
const untrackedCheck = Bash('git ls-files --others --exclude-standard 2>/dev/null || echo ""');
|
||||
|
||||
if (diffCheck?.trim() || untrackedCheck?.trim()) {
|
||||
Bash('git add -A');
|
||||
Bash(`git commit -m "$(cat <<'EOF'\n${commitMsg}\nEOF\n)"`);
|
||||
const commitHash = Bash('git rev-parse --short HEAD 2>/dev/null')?.trim();
|
||||
task.execution.git_commit = commitHash;
|
||||
} else {
|
||||
task.execution.git_commit = 'no-commit';
|
||||
}
|
||||
|
||||
task.status = 'completed';
|
||||
task.execution.completed_at = new Date().toISOString();
|
||||
task.updated_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${taskId}.json`, JSON.stringify(task, null, 2));
|
||||
|
||||
session.completed.push(taskId);
|
||||
|
||||
// Append progress
|
||||
const progressEntry = `## ${task.id} — ${task.title}\n` +
|
||||
`- Status: completed\n` +
|
||||
`- Type: ${task.task_type}\n` +
|
||||
`- Chain: ${chain.join(' → ')}\n` +
|
||||
`- Commit: ${task.execution.git_commit || '-'}\n` +
|
||||
`- Mode: coordinate (${session.cli_tool})\n\n`;
|
||||
const currentProgress = Read(`${sessionDir}/progress.md`);
|
||||
Write(`${sessionDir}/progress.md`, currentProgress + progressEntry);
|
||||
|
||||
// ━━━ Advance to next task ━━━
|
||||
const allTaskIds = session.tasks;
|
||||
const completedSet = new Set([...session.completed, ...session.failed, ...session.skipped]);
|
||||
const nextTaskId = allTaskIds.find(id => !completedSet.has(id));
|
||||
|
||||
if (nextTaskId) {
|
||||
// Load next task
|
||||
const nextTask = JSON.parse(Read(`.workflow/.idaw/tasks/${nextTaskId}.json`));
|
||||
const nextType = nextTask.task_type || inferTaskType(nextTask.title, nextTask.description);
|
||||
const nextChain = nextTask.skill_chain || SKILL_CHAIN_MAP[nextType] || SKILL_CHAIN_MAP['feature'];
|
||||
|
||||
nextTask.status = 'in_progress';
|
||||
nextTask.task_type = nextType;
|
||||
nextTask.execution.started_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${nextTaskId}.json`, JSON.stringify(nextTask, null, 2));
|
||||
|
||||
session.current_task = nextTaskId;
|
||||
session.current_skill_index = 0;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Pre-analysis for complex tasks
|
||||
if (['bugfix', 'bugfix-hotfix', 'feature-complex'].includes(nextType)) {
|
||||
const affectedFiles = (nextTask.context?.affected_files || []).join(', ');
|
||||
Bash(`ccw cli -p 'PURPOSE: Pre-analyze context for ${nextTask.title}. TASK: Check ${affectedFiles || "related files"}. MODE: analysis. EXPECTED: 3-5 bullet points.' --tool gemini --mode analysis 2>&1 || true`);
|
||||
}
|
||||
|
||||
const nextSkillName = nextChain[0];
|
||||
const nextPrompt = assembleCliPrompt(nextSkillName, nextTask, null, true);
|
||||
|
||||
session.prompts_used.push({
|
||||
task_id: nextTaskId,
|
||||
skill_index: 0,
|
||||
skill: nextSkillName,
|
||||
prompt: nextPrompt,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
const taskNum = session.completed.length + 1;
|
||||
const totalTasks = session.tasks.length;
|
||||
console.log(`\n[${taskNum}/${totalTasks}] ${nextTaskId}: ${nextTask.title}`);
|
||||
console.log(` Chain: ${nextChain.join(' → ')}`);
|
||||
|
||||
Bash(
|
||||
`ccw cli -p "${escapeForShell(nextPrompt)}" --tool ${session.cli_tool} --mode write`,
|
||||
{ run_in_background: true }
|
||||
);
|
||||
return; // Wait for hook
|
||||
}
|
||||
|
||||
// ━━━ All tasks complete — Phase 6: Report ━━━
|
||||
session.status = session.failed.length > 0 && session.completed.length === 0 ? 'failed' : 'completed';
|
||||
session.current_task = null;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
const summary = `\n---\n## Summary (coordinate mode)\n` +
|
||||
`- CLI Tool: ${session.cli_tool}\n` +
|
||||
`- Completed: ${session.completed.length}\n` +
|
||||
`- Failed: ${session.failed.length}\n` +
|
||||
`- Skipped: ${session.skipped.length}\n` +
|
||||
`- Total: ${session.tasks.length}\n`;
|
||||
const finalProgress = Read(`${sessionDir}/progress.md`);
|
||||
Write(`${sessionDir}/progress.md`, finalProgress + summary);
|
||||
|
||||
console.log('\n=== IDAW Coordinate Complete ===');
|
||||
console.log(`Session: ${sessionId}`);
|
||||
console.log(`Completed: ${session.completed.length}/${session.tasks.length}`);
|
||||
if (session.failed.length > 0) console.log(`Failed: ${session.failed.join(', ')}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### assembleCliPrompt
|
||||
|
||||
```javascript
|
||||
function assembleCliPrompt(skillName, task, previousResult, autoYes) {
|
||||
let prompt = '';
|
||||
const yFlag = autoYes ? ' -y' : '';
|
||||
|
||||
// Map skill to command invocation
|
||||
if (skillName === 'workflow-lite-plan') {
|
||||
const goal = sanitize(`${task.title}\n${task.description}`);
|
||||
prompt = `/workflow-lite-plan${yFlag} "${goal}"`;
|
||||
if (task.task_type === 'bugfix') prompt = `/workflow-lite-plan${yFlag} --bugfix "${goal}"`;
|
||||
if (task.task_type === 'bugfix-hotfix') prompt = `/workflow-lite-plan${yFlag} --hotfix "${goal}"`;
|
||||
|
||||
} else if (skillName === 'workflow-plan') {
|
||||
prompt = `/workflow-plan${yFlag} "${sanitize(task.title)}"`;
|
||||
|
||||
} else if (skillName === 'workflow-execute') {
|
||||
if (previousResult?.sessionId) {
|
||||
prompt = `/workflow-execute${yFlag} --resume-session="${previousResult.sessionId}"`;
|
||||
} else {
|
||||
prompt = `/workflow-execute${yFlag}`;
|
||||
}
|
||||
|
||||
} else if (skillName === 'workflow-test-fix') {
|
||||
if (previousResult?.sessionId) {
|
||||
prompt = `/workflow-test-fix${yFlag} "${previousResult.sessionId}"`;
|
||||
} else {
|
||||
prompt = `/workflow-test-fix${yFlag} "${sanitize(task.title)}"`;
|
||||
}
|
||||
|
||||
} else if (skillName === 'workflow-tdd-plan') {
|
||||
prompt = `/workflow-tdd-plan${yFlag} "${sanitize(task.title)}"`;
|
||||
|
||||
} else if (skillName === 'workflow:refactor-cycle') {
|
||||
prompt = `/workflow:refactor-cycle${yFlag} "${sanitize(task.title)}"`;
|
||||
|
||||
} else if (skillName === 'review-cycle') {
|
||||
if (previousResult?.sessionId) {
|
||||
prompt = `/review-cycle${yFlag} --session="${previousResult.sessionId}"`;
|
||||
} else {
|
||||
prompt = `/review-cycle${yFlag}`;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Generic fallback
|
||||
prompt = `/${skillName}${yFlag} "${sanitize(task.title)}"`;
|
||||
}
|
||||
|
||||
// Append task context
|
||||
prompt += `\n\nTask: ${task.title}\nDescription: ${task.description}`;
|
||||
if (task.context?.affected_files?.length > 0) {
|
||||
prompt += `\nAffected files: ${task.context.affected_files.join(', ')}`;
|
||||
}
|
||||
if (task.context?.acceptance_criteria?.length > 0) {
|
||||
prompt += `\nAcceptance criteria: ${task.context.acceptance_criteria.join('; ')}`;
|
||||
}
|
||||
|
||||
return prompt;
|
||||
}
|
||||
```
|
||||
|
||||
### sanitize & escapeForShell
|
||||
|
||||
```javascript
|
||||
function sanitize(text) {
|
||||
return text
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/`/g, '\\`');
|
||||
}
|
||||
|
||||
function escapeForShell(prompt) {
|
||||
return prompt.replace(/'/g, "'\\''");
|
||||
}
|
||||
```
|
||||
|
||||
### parseCliOutput
|
||||
|
||||
```javascript
|
||||
function parseCliOutput(output) {
|
||||
// Extract session ID from CLI output (e.g., WFS-xxx, session-xxx)
|
||||
const sessionMatch = output.match(/(?:session|WFS|Session ID)[:\s]*([\w-]+)/i);
|
||||
const success = !/(?:error|failed|fatal)/i.test(output) || /completed|success/i.test(output);
|
||||
|
||||
return {
|
||||
success,
|
||||
sessionId: sessionMatch?.[1] || null,
|
||||
raw: output?.substring(0, 500)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## CLI-Assisted Analysis
|
||||
|
||||
Same as `/idaw:run` — integrated at two points:
|
||||
|
||||
### Pre-Task Context Analysis
|
||||
For `bugfix`, `bugfix-hotfix`, `feature-complex` tasks: auto-invoke `ccw cli --tool gemini --mode analysis` before launching skill chain.
|
||||
|
||||
### Error Recovery with CLI Diagnosis
|
||||
When a skill's CLI execution fails: invoke diagnosis → retry once → if still fails, mark failed and advance.
|
||||
|
||||
```
|
||||
Skill CLI fails → CLI diagnosis (gemini) → Retry CLI → Still fails → mark failed → next task
|
||||
```
|
||||
|
||||
## State Flow
|
||||
|
||||
```
|
||||
Phase 4: Launch first skill
|
||||
↓
|
||||
ccw cli --tool claude --mode write (background)
|
||||
↓
|
||||
★ STOP — wait for hook callback
|
||||
↓
|
||||
Phase 5: handleStepCompletion()
|
||||
├─ Skill succeeded + more in chain → launch next skill → STOP
|
||||
├─ Skill succeeded + chain complete → git checkpoint → next task → STOP
|
||||
├─ Skill failed → CLI diagnosis → retry → STOP
|
||||
└─ All tasks done → Phase 6: Report
|
||||
```
|
||||
|
||||
## Session State (session.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "IDA-fix-login-20260301",
|
||||
"mode": "coordinate",
|
||||
"cli_tool": "claude",
|
||||
"status": "running|waiting|completed|failed",
|
||||
"created_at": "ISO",
|
||||
"updated_at": "ISO",
|
||||
"tasks": ["IDAW-001", "IDAW-002"],
|
||||
"current_task": "IDAW-001",
|
||||
"current_skill_index": 0,
|
||||
"completed": [],
|
||||
"failed": [],
|
||||
"skipped": [],
|
||||
"prompts_used": [
|
||||
{
|
||||
"task_id": "IDAW-001",
|
||||
"skill_index": 0,
|
||||
"skill": "workflow-lite-plan",
|
||||
"prompt": "/workflow-lite-plan -y \"Fix login timeout\"",
|
||||
"timestamp": "ISO"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Differences from /idaw:run
|
||||
|
||||
| Aspect | /idaw:run | /idaw:run-coordinate |
|
||||
|--------|-----------|---------------------|
|
||||
| Execution | `Skill()` blocking in main process | `ccw cli` background + hook callback |
|
||||
| Context window | Shared (each skill uses main context) | Isolated (each CLI gets fresh context) |
|
||||
| Concurrency | Sequential blocking | Sequential non-blocking (hook-driven) |
|
||||
| State tracking | session.json + task.json | session.json + task.json + prompts_used |
|
||||
| Tool selection | N/A (Skill native) | `--tool claude\|gemini\|qwen` |
|
||||
| Resume | Via `/idaw:resume` (same) | Via `/idaw:resume` (same, detects mode) |
|
||||
| Best for | Short chains, interactive | Long chains, autonomous, context-heavy |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Execute all pending tasks via claude CLI
|
||||
/idaw:run-coordinate -y
|
||||
|
||||
# Use specific CLI tool
|
||||
/idaw:run-coordinate -y --tool gemini
|
||||
|
||||
# Execute specific tasks
|
||||
/idaw:run-coordinate --task IDAW-001,IDAW-003 --tool claude
|
||||
|
||||
# Dry run (show plan without executing)
|
||||
/idaw:run-coordinate --dry-run
|
||||
|
||||
# Interactive mode
|
||||
/idaw:run-coordinate
|
||||
```
|
||||
539
.claude/commands/idaw/run.md
Normal file
539
.claude/commands/idaw/run.md
Normal file
@@ -0,0 +1,539 @@
|
||||
---
|
||||
name: run
|
||||
description: IDAW orchestrator - execute task skill chains serially with git checkpoints
|
||||
argument-hint: "[-y|--yes] [--task <id>[,<id>,...]] [--dry-run]"
|
||||
allowed-tools: Skill(*), TodoWrite(*), AskUserQuestion(*), Read(*), Write(*), Bash(*), Glob(*)
|
||||
---
|
||||
|
||||
# IDAW Run Command (/idaw:run)
|
||||
|
||||
## Auto Mode
|
||||
|
||||
When `--yes` or `-y`: Skip all confirmations, auto-skip on failure, proceed with dirty git.
|
||||
|
||||
## Skill Chain Mapping
|
||||
|
||||
```javascript
|
||||
const SKILL_CHAIN_MAP = {
|
||||
'bugfix': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||
'bugfix-hotfix': ['workflow-lite-plan'],
|
||||
'feature': ['workflow-lite-plan', 'workflow-test-fix'],
|
||||
'feature-complex': ['workflow-plan', 'workflow-execute', 'workflow-test-fix'],
|
||||
'refactor': ['workflow:refactor-cycle'],
|
||||
'tdd': ['workflow-tdd-plan', 'workflow-execute'],
|
||||
'test': ['workflow-test-fix'],
|
||||
'test-fix': ['workflow-test-fix'],
|
||||
'review': ['review-cycle'],
|
||||
'docs': ['workflow-lite-plan']
|
||||
};
|
||||
```
|
||||
|
||||
## Task Type Inference
|
||||
|
||||
```javascript
|
||||
function inferTaskType(title, description) {
|
||||
const text = `${title} ${description}`.toLowerCase();
|
||||
if (/urgent|production|critical/.test(text) && /fix|bug/.test(text)) return 'bugfix-hotfix';
|
||||
if (/refactor|重构|tech.*debt/.test(text)) return 'refactor';
|
||||
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
|
||||
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
|
||||
if (/generate test|写测试|add test/.test(text)) return 'test';
|
||||
if (/review|code review/.test(text)) return 'review';
|
||||
if (/docs|documentation|readme/.test(text)) return 'docs';
|
||||
if (/fix|bug|error|crash|fail/.test(text)) return 'bugfix';
|
||||
if (/complex|multi-module|architecture/.test(text)) return 'feature-complex';
|
||||
return 'feature';
|
||||
}
|
||||
```
|
||||
|
||||
## 6-Phase Execution
|
||||
|
||||
### Phase 1: Load Tasks
|
||||
|
||||
```javascript
|
||||
const args = $ARGUMENTS;
|
||||
const autoYes = /(-y|--yes)/.test(args);
|
||||
const dryRun = /--dry-run/.test(args);
|
||||
const taskFilter = args.match(/--task\s+([\w,-]+)/)?.[1]?.split(',') || null;
|
||||
|
||||
// Load task files
|
||||
const taskFiles = Glob('.workflow/.idaw/tasks/IDAW-*.json') || [];
|
||||
|
||||
if (taskFiles.length === 0) {
|
||||
console.log('No IDAW tasks found. Use /idaw:add to create tasks.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and filter
|
||||
let tasks = taskFiles.map(f => JSON.parse(Read(f)));
|
||||
|
||||
if (taskFilter) {
|
||||
tasks = tasks.filter(t => taskFilter.includes(t.id));
|
||||
} else {
|
||||
tasks = tasks.filter(t => t.status === 'pending');
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
console.log('No pending tasks to execute. Use /idaw:add to add tasks or --task to specify IDs.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort: priority ASC (1=critical first), then ID ASC
|
||||
tasks.sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 2: Session Setup
|
||||
|
||||
```javascript
|
||||
// Generate session ID: IDA-{slug}-YYYYMMDD
|
||||
const slug = tasks[0].title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.substring(0, 20)
|
||||
.replace(/-$/, '');
|
||||
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
let sessionId = `IDA-${slug}-${dateStr}`;
|
||||
|
||||
// Check collision
|
||||
const existingSession = Glob(`.workflow/.idaw/sessions/${sessionId}/session.json`);
|
||||
if (existingSession?.length > 0) {
|
||||
sessionId = `${sessionId}-2`;
|
||||
}
|
||||
|
||||
const sessionDir = `.workflow/.idaw/sessions/${sessionId}`;
|
||||
Bash(`mkdir -p "${sessionDir}"`);
|
||||
|
||||
const session = {
|
||||
session_id: sessionId,
|
||||
status: 'running',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tasks: tasks.map(t => t.id),
|
||||
current_task: null,
|
||||
completed: [],
|
||||
failed: [],
|
||||
skipped: []
|
||||
};
|
||||
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Initialize progress.md
|
||||
const progressHeader = `# IDAW Progress — ${sessionId}\nStarted: ${session.created_at}\n\n`;
|
||||
Write(`${sessionDir}/progress.md`, progressHeader);
|
||||
|
||||
// TodoWrite
|
||||
TodoWrite({
|
||||
todos: tasks.map((t, i) => ({
|
||||
content: `IDAW:[${i + 1}/${tasks.length}] ${t.title}`,
|
||||
status: i === 0 ? 'in_progress' : 'pending',
|
||||
activeForm: `Executing ${t.title}`
|
||||
}))
|
||||
});
|
||||
```
|
||||
|
||||
### Phase 3: Startup Protocol
|
||||
|
||||
```javascript
|
||||
// Check for existing running sessions
|
||||
const runningSessions = Glob('.workflow/.idaw/sessions/IDA-*/session.json')
|
||||
?.map(f => JSON.parse(Read(f)))
|
||||
.filter(s => s.status === 'running' && s.session_id !== sessionId) || [];
|
||||
|
||||
if (runningSessions.length > 0) {
|
||||
if (!autoYes) {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: `Found running session: ${runningSessions[0].session_id}. How to proceed?`,
|
||||
header: 'Conflict',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Resume existing', description: 'Use /idaw:resume instead' },
|
||||
{ label: 'Start fresh', description: 'Continue with new session' },
|
||||
{ label: 'Abort', description: 'Cancel this run' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
if (answer.answers?.Conflict === 'Resume existing') {
|
||||
console.log(`Use: /idaw:resume ${runningSessions[0].session_id}`);
|
||||
return;
|
||||
}
|
||||
if (answer.answers?.Conflict === 'Abort') return;
|
||||
}
|
||||
// autoYes or "Start fresh": proceed
|
||||
}
|
||||
|
||||
// Check git status
|
||||
const gitStatus = Bash('git status --porcelain 2>/dev/null');
|
||||
if (gitStatus?.trim()) {
|
||||
if (!autoYes) {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: 'Working tree has uncommitted changes. How to proceed?',
|
||||
header: 'Git',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Continue', description: 'Proceed with dirty tree' },
|
||||
{ label: 'Stash', description: 'git stash before running' },
|
||||
{ label: 'Abort', description: 'Stop and handle manually' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
if (answer.answers?.Git === 'Stash') {
|
||||
Bash('git stash push -m "idaw-pre-run"');
|
||||
}
|
||||
if (answer.answers?.Git === 'Abort') return;
|
||||
}
|
||||
// autoYes: proceed silently
|
||||
}
|
||||
|
||||
// Dry run: show plan and exit
|
||||
if (dryRun) {
|
||||
console.log(`# Dry Run — ${sessionId}\n`);
|
||||
for (const task of tasks) {
|
||||
const taskType = task.task_type || inferTaskType(task.title, task.description);
|
||||
const chain = task.skill_chain || SKILL_CHAIN_MAP[taskType] || SKILL_CHAIN_MAP['feature'];
|
||||
console.log(`## ${task.id}: ${task.title}`);
|
||||
console.log(` Type: ${taskType} | Priority: ${task.priority}`);
|
||||
console.log(` Chain: ${chain.join(' → ')}\n`);
|
||||
}
|
||||
console.log(`Total: ${tasks.length} tasks`);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Main Loop (serial, one task at a time)
|
||||
|
||||
```javascript
|
||||
for (let taskIdx = 0; taskIdx < tasks.length; taskIdx++) {
|
||||
const task = tasks[taskIdx];
|
||||
|
||||
// Skip completed/failed/skipped
|
||||
if (['completed', 'failed', 'skipped'].includes(task.status)) continue;
|
||||
|
||||
// Resolve skill chain
|
||||
const resolvedType = task.task_type || inferTaskType(task.title, task.description);
|
||||
const chain = task.skill_chain || SKILL_CHAIN_MAP[resolvedType] || SKILL_CHAIN_MAP['feature'];
|
||||
|
||||
// Update task status → in_progress
|
||||
task.status = 'in_progress';
|
||||
task.task_type = resolvedType; // persist inferred type
|
||||
task.execution.started_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
|
||||
// Update session
|
||||
session.current_task = task.id;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
console.log(`\n--- [${taskIdx + 1}/${tasks.length}] ${task.id}: ${task.title} ---`);
|
||||
console.log(`Chain: ${chain.join(' → ')}`);
|
||||
|
||||
// ━━━ Pre-Task CLI Context Analysis (for complex/bugfix tasks) ━━━
|
||||
if (['bugfix', 'bugfix-hotfix', 'feature-complex'].includes(resolvedType)) {
|
||||
console.log(` Pre-analysis: gathering context for ${resolvedType} task...`);
|
||||
const affectedFiles = (task.context?.affected_files || []).join(', ');
|
||||
const preAnalysisPrompt = `PURPOSE: Pre-analyze codebase context for IDAW task before execution.
|
||||
TASK: • Understand current state of: ${affectedFiles || 'files related to: ' + task.title} • Identify dependencies and risk areas • Note existing patterns to follow
|
||||
MODE: analysis
|
||||
CONTEXT: @**/*
|
||||
EXPECTED: Brief context summary (affected modules, dependencies, risk areas) in 3-5 bullet points
|
||||
CONSTRAINTS: Keep concise | Focus on execution-relevant context`;
|
||||
const preAnalysis = Bash(`ccw cli -p '${preAnalysisPrompt.replace(/'/g, "'\\''")}' --tool gemini --mode analysis 2>&1 || echo "Pre-analysis skipped"`);
|
||||
task.execution.skill_results.push({
|
||||
skill: 'cli-pre-analysis',
|
||||
status: 'completed',
|
||||
context_summary: preAnalysis?.substring(0, 500),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Execute each skill in chain
|
||||
let previousResult = null;
|
||||
let taskFailed = false;
|
||||
|
||||
for (let skillIdx = 0; skillIdx < chain.length; skillIdx++) {
|
||||
const skillName = chain[skillIdx];
|
||||
const skillArgs = assembleSkillArgs(skillName, task, previousResult, autoYes, skillIdx === 0);
|
||||
|
||||
console.log(` [${skillIdx + 1}/${chain.length}] ${skillName}`);
|
||||
|
||||
try {
|
||||
const result = Skill({ skill: skillName, args: skillArgs });
|
||||
previousResult = result;
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'completed',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
// ━━━ CLI-Assisted Error Recovery ━━━
|
||||
// Step 1: Invoke CLI diagnosis (auto-invoke trigger: self-repair fails)
|
||||
console.log(` Diagnosing failure: ${skillName}...`);
|
||||
const diagnosisPrompt = `PURPOSE: Diagnose why skill "${skillName}" failed during IDAW task execution.
|
||||
TASK: • Analyze error: ${String(error).substring(0, 300)} • Check affected files: ${(task.context?.affected_files || []).join(', ') || 'unknown'} • Identify root cause • Suggest fix strategy
|
||||
MODE: analysis
|
||||
CONTEXT: @**/* | Memory: IDAW task ${task.id}: ${task.title}
|
||||
EXPECTED: Root cause + actionable fix recommendation (1-2 sentences)
|
||||
CONSTRAINTS: Focus on actionable diagnosis`;
|
||||
const diagnosisResult = Bash(`ccw cli -p '${diagnosisPrompt.replace(/'/g, "'\\''")}' --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause 2>&1 || echo "CLI diagnosis unavailable"`);
|
||||
|
||||
task.execution.skill_results.push({
|
||||
skill: `cli-diagnosis:${skillName}`,
|
||||
status: 'completed',
|
||||
diagnosis: diagnosisResult?.substring(0, 500),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Step 2: Retry with diagnosis context
|
||||
console.log(` Retry with diagnosis: ${skillName}`);
|
||||
try {
|
||||
const retryResult = Skill({ skill: skillName, args: skillArgs });
|
||||
previousResult = retryResult;
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'completed-retry-with-diagnosis',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (retryError) {
|
||||
// Step 3: Failed after CLI-assisted retry
|
||||
task.execution.skill_results.push({
|
||||
skill: skillName,
|
||||
status: 'failed',
|
||||
error: String(retryError).substring(0, 200),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (autoYes) {
|
||||
taskFailed = true;
|
||||
break;
|
||||
} else {
|
||||
const answer = AskUserQuestion({
|
||||
questions: [{
|
||||
question: `${skillName} failed after CLI diagnosis + retry: ${String(retryError).substring(0, 100)}. How to proceed?`,
|
||||
header: 'Error',
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: 'Skip task', description: 'Mark task as failed, continue to next' },
|
||||
{ label: 'Abort', description: 'Stop entire run' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
if (answer.answers?.Error === 'Abort') {
|
||||
task.status = 'failed';
|
||||
task.execution.error = String(retryError).substring(0, 200);
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
session.failed.push(task.id);
|
||||
session.status = 'failed';
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
return;
|
||||
}
|
||||
taskFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Checkpoint (per task) — inline
|
||||
if (taskFailed) {
|
||||
task.status = 'failed';
|
||||
task.execution.error = 'Skill chain failed after retry';
|
||||
task.execution.completed_at = new Date().toISOString();
|
||||
session.failed.push(task.id);
|
||||
} else {
|
||||
// Git commit checkpoint
|
||||
const commitMsg = `feat(idaw): ${task.title} [${task.id}]`;
|
||||
const diffCheck = Bash('git diff --stat HEAD 2>/dev/null || echo ""');
|
||||
const untrackedCheck = Bash('git ls-files --others --exclude-standard 2>/dev/null || echo ""');
|
||||
|
||||
if (diffCheck?.trim() || untrackedCheck?.trim()) {
|
||||
Bash('git add -A');
|
||||
const commitResult = Bash(`git commit -m "$(cat <<'EOF'\n${commitMsg}\nEOF\n)"`);
|
||||
const commitHash = Bash('git rev-parse --short HEAD 2>/dev/null')?.trim();
|
||||
task.execution.git_commit = commitHash;
|
||||
} else {
|
||||
task.execution.git_commit = 'no-commit';
|
||||
}
|
||||
|
||||
task.status = 'completed';
|
||||
task.execution.completed_at = new Date().toISOString();
|
||||
session.completed.push(task.id);
|
||||
}
|
||||
|
||||
// Write task + session state
|
||||
task.updated_at = new Date().toISOString();
|
||||
Write(`.workflow/.idaw/tasks/${task.id}.json`, JSON.stringify(task, null, 2));
|
||||
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Append to progress.md
|
||||
const duration = task.execution.started_at && task.execution.completed_at
|
||||
? formatDuration(new Date(task.execution.completed_at) - new Date(task.execution.started_at))
|
||||
: 'unknown';
|
||||
|
||||
const progressEntry = `## ${task.id} — ${task.title}\n` +
|
||||
`- Status: ${task.status}\n` +
|
||||
`- Type: ${task.task_type}\n` +
|
||||
`- Chain: ${chain.join(' → ')}\n` +
|
||||
`- Commit: ${task.execution.git_commit || '-'}\n` +
|
||||
`- Duration: ${duration}\n\n`;
|
||||
|
||||
const currentProgress = Read(`${sessionDir}/progress.md`);
|
||||
Write(`${sessionDir}/progress.md`, currentProgress + progressEntry);
|
||||
|
||||
// Update TodoWrite
|
||||
if (taskIdx + 1 < tasks.length) {
|
||||
TodoWrite({
|
||||
todos: tasks.map((t, i) => ({
|
||||
content: `IDAW:[${i + 1}/${tasks.length}] ${t.title}`,
|
||||
status: i < taskIdx + 1 ? 'completed' : (i === taskIdx + 1 ? 'in_progress' : 'pending'),
|
||||
activeForm: `Executing ${t.title}`
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 6: Report
|
||||
|
||||
```javascript
|
||||
session.status = session.failed.length > 0 && session.completed.length === 0 ? 'failed' : 'completed';
|
||||
session.current_task = null;
|
||||
session.updated_at = new Date().toISOString();
|
||||
Write(`${sessionDir}/session.json`, JSON.stringify(session, null, 2));
|
||||
|
||||
// Final progress summary
|
||||
const summary = `\n---\n## Summary\n` +
|
||||
`- Completed: ${session.completed.length}\n` +
|
||||
`- Failed: ${session.failed.length}\n` +
|
||||
`- Skipped: ${session.skipped.length}\n` +
|
||||
`- Total: ${tasks.length}\n`;
|
||||
|
||||
const finalProgress = Read(`${sessionDir}/progress.md`);
|
||||
Write(`${sessionDir}/progress.md`, finalProgress + summary);
|
||||
|
||||
// Display report
|
||||
console.log('\n=== IDAW Run Complete ===');
|
||||
console.log(`Session: ${sessionId}`);
|
||||
console.log(`Completed: ${session.completed.length}/${tasks.length}`);
|
||||
if (session.failed.length > 0) console.log(`Failed: ${session.failed.join(', ')}`);
|
||||
if (session.skipped.length > 0) console.log(`Skipped: ${session.skipped.join(', ')}`);
|
||||
|
||||
// List git commits
|
||||
for (const taskId of session.completed) {
|
||||
const t = JSON.parse(Read(`.workflow/.idaw/tasks/${taskId}.json`));
|
||||
if (t.execution.git_commit && t.execution.git_commit !== 'no-commit') {
|
||||
console.log(` ${t.execution.git_commit} ${t.title}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### assembleSkillArgs
|
||||
|
||||
```javascript
|
||||
function assembleSkillArgs(skillName, task, previousResult, autoYes, isFirst) {
|
||||
let args = '';
|
||||
|
||||
if (isFirst) {
|
||||
// First skill: pass task goal — sanitize for shell safety
|
||||
const goal = `${task.title}\n${task.description}`
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/`/g, '\\`');
|
||||
args = `"${goal}"`;
|
||||
|
||||
// bugfix-hotfix: add --hotfix
|
||||
if (task.task_type === 'bugfix-hotfix') {
|
||||
args += ' --hotfix';
|
||||
}
|
||||
} else if (previousResult?.session_id) {
|
||||
// Subsequent skills: chain session
|
||||
args = `--session="${previousResult.session_id}"`;
|
||||
}
|
||||
|
||||
// Propagate -y
|
||||
if (autoYes && !args.includes('-y') && !args.includes('--yes')) {
|
||||
args = args ? `${args} -y` : '-y';
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
```
|
||||
|
||||
### formatDuration
|
||||
|
||||
```javascript
|
||||
function formatDuration(ms) {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (minutes > 0) return `${minutes}m ${remainingSeconds}s`;
|
||||
return `${seconds}s`;
|
||||
}
|
||||
```
|
||||
|
||||
## CLI-Assisted Analysis
|
||||
|
||||
IDAW integrates `ccw cli` (Gemini) for intelligent analysis at two key points:
|
||||
|
||||
### Pre-Task Context Analysis
|
||||
|
||||
For `bugfix`, `bugfix-hotfix`, and `feature-complex` tasks, IDAW automatically invokes CLI analysis **before** executing the skill chain to gather codebase context:
|
||||
|
||||
```
|
||||
Task starts → CLI pre-analysis (gemini) → Context gathered → Skill chain executes
|
||||
```
|
||||
|
||||
- Identifies dependencies and risk areas
|
||||
- Notes existing patterns to follow
|
||||
- Results stored in `task.execution.skill_results` as `cli-pre-analysis`
|
||||
|
||||
### Error Recovery with CLI Diagnosis
|
||||
|
||||
When a skill fails, instead of blind retry, IDAW uses CLI-assisted diagnosis:
|
||||
|
||||
```
|
||||
Skill fails → CLI diagnosis (gemini, analysis-diagnose-bug-root-cause)
|
||||
→ Root cause identified → Retry with diagnosis context
|
||||
→ Still fails → Skip (autoYes) or Ask user (interactive)
|
||||
```
|
||||
|
||||
- Uses `--rule analysis-diagnose-bug-root-cause` template
|
||||
- Diagnosis results stored in `task.execution.skill_results` as `cli-diagnosis:{skill}`
|
||||
- Follows CLAUDE.md auto-invoke trigger pattern: "self-repair fails → invoke CLI analysis"
|
||||
|
||||
### Execution Flow (with CLI analysis)
|
||||
|
||||
```
|
||||
Phase 4 Main Loop (per task):
|
||||
├─ [bugfix/complex only] CLI pre-analysis → context summary
|
||||
├─ Skill 1: execute
|
||||
│ ├─ Success → next skill
|
||||
│ └─ Failure → CLI diagnosis → retry → success/fail
|
||||
├─ Skill 2: execute ...
|
||||
└─ Phase 5: git checkpoint
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Execute all pending tasks
|
||||
/idaw:run -y
|
||||
|
||||
# Execute specific tasks
|
||||
/idaw:run --task IDAW-001,IDAW-003
|
||||
|
||||
# Dry run (show plan without executing)
|
||||
/idaw:run --dry-run
|
||||
|
||||
# Interactive mode (confirm at each step)
|
||||
/idaw:run
|
||||
```
|
||||
182
.claude/commands/idaw/status.md
Normal file
182
.claude/commands/idaw/status.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: status
|
||||
description: View IDAW task and session progress
|
||||
argument-hint: "[session-id]"
|
||||
allowed-tools: Read(*), Glob(*), Bash(*)
|
||||
---
|
||||
|
||||
# IDAW Status Command (/idaw:status)
|
||||
|
||||
## Overview
|
||||
|
||||
Read-only command to view IDAW task queue and execution session progress.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Phase 1: Determine View Mode
|
||||
|
||||
```javascript
|
||||
const sessionId = $ARGUMENTS?.trim();
|
||||
|
||||
if (sessionId) {
|
||||
// Specific session view
|
||||
showSession(sessionId);
|
||||
} else {
|
||||
// Overview: pending tasks + latest session
|
||||
showOverview();
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2: Show Overview
|
||||
|
||||
```javascript
|
||||
function showOverview() {
|
||||
// 1. Load all tasks
|
||||
const taskFiles = Glob('.workflow/.idaw/tasks/IDAW-*.json') || [];
|
||||
|
||||
if (taskFiles.length === 0) {
|
||||
console.log('No IDAW tasks found. Use /idaw:add to create tasks.');
|
||||
return;
|
||||
}
|
||||
|
||||
const tasks = taskFiles.map(f => JSON.parse(Read(f)));
|
||||
|
||||
// 2. Group by status
|
||||
const byStatus = {
|
||||
pending: tasks.filter(t => t.status === 'pending'),
|
||||
in_progress: tasks.filter(t => t.status === 'in_progress'),
|
||||
completed: tasks.filter(t => t.status === 'completed'),
|
||||
failed: tasks.filter(t => t.status === 'failed'),
|
||||
skipped: tasks.filter(t => t.status === 'skipped')
|
||||
};
|
||||
|
||||
// 3. Display task summary table
|
||||
console.log('# IDAW Tasks\n');
|
||||
console.log('| ID | Title | Type | Priority | Status |');
|
||||
console.log('|----|-------|------|----------|--------|');
|
||||
|
||||
// Sort: priority ASC, then ID ASC
|
||||
const sorted = [...tasks].sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
|
||||
for (const t of sorted) {
|
||||
const type = t.task_type || '(infer)';
|
||||
console.log(`| ${t.id} | ${t.title.substring(0, 40)} | ${type} | ${t.priority} | ${t.status} |`);
|
||||
}
|
||||
|
||||
console.log(`\nTotal: ${tasks.length} | Pending: ${byStatus.pending.length} | Completed: ${byStatus.completed.length} | Failed: ${byStatus.failed.length}`);
|
||||
|
||||
// 4. Show latest session (if any)
|
||||
const sessionDirs = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || [];
|
||||
if (sessionDirs.length > 0) {
|
||||
// Sort by modification time (newest first) — Glob returns sorted by mtime
|
||||
const latestSessionFile = sessionDirs[0];
|
||||
const session = JSON.parse(Read(latestSessionFile));
|
||||
console.log(`\n## Latest Session: ${session.session_id}`);
|
||||
console.log(`Status: ${session.status} | Tasks: ${session.tasks?.length || 0}`);
|
||||
console.log(`Completed: ${session.completed?.length || 0} | Failed: ${session.failed?.length || 0} | Skipped: ${session.skipped?.length || 0}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Show Specific Session
|
||||
|
||||
```javascript
|
||||
function showSession(sessionId) {
|
||||
const sessionFile = `.workflow/.idaw/sessions/${sessionId}/session.json`;
|
||||
const progressFile = `.workflow/.idaw/sessions/${sessionId}/progress.md`;
|
||||
|
||||
// Try reading session
|
||||
try {
|
||||
const session = JSON.parse(Read(sessionFile));
|
||||
|
||||
console.log(`# IDAW Session: ${session.session_id}\n`);
|
||||
console.log(`Status: ${session.status}`);
|
||||
console.log(`Created: ${session.created_at}`);
|
||||
console.log(`Updated: ${session.updated_at}`);
|
||||
console.log(`Current Task: ${session.current_task || 'none'}\n`);
|
||||
|
||||
// Task detail table
|
||||
console.log('| ID | Title | Status | Commit |');
|
||||
console.log('|----|-------|--------|--------|');
|
||||
|
||||
for (const taskId of session.tasks) {
|
||||
const taskFile = `.workflow/.idaw/tasks/${taskId}.json`;
|
||||
try {
|
||||
const task = JSON.parse(Read(taskFile));
|
||||
const commit = task.execution?.git_commit?.substring(0, 7) || '-';
|
||||
console.log(`| ${task.id} | ${task.title.substring(0, 40)} | ${task.status} | ${commit} |`);
|
||||
} catch {
|
||||
console.log(`| ${taskId} | (file not found) | unknown | - |`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nCompleted: ${session.completed?.length || 0} | Failed: ${session.failed?.length || 0} | Skipped: ${session.skipped?.length || 0}`);
|
||||
|
||||
// Show progress.md if exists
|
||||
try {
|
||||
const progress = Read(progressFile);
|
||||
console.log('\n---\n');
|
||||
console.log(progress);
|
||||
} catch {
|
||||
// No progress file yet
|
||||
}
|
||||
|
||||
} catch {
|
||||
// Session not found — try listing all sessions
|
||||
console.log(`Session "${sessionId}" not found.\n`);
|
||||
listSessions();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: List All Sessions
|
||||
|
||||
```javascript
|
||||
function listSessions() {
|
||||
const sessionFiles = Glob('.workflow/.idaw/sessions/IDA-*/session.json') || [];
|
||||
|
||||
if (sessionFiles.length === 0) {
|
||||
console.log('No IDAW sessions found. Use /idaw:run to start execution.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('# IDAW Sessions\n');
|
||||
console.log('| Session ID | Status | Tasks | Completed | Failed |');
|
||||
console.log('|------------|--------|-------|-----------|--------|');
|
||||
|
||||
for (const f of sessionFiles) {
|
||||
try {
|
||||
const session = JSON.parse(Read(f));
|
||||
console.log(`| ${session.session_id} | ${session.status} | ${session.tasks?.length || 0} | ${session.completed?.length || 0} | ${session.failed?.length || 0} |`);
|
||||
} catch {
|
||||
// Skip malformed
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\nUse /idaw:status <session-id> for details.');
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Show overview (pending tasks + latest session)
|
||||
/idaw:status
|
||||
|
||||
# Show specific session details
|
||||
/idaw:status IDA-auth-fix-20260301
|
||||
|
||||
# Output example:
|
||||
# IDAW Tasks
|
||||
#
|
||||
# | ID | Title | Type | Priority | Status |
|
||||
# |----------|------------------------------------|--------|----------|-----------|
|
||||
# | IDAW-001 | Fix auth token refresh | bugfix | 1 | completed |
|
||||
# | IDAW-002 | Add rate limiting | feature| 2 | pending |
|
||||
# | IDAW-003 | Refactor payment module | refact | 3 | pending |
|
||||
#
|
||||
# Total: 3 | Pending: 2 | Completed: 1 | Failed: 0
|
||||
```
|
||||
@@ -496,7 +496,7 @@ if (fileExists(projectPath)) {
|
||||
}
|
||||
|
||||
// Update specs/*.md: remove learnings referencing deleted sessions
|
||||
const guidelinesPath = '.workflow/specs/*.md'
|
||||
const guidelinesPath = '.ccw/specs/*.md'
|
||||
if (fileExists(guidelinesPath)) {
|
||||
const guidelines = JSON.parse(Read(guidelinesPath))
|
||||
const deletedSessionIds = results.deleted
|
||||
|
||||
@@ -208,7 +208,7 @@ Task(
|
||||
### Project Context (MANDATORY)
|
||||
Read and incorporate:
|
||||
- \`.workflow/project-tech.json\` (if exists): Technology stack, architecture
|
||||
- \`.workflow/specs/*.md\` (if exists): Constraints, conventions -- apply as HARD CONSTRAINTS on sub-domain splitting and plan structure
|
||||
- \`.ccw/specs/*.md\` (if exists): Constraints, conventions -- apply as HARD CONSTRAINTS on sub-domain splitting and plan structure
|
||||
|
||||
### Input Requirements
|
||||
${taskDescription}
|
||||
@@ -357,7 +357,7 @@ subDomains.map(sub =>
|
||||
### Project Context (MANDATORY)
|
||||
Read and incorporate:
|
||||
- \`.workflow/project-tech.json\` (if exists): Technology stack, architecture
|
||||
- \`.workflow/specs/*.md\` (if exists): Constraints, conventions -- apply as HARD CONSTRAINTS
|
||||
- \`.ccw/specs/*.md\` (if exists): Constraints, conventions -- apply as HARD CONSTRAINTS
|
||||
|
||||
## Dual Output Tasks
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ examples:
|
||||
|
||||
## Overview
|
||||
|
||||
Interactive multi-round wizard that analyzes the current project (via `project-tech.json`) and asks targeted questions to populate `.workflow/specs/*.md` with coding conventions, constraints, and quality rules.
|
||||
Interactive multi-round wizard that analyzes the current project (via `project-tech.json`) and asks targeted questions to populate `.ccw/specs/*.md` with coding conventions, constraints, and quality rules.
|
||||
|
||||
**Design Principle**: Questions are dynamically generated based on the project's tech stack, architecture, and patterns — not generic boilerplate.
|
||||
|
||||
@@ -55,7 +55,7 @@ Step 5: Display Summary
|
||||
|
||||
```bash
|
||||
bash(test -f .workflow/project-tech.json && echo "TECH_EXISTS" || echo "TECH_NOT_FOUND")
|
||||
bash(test -f .workflow/specs/coding-conventions.md && echo "SPECS_EXISTS" || echo "SPECS_NOT_FOUND")
|
||||
bash(test -f .ccw/specs/coding-conventions.md && echo "SPECS_EXISTS" || echo "SPECS_NOT_FOUND")
|
||||
```
|
||||
|
||||
**If TECH_NOT_FOUND**: Exit with message
|
||||
@@ -332,9 +332,16 @@ For each category of collected answers, append rules to the corresponding spec M
|
||||
|
||||
```javascript
|
||||
// Helper: append rules to a spec MD file with category support
|
||||
// Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)
|
||||
function appendRulesToSpecFile(filePath, rules, defaultCategory = 'general') {
|
||||
if (rules.length === 0) return
|
||||
|
||||
// Ensure .ccw/specs/ directory exists
|
||||
const specDir = path.dirname(filePath)
|
||||
if (!fs.existsSync(specDir)) {
|
||||
fs.mkdirSync(specDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if (!file_exists(filePath)) {
|
||||
// Create file with frontmatter including category
|
||||
@@ -360,19 +367,19 @@ keywords: [${defaultCategory}, ${filePath.includes('conventions') ? 'convention'
|
||||
Write(filePath, newContent)
|
||||
}
|
||||
|
||||
// Write conventions (general category)
|
||||
appendRulesToSpecFile('.workflow/specs/coding-conventions.md',
|
||||
// Write conventions (general category) - use .ccw/specs/ (same as frontend/backend)
|
||||
appendRulesToSpecFile('.ccw/specs/coding-conventions.md',
|
||||
[...newCodingStyle, ...newNamingPatterns, ...newFileStructure, ...newDocumentation],
|
||||
'general')
|
||||
|
||||
// Write constraints (planning category)
|
||||
appendRulesToSpecFile('.workflow/specs/architecture-constraints.md',
|
||||
appendRulesToSpecFile('.ccw/specs/architecture-constraints.md',
|
||||
[...newArchitecture, ...newTechStack, ...newPerformance, ...newSecurity],
|
||||
'planning')
|
||||
|
||||
// Write quality rules (execution category)
|
||||
if (newQualityRules.length > 0) {
|
||||
const qualityPath = '.workflow/specs/quality-rules.md'
|
||||
const qualityPath = '.ccw/specs/quality-rules.md'
|
||||
if (!file_exists(qualityPath)) {
|
||||
Write(qualityPath, `---
|
||||
title: Quality Rules
|
||||
|
||||
@@ -54,7 +54,7 @@ Step 1: Gather Requirements (Interactive)
|
||||
└─ Ask content (rule text)
|
||||
|
||||
Step 2: Determine Target File
|
||||
├─ specs dimension → .workflow/specs/coding-conventions.md or architecture-constraints.md
|
||||
├─ specs dimension → .ccw/specs/coding-conventions.md or architecture-constraints.md
|
||||
└─ personal dimension → ~/.ccw/specs/personal/ or .ccw/specs/personal/
|
||||
|
||||
Step 3: Write Spec
|
||||
@@ -109,7 +109,7 @@ if (!dimension) {
|
||||
options: [
|
||||
{
|
||||
label: "Project Spec",
|
||||
description: "Coding conventions, constraints, quality rules for this project (stored in .workflow/specs/)"
|
||||
description: "Coding conventions, constraints, quality rules for this project (stored in .ccw/specs/)"
|
||||
},
|
||||
{
|
||||
label: "Personal Spec",
|
||||
@@ -234,19 +234,19 @@ let targetFile: string
|
||||
let targetDir: string
|
||||
|
||||
if (dimension === 'specs') {
|
||||
// Project specs
|
||||
targetDir = '.workflow/specs'
|
||||
// Project specs - use .ccw/specs/ (same as frontend/backend spec-index-builder)
|
||||
targetDir = '.ccw/specs'
|
||||
if (isConstraint) {
|
||||
targetFile = path.join(targetDir, 'architecture-constraints.md')
|
||||
} else {
|
||||
targetFile = path.join(targetDir, 'coding-conventions.md')
|
||||
}
|
||||
} else {
|
||||
// Personal specs
|
||||
// Personal specs - use .ccw/personal/ (same as backend spec-index-builder)
|
||||
if (scope === 'global') {
|
||||
targetDir = path.join(os.homedir(), '.ccw', 'specs', 'personal')
|
||||
targetDir = path.join(os.homedir(), '.ccw', 'personal')
|
||||
} else {
|
||||
targetDir = path.join('.ccw', 'specs', 'personal')
|
||||
targetDir = path.join('.ccw', 'personal')
|
||||
}
|
||||
|
||||
// Create category-based filename
|
||||
@@ -333,7 +333,7 @@ Use 'ccw spec load --category ${category}' to load specs by category
|
||||
|
||||
### Project Specs (dimension: specs)
|
||||
```
|
||||
.workflow/specs/
|
||||
.ccw/specs/
|
||||
├── coding-conventions.md ← conventions, learnings
|
||||
├── architecture-constraints.md ← constraints
|
||||
└── quality-rules.md ← quality rules
|
||||
@@ -341,14 +341,14 @@ Use 'ccw spec load --category ${category}' to load specs by category
|
||||
|
||||
### Personal Specs (dimension: personal)
|
||||
```
|
||||
# Global (~/.ccw/specs/personal/)
|
||||
~/.ccw/specs/personal/
|
||||
# Global (~/.ccw/personal/)
|
||||
~/.ccw/personal/
|
||||
├── conventions.md ← personal conventions (all projects)
|
||||
├── constraints.md ← personal constraints (all projects)
|
||||
└── learnings.md ← personal learnings (all projects)
|
||||
|
||||
# Project-local (.ccw/specs/personal/)
|
||||
.ccw/specs/personal/
|
||||
# Project-local (.ccw/personal/)
|
||||
.ccw/personal/
|
||||
├── conventions.md ← personal conventions (this project only)
|
||||
├── constraints.md ← personal constraints (this project only)
|
||||
└── learnings.md ← personal learnings (this project only)
|
||||
|
||||
@@ -11,7 +11,7 @@ examples:
|
||||
# Workflow Init Command (/workflow:init)
|
||||
|
||||
## Overview
|
||||
Initialize `.workflow/project-tech.json` and `.workflow/specs/*.md` with comprehensive project understanding by delegating analysis to **cli-explore-agent**.
|
||||
Initialize `.workflow/project-tech.json` and `.ccw/specs/*.md` with comprehensive project understanding by delegating analysis to **cli-explore-agent**.
|
||||
|
||||
**Dual File System**:
|
||||
- `project-tech.json`: Auto-generated technical analysis (stack, architecture, components)
|
||||
@@ -58,7 +58,7 @@ Analysis Flow:
|
||||
|
||||
Output:
|
||||
├─ .workflow/project-tech.json (+ .backup if regenerate)
|
||||
└─ .workflow/specs/*.md (scaffold or configured, unless --skip-specs)
|
||||
└─ .ccw/specs/*.md (scaffold or configured, unless --skip-specs)
|
||||
```
|
||||
|
||||
## Implementation
|
||||
@@ -75,14 +75,14 @@ const skipSpecs = $ARGUMENTS.includes('--skip-specs')
|
||||
|
||||
```bash
|
||||
bash(test -f .workflow/project-tech.json && echo "TECH_EXISTS" || echo "TECH_NOT_FOUND")
|
||||
bash(test -f .workflow/specs/coding-conventions.md && echo "SPECS_EXISTS" || echo "SPECS_NOT_FOUND")
|
||||
bash(test -f .ccw/specs/coding-conventions.md && echo "SPECS_EXISTS" || echo "SPECS_NOT_FOUND")
|
||||
```
|
||||
|
||||
**If BOTH_EXIST and no --regenerate**: Exit early
|
||||
```
|
||||
Project already initialized:
|
||||
- Tech analysis: .workflow/project-tech.json
|
||||
- Guidelines: .workflow/specs/*.md
|
||||
- Guidelines: .ccw/specs/*.md
|
||||
|
||||
Use /workflow:init --regenerate to rebuild tech analysis
|
||||
Use /workflow:session:solidify to add guidelines
|
||||
@@ -171,7 +171,7 @@ Project root: ${projectRoot}
|
||||
// Skip spec initialization if --skip-specs flag is provided
|
||||
if (!skipSpecs) {
|
||||
// Initialize spec system if not already initialized
|
||||
const specsCheck = Bash('test -f .workflow/specs/coding-conventions.md && echo EXISTS || echo NOT_FOUND')
|
||||
const specsCheck = Bash('test -f .ccw/specs/coding-conventions.md && echo EXISTS || echo NOT_FOUND')
|
||||
if (specsCheck.includes('NOT_FOUND')) {
|
||||
console.log('Initializing spec system...')
|
||||
Bash('ccw spec init')
|
||||
@@ -186,7 +186,7 @@ if (!skipSpecs) {
|
||||
|
||||
```javascript
|
||||
const projectTech = JSON.parse(Read('.workflow/project-tech.json'));
|
||||
const specsInitialized = !skipSpecs && file_exists('.workflow/specs/coding-conventions.md');
|
||||
const specsInitialized = !skipSpecs && file_exists('.ccw/specs/coding-conventions.md');
|
||||
|
||||
console.log(`
|
||||
Project initialized successfully
|
||||
@@ -206,7 +206,7 @@ Components: ${projectTech.overview.key_components.length} core modules
|
||||
---
|
||||
Files created:
|
||||
- Tech analysis: .workflow/project-tech.json
|
||||
${!skipSpecs ? `- Specs: .workflow/specs/ ${specsInitialized ? '(initialized)' : ''}` : '- Specs: (skipped via --skip-specs)'}
|
||||
${!skipSpecs ? `- Specs: .ccw/specs/ ${specsInitialized ? '(initialized)' : ''}` : '- Specs: (skipped via --skip-specs)'}
|
||||
${regenerate ? '- Backup: .workflow/project-tech.json.backup' : ''}
|
||||
`);
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ When `--yes` or `-y`: Auto-categorize and add guideline without confirmation.
|
||||
|
||||
## Overview
|
||||
|
||||
Crystallizes ephemeral session context (insights, decisions, constraints) into permanent project guidelines stored in `.workflow/specs/*.md`. This ensures valuable learnings persist across sessions and inform future planning.
|
||||
Crystallizes ephemeral session context (insights, decisions, constraints) into permanent project guidelines stored in `.ccw/specs/*.md`. This ensures valuable learnings persist across sessions and inform future planning.
|
||||
|
||||
## Use Cases
|
||||
|
||||
@@ -112,8 +112,10 @@ ELSE (convention/constraint/learning):
|
||||
|
||||
### Step 1: Ensure Guidelines File Exists
|
||||
|
||||
**Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)**
|
||||
|
||||
```bash
|
||||
bash(test -f .workflow/specs/coding-conventions.md && echo "EXISTS" || echo "NOT_FOUND")
|
||||
bash(test -f .ccw/specs/coding-conventions.md && echo "EXISTS" || echo "NOT_FOUND")
|
||||
```
|
||||
|
||||
**If NOT_FOUND**, initialize spec system:
|
||||
@@ -187,9 +189,10 @@ function buildEntry(rule, type, category, sessionId) {
|
||||
|
||||
```javascript
|
||||
// Map type+category to target spec file
|
||||
// Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)
|
||||
const specFileMap = {
|
||||
convention: '.workflow/specs/coding-conventions.md',
|
||||
constraint: '.workflow/specs/architecture-constraints.md'
|
||||
convention: '.ccw/specs/coding-conventions.md',
|
||||
constraint: '.ccw/specs/architecture-constraints.md'
|
||||
}
|
||||
|
||||
if (type === 'convention' || type === 'constraint') {
|
||||
@@ -204,7 +207,8 @@ if (type === 'convention' || type === 'constraint') {
|
||||
}
|
||||
} else if (type === 'learning') {
|
||||
// Learnings go to coding-conventions.md as a special section
|
||||
const targetFile = '.workflow/specs/coding-conventions.md'
|
||||
// Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)
|
||||
const targetFile = '.ccw/specs/coding-conventions.md'
|
||||
const existing = Read(targetFile)
|
||||
const entry = buildEntry(rule, type, category, sessionId)
|
||||
const learningText = `- [learning/${category}] ${entry.insight} (${entry.date})`
|
||||
@@ -228,7 +232,7 @@ Type: ${type}
|
||||
Category: ${category}
|
||||
Rule: "${rule}"
|
||||
|
||||
Location: .workflow/specs/*.md -> ${type}s.${category}
|
||||
Location: .ccw/specs/*.md -> ${type}s.${category}
|
||||
|
||||
Total ${type}s in ${category}: ${count}
|
||||
```
|
||||
@@ -373,13 +377,9 @@ AskUserQuestion({
|
||||
/workflow:session:solidify "Use async/await instead of callbacks" --type convention --category coding_style
|
||||
```
|
||||
|
||||
Result in `specs/*.md`:
|
||||
```json
|
||||
{
|
||||
"conventions": {
|
||||
"coding_style": ["Use async/await instead of callbacks"]
|
||||
}
|
||||
}
|
||||
Result in `.ccw/specs/coding-conventions.md`:
|
||||
```markdown
|
||||
- [coding_style] Use async/await instead of callbacks
|
||||
```
|
||||
|
||||
### Add an Architectural Constraint
|
||||
@@ -387,13 +387,9 @@ Result in `specs/*.md`:
|
||||
/workflow:session:solidify "No direct DB access from controllers" --type constraint --category architecture
|
||||
```
|
||||
|
||||
Result:
|
||||
```json
|
||||
{
|
||||
"constraints": {
|
||||
"architecture": ["No direct DB access from controllers"]
|
||||
}
|
||||
}
|
||||
Result in `.ccw/specs/architecture-constraints.md`:
|
||||
```markdown
|
||||
- [architecture] No direct DB access from controllers
|
||||
```
|
||||
|
||||
### Capture a Session Learning
|
||||
@@ -401,18 +397,9 @@ Result:
|
||||
/workflow:session:solidify "Cache invalidation requires event sourcing for consistency" --type learning
|
||||
```
|
||||
|
||||
Result:
|
||||
```json
|
||||
{
|
||||
"learnings": [
|
||||
{
|
||||
"date": "2024-12-28",
|
||||
"session_id": "WFS-auth-feature",
|
||||
"insight": "Cache invalidation requires event sourcing for consistency",
|
||||
"category": "architecture"
|
||||
}
|
||||
]
|
||||
}
|
||||
Result in `.ccw/specs/coding-conventions.md`:
|
||||
```markdown
|
||||
- [learning/architecture] Cache invalidation requires event sourcing for consistency (2024-12-28)
|
||||
```
|
||||
|
||||
### Compress Recent Memories
|
||||
|
||||
@@ -44,7 +44,7 @@ ERROR: Invalid session type. Valid types: workflow, review, tdd, test, docs
|
||||
```bash
|
||||
# Check if project state exists (both files required)
|
||||
bash(test -f .workflow/project-tech.json && echo "TECH_EXISTS" || echo "TECH_NOT_FOUND")
|
||||
bash(test -f .workflow/specs/*.md && echo "GUIDELINES_EXISTS" || echo "GUIDELINES_NOT_FOUND")
|
||||
bash(test -f .ccw/specs/*.md && echo "GUIDELINES_EXISTS" || echo "GUIDELINES_NOT_FOUND")
|
||||
```
|
||||
|
||||
**If either NOT_FOUND**, delegate to `/workflow:init`:
|
||||
@@ -60,7 +60,7 @@ Skill(skill="workflow:init");
|
||||
- If BOTH_EXIST: `PROJECT_STATE: initialized`
|
||||
- If NOT_FOUND: Calls `/workflow:init` → creates:
|
||||
- `.workflow/project-tech.json` with full technical analysis
|
||||
- `.workflow/specs/*.md` with empty scaffold
|
||||
- `.ccw/specs/*.md` with empty scaffold
|
||||
|
||||
**Note**: `/workflow:init` uses cli-explore-agent to build comprehensive project understanding (technology stack, architecture, key components). This step runs once per project. Subsequent executions skip initialization.
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ Tech [${detectCategory(summary)}]:
|
||||
${techEntry.title}
|
||||
|
||||
Target files:
|
||||
.workflow/specs/*.md
|
||||
.ccw/specs/*.md
|
||||
.workflow/project-tech.json
|
||||
`)
|
||||
|
||||
@@ -138,12 +138,13 @@ if (!autoYes) {
|
||||
|
||||
```javascript
|
||||
// ── Update specs/*.md ──
|
||||
// Uses .ccw/specs/ directory (same as frontend/backend spec-index-builder)
|
||||
if (guidelineUpdates.length > 0) {
|
||||
// Map guideline types to spec files
|
||||
const specFileMap = {
|
||||
convention: '.workflow/specs/coding-conventions.md',
|
||||
constraint: '.workflow/specs/architecture-constraints.md',
|
||||
learning: '.workflow/specs/coding-conventions.md' // learnings appended to conventions
|
||||
convention: '.ccw/specs/coding-conventions.md',
|
||||
constraint: '.ccw/specs/architecture-constraints.md',
|
||||
learning: '.ccw/specs/coding-conventions.md' // learnings appended to conventions
|
||||
}
|
||||
|
||||
for (const g of guidelineUpdates) {
|
||||
|
||||
@@ -477,8 +477,8 @@ ${recommendations.map(r => \`- ${r}\`).join('\\n')}
|
||||
const projectTech = file_exists('.workflow/project-tech.json')
|
||||
? JSON.parse(Read('.workflow/project-tech.json')) : null
|
||||
// Read specs/*.md (if exists)
|
||||
const projectGuidelines = file_exists('.workflow/specs/*.md')
|
||||
? JSON.parse(Read('.workflow/specs/*.md')) : null
|
||||
const projectGuidelines = file_exists('.ccw/specs/*.md')
|
||||
? JSON.parse(Read('.ccw/specs/*.md')) : null
|
||||
```
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -104,6 +104,17 @@ const sessionFolder = `.workflow/.lite-plan/${sessionId}`
|
||||
bash(`mkdir -p ${sessionFolder} && test -d ${sessionFolder} && echo "SUCCESS: ${sessionFolder}" || echo "FAILED: ${sessionFolder}"`)
|
||||
```
|
||||
|
||||
**TodoWrite (Phase 1 start)**:
|
||||
```javascript
|
||||
TodoWrite({ todos: [
|
||||
{ content: "Phase 1: Exploration", status: "in_progress", activeForm: "Exploring codebase" },
|
||||
{ content: "Phase 2: Clarification", status: "pending", activeForm: "Collecting clarifications" },
|
||||
{ content: "Phase 3: Planning", status: "pending", activeForm: "Generating plan" },
|
||||
{ content: "Phase 4: Confirmation", status: "pending", activeForm: "Awaiting confirmation" },
|
||||
{ content: "Phase 5: Execution", status: "pending", activeForm: "Executing tasks" }
|
||||
]})
|
||||
```
|
||||
|
||||
**Exploration Decision Logic**:
|
||||
```javascript
|
||||
// Check if task description already contains prior analysis context (from analyze-with-file)
|
||||
@@ -307,6 +318,17 @@ Angles explored: ${explorationManifest.explorations.map(e => e.angle).join(', ')
|
||||
`)
|
||||
```
|
||||
|
||||
**TodoWrite (Phase 1 complete)**:
|
||||
```javascript
|
||||
TodoWrite({ todos: [
|
||||
{ content: "Phase 1: Exploration", status: "completed", activeForm: "Exploring codebase" },
|
||||
{ content: "Phase 2: Clarification", status: "in_progress", activeForm: "Collecting clarifications" },
|
||||
{ content: "Phase 3: Planning", status: "pending", activeForm: "Generating plan" },
|
||||
{ content: "Phase 4: Confirmation", status: "pending", activeForm: "Awaiting confirmation" },
|
||||
{ content: "Phase 5: Execution", status: "pending", activeForm: "Executing tasks" }
|
||||
]})
|
||||
```
|
||||
|
||||
**Output**:
|
||||
- `${sessionFolder}/exploration-{angle1}.json`
|
||||
- `${sessionFolder}/exploration-{angle2}.json`
|
||||
@@ -560,6 +582,17 @@ Note: Use files[].change (not modification_points), convergence.criteria (not ac
|
||||
|
||||
**Output**: `${sessionFolder}/plan.json`
|
||||
|
||||
**TodoWrite (Phase 3 complete)**:
|
||||
```javascript
|
||||
TodoWrite({ todos: [
|
||||
{ content: "Phase 1: Exploration", status: "completed", activeForm: "Exploring codebase" },
|
||||
{ content: "Phase 2: Clarification", status: "completed", activeForm: "Collecting clarifications" },
|
||||
{ content: "Phase 3: Planning", status: "completed", activeForm: "Generating plan" },
|
||||
{ content: "Phase 4: Confirmation", status: "in_progress", activeForm: "Awaiting confirmation" },
|
||||
{ content: "Phase 5: Execution", status: "pending", activeForm: "Executing tasks" }
|
||||
]})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Task Confirmation & Execution Selection
|
||||
@@ -649,6 +682,19 @@ if (autoYes) {
|
||||
}
|
||||
```
|
||||
|
||||
**TodoWrite (Phase 4 confirmed)**:
|
||||
```javascript
|
||||
const executionLabel = userSelection.execution_method
|
||||
|
||||
TodoWrite({ todos: [
|
||||
{ content: "Phase 1: Exploration", status: "completed", activeForm: "Exploring codebase" },
|
||||
{ content: "Phase 2: Clarification", status: "completed", activeForm: "Collecting clarifications" },
|
||||
{ content: "Phase 3: Planning", status: "completed", activeForm: "Generating plan" },
|
||||
{ content: `Phase 4: Confirmed [${executionLabel}]`, status: "completed", activeForm: "Confirmed" },
|
||||
{ content: `Phase 5: Execution [${executionLabel}]`, status: "in_progress", activeForm: `Executing [${executionLabel}]` }
|
||||
]})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Handoff to Execution
|
||||
|
||||
@@ -350,9 +350,9 @@ executionCalls = createExecutionCalls(getTasks(planObject), executionMethod).map
|
||||
|
||||
TodoWrite({
|
||||
todos: executionCalls.map(c => ({
|
||||
content: `${c.executionType === "parallel" ? "⚡" : "→"} ${c.id} (${c.tasks.length} tasks)`,
|
||||
content: `${c.executionType === "parallel" ? "⚡" : "→"} ${c.id} [${c.executor}] (${c.tasks.length} tasks)`,
|
||||
status: "pending",
|
||||
activeForm: `Executing ${c.id}`
|
||||
activeForm: `Executing ${c.id} [${c.executor}]`
|
||||
}))
|
||||
})
|
||||
```
|
||||
|
||||
@@ -258,6 +258,19 @@ AskUserQuestion({
|
||||
- Need More Analysis → Phase 2 with feedback
|
||||
- Cancel → Save session for resumption
|
||||
|
||||
**TodoWrite Update (Phase 4 Decision)**:
|
||||
```javascript
|
||||
const executionLabel = userSelection.execution_method // "Agent" / "Codex" / "Auto"
|
||||
|
||||
TodoWrite({ todos: [
|
||||
{ content: "Phase 1: Context Gathering", status: "completed", activeForm: "Gathering context" },
|
||||
{ content: "Phase 2: Multi-CLI Discussion", status: "completed", activeForm: "Running discussion" },
|
||||
{ content: "Phase 3: Present Options", status: "completed", activeForm: "Presenting options" },
|
||||
{ content: `Phase 4: User Decision [${executionLabel}]`, status: "completed", activeForm: "Decision recorded" },
|
||||
{ content: `Phase 5: Plan Generation [${executionLabel}]`, status: "in_progress", activeForm: `Generating plan [${executionLabel}]` }
|
||||
]})
|
||||
```
|
||||
|
||||
### Phase 5: Plan Generation & Execution Handoff
|
||||
|
||||
**Step 1: Build Context-Package** (Orchestrator responsibility):
|
||||
|
||||
@@ -357,9 +357,9 @@ executionCalls = createExecutionCalls(getTasks(planObject), executionMethod).map
|
||||
|
||||
TodoWrite({
|
||||
todos: executionCalls.map(c => ({
|
||||
content: `${c.executionType === "parallel" ? "⚡" : "→"} ${c.id} (${c.tasks.length} tasks)`,
|
||||
content: `${c.executionType === "parallel" ? "⚡" : "→"} ${c.id} [${c.executor}] (${c.tasks.length} tasks)`,
|
||||
status: "pending",
|
||||
activeForm: `Executing ${c.id}`
|
||||
activeForm: `Executing ${c.id} [${c.executor}]`
|
||||
}))
|
||||
})
|
||||
```
|
||||
|
||||
@@ -338,13 +338,20 @@ Output:
|
||||
)
|
||||
```
|
||||
|
||||
**Executor Label** (computed after Step 4.0):
|
||||
```javascript
|
||||
const executorLabel = userConfig.executionMethod === 'agent' ? 'Agent'
|
||||
: userConfig.executionMethod === 'hybrid' ? 'Hybrid'
|
||||
: `CLI (${userConfig.preferredCliTool})`
|
||||
```
|
||||
|
||||
### TodoWrite Update (Phase 4 in progress)
|
||||
|
||||
```json
|
||||
[
|
||||
{"content": "Phase 1: Session Discovery", "status": "completed", "activeForm": "Executing session discovery"},
|
||||
{"content": "Phase 2: Context Gathering", "status": "completed", "activeForm": "Executing context gathering"},
|
||||
{"content": "Phase 4: Task Generation", "status": "in_progress", "activeForm": "Executing task generation"}
|
||||
{"content": "Phase 4: Task Generation [${executorLabel}]", "status": "in_progress", "activeForm": "Generating tasks [${executorLabel}]"}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -354,7 +361,7 @@ Output:
|
||||
[
|
||||
{"content": "Phase 1: Session Discovery", "status": "completed", "activeForm": "Executing session discovery"},
|
||||
{"content": "Phase 2: Context Gathering", "status": "completed", "activeForm": "Executing context gathering"},
|
||||
{"content": "Phase 4: Task Generation", "status": "completed", "activeForm": "Executing task generation"}
|
||||
{"content": "Phase 4: Task Generation [${executorLabel}]", "status": "completed", "activeForm": "Generating tasks [${executorLabel}]"}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
@@ -1,896 +0,0 @@
|
||||
---
|
||||
name: workflow-wave-plan
|
||||
description: CSV Wave planning and execution - explore via wave, resolve conflicts, execute from CSV with linked exploration context. Triggers on "workflow:wave-plan".
|
||||
argument-hint: "<task description> [--yes|-y] [--concurrency|-c N]"
|
||||
allowed-tools: Task, AskUserQuestion, Read, Write, Edit, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# Workflow Wave Plan
|
||||
|
||||
CSV Wave-based planning and execution. Uses structured CSV state for both exploration and execution, with cross-phase context propagation via `context_from` linking.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Requirement
|
||||
↓
|
||||
┌─ Phase 1: Decompose ─────────────────────┐
|
||||
│ Analyze requirement → explore.csv │
|
||||
│ (1 row per exploration angle) │
|
||||
└────────────────────┬──────────────────────┘
|
||||
↓
|
||||
┌─ Phase 2: Wave Explore ──────────────────┐
|
||||
│ Wave loop: spawn Explore agents │
|
||||
│ → findings/key_files → explore.csv │
|
||||
└────────────────────┬──────────────────────┘
|
||||
↓
|
||||
┌─ Phase 3: Synthesize & Plan ─────────────┐
|
||||
│ Read explore findings → cross-reference │
|
||||
│ → resolve conflicts → tasks.csv │
|
||||
│ (context_from links to E* explore rows) │
|
||||
└────────────────────┬──────────────────────┘
|
||||
↓
|
||||
┌─ Phase 4: Wave Execute ──────────────────┐
|
||||
│ Wave loop: build prev_context from CSV │
|
||||
│ → spawn code-developer agents per wave │
|
||||
│ → results → tasks.csv │
|
||||
└────────────────────┬──────────────────────┘
|
||||
↓
|
||||
┌─ Phase 5: Aggregate ─────────────────────┐
|
||||
│ results.csv + context.md + summary │
|
||||
└───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Context Flow
|
||||
|
||||
```
|
||||
explore.csv tasks.csv
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ E1: arch │──────────→│ T1: setup│ context_from: E1;E2
|
||||
│ findings │ │ prev_ctx │← E1+E2 findings
|
||||
├──────────┤ ├──────────┤
|
||||
│ E2: deps │──────────→│ T2: impl │ context_from: E1;T1
|
||||
│ findings │ │ prev_ctx │← E1+T1 findings
|
||||
├──────────┤ ├──────────┤
|
||||
│ E3: test │──┐ ┌───→│ T3: test │ context_from: E3;T2
|
||||
│ findings │ └───┘ │ prev_ctx │← E3+T2 findings
|
||||
└──────────┘ └──────────┘
|
||||
|
||||
Two context channels:
|
||||
1. Directed: context_from → prev_context (from CSV findings)
|
||||
2. Broadcast: discoveries.ndjson (append-only shared board)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSV Schemas
|
||||
|
||||
### explore.csv
|
||||
|
||||
| Column | Type | Set By | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `id` | string | Decomposer | E1, E2, ... |
|
||||
| `angle` | string | Decomposer | Exploration angle name |
|
||||
| `description` | string | Decomposer | What to explore from this angle |
|
||||
| `focus` | string | Decomposer | Keywords and focus areas |
|
||||
| `deps` | string | Decomposer | Semicolon-separated dep IDs (usually empty) |
|
||||
| `wave` | integer | Wave Engine | Wave number (usually 1) |
|
||||
| `status` | enum | Agent | pending / completed / failed |
|
||||
| `findings` | string | Agent | Discoveries (max 800 chars) |
|
||||
| `key_files` | string | Agent | Relevant files (semicolon-separated) |
|
||||
| `error` | string | Agent | Error message if failed |
|
||||
|
||||
### tasks.csv
|
||||
|
||||
| Column | Type | Set By | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `id` | string | Planner | T1, T2, ... |
|
||||
| `title` | string | Planner | Task title |
|
||||
| `description` | string | Planner | Self-contained task description — what to implement |
|
||||
| `test` | string | Planner | Test cases: what tests to write and how to verify (unit/integration/edge) |
|
||||
| `acceptance_criteria` | string | Planner | Measurable conditions that define "done" |
|
||||
| `scope` | string | Planner | Target file/directory glob — constrains agent write area, prevents cross-task file conflicts |
|
||||
| `hints` | string | Planner | Implementation tips + reference files. Format: `tips text \|\| file1;file2`. Either part is optional |
|
||||
| `execution_directives` | string | Planner | Execution constraints: commands to run for verification, tool restrictions |
|
||||
| `deps` | string | Planner | Dependency task IDs: T1;T2 |
|
||||
| `context_from` | string | Planner | Context source IDs: **E1;E2;T1** |
|
||||
| `wave` | integer | Wave Engine | Wave number (computed from deps) |
|
||||
| `status` | enum | Agent | pending / completed / failed / skipped |
|
||||
| `findings` | string | Agent | Execution findings (max 500 chars) |
|
||||
| `files_modified` | string | Agent | Files modified (semicolon-separated) |
|
||||
| `tests_passed` | boolean | Agent | Whether all defined test cases passed (true/false) |
|
||||
| `acceptance_met` | string | Agent | Summary of which acceptance criteria were met/unmet |
|
||||
| `error` | string | Agent | Error if failed |
|
||||
|
||||
**context_from prefix convention**: `E*` → explore.csv lookup, `T*` → tasks.csv lookup.
|
||||
|
||||
---
|
||||
|
||||
## Session Structure
|
||||
|
||||
```
|
||||
.workflow/.wave-plan/{session-id}/
|
||||
├── explore.csv # Exploration state
|
||||
├── tasks.csv # Execution state
|
||||
├── discoveries.ndjson # Shared discovery board
|
||||
├── explore-results/ # Detailed per-angle results
|
||||
│ ├── E1.json
|
||||
│ └── E2.json
|
||||
├── task-results/ # Detailed per-task results
|
||||
│ ├── T1.json
|
||||
│ └── T2.json
|
||||
├── results.csv # Final results export
|
||||
└── context.md # Full context summary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Initialization
|
||||
|
||||
```javascript
|
||||
const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
// Parse flags
|
||||
const AUTO_YES = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
|
||||
const concurrencyMatch = $ARGUMENTS.match(/(?:--concurrency|-c)\s+(\d+)/)
|
||||
const maxConcurrency = concurrencyMatch ? parseInt(concurrencyMatch[1]) : 4
|
||||
|
||||
const requirement = $ARGUMENTS
|
||||
.replace(/--yes|-y|--concurrency\s+\d+|-c\s+\d+/g, '')
|
||||
.trim()
|
||||
|
||||
const slug = requirement.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
||||
.substring(0, 40)
|
||||
const dateStr = getUtc8ISOString().substring(0, 10).replace(/-/g, '')
|
||||
const sessionId = `wp-${slug}-${dateStr}`
|
||||
const sessionFolder = `.workflow/.wave-plan/${sessionId}`
|
||||
|
||||
Bash(`mkdir -p ${sessionFolder}/explore-results ${sessionFolder}/task-results`)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Decompose → explore.csv
|
||||
|
||||
### 1.1 Analyze Requirement
|
||||
|
||||
```javascript
|
||||
const complexity = analyzeComplexity(requirement)
|
||||
// Low: 1 angle | Medium: 2-3 angles | High: 3-4 angles
|
||||
|
||||
const ANGLE_PRESETS = {
|
||||
architecture: ['architecture', 'dependencies', 'integration-points', 'modularity'],
|
||||
security: ['security', 'auth-patterns', 'dataflow', 'validation'],
|
||||
performance: ['performance', 'bottlenecks', 'caching', 'data-access'],
|
||||
bugfix: ['error-handling', 'dataflow', 'state-management', 'edge-cases'],
|
||||
feature: ['patterns', 'integration-points', 'testing', 'dependencies']
|
||||
}
|
||||
|
||||
function selectAngles(text, count) {
|
||||
let preset = 'feature'
|
||||
if (/refactor|architect|restructure|modular/.test(text)) preset = 'architecture'
|
||||
else if (/security|auth|permission|access/.test(text)) preset = 'security'
|
||||
else if (/performance|slow|optimi|cache/.test(text)) preset = 'performance'
|
||||
else if (/fix|bug|error|broken/.test(text)) preset = 'bugfix'
|
||||
return ANGLE_PRESETS[preset].slice(0, count)
|
||||
}
|
||||
|
||||
const angleCount = complexity === 'High' ? 4 : complexity === 'Medium' ? 3 : 1
|
||||
const angles = selectAngles(requirement, angleCount)
|
||||
```
|
||||
|
||||
### 1.2 Generate explore.csv
|
||||
|
||||
```javascript
|
||||
const header = 'id,angle,description,focus,deps,wave,status,findings,key_files,error'
|
||||
const rows = angles.map((angle, i) => {
|
||||
const id = `E${i + 1}`
|
||||
const desc = `Explore codebase from ${angle} perspective for: ${requirement}`
|
||||
return `"${id}","${angle}","${escCSV(desc)}","${angle}","",1,"pending","","",""`
|
||||
})
|
||||
|
||||
Write(`${sessionFolder}/explore.csv`, [header, ...rows].join('\n'))
|
||||
```
|
||||
|
||||
All exploration rows default to wave 1 (independent parallel). If angle dependencies exist, compute waves.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Wave Explore
|
||||
|
||||
Execute exploration waves using `Task(Explore)` agents.
|
||||
|
||||
### 2.1 Wave Loop
|
||||
|
||||
```javascript
|
||||
const exploreCSV = parseCSV(Read(`${sessionFolder}/explore.csv`))
|
||||
const maxExploreWave = Math.max(...exploreCSV.map(r => parseInt(r.wave)))
|
||||
|
||||
for (let wave = 1; wave <= maxExploreWave; wave++) {
|
||||
const waveRows = exploreCSV.filter(r =>
|
||||
parseInt(r.wave) === wave && r.status === 'pending'
|
||||
)
|
||||
if (waveRows.length === 0) continue
|
||||
|
||||
// Skip rows with failed dependencies
|
||||
const validRows = waveRows.filter(r => {
|
||||
if (!r.deps) return true
|
||||
return r.deps.split(';').filter(Boolean).every(depId => {
|
||||
const dep = exploreCSV.find(d => d.id === depId)
|
||||
return dep && dep.status === 'completed'
|
||||
})
|
||||
})
|
||||
|
||||
waveRows.filter(r => !validRows.includes(r)).forEach(r => {
|
||||
r.status = 'skipped'
|
||||
r.error = 'Dependency failed/skipped'
|
||||
})
|
||||
|
||||
// ★ Spawn ALL explore agents in SINGLE message → parallel execution
|
||||
const results = validRows.map(row =>
|
||||
Task({
|
||||
subagent_type: "Explore",
|
||||
run_in_background: false,
|
||||
description: `Explore: ${row.angle}`,
|
||||
prompt: buildExplorePrompt(row, requirement, sessionFolder)
|
||||
})
|
||||
)
|
||||
|
||||
// Collect results from JSON files → update explore.csv
|
||||
validRows.forEach((row, i) => {
|
||||
const resultPath = `${sessionFolder}/explore-results/${row.id}.json`
|
||||
if (fileExists(resultPath)) {
|
||||
const result = JSON.parse(Read(resultPath))
|
||||
row.status = result.status || 'completed'
|
||||
row.findings = truncate(result.findings, 800)
|
||||
row.key_files = Array.isArray(result.key_files)
|
||||
? result.key_files.join(';')
|
||||
: (result.key_files || '')
|
||||
row.error = result.error || ''
|
||||
} else {
|
||||
// Fallback: parse from agent output text
|
||||
row.status = 'completed'
|
||||
row.findings = truncate(results[i], 800)
|
||||
}
|
||||
})
|
||||
|
||||
writeCSV(`${sessionFolder}/explore.csv`, exploreCSV)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Explore Agent Prompt
|
||||
|
||||
```javascript
|
||||
function buildExplorePrompt(row, requirement, sessionFolder) {
|
||||
return `## Exploration: ${row.angle}
|
||||
|
||||
**Requirement**: ${requirement}
|
||||
**Focus**: ${row.focus}
|
||||
|
||||
### MANDATORY FIRST STEPS
|
||||
1. Read shared discoveries: ${sessionFolder}/discoveries.ndjson (if exists, skip if not)
|
||||
2. Read project context: .workflow/project-tech.json (if exists)
|
||||
|
||||
---
|
||||
|
||||
## Instructions
|
||||
Explore the codebase from the **${row.angle}** perspective:
|
||||
1. Discover relevant files, modules, and patterns
|
||||
2. Identify integration points and dependencies
|
||||
3. Note constraints, risks, and conventions
|
||||
4. Find existing patterns to follow
|
||||
5. Share discoveries: append findings to ${sessionFolder}/discoveries.ndjson
|
||||
|
||||
## Output
|
||||
Write findings to: ${sessionFolder}/explore-results/${row.id}.json
|
||||
|
||||
JSON format:
|
||||
{
|
||||
"status": "completed",
|
||||
"findings": "Concise summary of ${row.angle} discoveries (max 800 chars)",
|
||||
"key_files": ["relevant/file1.ts", "relevant/file2.ts"],
|
||||
"details": {
|
||||
"patterns": ["pattern descriptions"],
|
||||
"integration_points": [{"file": "path", "description": "..."}],
|
||||
"constraints": ["constraint descriptions"],
|
||||
"recommendations": ["recommendation descriptions"]
|
||||
}
|
||||
}
|
||||
|
||||
Also provide a 2-3 sentence summary.`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Synthesize & Plan → tasks.csv
|
||||
|
||||
Read exploration findings, cross-reference, resolve conflicts, generate execution tasks.
|
||||
|
||||
### 3.1 Load Explore Results
|
||||
|
||||
```javascript
|
||||
const exploreCSV = parseCSV(Read(`${sessionFolder}/explore.csv`))
|
||||
const completed = exploreCSV.filter(r => r.status === 'completed')
|
||||
|
||||
// Load detailed result JSONs where available
|
||||
const detailedResults = {}
|
||||
completed.forEach(r => {
|
||||
const path = `${sessionFolder}/explore-results/${r.id}.json`
|
||||
if (fileExists(path)) detailedResults[r.id] = JSON.parse(Read(path))
|
||||
})
|
||||
```
|
||||
|
||||
### 3.2 Conflict Resolution Protocol
|
||||
|
||||
Cross-reference findings across all exploration angles:
|
||||
|
||||
```javascript
|
||||
// 1. Identify common files referenced by multiple angles
|
||||
const fileRefs = {}
|
||||
completed.forEach(r => {
|
||||
r.key_files.split(';').filter(Boolean).forEach(f => {
|
||||
if (!fileRefs[f]) fileRefs[f] = []
|
||||
fileRefs[f].push({ angle: r.angle, id: r.id })
|
||||
})
|
||||
})
|
||||
const sharedFiles = Object.entries(fileRefs).filter(([_, refs]) => refs.length > 1)
|
||||
|
||||
// 2. Detect conflicting recommendations
|
||||
// Compare recommendations from different angles for same file/module
|
||||
// Flag contradictions (angle A says "refactor X" vs angle B says "extend X")
|
||||
|
||||
// 3. Resolution rules:
|
||||
// a. Safety first — when approaches conflict, choose safer option
|
||||
// b. Consistency — prefer approaches aligned with existing patterns
|
||||
// c. Scope — prefer minimal-change approaches
|
||||
// d. Document — note all resolved conflicts for transparency
|
||||
|
||||
const synthesis = {
|
||||
sharedFiles,
|
||||
conflicts: detectConflicts(completed, detailedResults),
|
||||
resolutions: [],
|
||||
allKeyFiles: [...new Set(completed.flatMap(r => r.key_files.split(';').filter(Boolean)))]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Generate tasks.csv
|
||||
|
||||
Decompose into execution tasks based on synthesized exploration:
|
||||
|
||||
```javascript
|
||||
// Task decomposition rules:
|
||||
// 1. Group by feature/module (not per-file)
|
||||
// 2. Each description is self-contained (agent sees only its row + prev_context)
|
||||
// 3. deps only when task B requires task A's output
|
||||
// 4. context_from links relevant explore rows (E*) and predecessor tasks (T*)
|
||||
// 5. Prefer parallel (minimize deps)
|
||||
// 6. Use exploration findings: key_files → target files, patterns → references,
|
||||
// integration_points → dependency relationships, constraints → included in description
|
||||
// 7. Each task MUST include: test (how to verify), acceptance_criteria (what defines done)
|
||||
// 8. scope must not overlap between tasks in the same wave
|
||||
// 9. hints = implementation tips + reference files (format: tips || file1;file2)
|
||||
// 10. execution_directives = commands to run for verification, tool restrictions
|
||||
|
||||
const tasks = []
|
||||
// Claude decomposes requirement using exploration synthesis
|
||||
// Example:
|
||||
// tasks.push({ id: 'T1', title: 'Setup types', description: '...', test: 'Verify types compile', acceptance_criteria: 'All interfaces exported', scope: 'src/types/**', hints: 'Follow existing type patterns || src/types/index.ts', execution_directives: 'tsc --noEmit', deps: '', context_from: 'E1;E2' })
|
||||
// tasks.push({ id: 'T2', title: 'Implement core', description: '...', test: 'Unit test: core logic', acceptance_criteria: 'All functions pass tests', scope: 'src/core/**', hints: 'Reuse BaseService || src/services/Base.ts', execution_directives: 'npm test -- --grep core', deps: 'T1', context_from: 'E1;E2;T1' })
|
||||
// tasks.push({ id: 'T3', title: 'Add tests', description: '...', test: 'Integration test suite', acceptance_criteria: '>80% coverage', scope: 'tests/**', hints: 'Follow existing test patterns || tests/auth.test.ts', execution_directives: 'npm test', deps: 'T2', context_from: 'E3;T2' })
|
||||
|
||||
// Compute waves
|
||||
const waves = computeWaves(tasks)
|
||||
tasks.forEach(t => { t.wave = waves[t.id] })
|
||||
|
||||
// Write tasks.csv
|
||||
const header = 'id,title,description,test,acceptance_criteria,scope,hints,execution_directives,deps,context_from,wave,status,findings,files_modified,tests_passed,acceptance_met,error'
|
||||
const rows = tasks.map(t =>
|
||||
[t.id, escCSV(t.title), escCSV(t.description), escCSV(t.test), escCSV(t.acceptance_criteria), escCSV(t.scope), escCSV(t.hints), escCSV(t.execution_directives), t.deps, t.context_from, t.wave, 'pending', '', '', '', '', '']
|
||||
.map(v => `"${v}"`).join(',')
|
||||
)
|
||||
|
||||
Write(`${sessionFolder}/tasks.csv`, [header, ...rows].join('\n'))
|
||||
```
|
||||
|
||||
### 3.4 User Confirmation
|
||||
|
||||
```javascript
|
||||
if (!AUTO_YES) {
|
||||
const maxWave = Math.max(...tasks.map(t => t.wave))
|
||||
|
||||
console.log(`
|
||||
## Execution Plan
|
||||
|
||||
Explore: ${completed.length} angles completed
|
||||
Conflicts resolved: ${synthesis.conflicts.length}
|
||||
Tasks: ${tasks.length} across ${maxWave} waves
|
||||
|
||||
${Array.from({length: maxWave}, (_, i) => i + 1).map(w => {
|
||||
const wt = tasks.filter(t => t.wave === w)
|
||||
return `### Wave ${w} (${wt.length} tasks, concurrent)
|
||||
${wt.map(t => ` - [${t.id}] ${t.title} (from: ${t.context_from})`).join('\n')}`
|
||||
}).join('\n')}
|
||||
`)
|
||||
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: `Proceed with ${tasks.length} tasks across ${maxWave} waves?`,
|
||||
header: "Confirm",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Execute", description: "Proceed with wave execution" },
|
||||
{ label: "Modify", description: `Edit ${sessionFolder}/tasks.csv then re-run` },
|
||||
{ label: "Cancel", description: "Abort" }
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Wave Execute
|
||||
|
||||
Execute tasks from tasks.csv in wave order, with prev_context built from both explore.csv and tasks.csv.
|
||||
|
||||
### 4.1 Wave Loop
|
||||
|
||||
```javascript
|
||||
const exploreCSV = parseCSV(Read(`${sessionFolder}/explore.csv`))
|
||||
const failedIds = new Set()
|
||||
const skippedIds = new Set()
|
||||
|
||||
let tasksCSV = parseCSV(Read(`${sessionFolder}/tasks.csv`))
|
||||
const maxWave = Math.max(...tasksCSV.map(r => parseInt(r.wave)))
|
||||
|
||||
for (let wave = 1; wave <= maxWave; wave++) {
|
||||
// Re-read master CSV (updated by previous wave)
|
||||
tasksCSV = parseCSV(Read(`${sessionFolder}/tasks.csv`))
|
||||
|
||||
const waveRows = tasksCSV.filter(r =>
|
||||
parseInt(r.wave) === wave && r.status === 'pending'
|
||||
)
|
||||
if (waveRows.length === 0) continue
|
||||
|
||||
// Skip on failed dependencies (cascade)
|
||||
const validRows = []
|
||||
for (const row of waveRows) {
|
||||
const deps = (row.deps || '').split(';').filter(Boolean)
|
||||
if (deps.some(d => failedIds.has(d) || skippedIds.has(d))) {
|
||||
skippedIds.add(row.id)
|
||||
row.status = 'skipped'
|
||||
row.error = 'Dependency failed/skipped'
|
||||
continue
|
||||
}
|
||||
validRows.push(row)
|
||||
}
|
||||
|
||||
if (validRows.length === 0) {
|
||||
writeCSV(`${sessionFolder}/tasks.csv`, tasksCSV)
|
||||
continue
|
||||
}
|
||||
|
||||
// Build prev_context for each row from explore.csv + tasks.csv
|
||||
validRows.forEach(row => {
|
||||
row._prev_context = buildPrevContext(row.context_from, exploreCSV, tasksCSV)
|
||||
})
|
||||
|
||||
// ★ Spawn ALL task agents in SINGLE message → parallel execution
|
||||
const results = validRows.map(row =>
|
||||
Task({
|
||||
subagent_type: "code-developer",
|
||||
run_in_background: false,
|
||||
description: row.title,
|
||||
prompt: buildExecutePrompt(row, requirement, sessionFolder)
|
||||
})
|
||||
)
|
||||
|
||||
// Collect results → update tasks.csv
|
||||
validRows.forEach((row, i) => {
|
||||
const resultPath = `${sessionFolder}/task-results/${row.id}.json`
|
||||
if (fileExists(resultPath)) {
|
||||
const result = JSON.parse(Read(resultPath))
|
||||
row.status = result.status || 'completed'
|
||||
row.findings = truncate(result.findings, 500)
|
||||
row.files_modified = Array.isArray(result.files_modified)
|
||||
? result.files_modified.join(';')
|
||||
: (result.files_modified || '')
|
||||
row.tests_passed = String(result.tests_passed ?? '')
|
||||
row.acceptance_met = result.acceptance_met || ''
|
||||
row.error = result.error || ''
|
||||
} else {
|
||||
row.status = 'completed'
|
||||
row.findings = truncate(results[i], 500)
|
||||
}
|
||||
|
||||
if (row.status === 'failed') failedIds.add(row.id)
|
||||
delete row._prev_context // runtime-only, don't persist
|
||||
})
|
||||
|
||||
writeCSV(`${sessionFolder}/tasks.csv`, tasksCSV)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 prev_context Builder
|
||||
|
||||
The key function linking exploration context to execution:
|
||||
|
||||
```javascript
|
||||
function buildPrevContext(contextFrom, exploreCSV, tasksCSV) {
|
||||
if (!contextFrom) return 'No previous context available'
|
||||
|
||||
const ids = contextFrom.split(';').filter(Boolean)
|
||||
const entries = []
|
||||
|
||||
ids.forEach(id => {
|
||||
if (id.startsWith('E')) {
|
||||
// ← Look up in explore.csv (cross-phase link)
|
||||
const row = exploreCSV.find(r => r.id === id)
|
||||
if (row && row.status === 'completed' && row.findings) {
|
||||
entries.push(`[Explore ${row.angle}] ${row.findings}`)
|
||||
if (row.key_files) entries.push(` Key files: ${row.key_files}`)
|
||||
}
|
||||
} else if (id.startsWith('T')) {
|
||||
// ← Look up in tasks.csv (same-phase link)
|
||||
const row = tasksCSV.find(r => r.id === id)
|
||||
if (row && row.status === 'completed' && row.findings) {
|
||||
entries.push(`[Task ${row.id}: ${row.title}] ${row.findings}`)
|
||||
if (row.files_modified) entries.push(` Modified: ${row.files_modified}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return entries.length > 0 ? entries.join('\n') : 'No previous context available'
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Execute Agent Prompt
|
||||
|
||||
```javascript
|
||||
function buildExecutePrompt(row, requirement, sessionFolder) {
|
||||
return `## Task: ${row.title}
|
||||
|
||||
**ID**: ${row.id}
|
||||
**Goal**: ${requirement}
|
||||
**Scope**: ${row.scope || 'Not specified'}
|
||||
|
||||
## Description
|
||||
${row.description}
|
||||
|
||||
### Implementation Hints & Reference Files
|
||||
${row.hints || 'None'}
|
||||
|
||||
> Format: \`tips text || file1;file2\`. Read ALL reference files (after ||) before starting. Apply tips (before ||) as guidance.
|
||||
|
||||
### Execution Directives
|
||||
${row.execution_directives || 'None'}
|
||||
|
||||
> Commands to run for verification, tool restrictions, or environment requirements.
|
||||
|
||||
### Test Cases
|
||||
${row.test || 'None specified'}
|
||||
|
||||
### Acceptance Criteria
|
||||
${row.acceptance_criteria || 'None specified'}
|
||||
|
||||
## Previous Context (from exploration and predecessor tasks)
|
||||
${row._prev_context}
|
||||
|
||||
### MANDATORY FIRST STEPS
|
||||
1. Read shared discoveries: ${sessionFolder}/discoveries.ndjson (if exists, skip if not)
|
||||
2. Read project context: .workflow/project-tech.json (if exists)
|
||||
|
||||
---
|
||||
|
||||
## Execution Protocol
|
||||
|
||||
1. **Read references**: Parse hints — read all files listed after \`||\` to understand existing patterns
|
||||
2. **Read discoveries**: Load ${sessionFolder}/discoveries.ndjson for shared exploration findings
|
||||
3. **Use context**: Apply previous tasks' findings from prev_context above
|
||||
4. **Stay in scope**: ONLY create/modify files within ${row.scope || 'project'} — do NOT touch files outside this boundary
|
||||
5. **Apply hints**: Follow implementation tips from hints (before \`||\`)
|
||||
6. **Implement**: Execute changes described in the task description
|
||||
7. **Write tests**: Implement the test cases defined above
|
||||
8. **Run directives**: Execute commands from execution_directives to verify your work
|
||||
9. **Verify acceptance**: Ensure all acceptance criteria are met before reporting completion
|
||||
10. **Share discoveries**: Append exploration findings to shared board:
|
||||
\\\`\\\`\\\`bash
|
||||
echo '{"ts":"<ISO>","worker":"${row.id}","type":"<type>","data":{...}}' >> ${sessionFolder}/discoveries.ndjson
|
||||
\\\`\\\`\\\`
|
||||
11. **Report result**: Write JSON to output file
|
||||
|
||||
## Output
|
||||
Write results to: ${sessionFolder}/task-results/${row.id}.json
|
||||
|
||||
{
|
||||
"status": "completed" | "failed",
|
||||
"findings": "What was done (max 500 chars)",
|
||||
"files_modified": ["file1.ts", "file2.ts"],
|
||||
"tests_passed": true | false,
|
||||
"acceptance_met": "Summary of which acceptance criteria were met/unmet",
|
||||
"error": ""
|
||||
}
|
||||
|
||||
**IMPORTANT**: Set status to "completed" ONLY if:
|
||||
- All test cases pass
|
||||
- All acceptance criteria are met
|
||||
Otherwise set status to "failed" with details in error field.`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Aggregate
|
||||
|
||||
### 5.1 Generate Results
|
||||
|
||||
```javascript
|
||||
const finalTasks = parseCSV(Read(`${sessionFolder}/tasks.csv`))
|
||||
const exploreCSV = parseCSV(Read(`${sessionFolder}/explore.csv`))
|
||||
|
||||
Bash(`cp "${sessionFolder}/tasks.csv" "${sessionFolder}/results.csv"`)
|
||||
|
||||
const completed = finalTasks.filter(r => r.status === 'completed')
|
||||
const failed = finalTasks.filter(r => r.status === 'failed')
|
||||
const skipped = finalTasks.filter(r => r.status === 'skipped')
|
||||
const maxWave = Math.max(...finalTasks.map(r => parseInt(r.wave)))
|
||||
```
|
||||
|
||||
### 5.2 Generate context.md
|
||||
|
||||
```javascript
|
||||
const contextMd = `# Wave Plan Results
|
||||
|
||||
**Requirement**: ${requirement}
|
||||
**Session**: ${sessionId}
|
||||
**Timestamp**: ${getUtc8ISOString()}
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Explore Angles | ${exploreCSV.length} |
|
||||
| Total Tasks | ${finalTasks.length} |
|
||||
| Completed | ${completed.length} |
|
||||
| Failed | ${failed.length} |
|
||||
| Skipped | ${skipped.length} |
|
||||
| Waves | ${maxWave} |
|
||||
|
||||
## Exploration Results
|
||||
|
||||
${exploreCSV.map(e => `### ${e.id}: ${e.angle} (${e.status})
|
||||
${e.findings || 'N/A'}
|
||||
Key files: ${e.key_files || 'none'}`).join('\n\n')}
|
||||
|
||||
## Task Results
|
||||
|
||||
${finalTasks.map(t => `### ${t.id}: ${t.title} (${t.status})
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Wave | ${t.wave} |
|
||||
| Scope | ${t.scope || 'none'} |
|
||||
| Dependencies | ${t.deps || 'none'} |
|
||||
| Context From | ${t.context_from || 'none'} |
|
||||
| Tests Passed | ${t.tests_passed || 'N/A'} |
|
||||
| Acceptance Met | ${t.acceptance_met || 'N/A'} |
|
||||
| Error | ${t.error || 'none'} |
|
||||
|
||||
**Description**: ${t.description}
|
||||
|
||||
**Test Cases**: ${t.test || 'N/A'}
|
||||
|
||||
**Acceptance Criteria**: ${t.acceptance_criteria || 'N/A'}
|
||||
|
||||
**Hints**: ${t.hints || 'N/A'}
|
||||
|
||||
**Execution Directives**: ${t.execution_directives || 'N/A'}
|
||||
|
||||
**Findings**: ${t.findings || 'N/A'}
|
||||
|
||||
**Files Modified**: ${t.files_modified || 'none'}`).join('\n\n---\n\n')}
|
||||
|
||||
## All Modified Files
|
||||
|
||||
${[...new Set(finalTasks.flatMap(t =>
|
||||
(t.files_modified || '').split(';')).filter(Boolean)
|
||||
)].map(f => '- ' + f).join('\n') || 'None'}
|
||||
`
|
||||
|
||||
Write(`${sessionFolder}/context.md`, contextMd)
|
||||
```
|
||||
|
||||
### 5.3 Summary & Next Steps
|
||||
|
||||
```javascript
|
||||
console.log(`
|
||||
## Wave Plan Complete
|
||||
|
||||
Session: ${sessionFolder}
|
||||
Explore: ${exploreCSV.filter(r => r.status === 'completed').length}/${exploreCSV.length} angles
|
||||
Tasks: ${completed.length}/${finalTasks.length} completed, ${failed.length} failed, ${skipped.length} skipped
|
||||
Waves: ${maxWave}
|
||||
|
||||
Files:
|
||||
- explore.csv — exploration state
|
||||
- tasks.csv — execution state
|
||||
- results.csv — final results
|
||||
- context.md — full report
|
||||
- discoveries.ndjson — shared discoveries
|
||||
`)
|
||||
|
||||
if (!AUTO_YES && failed.length > 0) {
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: `${failed.length} tasks failed. Next action?`,
|
||||
header: "Next Step",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Retry Failed", description: "Reset failed + skipped, re-execute Phase 4" },
|
||||
{ label: "View Report", description: "Display context.md" },
|
||||
{ label: "Done", description: "Complete session" }
|
||||
]
|
||||
}]
|
||||
})
|
||||
// If Retry: reset failed/skipped status to pending, re-run Phase 4
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utilities
|
||||
|
||||
### Wave Computation (Kahn's BFS)
|
||||
|
||||
```javascript
|
||||
function computeWaves(tasks) {
|
||||
const inDegree = {}, adj = {}, depth = {}
|
||||
tasks.forEach(t => { inDegree[t.id] = 0; adj[t.id] = []; depth[t.id] = 1 })
|
||||
|
||||
tasks.forEach(t => {
|
||||
const deps = (t.deps || '').split(';').filter(Boolean)
|
||||
deps.forEach(dep => {
|
||||
if (adj[dep]) { adj[dep].push(t.id); inDegree[t.id]++ }
|
||||
})
|
||||
})
|
||||
|
||||
const queue = Object.keys(inDegree).filter(id => inDegree[id] === 0)
|
||||
queue.forEach(id => { depth[id] = 1 })
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()
|
||||
adj[current].forEach(next => {
|
||||
depth[next] = Math.max(depth[next], depth[current] + 1)
|
||||
inDegree[next]--
|
||||
if (inDegree[next] === 0) queue.push(next)
|
||||
})
|
||||
}
|
||||
|
||||
if (Object.values(inDegree).some(d => d > 0)) {
|
||||
throw new Error('Circular dependency detected')
|
||||
}
|
||||
|
||||
return depth // { taskId: waveNumber }
|
||||
}
|
||||
```
|
||||
|
||||
### CSV Helpers
|
||||
|
||||
```javascript
|
||||
function escCSV(s) { return String(s || '').replace(/"/g, '""') }
|
||||
|
||||
function parseCSV(content) {
|
||||
const lines = content.trim().split('\n')
|
||||
const header = lines[0].split(',').map(h => h.replace(/"/g, '').trim())
|
||||
return lines.slice(1).filter(l => l.trim()).map(line => {
|
||||
const values = parseCSVLine(line)
|
||||
const row = {}
|
||||
header.forEach((col, i) => { row[col] = (values[i] || '').replace(/^"|"$/g, '') })
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
function writeCSV(path, rows) {
|
||||
if (rows.length === 0) return
|
||||
// Exclude runtime-only columns (prefixed with _)
|
||||
const cols = Object.keys(rows[0]).filter(k => !k.startsWith('_'))
|
||||
const header = cols.join(',')
|
||||
const lines = rows.map(r =>
|
||||
cols.map(c => `"${escCSV(r[c])}"`).join(',')
|
||||
)
|
||||
Write(path, [header, ...lines].join('\n'))
|
||||
}
|
||||
|
||||
function truncate(s, max) {
|
||||
s = String(s || '')
|
||||
return s.length > max ? s.substring(0, max - 3) + '...' : s
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Discovery Board Protocol
|
||||
|
||||
Shared `discoveries.ndjson` — append-only NDJSON accessible to all agents across all phases.
|
||||
|
||||
**Lifecycle**:
|
||||
- Created by the first agent to write a discovery
|
||||
- Carries over across all phases and waves — never cleared
|
||||
- Agents append via `echo '...' >> discoveries.ndjson`
|
||||
|
||||
**Format**: NDJSON, each line is a self-contained JSON:
|
||||
|
||||
```jsonl
|
||||
{"ts":"...","worker":"E1","type":"code_pattern","data":{"name":"repo-pattern","file":"src/repos/Base.ts"}}
|
||||
{"ts":"...","worker":"T2","type":"integration_point","data":{"file":"src/auth/index.ts","exports":["auth"]}}
|
||||
```
|
||||
|
||||
**Discovery Types**:
|
||||
|
||||
| type | Dedup Key | Description |
|
||||
|------|-----------|-------------|
|
||||
| `code_pattern` | `data.name` | Reusable code pattern found |
|
||||
| `integration_point` | `data.file` | Module connection point |
|
||||
| `convention` | singleton | Code style conventions |
|
||||
| `blocker` | `data.issue` | Blocking issue encountered |
|
||||
| `tech_stack` | singleton | Project technology stack |
|
||||
| `test_command` | singleton | Test commands discovered |
|
||||
|
||||
**Protocol Rules**:
|
||||
1. Read board before own exploration → skip covered areas
|
||||
2. Write discoveries immediately via `echo >>` → don't batch
|
||||
3. Deduplicate — check existing entries; skip if same type + dedup key exists
|
||||
4. Append-only — never modify or delete existing lines
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
| Explore agent failure | Mark as failed in explore.csv, exclude from planning |
|
||||
| All explores failed | Fallback: plan directly from requirement without exploration |
|
||||
| Execute agent failure | Mark as failed, skip dependents (cascade) |
|
||||
| Agent timeout | Mark as failed in results, continue with wave |
|
||||
| Circular dependency | Abort wave computation, report cycle |
|
||||
| CSV parse error | Validate CSV format before execution, show line number |
|
||||
| discoveries.ndjson corrupt | Ignore malformed lines, continue with valid entries |
|
||||
|
||||
---
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. **Wave Order is Sacred**: Never execute wave N before wave N-1 completes
|
||||
2. **CSV is Source of Truth**: Read master CSV before each wave, write after
|
||||
3. **Context via CSV**: prev_context built from CSV findings, not from memory
|
||||
4. **E* ↔ T* Linking**: tasks.csv `context_from` references explore.csv rows for cross-phase context
|
||||
5. **Skip on Failure**: Failed dep → skip dependent (cascade)
|
||||
6. **Discovery Board Append-Only**: Never clear or modify discoveries.ndjson
|
||||
7. **Explore Before Execute**: Phase 2 completes before Phase 4 starts
|
||||
8. **DO NOT STOP**: Continuous execution until all waves complete or remaining skipped
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Exploration Angles**: 1 for simple, 3-4 for complex; avoid redundant angles
|
||||
2. **Context Linking**: Link every task to at least one explore row (E*) — exploration was done for a reason
|
||||
3. **Task Granularity**: 3-10 tasks optimal; too many = overhead, too few = no parallelism
|
||||
4. **Minimize Cross-Wave Deps**: More tasks in wave 1 = more parallelism
|
||||
5. **Specific Descriptions**: Agent sees only its CSV row + prev_context — make description self-contained
|
||||
6. **Non-Overlapping Scopes**: Same-wave tasks must not write to the same files
|
||||
7. **Context From ≠ Deps**: `deps` = execution order constraint; `context_from` = information flow
|
||||
|
||||
---
|
||||
|
||||
## Usage Recommendations
|
||||
|
||||
| Scenario | Recommended Approach |
|
||||
|----------|---------------------|
|
||||
| Complex feature (unclear architecture) | `workflow:wave-plan` — explore first, then plan |
|
||||
| Simple known-pattern task | `$csv-wave-pipeline` — skip exploration, direct execution |
|
||||
| Independent parallel tasks | `$csv-wave-pipeline -c 8` — single wave, max parallelism |
|
||||
| Diamond dependency (A→B,C→D) | `workflow:wave-plan` — 3 waves with context propagation |
|
||||
| Unknown codebase | `workflow:wave-plan` — exploration phase is essential |
|
||||
@@ -1,261 +1,503 @@
|
||||
---
|
||||
name: team-planex
|
||||
description: |
|
||||
Inline planning + delegated execution pipeline. Main flow does planning directly,
|
||||
spawns Codex executor per issue immediately. All execution via Codex CLI only.
|
||||
Plan-and-execute pipeline with inverted control. Accepts issues.jsonl or roadmap
|
||||
session from roadmap-with-file. Delegates planning to issue-plan-agent (background),
|
||||
executes inline (main flow). Interleaved plan-execute loop.
|
||||
---
|
||||
|
||||
# Team PlanEx (Codex)
|
||||
# Team PlanEx
|
||||
|
||||
主流程内联规划 + 委托执行。SKILL.md 自身完成规划(不再 spawn planner agent),每完成一个 issue 的 solution 后立即 spawn executor agent 并行实现,无需等待所有规划完成。
|
||||
接收 `issues.jsonl` 或 roadmap session 作为输入,委托规划 + 内联执行。Spawn issue-plan-agent 后台规划下一个 issue,主流程内联执行当前已规划的 issue。交替循环:规划 → 执行 → 规划下一个 → 执行 → 直到所有 issue 完成。
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ SKILL.md (主流程 = 规划 + 节拍控制) │
|
||||
│ │
|
||||
│ Phase 1: 解析输入 + 初始化 session │
|
||||
│ Phase 2: 逐 issue 规划循环 (内联) │
|
||||
│ ├── issue-plan → 写 solution artifact │
|
||||
│ ├── spawn executor agent ────────────┼──> [executor] 实现
|
||||
│ └── continue (不等 executor) │
|
||||
│ Phase 3: 等待所有 executors │
|
||||
│ Phase 4: 汇总报告 │
|
||||
└────────────────────────────────────────┘
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ SKILL.md (主流程 = 内联执行 + 循环控制) │
|
||||
│ │
|
||||
│ Phase 1: 加载 issues.jsonl / roadmap session │
|
||||
│ Phase 2: 规划-执行交替循环 │
|
||||
│ ├── 取下一个未规划 issue → spawn planner (bg) │
|
||||
│ ├── 取下一个已规划 issue → 内联执行 │
|
||||
│ │ ├── 内联实现 (Read/Edit/Write/Bash) │
|
||||
│ │ ├── 验证测试 + 自修复 (max 3 retries) │
|
||||
│ │ └── git commit │
|
||||
│ └── 循环直到所有 issue 规划+执行完毕 │
|
||||
│ Phase 3: 汇总报告 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
|
||||
Planner (single reusable agent, background):
|
||||
issue-plan-agent spawn once → send_input per issue → write solution JSON → write .ready marker
|
||||
```
|
||||
|
||||
## Beat Model (Interleaved Plan-Execute Loop)
|
||||
|
||||
```
|
||||
Interleaved Loop (Phase 2, single planner reused via send_input):
|
||||
═══════════════════════════════════════════════════════════════
|
||||
Beat 1 Beat 2 Beat 3
|
||||
| | |
|
||||
spawn+plan(A) send_input(C) (drain)
|
||||
↓ ↓ ↓
|
||||
poll → A.ready poll → B.ready poll → C.ready
|
||||
↓ ↓ ↓
|
||||
exec(A) exec(B) exec(C)
|
||||
↓ ↓ ↓
|
||||
send_input(B) send_input(done) done
|
||||
(eager delegate) (all delegated)
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
Planner timeline (never idle between issues):
|
||||
─────────────────────────────────────────────────────────────
|
||||
Planner: ├─plan(A)─┤├─plan(B)─┤├─plan(C)─┤ done
|
||||
Main: 2a(spawn) ├─exec(A)──┤├─exec(B)──┤├─exec(C)──┤
|
||||
^2f→send(B) ^2f→send(C)
|
||||
─────────────────────────────────────────────────────────────
|
||||
|
||||
Single Beat Detail:
|
||||
───────────────────────────────────────────────────────────────
|
||||
1. Delegate next 2. Poll ready 3. Execute 4. Verify + commit
|
||||
(2a or 2f eager) solutions inline + eager delegate
|
||||
| | | |
|
||||
send_input(bg) Glob(*.ready) Read/Edit/Write test → commit
|
||||
│ → send_input(next)
|
||||
wait if none ready
|
||||
───────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Agent Registry
|
||||
|
||||
| Agent | Role File | Responsibility |
|
||||
|-------|-----------|----------------|
|
||||
| `executor` | `~/.codex/agents/planex-executor.md` | Codex CLI implementation per issue |
|
||||
| Agent | Role File | Responsibility | Pattern |
|
||||
|-------|-----------|----------------|---------|
|
||||
| `issue-plan-agent` | `~/.codex/agents/issue-plan-agent.md` | Explore codebase + generate solutions, single instance reused via send_input | 2.3 Deep Interaction |
|
||||
|
||||
> Executor agent must be deployed to `~/.codex/agents/` before use.
|
||||
> Source: `.codex/skills/team-planex/agents/`
|
||||
> **COMPACT PROTECTION**: Agent files are execution documents. When context compression occurs and agent instructions are reduced to summaries, **you MUST immediately `Read` the corresponding agent.md to reload before continuing execution**.
|
||||
|
||||
---
|
||||
|
||||
## Input Parsing
|
||||
## Subagent API Reference
|
||||
|
||||
Supported input types (parse from `$ARGUMENTS`):
|
||||
|
||||
| Type | Detection | Handler |
|
||||
|------|-----------|---------|
|
||||
| Issue IDs | `ISS-\d{8}-\d{6}` regex | Use directly for planning |
|
||||
| Text | `--text '...'` flag | Create issue(s) first via CLI |
|
||||
| Plan file | `--plan <path>` flag | Read file, parse phases, batch create issues |
|
||||
|
||||
### Issue Creation (when needed)
|
||||
|
||||
For `--text` input:
|
||||
```bash
|
||||
ccw issue create --data '{"title":"<title>","description":"<description>"}' --json
|
||||
```
|
||||
|
||||
For `--plan` input:
|
||||
- Match `## Phase N: Title`, `## Step N: Title`, or `### N. Title`
|
||||
- Each match → one issue (title + description from section content)
|
||||
- Fallback: no structure found → entire file as single issue
|
||||
|
||||
---
|
||||
|
||||
## Session Setup
|
||||
|
||||
Before processing issues, initialize session directory:
|
||||
### spawn_agent
|
||||
|
||||
```javascript
|
||||
const slug = toSlug(inputDescription).slice(0, 20)
|
||||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '')
|
||||
const sessionDir = `.workflow/.team/PEX-${slug}-${date}`
|
||||
const artifactsDir = `${sessionDir}/artifacts/solutions`
|
||||
|
||||
Bash(`mkdir -p "${artifactsDir}"`)
|
||||
|
||||
Write({
|
||||
file_path: `${sessionDir}/team-session.json`,
|
||||
content: JSON.stringify({
|
||||
session_id: `PEX-${slug}-${date}`,
|
||||
input_type: inputType,
|
||||
input: rawInput,
|
||||
status: "running",
|
||||
started_at: new Date().toISOString(),
|
||||
executors: []
|
||||
}, null, 2)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Parse Input + Initialize
|
||||
|
||||
1. Parse `$ARGUMENTS` to determine input type
|
||||
2. Create issues if needed (--text / --plan)
|
||||
3. Collect all issue IDs
|
||||
4. Initialize session directory
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Inline Planning Loop
|
||||
|
||||
For each issue, execute planning inline (no planner agent):
|
||||
|
||||
### 2a. Generate Solution via issue-plan-agent
|
||||
|
||||
```javascript
|
||||
const planAgent = spawn_agent({
|
||||
const agentId = spawn_agent({
|
||||
message: `
|
||||
## TASK ASSIGNMENT
|
||||
|
||||
### MANDATORY FIRST STEPS (Agent Execute)
|
||||
1. **Read role definition**: ~/.codex/agents/issue-plan-agent.md (MUST read first)
|
||||
2. Read: .workflow/project-tech.json
|
||||
3. Read: .workflow/project-guidelines.json
|
||||
|
||||
---
|
||||
|
||||
issue_ids: ["${issueId}"]
|
||||
project_root: "${projectRoot}"
|
||||
|
||||
## Requirements
|
||||
- Generate solution for this issue
|
||||
- Auto-bind single solution
|
||||
- Output solution JSON when complete
|
||||
${taskContext}
|
||||
`
|
||||
})
|
||||
|
||||
const result = wait({ ids: [planAgent], timeout_ms: 600000 })
|
||||
close_agent({ id: planAgent })
|
||||
```
|
||||
|
||||
### 2b. Write Solution Artifact
|
||||
### wait
|
||||
|
||||
```javascript
|
||||
const solution = parseSolution(result)
|
||||
|
||||
Write({
|
||||
file_path: `${artifactsDir}/${issueId}.json`,
|
||||
content: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
issue_id: issueId,
|
||||
solution: solution,
|
||||
planned_at: new Date().toISOString()
|
||||
}, null, 2)
|
||||
const result = wait({
|
||||
ids: [agentId],
|
||||
timeout_ms: 600000 // 10 minutes
|
||||
})
|
||||
```
|
||||
|
||||
### 2c. Spawn Executor Immediately
|
||||
### send_input
|
||||
|
||||
Continue interaction with active agent (reuse for next issue).
|
||||
|
||||
```javascript
|
||||
const executorId = spawn_agent({
|
||||
send_input({
|
||||
id: agentId,
|
||||
message: `
|
||||
## NEXT ISSUE
|
||||
|
||||
issue_ids: ["<nextIssueId>"]
|
||||
|
||||
## Output Requirements
|
||||
1. Generate solution for this issue
|
||||
2. Write solution JSON to: <artifactsDir>/<nextIssueId>.json
|
||||
3. Write ready marker to: <artifactsDir>/<nextIssueId>.ready
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
### close_agent
|
||||
|
||||
```javascript
|
||||
close_agent({ id: agentId })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Input
|
||||
|
||||
Accepts output from `roadmap-with-file` or direct `issues.jsonl` path.
|
||||
|
||||
Supported input forms (parse from `$ARGUMENTS`):
|
||||
|
||||
| Form | Detection | Example |
|
||||
|------|-----------|---------|
|
||||
| Roadmap session | `RMAP-` prefix or `--session` flag | `team-planex --session RMAP-auth-20260301` |
|
||||
| Issues JSONL path | `.jsonl` extension | `team-planex .workflow/issues/issues.jsonl` |
|
||||
| Issue IDs | `ISS-\d{8}-\d{3,6}` regex | `team-planex ISS-20260301-001 ISS-20260301-002` |
|
||||
|
||||
### Input Resolution
|
||||
|
||||
| Input Form | Resolution |
|
||||
|------------|------------|
|
||||
| `--session RMAP-*` | Read `.workflow/.roadmap/<sessionId>/roadmap.md` → extract issue IDs from Roadmap table → load issue data from `.workflow/issues/issues.jsonl` |
|
||||
| `.jsonl` path | Read file → parse each line as JSON → collect all issues |
|
||||
| Issue IDs | Use directly → fetch details via `ccw issue status <id> --json` |
|
||||
|
||||
### Issue Record Fields Used
|
||||
|
||||
| Field | Usage |
|
||||
|-------|-------|
|
||||
| `id` | Issue identifier for planning and execution |
|
||||
| `title` | Commit message and reporting |
|
||||
| `status` | Skip if already `completed` |
|
||||
| `tags` | Wave ordering: `wave-1` before `wave-2` |
|
||||
| `extended_context.notes.depends_on_issues` | Execution ordering |
|
||||
|
||||
### Wave Ordering
|
||||
|
||||
Issues are sorted by wave tag for execution order:
|
||||
|
||||
| Priority | Rule |
|
||||
|----------|------|
|
||||
| 1 | Lower wave number first (`wave-1` before `wave-2`) |
|
||||
| 2 | Within same wave: issues without `depends_on_issues` first |
|
||||
| 3 | Within same wave + no deps: original order from JSONL |
|
||||
|
||||
---
|
||||
|
||||
## Session Setup
|
||||
|
||||
Initialize session directory before processing:
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| Slug | `toSlug(<first issue title>)` truncated to 20 chars |
|
||||
| Date | `YYYYMMDD` format |
|
||||
| Session dir | `.workflow/.team/PEX-<slug>-<date>` |
|
||||
| Solutions dir | `<sessionDir>/artifacts/solutions` |
|
||||
|
||||
Create directories:
|
||||
|
||||
```bash
|
||||
mkdir -p "<sessionDir>/artifacts/solutions"
|
||||
```
|
||||
|
||||
Write `<sessionDir>/team-session.json`:
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| `session_id` | `PEX-<slug>-<date>` |
|
||||
| `input_type` | `roadmap` / `jsonl` / `issue_ids` |
|
||||
| `source_session` | Roadmap session ID (if applicable) |
|
||||
| `issue_ids` | Array of all issue IDs to process (wave-sorted) |
|
||||
| `status` | `"running"` |
|
||||
| `started_at` | ISO timestamp |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Load Issues + Initialize
|
||||
|
||||
1. Parse `$ARGUMENTS` to determine input form
|
||||
2. Resolve issues (see Input Resolution table)
|
||||
3. Filter out issues with `status: completed`
|
||||
4. Sort by wave ordering
|
||||
5. Collect into `<issueQueue>` (ordered list of issue IDs to process)
|
||||
6. Initialize session directory and `team-session.json`
|
||||
7. Set `<plannedSet>` = {} , `<executedSet>` = {} , `<plannerAgent>` = null (single reusable planner)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Plan-Execute Loop
|
||||
|
||||
Interleaved loop that keeps planner agent busy at all times. Each beat: (1) delegate next issue to planner if idle, (2) poll for ready solutions, (3) execute inline, (4) after execution completes, immediately delegate next issue to planner before polling again.
|
||||
|
||||
### Loop Entry
|
||||
|
||||
Set `<queueIndex>` = 0 (pointer into `<issueQueue>`).
|
||||
|
||||
### 2a. Delegate Next Issue to Planner
|
||||
|
||||
Single planner agent is spawned once and reused via `send_input` for subsequent issues.
|
||||
|
||||
| Condition | Action |
|
||||
|-----------|--------|
|
||||
| `<plannerAgent>` is null AND `<queueIndex>` < `<issueQueue>.length` | Spawn planner (first issue), advance `<queueIndex>` |
|
||||
| `<plannerAgent>` exists, idle AND `<queueIndex>` < `<issueQueue>.length` | `send_input` next issue, advance `<queueIndex>` |
|
||||
| `<plannerAgent>` is busy | Skip (wait for current planning to finish) |
|
||||
| `<queueIndex>` >= `<issueQueue>.length` | No more issues to plan |
|
||||
|
||||
**First issue — spawn planner**:
|
||||
|
||||
```javascript
|
||||
const plannerAgent = spawn_agent({
|
||||
message: `
|
||||
## TASK ASSIGNMENT
|
||||
|
||||
### MANDATORY FIRST STEPS (Agent Execute)
|
||||
1. **Read role definition**: ~/.codex/agents/planex-executor.md (MUST read first)
|
||||
1. **Read role definition**: ~/.codex/agents/issue-plan-agent.md (MUST read first)
|
||||
2. Read: .workflow/project-tech.json
|
||||
3. Read: .workflow/project-guidelines.json
|
||||
|
||||
---
|
||||
|
||||
## Issue
|
||||
Issue ID: ${issueId}
|
||||
Solution file: ${artifactsDir}/${issueId}.json
|
||||
Session: ${sessionDir}
|
||||
issue_ids: ["<issueId>"]
|
||||
project_root: "<projectRoot>"
|
||||
|
||||
## Execution
|
||||
Load solution from file → implement via Codex CLI → verify tests → commit → report.
|
||||
## Output Requirements
|
||||
1. Generate solution for this issue
|
||||
2. Write solution JSON to: <artifactsDir>/<issueId>.json
|
||||
3. Write ready marker to: <artifactsDir>/<issueId>.ready
|
||||
- Marker content: {"issue_id":"<issueId>","task_count":<task_count>,"file_count":<file_count>}
|
||||
|
||||
## Multi-Issue Mode
|
||||
You will receive additional issues via follow-up messages. After completing each issue,
|
||||
output results and wait for next instruction.
|
||||
`
|
||||
})
|
||||
|
||||
executorIds.push(executorId)
|
||||
executorIssueMap[executorId] = issueId
|
||||
```
|
||||
|
||||
### 2d. Continue to Next Issue
|
||||
**Subsequent issues — send_input**:
|
||||
|
||||
Do NOT wait for executor. Proceed to next issue immediately.
|
||||
```javascript
|
||||
send_input({
|
||||
id: plannerAgent,
|
||||
message: `
|
||||
## NEXT ISSUE
|
||||
|
||||
issue_ids: ["<nextIssueId>"]
|
||||
|
||||
## Output Requirements
|
||||
1. Generate solution for this issue
|
||||
2. Write solution JSON to: <artifactsDir>/<nextIssueId>.json
|
||||
3. Write ready marker to: <artifactsDir>/<nextIssueId>.ready
|
||||
- Marker content: {"issue_id":"<nextIssueId>","task_count":<task_count>,"file_count":<file_count>}
|
||||
`
|
||||
})
|
||||
```
|
||||
|
||||
Record `<planningIssueId>` = current issue ID.
|
||||
|
||||
### 2b. Poll for Ready Solutions
|
||||
|
||||
Poll `<artifactsDir>/*.ready` using Glob.
|
||||
|
||||
| Condition | Action |
|
||||
|-----------|--------|
|
||||
| New `.ready` found (not in `<executedSet>`) | Load `<issueId>.json` solution → proceed to 2c |
|
||||
| `<plannerAgent>` busy, no `.ready` yet | Check planner: `wait({ ids: [<plannerAgent>], timeout_ms: 30000 })` |
|
||||
| Planner finished current issue | Mark planner idle, re-poll |
|
||||
| Planner timed out (30s wait) | Re-poll (planner still working) |
|
||||
| No `.ready`, planner idle, all issues delegated | Exit loop → Phase 3 |
|
||||
| Idle >5 minutes total | Exit loop → Phase 3 |
|
||||
|
||||
### 2c. Inline Execution
|
||||
|
||||
Main flow implements the solution directly. For each task in `solution.tasks`, ordered by `depends_on` sequence:
|
||||
|
||||
| Step | Action | Tool |
|
||||
|------|--------|------|
|
||||
| 1. Read context | Read all files referenced in current task | Read |
|
||||
| 2. Identify patterns | Note imports, naming conventions, existing structure | — (inline reasoning) |
|
||||
| 3. Apply changes | Modify existing files or create new files | Edit (prefer) / Write (new files) |
|
||||
| 4. Build check | Run project build command if available | Bash |
|
||||
|
||||
Build verification:
|
||||
|
||||
```bash
|
||||
npm run build 2>&1 || echo BUILD_FAILED
|
||||
```
|
||||
|
||||
| Build Result | Action |
|
||||
|--------------|--------|
|
||||
| Success | Proceed to 2d |
|
||||
| Failure | Analyze error → fix source → rebuild (max 3 retries) |
|
||||
| No build command | Skip, proceed to 2d |
|
||||
|
||||
### 2d. Verify Tests
|
||||
|
||||
Detect test command:
|
||||
|
||||
| Priority | Detection |
|
||||
|----------|-----------|
|
||||
| 1 | `package.json` → `scripts.test` |
|
||||
| 2 | `package.json` → `scripts.test:unit` |
|
||||
| 3 | `pytest.ini` / `setup.cfg` (Python) |
|
||||
| 4 | `Makefile` test target |
|
||||
|
||||
Run tests. If tests fail → self-repair loop:
|
||||
|
||||
| Attempt | Action |
|
||||
|---------|--------|
|
||||
| 1–3 | Analyze test output → diagnose → fix source code → re-run tests |
|
||||
| After 3 | Mark issue as failed, log to `<sessionDir>/errors.json`, continue |
|
||||
|
||||
### 2e. Git Commit
|
||||
|
||||
Stage and commit changes for this issue:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat(<issueId>): <solution-title>"
|
||||
```
|
||||
|
||||
| Outcome | Action |
|
||||
|---------|--------|
|
||||
| Commit succeeds | Record commit hash |
|
||||
| Commit fails (nothing to commit) | Warn, continue |
|
||||
| Pre-commit hook fails | Attempt fix once, then warn and continue |
|
||||
|
||||
### 2f. Update Status + Eagerly Delegate Next
|
||||
|
||||
Update issue status:
|
||||
|
||||
```bash
|
||||
ccw issue update <issueId> --status completed
|
||||
```
|
||||
|
||||
Add `<issueId>` to `<executedSet>`.
|
||||
|
||||
**Eager delegation**: Immediately check planner state and delegate next issue before returning to poll:
|
||||
|
||||
| Planner State | Action |
|
||||
|---------------|--------|
|
||||
| Idle AND more issues in queue | `send_input` next issue → advance `<queueIndex>` |
|
||||
| Busy (still planning) | Skip — planner already working |
|
||||
| All issues delegated | Skip — nothing to delegate |
|
||||
|
||||
This ensures planner is never idle between beats. Return to 2b for next beat.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Wait All Executors
|
||||
## Phase 3: Report
|
||||
|
||||
### 3a. Cleanup
|
||||
|
||||
Close the planner agent. Ignore cleanup failures.
|
||||
|
||||
```javascript
|
||||
if (executorIds.length > 0) {
|
||||
const execResults = wait({ ids: executorIds, timeout_ms: 1800000 })
|
||||
if (plannerAgent) {
|
||||
try { close_agent({ id: plannerAgent }) } catch {}
|
||||
}
|
||||
```
|
||||
|
||||
if (execResults.timed_out) {
|
||||
const pending = executorIds.filter(id => !execResults.status[id]?.completed)
|
||||
if (pending.length > 0) {
|
||||
const pendingIssues = pending.map(id => executorIssueMap[id])
|
||||
Write({
|
||||
file_path: `${sessionDir}/pending-executors.json`,
|
||||
content: JSON.stringify({ pending_issues: pendingIssues, executor_ids: pending }, null, 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
### 3b. Generate Report
|
||||
|
||||
// Collect summaries
|
||||
const summaries = executorIds.map(id => ({
|
||||
issue_id: executorIssueMap[id],
|
||||
status: execResults.status[id]?.completed ? 'completed' : 'timeout',
|
||||
output: execResults.status[id]?.completed ?? null
|
||||
}))
|
||||
Update `<sessionDir>/team-session.json`:
|
||||
|
||||
// Cleanup
|
||||
executorIds.forEach(id => {
|
||||
try { close_agent({ id }) } catch { /* already closed */ }
|
||||
})
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| `status` | `"completed"` |
|
||||
| `completed_at` | ISO timestamp |
|
||||
| `results.total` | Total issues in queue |
|
||||
| `results.completed` | Count in `<executedSet>` |
|
||||
| `results.failed` | Count of failed issues |
|
||||
|
||||
Output summary:
|
||||
|
||||
```
|
||||
## Pipeline Complete
|
||||
|
||||
**Total issues**: <total>
|
||||
**Completed**: <completed>
|
||||
**Failed**: <failed>
|
||||
|
||||
<per-issue status list>
|
||||
|
||||
Session: <sessionDir>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File-Based Coordination Protocol
|
||||
|
||||
| File | Writer | Reader | Purpose |
|
||||
|------|--------|--------|---------|
|
||||
| `<artifactsDir>/<issueId>.json` | planner | main flow | Solution data |
|
||||
| `<artifactsDir>/<issueId>.ready` | planner | main flow | Atomicity signal |
|
||||
|
||||
### Ready Marker Format
|
||||
|
||||
```json
|
||||
{
|
||||
"issue_id": "<issueId>",
|
||||
"task_count": "<task_count>",
|
||||
"file_count": "<file_count>"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Report
|
||||
## Session Directory
|
||||
|
||||
```javascript
|
||||
const completed = summaries.filter(s => s.status === 'completed').length
|
||||
const failed = summaries.filter(s => s.status === 'timeout').length
|
||||
|
||||
// Update session
|
||||
Write({
|
||||
file_path: `${sessionDir}/team-session.json`,
|
||||
content: JSON.stringify({
|
||||
...session,
|
||||
status: "completed",
|
||||
completed_at: new Date().toISOString(),
|
||||
results: { total: executorIds.length, completed, failed }
|
||||
}, null, 2)
|
||||
})
|
||||
|
||||
return `
|
||||
## Pipeline Complete
|
||||
|
||||
**Total issues**: ${executorIds.length}
|
||||
**Completed**: ${completed}
|
||||
**Timed out**: ${failed}
|
||||
|
||||
${summaries.map(s => `- ${s.issue_id}: ${s.status}`).join('\n')}
|
||||
|
||||
Session: ${sessionDir}
|
||||
`
|
||||
```
|
||||
.workflow/.team/PEX-<slug>-<date>/
|
||||
├── team-session.json
|
||||
├── artifacts/
|
||||
│ └── solutions/
|
||||
│ ├── <issueId>.json
|
||||
│ └── <issueId>.ready
|
||||
└── errors.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Commands
|
||||
## Lifecycle Management
|
||||
|
||||
During execution, the user may issue:
|
||||
### Timeout Protocol
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| `check` / `status` | Show executor progress summary |
|
||||
| `resume` / `continue` | Urge stalled executor |
|
||||
| Phase | Default Timeout | On Timeout |
|
||||
|-------|-----------------|------------|
|
||||
| Phase 1 (Load) | 60s | Report error, stop |
|
||||
| Phase 2 (Planner wait) | 600s per issue | Skip issue, write `.error` marker |
|
||||
| Phase 2 (Execution) | No timeout | Self-repair up to 3 retries |
|
||||
| Phase 2 (Loop idle) | 5 min total idle | Break loop → Phase 3 |
|
||||
|
||||
### Cleanup Protocol
|
||||
|
||||
At workflow end, close the planner agent:
|
||||
|
||||
```javascript
|
||||
if (plannerAgent) {
|
||||
try { close_agent({ id: plannerAgent }) } catch { /* already closed */ }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| issue-plan-agent timeout (>10 min) | Skip issue, log error, continue to next |
|
||||
| issue-plan-agent failure | Retry once, then skip with error log |
|
||||
| Solution file not written | Executor reports error, logs to `${sessionDir}/errors.json` |
|
||||
| Executor (Codex CLI) failure | Executor handles resume; logs CLI resume command |
|
||||
| No issues to process | Report error: no issues found |
|
||||
| Phase | Scenario | Resolution |
|
||||
|-------|----------|------------|
|
||||
| 1 | Invalid input (no issues, bad JSONL) | Report error, stop |
|
||||
| 1 | Roadmap session not found | Report error, stop |
|
||||
| 1 | Issue fetch fails | Retry once, then skip issue |
|
||||
| 2 | Planner spawn fails | Retry once, then skip issue |
|
||||
| 2 | issue-plan-agent timeout (>10 min) | Skip issue, write `.error` marker, continue |
|
||||
| 2 | Solution file corrupt / unreadable | Skip, log to `errors.json`, continue |
|
||||
| 2 | Implementation error | Self-repair up to 3 retries per task |
|
||||
| 2 | Tests failing after 3 retries | Mark issue failed, log, continue |
|
||||
| 2 | Git commit fails | Warn, mark completed anyway |
|
||||
| 2 | Polling idle >5 minutes | Break loop → Phase 3 |
|
||||
| 3 | Agent cleanup fails | Ignore |
|
||||
|
||||
---
|
||||
|
||||
## User Commands
|
||||
|
||||
| Command | Action |
|
||||
|---------|--------|
|
||||
| `check` / `status` | Show progress: planned / executing / completed / failed counts |
|
||||
| `resume` / `continue` | Re-enter loop from Phase 2 |
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
---
|
||||
name: planex-executor
|
||||
description: |
|
||||
PlanEx executor agent. Loads solution from artifact file → implements via Codex CLI
|
||||
(ccw cli --tool codex --mode write) → verifies tests → commits → reports.
|
||||
Deploy to: ~/.codex/agents/planex-executor.md
|
||||
color: green
|
||||
---
|
||||
|
||||
# PlanEx Executor
|
||||
|
||||
Single-issue implementation agent. Loads solution from JSON artifact, executes
|
||||
implementation via Codex CLI, verifies with tests, commits, and outputs a structured
|
||||
completion report.
|
||||
|
||||
## Identity
|
||||
|
||||
- **Tag**: `[executor]`
|
||||
- **Backend**: Codex CLI only (`ccw cli --tool codex --mode write`)
|
||||
- **Granularity**: One issue per agent instance
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
| Action | Allowed |
|
||||
|--------|---------|
|
||||
| Read solution artifact from disk | Yes |
|
||||
| Implement via Codex CLI | Yes |
|
||||
| Run tests for verification | Yes |
|
||||
| git commit completed work | Yes |
|
||||
| Create or modify issues | No |
|
||||
| Spawn subagents | No |
|
||||
| Interact with user (AskUserQuestion) | No |
|
||||
|
||||
---
|
||||
|
||||
## Execution Flow
|
||||
|
||||
### Step 1: Load Context
|
||||
|
||||
After reading role definition:
|
||||
- Extract issue ID, solution file path, session dir from task message
|
||||
|
||||
### Step 2: Load Solution
|
||||
|
||||
Read solution artifact:
|
||||
|
||||
```javascript
|
||||
const solutionData = JSON.parse(Read(solutionFile))
|
||||
const solution = solutionData.solution
|
||||
```
|
||||
|
||||
If file not found or invalid:
|
||||
- Log error: `[executor] ERROR: Solution file not found: ${solutionFile}`
|
||||
- Output: `EXEC_FAILED:${issueId}:solution_file_missing`
|
||||
- Stop execution
|
||||
|
||||
Verify solution has required fields:
|
||||
- `solution.bound.title` or `solution.title`
|
||||
- `solution.bound.tasks` or `solution.tasks`
|
||||
|
||||
### Step 3: Update Issue Status
|
||||
|
||||
```bash
|
||||
ccw issue update ${issueId} --status executing
|
||||
```
|
||||
|
||||
### Step 4: Codex CLI Execution
|
||||
|
||||
Build execution prompt and invoke Codex:
|
||||
|
||||
```bash
|
||||
ccw cli -p "$(cat <<'PROMPT_EOF'
|
||||
## Issue
|
||||
ID: ${issueId}
|
||||
Title: ${solution.bound.title}
|
||||
|
||||
## Solution Plan
|
||||
${JSON.stringify(solution.bound, null, 2)}
|
||||
|
||||
## Implementation Requirements
|
||||
1. Follow the solution plan tasks in order
|
||||
2. Write clean, minimal code following existing patterns
|
||||
3. Run tests after each significant change
|
||||
4. Ensure all existing tests still pass
|
||||
5. Do NOT over-engineer - implement exactly what the solution specifies
|
||||
|
||||
## Quality Checklist
|
||||
- [ ] All solution tasks implemented
|
||||
- [ ] No TypeScript/linting errors (run: npx tsc --noEmit)
|
||||
- [ ] Existing tests pass
|
||||
- [ ] New tests added where specified in solution
|
||||
- [ ] No security vulnerabilities introduced
|
||||
PROMPT_EOF
|
||||
)" --tool codex --mode write --id planex-${issueId}
|
||||
```
|
||||
|
||||
**STOP after spawn** — Codex CLI executes in background. Do NOT poll or wait inside this agent. The CLI process handles implementation autonomously.
|
||||
|
||||
Wait for CLI completion signal before proceeding to Step 5.
|
||||
|
||||
### Step 5: Verify Tests
|
||||
|
||||
Detect and run project test command:
|
||||
|
||||
```javascript
|
||||
// Detection priority:
|
||||
// 1. package.json scripts.test
|
||||
// 2. package.json scripts.test:unit
|
||||
// 3. pytest.ini / setup.cfg (Python)
|
||||
// 4. Makefile test target
|
||||
|
||||
const testCmd = detectTestCommand()
|
||||
|
||||
if (testCmd) {
|
||||
const testResult = Bash(`${testCmd} 2>&1 || echo TEST_FAILED`)
|
||||
|
||||
if (testResult.includes('TEST_FAILED') || testResult.includes('FAIL')) {
|
||||
const resumeCmd = `ccw cli -p "Fix failing tests" --resume planex-${issueId} --tool codex --mode write`
|
||||
|
||||
Write({
|
||||
file_path: `${sessionDir}/errors.json`,
|
||||
content: JSON.stringify({
|
||||
issue_id: issueId,
|
||||
type: 'test_failure',
|
||||
test_output: testResult.slice(0, 2000),
|
||||
resume_cmd: resumeCmd,
|
||||
timestamp: new Date().toISOString()
|
||||
}, null, 2)
|
||||
})
|
||||
|
||||
Output: `EXEC_FAILED:${issueId}:tests_failing`
|
||||
Stop.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Commit
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "feat(${issueId}): ${solution.bound.title}"
|
||||
```
|
||||
|
||||
If commit fails (nothing to commit, pre-commit hook error):
|
||||
- Log warning: `[executor] WARN: Commit failed for ${issueId}, continuing`
|
||||
- Still proceed to Step 7
|
||||
|
||||
### Step 7: Update Issue & Report
|
||||
|
||||
```bash
|
||||
ccw issue update ${issueId} --status completed
|
||||
```
|
||||
|
||||
Output completion report:
|
||||
|
||||
```
|
||||
## [executor] Implementation Complete
|
||||
|
||||
**Issue**: ${issueId}
|
||||
**Title**: ${solution.bound.title}
|
||||
**Backend**: codex
|
||||
**Tests**: ${testCmd ? 'passing' : 'skipped (no test command found)'}
|
||||
**Commit**: ${commitHash}
|
||||
**Status**: resolved
|
||||
|
||||
EXEC_DONE:${issueId}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resume Protocol
|
||||
|
||||
If Codex CLI execution fails or times out:
|
||||
|
||||
```bash
|
||||
ccw cli -p "Continue implementation from where stopped" \
|
||||
--resume planex-${issueId} \
|
||||
--tool codex --mode write \
|
||||
--id planex-${issueId}-retry
|
||||
```
|
||||
|
||||
Resume command is always logged to `${sessionDir}/errors.json` on any failure.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Resolution |
|
||||
|----------|------------|
|
||||
| Solution file missing | Output `EXEC_FAILED:<id>:solution_file_missing`, stop |
|
||||
| Solution JSON malformed | Output `EXEC_FAILED:<id>:solution_invalid`, stop |
|
||||
| Issue status update fails | Log warning, continue |
|
||||
| Codex CLI failure | Log resume command to errors.json, output `EXEC_FAILED:<id>:codex_failed` |
|
||||
| Tests failing | Log test output + resume command, output `EXEC_FAILED:<id>:tests_failing` |
|
||||
| Commit fails | Log warning, still output `EXEC_DONE:<id>` (implementation complete) |
|
||||
| No test command found | Skip test step, proceed to commit |
|
||||
|
||||
## Key Reminders
|
||||
|
||||
**ALWAYS**:
|
||||
- Output `EXEC_DONE:<issueId>` on its own line when implementation succeeds
|
||||
- Output `EXEC_FAILED:<issueId>:<reason>` on its own line when implementation fails
|
||||
- Log resume command to errors.json on any failure
|
||||
- Use `[executor]` prefix in all status messages
|
||||
|
||||
**NEVER**:
|
||||
- Use any execution backend other than Codex CLI
|
||||
- Create, modify, or read issues beyond the assigned issueId
|
||||
- Spawn subagents
|
||||
- Ask the user for clarification (fail fast with structured error)
|
||||
1
ccw/frontend/src/components/.gitignore
vendored
Normal file
1
ccw/frontend/src/components/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.ace-tool/
|
||||
@@ -63,6 +63,8 @@ export interface HookQuickTemplatesProps {
|
||||
}
|
||||
|
||||
// ========== Hook Templates ==========
|
||||
// NOTE: Hook input is received via stdin (not environment variable)
|
||||
// Use: const fs=require('fs');const p=JSON.parse(fs.readFileSync(0,'utf8')||'{}');
|
||||
|
||||
/**
|
||||
* Predefined hook templates for quick installation
|
||||
@@ -90,7 +92,7 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const p=JSON.parse(process.env.HOOK_INPUT||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(/workflow-session\\.json$|session-metadata\\.json$/.test(file)){const fs=require("fs");try{const content=fs.readFileSync(file,"utf8");const data=JSON.parse(content);const cp=require("child_process");const payload=JSON.stringify({type:"SESSION_STATE_CHANGED",file:file,sessionId:data.session_id||"",status:data.status||"unknown",project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})}catch(e){}}'
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(/workflow-session\\.json$|session-metadata\\.json$/.test(file)){try{const content=fs.readFileSync(file,"utf8");const data=JSON.parse(content);const cp=require("child_process");const payload=JSON.stringify({type:"SESSION_STATE_CHANGED",file:file,sessionId:data.session_id||"",status:data.status||"unknown",project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})}catch(e){}}'
|
||||
]
|
||||
},
|
||||
// --- Notification ---
|
||||
@@ -117,7 +119,7 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const p=JSON.parse(process.env.HOOK_INPUT||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");cp.spawnSync("npx",["prettier","--write",file],{stdio:"inherit",shell:true})}'
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");cp.spawnSync("npx",["prettier","--write",file],{stdio:"inherit",shell:true})}'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -130,7 +132,7 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const p=JSON.parse(process.env.HOOK_INPUT||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");cp.spawnSync("npx",["eslint","--fix",file],{stdio:"inherit",shell:true})}'
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");cp.spawnSync("npx",["eslint","--fix",file],{stdio:"inherit",shell:true})}'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -143,7 +145,7 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const p=JSON.parse(process.env.HOOK_INPUT||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(/\\.env|secret|credential|\\.key$/.test(file)){process.stderr.write("Blocked: modifying sensitive file "+file);process.exit(2)}'
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(/\\.env|secret|credential|\\.key$/.test(file)){process.stderr.write("Blocked: modifying sensitive file "+file);process.exit(2)}'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -169,7 +171,7 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const p=JSON.parse(process.env.HOOK_INPUT||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");const payload=JSON.stringify({type:"FILE_MODIFIED",file:file,project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})}'
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const file=(p.tool_input&&p.tool_input.file_path)||"";if(file){const cp=require("child_process");const payload=JSON.stringify({type:"FILE_MODIFIED",file:file,project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})}'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -181,7 +183,7 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
'const p=JSON.parse(process.env.HOOK_INPUT||"{}");const cp=require("child_process");const payload=JSON.stringify({type:"SESSION_SUMMARY",transcript:p.transcript_path||"",project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})'
|
||||
'const fs=require("fs");const p=JSON.parse(fs.readFileSync(0,"utf8")||"{}");const cp=require("child_process");const payload=JSON.stringify({type:"SESSION_SUMMARY",transcript:p.transcript_path||"",project:process.env.CLAUDE_PROJECT_DIR||process.cwd(),timestamp:Date.now()});cp.spawnSync("curl",["-s","-X","POST","-H","Content-Type: application/json","-d",payload,"http://localhost:3456/api/hook"],{stdio:"inherit",shell:true})'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -221,7 +223,7 @@ export const HOOK_TEMPLATES: readonly HookTemplate[] = [
|
||||
description: 'Sync memory V2 status to dashboard on changes',
|
||||
category: 'notification',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: 'core_memory',
|
||||
matcher: 'mcp__ccw-tools__core_memory',
|
||||
command: 'node',
|
||||
args: [
|
||||
'-e',
|
||||
|
||||
@@ -84,6 +84,9 @@ interface HookTemplate {
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// NOTE: Hook input is received via stdin (not environment variable)
|
||||
// Node.js: const fs=require('fs');const p=JSON.parse(fs.readFileSync(0,'utf8')||'{}');
|
||||
// Bash: INPUT=$(cat)
|
||||
const HOOK_TEMPLATES: Record<string, HookTemplate> = {
|
||||
'memory-update-queue': {
|
||||
event: 'Stop',
|
||||
@@ -95,13 +98,13 @@ const HOOK_TEMPLATES: Record<string, HookTemplate> = {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'node',
|
||||
args: ['-e', "const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({prompt:p.user_prompt||''})],{stdio:'inherit'})"],
|
||||
args: ['-e', "const fs=require('fs');const p=JSON.parse(fs.readFileSync(0,'utf8')||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({prompt:p.prompt||''})],{stdio:'inherit'})"],
|
||||
},
|
||||
'skill-context-auto': {
|
||||
event: 'UserPromptSubmit',
|
||||
matcher: '',
|
||||
command: 'node',
|
||||
args: ['-e', "const p=JSON.parse(process.env.HOOK_INPUT||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({mode:'auto',prompt:p.user_prompt||''})],{stdio:'inherit'})"],
|
||||
args: ['-e', "const fs=require('fs');const p=JSON.parse(fs.readFileSync(0,'utf8')||'{}');require('child_process').spawnSync('ccw',['tool','exec','skill_context_loader',JSON.stringify({mode:'auto',prompt:p.prompt||''})],{stdio:'inherit'})"],
|
||||
},
|
||||
'danger-bash-confirm': {
|
||||
event: 'PreToolUse',
|
||||
@@ -114,7 +117,7 @@ const HOOK_TEMPLATES: Record<string, HookTemplate> = {
|
||||
event: 'PreToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); PROTECTED=".env|.git/|package-lock.json|yarn.lock|.credentials|secrets|id_rsa|.pem$|.key$"; if echo "$FILE" | grep -qiE "$PROTECTED"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"Protected file cannot be modified: $FILE\\"}}" && exit 0; fi; exit 0'],
|
||||
args: ['-c', 'INPUT=$(cat); FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); PROTECTED=".env|.git/|package-lock.json|yarn.lock|.credentials|secrets|id_rsa|.pem$|.key$"; if echo "$FILE" | grep -qiE "$PROTECTED"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"Protected file cannot be modified: $FILE\\"}}" >&2 && exit 2; fi; exit 0'],
|
||||
timeout: 5000,
|
||||
},
|
||||
'danger-git-destructive': {
|
||||
@@ -135,7 +138,7 @@ const HOOK_TEMPLATES: Record<string, HookTemplate> = {
|
||||
event: 'PreToolUse',
|
||||
matcher: 'Write|Edit|Bash',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INPUT=$(cat); TOOL=$(echo "$INPUT" | jq -r ".tool_name // empty"); if [ "$TOOL" = "Bash" ]; then CMD=$(echo "$INPUT" | jq -r ".tool_input.command // empty"); SYS_PATHS="/etc/|/usr/|/bin/|/sbin/|/boot/|/sys/|/proc/|C:\\\\Windows|C:\\\\Program Files"; if echo "$CMD" | grep -qiE "$SYS_PATHS"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"ask\\",\\"permissionDecisionReason\\":\\"System path operation requires confirmation\\"}}" && exit 0; fi; else FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); SYS_PATHS="/etc/|/usr/|/bin/|/sbin/|C:\\\\Windows|C:\\\\Program Files"; if echo "$FILE" | grep -qiE "$SYS_PATHS"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"Cannot modify system file: $FILE\\"}}" && exit 0; fi; fi; exit 0'],
|
||||
args: ['-c', 'INPUT=$(cat); TOOL=$(echo "$INPUT" | jq -r ".tool_name // empty"); if [ "$TOOL" = "Bash" ]; then CMD=$(echo "$INPUT" | jq -r ".tool_input.command // empty"); SYS_PATHS="/etc/|/usr/|/bin/|/sbin/|/boot/|/sys/|/proc/|C:\\\\Windows|C:\\\\Program Files"; if echo "$CMD" | grep -qiE "$SYS_PATHS"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"ask\\",\\"permissionDecisionReason\\":\\"System path operation requires confirmation\\"}}" && exit 0; fi; else FILE=$(echo "$INPUT" | jq -r ".tool_input.file_path // .tool_input.path // empty"); SYS_PATHS="/etc/|/usr/|/bin/|/sbin/|C:\\\\Windows|C:\\\\Program Files"; if echo "$FILE" | grep -qiE "$SYS_PATHS"; then echo "{\\"hookSpecificOutput\\":{\\"hookEventName\\":\\"PreToolUse\\",\\"permissionDecision\\":\\"deny\\",\\"permissionDecisionReason\\":\\"Cannot modify system file: $FILE\\"}}" >&2 && exit 2; fi; fi; exit 0'],
|
||||
timeout: 5000,
|
||||
},
|
||||
'danger-permission-change': {
|
||||
|
||||
@@ -105,15 +105,15 @@ const eventConfig: Record<
|
||||
|
||||
// Event label keys for i18n
|
||||
const eventLabelKeys: Record<HookEvent, string> = {
|
||||
SessionStart: 'hooks.events.sessionStart',
|
||||
UserPromptSubmit: 'hooks.events.userPromptSubmit',
|
||||
SessionEnd: 'hooks.events.sessionEnd',
|
||||
SessionStart: 'specs.hook.event.SessionStart',
|
||||
UserPromptSubmit: 'specs.hook.event.UserPromptSubmit',
|
||||
SessionEnd: 'specs.hook.event.SessionEnd',
|
||||
};
|
||||
|
||||
// Scope label keys for i18n
|
||||
const scopeLabelKeys: Record<HookScope, string> = {
|
||||
global: 'hooks.scope.global',
|
||||
project: 'hooks.scope.project',
|
||||
global: 'specs.hook.scope.global',
|
||||
project: 'specs.hook.scope.project',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -136,7 +136,7 @@ export function HookCard({
|
||||
variant: 'default' as const,
|
||||
icon: <Zap className="h-3 w-3" />,
|
||||
};
|
||||
const eventLabel = formatMessage({ id: eventLabelKeys[hook.event] || 'hooks.events.unknown' });
|
||||
const eventLabel = formatMessage({ id: eventLabelKeys[hook.event] || 'specs.hook.event.SessionStart' });
|
||||
|
||||
const scopeIcon = hook.scope === 'global' ? <Globe className="h-3 w-3" /> : <Folder className="h-3 w-3" />;
|
||||
const scopeLabel = formatMessage({ id: scopeLabelKeys[hook.scope] });
|
||||
@@ -194,7 +194,7 @@ export function HookCard({
|
||||
disabled={actionsDisabled}
|
||||
className="ml-4"
|
||||
>
|
||||
{formatMessage({ id: 'hooks.actions.install' })}
|
||||
{formatMessage({ id: 'specs.hook.install' })}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -256,7 +256,7 @@ export function HookCard({
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={(e) => handleAction(e, 'edit')}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
{formatMessage({ id: 'hooks.actions.edit' })}
|
||||
{formatMessage({ id: 'specs.hook.edit' })}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
@@ -264,7 +264,7 @@ export function HookCard({
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{formatMessage({ id: 'hooks.actions.uninstall' })}
|
||||
{formatMessage({ id: 'specs.hook.uninstall' })}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -46,7 +46,7 @@ import {
|
||||
Layers,
|
||||
Filter,
|
||||
} from 'lucide-react';
|
||||
import { useInstallRecommendedHooks } from '@/hooks/useSystemSettings';
|
||||
import { useInstallRecommendedHooks, useSystemSettings } from '@/hooks/useSystemSettings';
|
||||
import type { InjectionPreviewFile, InjectionPreviewResponse } from '@/lib/api';
|
||||
import { getInjectionPreview, COMMAND_PREVIEWS, type CommandPreviewConfig } from '@/lib/api';
|
||||
|
||||
@@ -197,6 +197,9 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
// State for hooks installation
|
||||
const [installingHookIds, setInstallingHookIds] = useState<string[]>([]);
|
||||
|
||||
// Fetch system settings (for hooks installation status)
|
||||
const systemSettingsQuery = useSystemSettings();
|
||||
|
||||
// State for injection preview
|
||||
const [previewMode, setPreviewMode] = useState<'required' | 'all'>('required');
|
||||
const [categoryFilter, setCategoryFilter] = useState<SpecCategory | 'all'>('all');
|
||||
@@ -349,10 +352,18 @@ export function InjectionControlTab({ className }: InjectionControlTabProps) {
|
||||
|
||||
const installedHookIds = useMemo(() => {
|
||||
const installed = new Set<string>();
|
||||
const hooks = systemSettingsQuery.data?.recommendedHooks;
|
||||
if (hooks) {
|
||||
hooks.forEach(hook => {
|
||||
if (hook.installed) {
|
||||
installed.add(hook.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
return installed;
|
||||
}, []);
|
||||
}, [systemSettingsQuery.data?.recommendedHooks]);
|
||||
|
||||
const installedCount = 0;
|
||||
const installedCount = installedHookIds.size;
|
||||
const allHooksInstalled = installedCount === RECOMMENDED_HOOKS.length;
|
||||
|
||||
const handleInstallHook = useCallback(async (hookId: string) => {
|
||||
|
||||
@@ -104,41 +104,131 @@ export interface ApiError {
|
||||
// ========== CSRF Token Handling ==========
|
||||
|
||||
/**
|
||||
* In-memory CSRF token storage
|
||||
* The token is obtained from X-CSRF-Token response header and stored here
|
||||
* because the XSRF-TOKEN cookie is HttpOnly and cannot be read by JavaScript
|
||||
* CSRF token pool for concurrent request support
|
||||
* The pool maintains multiple tokens to support parallel mutating requests
|
||||
*/
|
||||
let csrfToken: string | null = null;
|
||||
const MAX_CSRF_TOKEN_POOL_SIZE = 5;
|
||||
|
||||
// Token pool queue - FIFO for fair distribution
|
||||
let csrfTokenQueue: string[] = [];
|
||||
|
||||
/**
|
||||
* Get CSRF token from memory
|
||||
* Get a CSRF token from the pool
|
||||
* @returns Token string or undefined if pool is empty
|
||||
*/
|
||||
function getCsrfToken(): string | null {
|
||||
return csrfToken;
|
||||
function getCsrfTokenFromPool(): string | undefined {
|
||||
return csrfTokenQueue.shift();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set CSRF token from response header
|
||||
* Add a CSRF token to the pool with deduplication
|
||||
* @param token - Token to add
|
||||
*/
|
||||
function addCsrfTokenToPool(token: string): void {
|
||||
if (!token) return;
|
||||
// Deduplication: don't add if already in pool
|
||||
if (csrfTokenQueue.includes(token)) return;
|
||||
// Limit pool size
|
||||
if (csrfTokenQueue.length >= MAX_CSRF_TOKEN_POOL_SIZE) return;
|
||||
csrfTokenQueue.push(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current pool size (for debugging)
|
||||
*/
|
||||
export function getCsrfPoolSize(): number {
|
||||
return csrfTokenQueue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock for deduplicating concurrent token fetch requests
|
||||
* Prevents multiple simultaneous calls to fetchTokenSynchronously
|
||||
*/
|
||||
let tokenFetchPromise: Promise<string> | null = null;
|
||||
|
||||
/**
|
||||
* Synchronously fetch a single token when pool is depleted
|
||||
* This blocks the request until a token is available
|
||||
* Uses lock mechanism to prevent concurrent fetch deduplication
|
||||
*/
|
||||
async function fetchTokenSynchronously(): Promise<string> {
|
||||
// If a fetch is already in progress, wait for it
|
||||
if (tokenFetchPromise) {
|
||||
return tokenFetchPromise;
|
||||
}
|
||||
|
||||
// Create new fetch promise and store as lock
|
||||
tokenFetchPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch('/api/csrf-token', {
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch CSRF token');
|
||||
}
|
||||
const data = await response.json();
|
||||
const token = data.csrfToken;
|
||||
if (!token) {
|
||||
throw new Error('No CSRF token in response');
|
||||
}
|
||||
return token;
|
||||
} finally {
|
||||
// Release lock after completion (success or failure)
|
||||
tokenFetchPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return tokenFetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set CSRF token from response header (adds to pool)
|
||||
*/
|
||||
function updateCsrfToken(response: Response): void {
|
||||
const token = response.headers.get('X-CSRF-Token');
|
||||
if (token) {
|
||||
csrfToken = token;
|
||||
addCsrfTokenToPool(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize CSRF token by fetching from server
|
||||
* Initialize CSRF token pool by fetching multiple tokens from server
|
||||
* Should be called once on app initialization
|
||||
*/
|
||||
export async function initializeCsrfToken(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/api/csrf-token', {
|
||||
// Prefetch 5 tokens for pool
|
||||
const response = await fetch(`/api/csrf-token?count=${MAX_CSRF_TOKEN_POOL_SIZE}`, {
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
updateCsrfToken(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to initialize CSRF token pool');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Handle both single token and batch response formats
|
||||
if (data.tokens && Array.isArray(data.tokens)) {
|
||||
// Batch response - add all tokens to pool
|
||||
for (const token of data.tokens) {
|
||||
addCsrfTokenToPool(token);
|
||||
}
|
||||
} else if (data.csrfToken) {
|
||||
// Single token response - add to pool
|
||||
addCsrfTokenToPool(data.csrfToken);
|
||||
}
|
||||
|
||||
console.log(`[CSRF] Token pool initialized with ${csrfTokenQueue.length} tokens`);
|
||||
} catch (error) {
|
||||
console.error('[CSRF] Failed to initialize CSRF token:', error);
|
||||
console.error('[CSRF] Failed to initialize CSRF token pool:', error);
|
||||
// Fallback: try to get at least one token
|
||||
try {
|
||||
const token = await fetchTokenSynchronously();
|
||||
addCsrfTokenToPool(token);
|
||||
} catch (fallbackError) {
|
||||
console.error('[CSRF] Fallback token fetch failed:', fallbackError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +245,18 @@ async function fetchApi<T>(
|
||||
|
||||
// Add CSRF token for mutating requests
|
||||
if (options.method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
|
||||
const token = getCsrfToken();
|
||||
let token = getCsrfTokenFromPool();
|
||||
|
||||
// If pool is depleted, synchronously fetch a new token
|
||||
if (!token) {
|
||||
console.warn('[CSRF] Token pool depleted, fetching synchronously');
|
||||
try {
|
||||
token = await fetchTokenSynchronously();
|
||||
} catch (error) {
|
||||
throw new Error('Failed to acquire CSRF token for request');
|
||||
}
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers.set('X-CSRF-Token', token);
|
||||
}
|
||||
@@ -172,7 +273,7 @@ async function fetchApi<T>(
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
// Update CSRF token from response header
|
||||
// Update CSRF token from response header (adds to pool)
|
||||
updateCsrfToken(response);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -2963,6 +3064,18 @@ export interface ReviewDimension {
|
||||
findings: ReviewFinding[];
|
||||
}
|
||||
|
||||
export interface ReviewSummary {
|
||||
phase?: string;
|
||||
status?: string;
|
||||
severityDistribution?: {
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
};
|
||||
criticalFiles?: string[];
|
||||
}
|
||||
|
||||
export interface ReviewSession {
|
||||
session_id: string;
|
||||
title?: string;
|
||||
@@ -2970,6 +3083,7 @@ export interface ReviewSession {
|
||||
type: 'review';
|
||||
phase?: string;
|
||||
reviewDimensions?: ReviewDimension[];
|
||||
reviewSummary?: ReviewSummary;
|
||||
_isActive?: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
@@ -2986,6 +3100,17 @@ export interface ReviewSessionsResponse {
|
||||
progress?: unknown;
|
||||
}>;
|
||||
};
|
||||
// New: Support activeSessions with review type
|
||||
activeSessions?: Array<{
|
||||
session_id: string;
|
||||
project?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
created_at?: string;
|
||||
hasReview?: boolean;
|
||||
reviewSummary?: ReviewSummary;
|
||||
reviewDimensions?: ReviewDimension[];
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2994,12 +3119,34 @@ export interface ReviewSessionsResponse {
|
||||
export async function fetchReviewSessions(): Promise<ReviewSession[]> {
|
||||
const data = await fetchApi<ReviewSessionsResponse>('/api/data');
|
||||
|
||||
// If reviewSessions field exists (legacy format), use it
|
||||
// Priority 1: Use activeSessions with type='review' or hasReview=true
|
||||
if (data.activeSessions) {
|
||||
const reviewSessions = data.activeSessions.filter(
|
||||
session => session.type === 'review' || session.hasReview
|
||||
);
|
||||
if (reviewSessions.length > 0) {
|
||||
return reviewSessions.map(session => ({
|
||||
session_id: session.session_id,
|
||||
title: session.project || session.session_id,
|
||||
description: '',
|
||||
type: 'review' as const,
|
||||
phase: session.reviewSummary?.phase,
|
||||
reviewDimensions: session.reviewDimensions || [],
|
||||
reviewSummary: session.reviewSummary,
|
||||
_isActive: true,
|
||||
created_at: session.created_at,
|
||||
updated_at: undefined,
|
||||
status: session.status
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Legacy reviewSessions field
|
||||
if (data.reviewSessions && data.reviewSessions.length > 0) {
|
||||
return data.reviewSessions;
|
||||
}
|
||||
|
||||
// Otherwise, transform reviewData.sessions into ReviewSession format
|
||||
// Priority 3: Legacy reviewData.sessions format
|
||||
if (data.reviewData?.sessions) {
|
||||
return data.reviewData.sessions.map(session => ({
|
||||
session_id: session.session_id,
|
||||
@@ -7408,6 +7555,7 @@ export interface SystemSettings {
|
||||
description: string;
|
||||
scope: 'global' | 'project';
|
||||
autoInstall: boolean;
|
||||
installed: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"aria": {
|
||||
"toggleNavigation": "Toggle navigation menu",
|
||||
"refreshWorkspace": "Refresh workspace",
|
||||
|
||||
@@ -96,6 +96,11 @@
|
||||
"message": "Try adjusting your filters or search query.",
|
||||
"noFixProgress": "No fix progress data available"
|
||||
},
|
||||
"notExecuted": {
|
||||
"title": "Review Not Yet Executed",
|
||||
"message": "This review session has been created but the review process has not been started yet. No findings have been generated.",
|
||||
"hint": "💡 Tip: Execute the review workflow to start analyzing code and generate findings."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Review Session Not Found",
|
||||
"message": "The requested review session could not be found."
|
||||
|
||||
@@ -303,12 +303,31 @@
|
||||
"hookCommand": "Command",
|
||||
"hookScope": "Scope",
|
||||
"hookTimeout": "Timeout (ms)",
|
||||
"hookFailMode": "Fail Mode"
|
||||
"hookFailMode": "Fail Mode",
|
||||
"editTitle": "Edit Spec: {title}",
|
||||
"editDescription": "Modify spec metadata and settings."
|
||||
},
|
||||
|
||||
"form": {
|
||||
"readMode": "Read Mode",
|
||||
"priority": "Priority",
|
||||
"keywords": "Keywords"
|
||||
"keywords": "Keywords",
|
||||
"keywordsPlaceholder": "Enter keywords, press Enter or comma to add",
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "Enter spec title",
|
||||
"addKeyword": "Add Keyword",
|
||||
"keywordsHint": "Keywords help match optional specs to relevant tasks",
|
||||
"fileInfo": "File: {file}",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
|
||||
"validation": {
|
||||
"titleRequired": "Title is required"
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"installSuccess": "Hook installed successfully",
|
||||
"installError": "Failed to install hook",
|
||||
"installAllSuccess": "All hooks installed successfully"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"aria": {
|
||||
"toggleNavigation": "切换导航菜单",
|
||||
"refreshWorkspace": "刷新工作区",
|
||||
|
||||
@@ -96,6 +96,11 @@
|
||||
"message": "尝试调整筛选条件或搜索查询。",
|
||||
"noFixProgress": "无修复进度数据"
|
||||
},
|
||||
"notExecuted": {
|
||||
"title": "审查尚未执行",
|
||||
"message": "此审查会话已创建,但审查流程尚未启动。尚未生成任何发现结果。",
|
||||
"hint": "💡 提示:请执行审查工作流以开始分析代码并生成发现结果。"
|
||||
},
|
||||
"notFound": {
|
||||
"title": "未找到审查会话",
|
||||
"message": "无法找到请求的审查会话。"
|
||||
|
||||
@@ -310,12 +310,31 @@
|
||||
"hookCommand": "执行命令",
|
||||
"hookScope": "作用域",
|
||||
"hookTimeout": "超时时间(ms)",
|
||||
"hookFailMode": "失败模式"
|
||||
"hookFailMode": "失败模式",
|
||||
"editTitle": "编辑规范:{title}",
|
||||
"editDescription": "修改规范元数据和设置。"
|
||||
},
|
||||
|
||||
"form": {
|
||||
"readMode": "读取模式",
|
||||
"priority": "优先级",
|
||||
"keywords": "关键词"
|
||||
"keywords": "关键词",
|
||||
"keywordsPlaceholder": "输入关键词,按回车或逗号添加",
|
||||
"title": "标题",
|
||||
"titlePlaceholder": "输入规范标题",
|
||||
"addKeyword": "添加关键词",
|
||||
"keywordsHint": "关键词有助于将选读规范匹配到相关任务",
|
||||
"fileInfo": "文件:{file}",
|
||||
"saving": "保存中..."
|
||||
},
|
||||
|
||||
"validation": {
|
||||
"titleRequired": "标题为必填项"
|
||||
},
|
||||
|
||||
"hooks": {
|
||||
"installSuccess": "钩子安装成功",
|
||||
"installError": "钩子安装失败",
|
||||
"installAllSuccess": "所有钩子安装成功"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,13 +765,32 @@ export function ReviewSessionPage() {
|
||||
{filteredFindings.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
<FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'reviewSession.empty.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'reviewSession.empty.message' })}
|
||||
</p>
|
||||
{/* Check if review hasn't been executed yet */}
|
||||
{reviewSession?.reviewSummary?.status === 'in_progress' &&
|
||||
(!reviewSession?.reviewDimensions || reviewSession.reviewDimensions.length === 0) ? (
|
||||
<>
|
||||
<AlertTriangle className="h-12 w-12 text-amber-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'reviewSession.notExecuted.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{formatMessage({ id: 'reviewSession.notExecuted.message' })}
|
||||
</p>
|
||||
<div className="text-xs text-muted-foreground bg-muted p-3 rounded-lg inline-block">
|
||||
{formatMessage({ id: 'reviewSession.notExecuted.hint' })}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
{formatMessage({ id: 'reviewSession.empty.title' })}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'reviewSession.empty.message' })}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
|
||||
@@ -3,6 +3,7 @@ import { randomBytes } from 'crypto';
|
||||
export interface CsrfTokenManagerOptions {
|
||||
tokenTtlMs?: number;
|
||||
cleanupIntervalMs?: number;
|
||||
maxTokensPerSession?: number;
|
||||
}
|
||||
|
||||
type CsrfTokenRecord = {
|
||||
@@ -13,14 +14,20 @@ type CsrfTokenRecord = {
|
||||
|
||||
const DEFAULT_TOKEN_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
||||
const DEFAULT_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||
const DEFAULT_MAX_TOKENS_PER_SESSION = 5;
|
||||
|
||||
export class CsrfTokenManager {
|
||||
private readonly tokenTtlMs: number;
|
||||
private readonly records = new Map<string, CsrfTokenRecord>();
|
||||
private readonly maxTokensPerSession: number;
|
||||
// sessionId -> (token -> record) - supports multiple tokens per session
|
||||
private readonly sessionTokens = new Map<string, Map<string, CsrfTokenRecord>>();
|
||||
// Quick lookup: token -> sessionId for validation
|
||||
private readonly tokenToSession = new Map<string, string>();
|
||||
private readonly cleanupTimer: NodeJS.Timeout | null;
|
||||
|
||||
constructor(options: CsrfTokenManagerOptions = {}) {
|
||||
this.tokenTtlMs = options.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
|
||||
this.maxTokensPerSession = options.maxTokensPerSession ?? DEFAULT_MAX_TOKENS_PER_SESSION;
|
||||
|
||||
const cleanupIntervalMs = options.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
|
||||
if (cleanupIntervalMs > 0) {
|
||||
@@ -40,50 +47,137 @@ export class CsrfTokenManager {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
}
|
||||
this.records.clear();
|
||||
this.sessionTokens.clear();
|
||||
this.tokenToSession.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single CSRF token for a session
|
||||
*/
|
||||
generateToken(sessionId: string): string {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
this.records.set(token, {
|
||||
sessionId,
|
||||
expiresAtMs: Date.now() + this.tokenTtlMs,
|
||||
used: false,
|
||||
});
|
||||
return token;
|
||||
const tokens = this.generateTokens(sessionId, 1);
|
||||
return tokens[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate multiple CSRF tokens for a session (pool pattern)
|
||||
* @param sessionId - Session identifier
|
||||
* @param count - Number of tokens to generate (max: maxTokensPerSession)
|
||||
* @returns Array of generated tokens
|
||||
*/
|
||||
generateTokens(sessionId: string, count: number): string[] {
|
||||
// Get or create session token map
|
||||
let sessionMap = this.sessionTokens.get(sessionId);
|
||||
if (!sessionMap) {
|
||||
sessionMap = new Map();
|
||||
this.sessionTokens.set(sessionId, sessionMap);
|
||||
}
|
||||
|
||||
// Limit count to max tokens per session
|
||||
const currentCount = sessionMap.size;
|
||||
const availableSlots = Math.max(0, this.maxTokensPerSession - currentCount);
|
||||
const tokensToGenerate = Math.min(count, availableSlots);
|
||||
|
||||
const tokens: string[] = [];
|
||||
const expiresAtMs = Date.now() + this.tokenTtlMs;
|
||||
|
||||
for (let i = 0; i < tokensToGenerate; i++) {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
const record: CsrfTokenRecord = {
|
||||
sessionId,
|
||||
expiresAtMs,
|
||||
used: false,
|
||||
};
|
||||
sessionMap.set(token, record);
|
||||
this.tokenToSession.set(token, sessionId);
|
||||
tokens.push(token);
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a CSRF token against a session
|
||||
* Marks token as used (single-use) on successful validation
|
||||
*/
|
||||
validateToken(token: string, sessionId: string): boolean {
|
||||
const record = this.records.get(token);
|
||||
// Quick lookup: get session from token
|
||||
const tokenSessionId = this.tokenToSession.get(token);
|
||||
if (!tokenSessionId) return false;
|
||||
if (tokenSessionId !== sessionId) return false;
|
||||
|
||||
// Get session's token map
|
||||
const sessionMap = this.sessionTokens.get(sessionId);
|
||||
if (!sessionMap) return false;
|
||||
|
||||
// Get token record
|
||||
const record = sessionMap.get(token);
|
||||
if (!record) return false;
|
||||
if (record.used) return false;
|
||||
if (record.sessionId !== sessionId) return false;
|
||||
|
||||
// Check expiration
|
||||
if (Date.now() > record.expiresAtMs) {
|
||||
this.records.delete(token);
|
||||
this.removeToken(token, sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as used (single-use enforcement)
|
||||
record.used = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a token from the pool
|
||||
*/
|
||||
private removeToken(token: string, sessionId: string): void {
|
||||
const sessionMap = this.sessionTokens.get(sessionId);
|
||||
if (sessionMap) {
|
||||
sessionMap.delete(token);
|
||||
// Clean up empty session maps
|
||||
if (sessionMap.size === 0) {
|
||||
this.sessionTokens.delete(sessionId);
|
||||
}
|
||||
}
|
||||
this.tokenToSession.delete(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of active tokens for a session
|
||||
*/
|
||||
getTokenCount(sessionId: string): number {
|
||||
const sessionMap = this.sessionTokens.get(sessionId);
|
||||
return sessionMap ? sessionMap.size : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of active tokens across all sessions
|
||||
*/
|
||||
getActiveTokenCount(): number {
|
||||
return this.tokenToSession.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired and used tokens
|
||||
*/
|
||||
cleanupExpiredTokens(nowMs: number = Date.now()): number {
|
||||
let removed = 0;
|
||||
|
||||
for (const [token, record] of this.records.entries()) {
|
||||
if (record.used || nowMs > record.expiresAtMs) {
|
||||
this.records.delete(token);
|
||||
removed += 1;
|
||||
for (const [sessionId, sessionMap] of this.sessionTokens.entries()) {
|
||||
for (const [token, record] of sessionMap.entries()) {
|
||||
if (record.used || nowMs > record.expiresAtMs) {
|
||||
sessionMap.delete(token);
|
||||
this.tokenToSession.delete(token);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
// Clean up empty session maps
|
||||
if (sessionMap.size === 0) {
|
||||
this.sessionTokens.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
getActiveTokenCount(): number {
|
||||
return this.records.size;
|
||||
}
|
||||
}
|
||||
|
||||
let csrfManagerInstance: CsrfTokenManager | null = null;
|
||||
|
||||
@@ -51,7 +51,8 @@ function setCsrfCookie(res: ServerResponse, token: string, maxAgeSeconds: number
|
||||
const attributes = [
|
||||
`XSRF-TOKEN=${encodeURIComponent(token)}`,
|
||||
'Path=/',
|
||||
'HttpOnly',
|
||||
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
|
||||
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
|
||||
'SameSite=Strict',
|
||||
`Max-Age=${maxAgeSeconds}`,
|
||||
];
|
||||
|
||||
@@ -71,7 +71,8 @@ function setCsrfCookie(res: ServerResponse, token: string, maxAgeSeconds: number
|
||||
const attributes = [
|
||||
`XSRF-TOKEN=${encodeURIComponent(token)}`,
|
||||
'Path=/',
|
||||
'HttpOnly',
|
||||
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
|
||||
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
|
||||
'SameSite=Strict',
|
||||
`Max-Age=${maxAgeSeconds}`,
|
||||
];
|
||||
@@ -79,17 +80,37 @@ function setCsrfCookie(res: ServerResponse, token: string, maxAgeSeconds: number
|
||||
}
|
||||
|
||||
export async function handleAuthRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
const { pathname, req, res } = ctx;
|
||||
const { pathname, req, res, url } = ctx;
|
||||
|
||||
if (pathname === '/api/csrf-token' && req.method === 'GET') {
|
||||
const sessionId = getOrCreateSessionId(req, res);
|
||||
const tokenManager = getCsrfTokenManager();
|
||||
const csrfToken = tokenManager.generateToken(sessionId);
|
||||
|
||||
res.setHeader('X-CSRF-Token', csrfToken);
|
||||
setCsrfCookie(res, csrfToken, 15 * 60);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ csrfToken }));
|
||||
// Check for count parameter (pool pattern)
|
||||
const countParam = url.searchParams.get('count');
|
||||
const count = countParam ? Math.min(Math.max(1, parseInt(countParam, 10) || 1), 10) : 1;
|
||||
|
||||
if (count === 1) {
|
||||
// Single token response (existing behavior)
|
||||
const csrfToken = tokenManager.generateToken(sessionId);
|
||||
res.setHeader('X-CSRF-Token', csrfToken);
|
||||
setCsrfCookie(res, csrfToken, 15 * 60);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({ csrfToken }));
|
||||
} else {
|
||||
// Batch token response (pool pattern)
|
||||
const tokens = tokenManager.generateTokens(sessionId, count);
|
||||
const firstToken = tokens[0];
|
||||
|
||||
// Set header and cookie with first token for compatibility
|
||||
res.setHeader('X-CSRF-Token', firstToken);
|
||||
setCsrfCookie(res, firstToken, 15 * 60);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(JSON.stringify({
|
||||
tokens,
|
||||
expiresIn: 15 * 60, // seconds
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,75 @@ function getHooksConfig(projectPath: string): { global: { path: string; hooks: u
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize hook data to Claude Code's official nested format
|
||||
* Official format: { matcher?: string, hooks: [{ type: 'command', command: string, timeout?: number }] }
|
||||
*
|
||||
* IMPORTANT: All timeout values from frontend are in MILLISECONDS and must be converted to SECONDS.
|
||||
* Official Claude Code spec requires timeout in seconds.
|
||||
*
|
||||
* @param {Object} hookData - Hook configuration (may be flat or nested format)
|
||||
* @returns {Object} Normalized hook data in official format
|
||||
*/
|
||||
function normalizeHookFormat(hookData: Record<string, unknown>): Record<string, unknown> {
|
||||
/**
|
||||
* Convert timeout from milliseconds to seconds
|
||||
* Frontend always sends milliseconds, Claude Code expects seconds
|
||||
*/
|
||||
const convertTimeout = (timeout: number): number => {
|
||||
// Always convert from milliseconds to seconds
|
||||
// This is safe because:
|
||||
// - Frontend (HookWizard) uses milliseconds (e.g., 5000ms)
|
||||
// - Claude Code official spec requires seconds
|
||||
// - Minimum valid timeout is 1 second, so any value < 1000ms becomes 1s
|
||||
return Math.max(1, Math.ceil(timeout / 1000));
|
||||
};
|
||||
|
||||
// If already in nested format with hooks array, validate and convert
|
||||
if (hookData.hooks && Array.isArray(hookData.hooks)) {
|
||||
// Ensure each hook in the array has required fields
|
||||
const normalizedHooks = (hookData.hooks as Array<Record<string, unknown>>).map(h => {
|
||||
const normalized: Record<string, unknown> = {
|
||||
type: h.type || 'command',
|
||||
command: h.command || '',
|
||||
};
|
||||
// Convert timeout from milliseconds to seconds
|
||||
if (typeof h.timeout === 'number') {
|
||||
normalized.timeout = convertTimeout(h.timeout);
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
|
||||
return {
|
||||
...(hookData.matcher !== undefined ? { matcher: hookData.matcher } : { matcher: '' }),
|
||||
hooks: normalizedHooks,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert flat format to nested format
|
||||
// Old format: { command: '...', timeout: 5000, name: '...', failMode: '...' }
|
||||
// New format: { matcher: '', hooks: [{ type: 'command', command: '...', timeout: 5 }] }
|
||||
if (hookData.command && typeof hookData.command === 'string') {
|
||||
const nestedHook: Record<string, unknown> = {
|
||||
type: 'command',
|
||||
command: hookData.command,
|
||||
};
|
||||
|
||||
// Convert timeout from milliseconds to seconds
|
||||
if (typeof hookData.timeout === 'number') {
|
||||
nestedHook.timeout = convertTimeout(hookData.timeout);
|
||||
}
|
||||
|
||||
return {
|
||||
matcher: typeof hookData.matcher === 'string' ? hookData.matcher : '',
|
||||
hooks: [nestedHook],
|
||||
};
|
||||
}
|
||||
|
||||
// Return as-is if we can't normalize (let Claude Code validate)
|
||||
return hookData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a hook to settings file
|
||||
* @param {string} projectPath
|
||||
@@ -125,17 +194,19 @@ function saveHookToSettings(
|
||||
settings.hooks[event] = [settings.hooks[event]];
|
||||
}
|
||||
|
||||
// Normalize hook data to official format
|
||||
const normalizedData = normalizeHookFormat(hookData);
|
||||
|
||||
// Check if we're replacing an existing hook
|
||||
if (typeof hookData.replaceIndex === 'number') {
|
||||
const index = hookData.replaceIndex;
|
||||
delete hookData.replaceIndex;
|
||||
const hooksForEvent = settings.hooks[event] as unknown[];
|
||||
if (index >= 0 && index < hooksForEvent.length) {
|
||||
hooksForEvent[index] = hookData;
|
||||
hooksForEvent[index] = normalizedData;
|
||||
}
|
||||
} else {
|
||||
// Add new hook
|
||||
(settings.hooks[event] as unknown[]).push(hookData);
|
||||
(settings.hooks[event] as unknown[]).push(normalizedData);
|
||||
}
|
||||
|
||||
// Ensure directory exists and write file
|
||||
|
||||
@@ -90,6 +90,27 @@ function readSettingsFile(filePath: string): Record<string, unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a recommended hook is installed in settings
|
||||
*/
|
||||
function isHookInstalled(
|
||||
settings: Record<string, unknown> & { hooks?: Record<string, unknown[]> },
|
||||
hook: typeof RECOMMENDED_HOOKS[number]
|
||||
): boolean {
|
||||
const hooks = settings.hooks;
|
||||
if (!hooks) return false;
|
||||
|
||||
const eventHooks = hooks[hook.event];
|
||||
if (!eventHooks || !Array.isArray(eventHooks)) return false;
|
||||
|
||||
// Check if hook exists in nested hooks array (by command)
|
||||
return eventHooks.some((entry) => {
|
||||
const entryHooks = (entry as Record<string, unknown>).hooks as Array<Record<string, unknown>> | undefined;
|
||||
if (!entryHooks || !Array.isArray(entryHooks)) return false;
|
||||
return entryHooks.some((h) => (h as Record<string, unknown>).command === hook.command);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system settings from global settings file
|
||||
*/
|
||||
@@ -97,12 +118,18 @@ function getSystemSettings(): {
|
||||
injectionControl: typeof DEFAULT_INJECTION_CONTROL;
|
||||
personalSpecDefaults: typeof DEFAULT_PERSONAL_SPEC_DEFAULTS;
|
||||
devProgressInjection: typeof DEFAULT_DEV_PROGRESS_INJECTION;
|
||||
recommendedHooks: typeof RECOMMENDED_HOOKS;
|
||||
recommendedHooks: Array<typeof RECOMMENDED_HOOKS[number] & { installed: boolean }>;
|
||||
} {
|
||||
const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record<string, unknown>;
|
||||
const settings = readSettingsFile(GLOBAL_SETTINGS_PATH) as Record<string, unknown> & { hooks?: Record<string, unknown[]> };
|
||||
const system = (settings.system || {}) as Record<string, unknown>;
|
||||
const user = (settings.user || {}) as Record<string, unknown>;
|
||||
|
||||
// Check installation status for each recommended hook
|
||||
const recommendedHooksWithStatus = RECOMMENDED_HOOKS.map(hook => ({
|
||||
...hook,
|
||||
installed: isHookInstalled(settings, hook)
|
||||
}));
|
||||
|
||||
return {
|
||||
injectionControl: {
|
||||
...DEFAULT_INJECTION_CONTROL,
|
||||
@@ -116,7 +143,7 @@ function getSystemSettings(): {
|
||||
...DEFAULT_DEV_PROGRESS_INJECTION,
|
||||
...((system.devProgressInjection || {}) as Record<string, unknown>)
|
||||
} as typeof DEFAULT_DEV_PROGRESS_INJECTION,
|
||||
recommendedHooks: RECOMMENDED_HOOKS
|
||||
recommendedHooks: recommendedHooksWithStatus
|
||||
};
|
||||
}
|
||||
|
||||
@@ -202,22 +229,27 @@ function installRecommendedHook(
|
||||
settings.hooks[event] = [];
|
||||
}
|
||||
|
||||
// Check if hook already exists (by command)
|
||||
// Check if hook already exists (by command in nested hooks array)
|
||||
const existingHooks = (settings.hooks[event] || []) as Array<Record<string, unknown>>;
|
||||
const existingIndex = existingHooks.findIndex(
|
||||
(h) => (h as Record<string, unknown>).command === hook.command
|
||||
);
|
||||
const existingIndex = existingHooks.findIndex((entry) => {
|
||||
const hooks = (entry as Record<string, unknown>).hooks as Array<Record<string, unknown>> | undefined;
|
||||
if (!hooks || !Array.isArray(hooks)) return false;
|
||||
return hooks.some((h) => (h as Record<string, unknown>).command === hook.command);
|
||||
});
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
return { success: true, installed: { id: hookId, event, status: 'already-exists' } };
|
||||
}
|
||||
|
||||
// Add new hook
|
||||
// Add new hook in Claude Code's official nested format
|
||||
// Format: { matcher: '', hooks: [{ type: 'command', command: '...', timeout: 5 }] }
|
||||
settings.hooks[event].push({
|
||||
name: hook.name,
|
||||
command: hook.command,
|
||||
timeout: 5000,
|
||||
failMode: 'silent'
|
||||
matcher: '',
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: hook.command,
|
||||
timeout: 5 // seconds, not milliseconds
|
||||
}]
|
||||
});
|
||||
|
||||
// Ensure directory exists
|
||||
|
||||
@@ -257,7 +257,8 @@ function setCsrfCookie(res: http.ServerResponse, token: string, maxAgeSeconds: n
|
||||
const attributes = [
|
||||
`XSRF-TOKEN=${encodeURIComponent(token)}`,
|
||||
'Path=/',
|
||||
'HttpOnly',
|
||||
// Note: XSRF-TOKEN must be readable by JavaScript for CSRF protection to work
|
||||
// The token is also sent via X-CSRF-Token header, so not having HttpOnly is safe
|
||||
'SameSite=Strict',
|
||||
`Max-Age=${maxAgeSeconds}`,
|
||||
];
|
||||
|
||||
@@ -96,7 +96,30 @@ export async function handler(params: Record<string, unknown>): Promise<ToolResu
|
||||
const cwd = getProjectRoot();
|
||||
|
||||
// Normalize paths to array
|
||||
const inputPaths = Array.isArray(paths) ? paths : [paths];
|
||||
// Handle case where paths might be passed as JSON-encoded string (MCP client bug)
|
||||
let inputPaths: string[];
|
||||
if (Array.isArray(paths)) {
|
||||
inputPaths = paths;
|
||||
} else if (typeof paths === 'string') {
|
||||
// Check if it's a JSON-encoded array
|
||||
const trimmed = paths.trim();
|
||||
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (Array.isArray(parsed)) {
|
||||
inputPaths = parsed;
|
||||
} else {
|
||||
inputPaths = [paths];
|
||||
}
|
||||
} catch {
|
||||
inputPaths = [paths];
|
||||
}
|
||||
} else {
|
||||
inputPaths = [paths];
|
||||
}
|
||||
} else {
|
||||
inputPaths = [String(paths)];
|
||||
}
|
||||
|
||||
// Collect all files to read
|
||||
const allFiles: string[] = [];
|
||||
|
||||
@@ -64,8 +64,25 @@ export function isBinaryFile(filePath: string): boolean {
|
||||
|
||||
/**
|
||||
* Convert glob pattern to regex
|
||||
* Supports: *, ?, and brace expansion {a,b,c}
|
||||
*/
|
||||
export function globToRegex(pattern: string): RegExp {
|
||||
// Handle brace expansion: *.{md,json,ts} -> (?:.*\.md|.*\.json|.*\.ts)
|
||||
const braceMatch = pattern.match(/^(.*)\{([^}]+)\}(.*)$/);
|
||||
if (braceMatch) {
|
||||
const [, prefix, options, suffix] = braceMatch;
|
||||
const optionList = options.split(',').map(opt => `${prefix}${opt}${suffix}`);
|
||||
// Create a regex that matches any of the expanded patterns
|
||||
const expandedPatterns = optionList.map(opt => {
|
||||
return opt
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*')
|
||||
.replace(/\?/g, '.');
|
||||
});
|
||||
return new RegExp(`^(?:${expandedPatterns.join('|')})$`, 'i');
|
||||
}
|
||||
|
||||
// Standard glob conversion
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*')
|
||||
|
||||
@@ -60,5 +60,91 @@ describe('CsrfTokenManager', async () => {
|
||||
assert.equal(manager.validateToken(token, 'session-1'), true);
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
// ========== Pool Pattern Tests ==========
|
||||
|
||||
it('generateTokens produces N unique tokens', () => {
|
||||
const manager = new mod.CsrfTokenManager({ cleanupIntervalMs: 0, maxTokensPerSession: 5 });
|
||||
const tokens = manager.generateTokens('session-1', 3);
|
||||
|
||||
assert.equal(tokens.length, 3);
|
||||
// All tokens should be unique
|
||||
assert.equal(new Set(tokens).size, 3);
|
||||
// All tokens should be valid hex
|
||||
for (const token of tokens) {
|
||||
assert.match(token, /^[a-f0-9]{64}$/);
|
||||
}
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
it('generateTokens respects maxTokensPerSession limit', () => {
|
||||
const manager = new mod.CsrfTokenManager({ cleanupIntervalMs: 0, maxTokensPerSession: 5 });
|
||||
// First batch of 5
|
||||
const tokens1 = manager.generateTokens('session-1', 5);
|
||||
assert.equal(tokens1.length, 5);
|
||||
|
||||
// Second batch should be empty (pool full)
|
||||
const tokens2 = manager.generateTokens('session-1', 3);
|
||||
assert.equal(tokens2.length, 0);
|
||||
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
it('getTokenCount returns correct count for session', () => {
|
||||
const manager = new mod.CsrfTokenManager({ cleanupIntervalMs: 0 });
|
||||
manager.generateTokens('session-1', 3);
|
||||
manager.generateTokens('session-2', 2);
|
||||
|
||||
assert.equal(manager.getTokenCount('session-1'), 3);
|
||||
assert.equal(manager.getTokenCount('session-2'), 2);
|
||||
assert.equal(manager.getTokenCount('session-3'), 0);
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
it('validateToken works with pool pattern (multiple tokens per session)', () => {
|
||||
const manager = new mod.CsrfTokenManager({ cleanupIntervalMs: 0 });
|
||||
const tokens = manager.generateTokens('session-1', 3);
|
||||
|
||||
// All tokens should be valid once
|
||||
assert.equal(manager.validateToken(tokens[0], 'session-1'), true);
|
||||
assert.equal(manager.validateToken(tokens[1], 'session-1'), true);
|
||||
assert.equal(manager.validateToken(tokens[2], 'session-1'), true);
|
||||
|
||||
// All tokens should now be invalid (used)
|
||||
assert.equal(manager.validateToken(tokens[0], 'session-1'), false);
|
||||
assert.equal(manager.validateToken(tokens[1], 'session-1'), false);
|
||||
assert.equal(manager.validateToken(tokens[2], 'session-1'), false);
|
||||
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
it('cleanupExpiredTokens handles multiple sessions', () => {
|
||||
const manager = new mod.CsrfTokenManager({ tokenTtlMs: 10, cleanupIntervalMs: 0 });
|
||||
manager.generateTokens('session-1', 3);
|
||||
manager.generateTokens('session-2', 2);
|
||||
|
||||
const removed = manager.cleanupExpiredTokens(Date.now() + 100);
|
||||
assert.equal(removed, 5);
|
||||
assert.equal(manager.getActiveTokenCount(), 0);
|
||||
assert.equal(manager.getTokenCount('session-1'), 0);
|
||||
assert.equal(manager.getTokenCount('session-2'), 0);
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
it('concurrent requests can use different tokens from pool', () => {
|
||||
const manager = new mod.CsrfTokenManager({ cleanupIntervalMs: 0 });
|
||||
const tokens = manager.generateTokens('session-1', 5);
|
||||
|
||||
// Simulate 5 concurrent requests using different tokens
|
||||
const results = tokens.map(token => manager.validateToken(token, 'session-1'));
|
||||
|
||||
// All should succeed
|
||||
assert.deepEqual(results, [true, true, true, true, true]);
|
||||
|
||||
// Token count should still be 5 (but all marked as used)
|
||||
assert.equal(manager.getTokenCount('session-1'), 5);
|
||||
|
||||
manager.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
161
docs/.review/skill-team-comparison-review.md
Normal file
161
docs/.review/skill-team-comparison-review.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Code Review Report
|
||||
|
||||
**Target**: D:\Claude_dms3\docs\skill-team-comparison.md
|
||||
**Date**: 2026-03-01
|
||||
**Dimensions**: Correctness, Readability (Maintainability)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Dimension | Critical | High | Medium | Low | Info |
|
||||
|-----------|----------|------|--------|-----|------|
|
||||
| **Correctness** | 0 | 1 | 2 | 1 | 0 |
|
||||
| **Readability** | 0 | 0 | 1 | 2 | 1 |
|
||||
|
||||
**Quality Gate**: ⚠️ WARN (1 High issue found)
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### Correctness Issues
|
||||
|
||||
#### CORR-001 [High] team-issue 角色任务前缀描述不准确
|
||||
|
||||
**Location**: 行 71, 288
|
||||
|
||||
**Issue**: 文档中 team-issue 的角色描述为 `explorer, planner, reviewer, integrator, implementer`,但未说明对应的任务前缀 (EXPLORE-*, SOLVE-*, AUDIT-*, MARSHAL-*, BUILD-*)。
|
||||
|
||||
**Current**:
|
||||
```
|
||||
| **team-issue** | explorer, planner, reviewer, integrator, implementer | general-purpose agents | Issue处理流程 | 探索→规划→审查→集成→实现 |
|
||||
```
|
||||
|
||||
**Expected**: 添加任务前缀说明以提高准确性
|
||||
|
||||
**Fix**:
|
||||
```markdown
|
||||
| **team-issue** | explorer (EXPLORE-*), planner (SOLVE-*), reviewer (AUDIT-*), integrator (MARSHAL-*), implementer (BUILD-*) | general-purpose agents | Issue处理流程 | 探索→规划→审查→集成→实现 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### CORR-002 [Medium] team-executor-v2 前置条件描述不完整
|
||||
|
||||
**Location**: 行 70, 84
|
||||
|
||||
**Issue**: 文档说 team-executor-v2 需要"现有team-coordinate会话",但实际它可以恢复任何 team-* 会话。
|
||||
|
||||
**Current**:
|
||||
```
|
||||
| **team-executor-v2** | (动态角色) | team-worker agents | 恢复执行 | 纯执行,无分析,需现有会话 |
|
||||
```
|
||||
|
||||
**Expected**: 更准确的描述
|
||||
|
||||
**Fix**:
|
||||
```markdown
|
||||
| **team-executor-v2** | (继承会话角色) | team-worker agents | 恢复执行 | 纯执行,无分析,需现有team会话 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### CORR-003 [Medium] 遗漏 workflow-wave-plan 命令
|
||||
|
||||
**Location**: 规划类命令对比表
|
||||
|
||||
**Issue**: 系统中存在 `workflow-wave-plan` 命令,但未在对比表中列出。
|
||||
|
||||
**Recommendation**: 在规划类命令中添加此命令
|
||||
|
||||
---
|
||||
|
||||
#### CORR-004 [Low] team-planex 角色描述
|
||||
|
||||
**Location**: 行 34, 294
|
||||
|
||||
**Issue**: team-planex 角色描述为 `planner, executor`,但实际实现可能有更多细节。
|
||||
|
||||
**Recommendation**: 验证并补充详细角色信息
|
||||
|
||||
---
|
||||
|
||||
### Readability Issues
|
||||
|
||||
#### READ-001 [Medium] 决策流程图格式
|
||||
|
||||
**Location**: 行 226-249
|
||||
|
||||
**Issue**: ASCII 决策流程图在某些 Markdown 渲染器中可能显示不正确。
|
||||
|
||||
**Recommendation**: 考虑使用 Mermaid 图表或添加渲染说明
|
||||
|
||||
---
|
||||
|
||||
#### READ-002 [Low] 表格宽度
|
||||
|
||||
**Location**: 多处表格
|
||||
|
||||
**Issue**: 部分表格列内容较长,在窄屏设备上可能需要水平滚动。
|
||||
|
||||
**Recommendation**: 可接受,但可考虑在未来版本中优化
|
||||
|
||||
---
|
||||
|
||||
#### READ-003 [Low] 命令调用方式一致性
|
||||
|
||||
**Location**: 命令速查表部分
|
||||
|
||||
**Issue**: 部分命令同时列出了 `Skill()` 和 `/command` 两种调用方式,部分只有一种。
|
||||
|
||||
**Recommendation**: 保持一致的格式
|
||||
|
||||
---
|
||||
|
||||
#### READ-004 [Info] 文档版本管理建议
|
||||
|
||||
**Location**: 文档末尾
|
||||
|
||||
**Suggestion**: 建议添加文档变更历史或链接到 CHANGELOG
|
||||
|
||||
---
|
||||
|
||||
## Recommended Actions
|
||||
|
||||
### Must Fix (Before Final)
|
||||
|
||||
1. **CORR-001**: 修复 team-issue 角色前缀描述
|
||||
|
||||
### Should Fix (Next Iteration)
|
||||
|
||||
2. **CORR-002**: 更新 team-executor-v2 描述
|
||||
3. **CORR-003**: 添加遗漏的命令
|
||||
|
||||
### Nice to Have
|
||||
|
||||
4. **READ-001**: 考虑图表格式优化
|
||||
5. **READ-004**: 添加版本管理
|
||||
|
||||
---
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
以下问题已在审查后立即修复:
|
||||
|
||||
### FIX-001: team-issue 角色前缀
|
||||
|
||||
**Before**:
|
||||
```
|
||||
| **team-issue** | explorer, planner, reviewer, integrator, implementer |
|
||||
```
|
||||
|
||||
**After**:
|
||||
```
|
||||
| **team-issue** | explorer (EXPLORE), planner (SOLVE), reviewer (AUDIT), integrator (MARSHAL), implementer (BUILD) |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Review completed: 2026-03-01*
|
||||
*Reviewer: Claude Code (team-coordinate-v2)*
|
||||
@@ -1,5 +1,7 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { withMermaid } from 'vitepress-plugin-mermaid'
|
||||
import { transformDemoBlocks } from './theme/markdownTransform'
|
||||
import path from 'path'
|
||||
|
||||
const repoName = process.env.GITHUB_REPOSITORY?.split('/')[1]
|
||||
const isUserOrOrgSite = Boolean(repoName && repoName.endsWith('.github.io'))
|
||||
@@ -11,7 +13,7 @@ const base =
|
||||
export default withMermaid(defineConfig({
|
||||
title: 'Claude Code Workflow Documentation',
|
||||
description: 'Claude Code Workspace - Advanced AI-Powered Development Environment',
|
||||
lang: 'zh-CN',
|
||||
lang: 'en-US',
|
||||
base,
|
||||
|
||||
// Ignore dead links for incomplete docs
|
||||
@@ -44,12 +46,22 @@ export default withMermaid(defineConfig({
|
||||
|
||||
// Vite build/dev optimizations
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '../../ccw/frontend/src'),
|
||||
'@/components': path.resolve(__dirname, '../../ccw/frontend/src/components'),
|
||||
'@/lib': path.resolve(__dirname, '../../ccw/frontend/src/lib')
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['flexsearch']
|
||||
include: ['flexsearch', 'react', 'react-dom']
|
||||
},
|
||||
build: {
|
||||
target: 'es2019',
|
||||
cssCodeSplit: true
|
||||
},
|
||||
ssr: {
|
||||
noExternal: ['react', 'react-dom', 'class-variance-authority', 'clsx', 'tailwind-merge']
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,12 +81,7 @@ export default withMermaid(defineConfig({
|
||||
{ text: 'Commands', link: '/commands/claude/' },
|
||||
{ text: 'Skills', link: '/skills/' },
|
||||
{ text: 'Features', link: '/features/spec' },
|
||||
{
|
||||
text: 'Languages',
|
||||
items: [
|
||||
{ text: '简体中文', link: '/zh/guide/ch01-what-is-claude-dms3' }
|
||||
]
|
||||
}
|
||||
{ text: 'Components', link: '/components/' }
|
||||
],
|
||||
|
||||
// Sidebar - 优化导航结构,增加二级标题和归类
|
||||
@@ -112,6 +119,7 @@ export default withMermaid(defineConfig({
|
||||
{ text: 'Workflow', link: '/commands/claude/workflow' },
|
||||
{ text: 'Session', link: '/commands/claude/session' },
|
||||
{ text: 'Issue', link: '/commands/claude/issue' },
|
||||
{ text: 'IDAW', link: '/commands/claude/idaw' },
|
||||
{ text: 'Memory', link: '/commands/claude/memory' },
|
||||
{ text: 'CLI', link: '/commands/claude/cli' },
|
||||
{ text: 'UI Design', link: '/commands/claude/ui-design' }
|
||||
@@ -128,6 +136,20 @@ export default withMermaid(defineConfig({
|
||||
}
|
||||
],
|
||||
'/skills/': [
|
||||
{
|
||||
text: 'Overview',
|
||||
collapsible: false,
|
||||
items: [
|
||||
{ text: 'Skills Guide', link: '/skills/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '📚 Conventions',
|
||||
collapsible: true,
|
||||
items: [
|
||||
{ text: 'Naming Conventions', link: '/skills/naming-conventions' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '⚡ Claude Skills',
|
||||
collapsible: true,
|
||||
@@ -181,6 +203,21 @@ export default withMermaid(defineConfig({
|
||||
]
|
||||
}
|
||||
],
|
||||
'/components/': [
|
||||
{
|
||||
text: 'UI Components',
|
||||
collapsible: true,
|
||||
items: [
|
||||
{ text: 'Overview', link: '/components/index' },
|
||||
{ text: 'Button', link: '/components/ui/button' },
|
||||
{ text: 'Card', link: '/components/ui/card' },
|
||||
{ text: 'Input', link: '/components/ui/input' },
|
||||
{ text: 'Select', link: '/components/ui/select' },
|
||||
{ text: 'Checkbox', link: '/components/ui/checkbox' },
|
||||
{ text: 'Badge', link: '/components/ui/badge' }
|
||||
]
|
||||
}
|
||||
],
|
||||
'/mcp/': [
|
||||
{
|
||||
text: '🔗 MCP Tools',
|
||||
@@ -275,7 +312,14 @@ export default withMermaid(defineConfig({
|
||||
'mermaid'
|
||||
],
|
||||
config: (md) => {
|
||||
// Add markdown-it plugins if needed
|
||||
md.core.ruler.before('block', 'demo-blocks', (state) => {
|
||||
const src = state.src
|
||||
const filePath = (state as any).path || ''
|
||||
const transformed = transformDemoBlocks(src, { path: filePath })
|
||||
if (transformed !== src) {
|
||||
state.src = transformed
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -299,13 +343,7 @@ export default withMermaid(defineConfig({
|
||||
{ text: '指南', link: '/zh/guide/ch01-what-is-claude-dms3' },
|
||||
{ text: '命令', link: '/zh/commands/claude/' },
|
||||
{ text: '技能', link: '/zh/skills/claude-index' },
|
||||
{ text: '功能', link: '/zh/features/spec' },
|
||||
{
|
||||
text: '语言',
|
||||
items: [
|
||||
{ text: 'English', link: '/guide/ch01-what-is-claude-dms3' }
|
||||
]
|
||||
}
|
||||
{ text: '功能', link: '/zh/features/spec' }
|
||||
],
|
||||
sidebar: {
|
||||
'/zh/guide/': [
|
||||
@@ -341,6 +379,7 @@ export default withMermaid(defineConfig({
|
||||
{ text: '工作流', link: '/zh/commands/claude/workflow' },
|
||||
{ text: '会话管理', link: '/zh/commands/claude/session' },
|
||||
{ text: 'Issue', link: '/zh/commands/claude/issue' },
|
||||
{ text: 'IDAW', link: '/zh/commands/claude/idaw' },
|
||||
{ text: 'Memory', link: '/zh/commands/claude/memory' },
|
||||
{ text: 'CLI', link: '/zh/commands/claude/cli' },
|
||||
{ text: 'UI 设计', link: '/zh/commands/claude/ui-design' }
|
||||
@@ -357,6 +396,20 @@ export default withMermaid(defineConfig({
|
||||
}
|
||||
],
|
||||
'/zh/skills/': [
|
||||
{
|
||||
text: '概述',
|
||||
collapsible: false,
|
||||
items: [
|
||||
{ text: '技能指南', link: '/zh/skills/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '📚 规范',
|
||||
collapsible: true,
|
||||
items: [
|
||||
{ text: '命名规范', link: '/zh/skills/naming-conventions' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '⚡ Claude Skills',
|
||||
collapsible: true,
|
||||
@@ -424,6 +477,34 @@ export default withMermaid(defineConfig({
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
'zh-CN': {
|
||||
label: '简体中文',
|
||||
lang: 'zh-CN',
|
||||
title: 'Claude Code Workflow 文档',
|
||||
description: 'Claude Code Workspace - 高级 AI 驱动开发环境',
|
||||
themeConfig: {
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: '本页目录'
|
||||
},
|
||||
nav: [
|
||||
{ text: '功能', link: '/zh-CN/features/dashboard' }
|
||||
],
|
||||
sidebar: {
|
||||
'/zh-CN/features/': [
|
||||
{
|
||||
text: '⚙️ 核心功能',
|
||||
collapsible: false,
|
||||
items: [
|
||||
{ text: 'Dashboard 面板', link: '/zh-CN/features/dashboard' },
|
||||
{ text: 'Terminal 终端监控', link: '/zh-CN/features/terminal' },
|
||||
{ text: 'Queue 队列管理', link: '/zh-CN/features/queue' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
326
docs/.vitepress/demos/ComponentGallery.tsx
Normal file
326
docs/.vitepress/demos/ComponentGallery.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* Component Gallery Demo
|
||||
* Interactive showcase of all UI components
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function ComponentGallery() {
|
||||
const [selectedCategory, setSelectedCategory] = useState('all')
|
||||
const [buttonVariant, setButtonVariant] = useState('default')
|
||||
const [switchState, setSwitchState] = useState(false)
|
||||
const [checkboxState, setCheckboxState] = useState(false)
|
||||
const [selectedTab, setSelectedTab] = useState('variants')
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', label: 'All Components' },
|
||||
{ id: 'buttons', label: 'Buttons' },
|
||||
{ id: 'forms', label: 'Forms' },
|
||||
{ id: 'feedback', label: 'Feedback' },
|
||||
{ id: 'navigation', label: 'Navigation' },
|
||||
{ id: 'overlays', label: 'Overlays' },
|
||||
]
|
||||
|
||||
const buttonVariants = ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">UI Component Library</h1>
|
||||
<p className="text-muted-foreground">Interactive showcase of all available UI components</p>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategory(cat.id)}
|
||||
className={`px-4 py-2 rounded-md text-sm transition-colors ${
|
||||
selectedCategory === cat.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'border hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Buttons Section */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'buttons') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Buttons</h2>
|
||||
<div className="space-y-6">
|
||||
{/* Variant Selector */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Variant</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{buttonVariants.map((variant) => (
|
||||
<button
|
||||
key={variant}
|
||||
onClick={() => setButtonVariant(variant)}
|
||||
className={`px-4 py-2 rounded-md text-sm capitalize transition-colors ${
|
||||
buttonVariant === variant
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-ring'
|
||||
: 'border hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{variant}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Button Sizes */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Sizes</label>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<button className={`h-8 rounded-md px-3 text-sm ${buttonVariant === 'default' ? 'bg-primary text-primary-foreground' : 'border'}`}>
|
||||
Small
|
||||
</button>
|
||||
<button className={`h-10 px-4 py-2 rounded-md text-sm ${buttonVariant === 'default' ? 'bg-primary text-primary-foreground' : 'border'}`}>
|
||||
Default
|
||||
</button>
|
||||
<button className={`h-11 rounded-md px-8 text-sm ${buttonVariant === 'default' ? 'bg-primary text-primary-foreground' : 'border'}`}>
|
||||
Large
|
||||
</button>
|
||||
<button className={`h-10 w-10 rounded-md flex items-center justify-center ${buttonVariant === 'default' ? 'bg-primary text-primary-foreground' : 'border'}`}>
|
||||
⚙
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All Button Variants */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">All Variants</label>
|
||||
<div className="flex flex-wrap gap-3 p-4 border rounded-lg bg-muted/20">
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-primary text-primary-foreground hover:opacity-90">Default</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-destructive text-destructive-foreground hover:opacity-90">Destructive</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm border bg-background hover:bg-accent">Outline</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-secondary text-secondary-foreground hover:opacity-80">Secondary</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm hover:bg-accent">Ghost</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm text-primary underline-offset-4 hover:underline">Link</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:opacity-90">Gradient</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Forms Section */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'forms') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Form Components</h2>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Input */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Input</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Error state"
|
||||
className="flex h-10 w-full rounded-md border border-destructive bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-destructive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Textarea */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Textarea</label>
|
||||
<textarea
|
||||
placeholder="Enter multi-line text..."
|
||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Checkbox */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Checkbox</label>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" className="h-4 w-4 rounded border border-primary" checked={checkboxState} onChange={(e) => setCheckboxState(e.target.checked)} />
|
||||
<span>Accept terms and conditions</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer opacity-50">
|
||||
<input type="checkbox" className="h-4 w-4 rounded border border-primary" />
|
||||
<span>Subscribe to newsletter</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Switch */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Switch</label>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<div className="relative">
|
||||
<input type="checkbox" className="sr-only peer" checked={switchState} onChange={(e) => setSwitchState(e.target.checked)} />
|
||||
<div className="w-9 h-5 bg-input rounded-full peer peer-focus:ring-2 peer-focus:ring-ring peer-checked:bg-primary after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-background after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full" />
|
||||
</div>
|
||||
<span className="text-sm">Enable notifications {switchState ? '(on)' : '(off)'}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Select */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Select</label>
|
||||
<select className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<option value="">Choose an option</option>
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="3">Option 3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Form Actions</label>
|
||||
<div className="flex gap-2">
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-primary text-primary-foreground hover:opacity-90">Submit</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm border hover:bg-accent">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Feedback Section */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'feedback') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Feedback Components</h2>
|
||||
|
||||
{/* Badges */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Badges</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-primary text-primary-foreground">Default</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-secondary text-secondary-foreground">Secondary</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-destructive text-destructive-foreground">Destructive</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-success text-white">Success</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-warning text-white">Warning</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-info text-white">Info</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold text-foreground">Outline</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Progress Bars</label>
|
||||
<div className="space-y-3 max-w-md">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Processing...</span>
|
||||
<span>65%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-primary rounded-full transition-all" style={{ width: '65%' }}/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>Uploading...</span>
|
||||
<span>30%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-500 rounded-full transition-all" style={{ width: '30%' }}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alerts */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Alerts</label>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 p-4 border rounded-lg bg-destructive/10 border-destructive/20 text-destructive">
|
||||
<span className="text-lg">⚠</span>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">Error occurred</div>
|
||||
<div className="text-xs mt-1 opacity-80">Something went wrong. Please try again.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 border rounded-lg bg-success/10 border-success/20 text-success">
|
||||
<span className="text-lg">✓</span>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">Success!</div>
|
||||
<div className="text-xs mt-1 opacity-80">Your changes have been saved.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Navigation Section */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'navigation') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Navigation Components</h2>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Tabs</label>
|
||||
<div className="border-b">
|
||||
<div className="flex gap-4">
|
||||
{['Overview', 'Documentation', 'API Reference', 'Examples'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setSelectedTab(tab.toLowerCase().replace(' ', '-'))}
|
||||
className={`pb-3 px-1 text-sm border-b-2 transition-colors ${
|
||||
selectedTab === tab.toLowerCase().replace(' ', '-')
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Breadcrumb</label>
|
||||
<nav className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<a href="#" className="hover:text-foreground">Home</a>
|
||||
<span>/</span>
|
||||
<a href="#" className="hover:text-foreground">Components</a>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">Library</span>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Overlays Section */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'overlays') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Overlay Components</h2>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4 text-sm">
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h3 className="font-medium mb-2">Dialog</h3>
|
||||
<p className="text-muted-foreground text-xs">Modal dialogs for focused user interactions.</p>
|
||||
<button className="mt-3 px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded">Open Dialog</button>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h3 className="font-medium mb-2">Drawer</h3>
|
||||
<p className="text-muted-foreground text-xs">Side panels that slide in from screen edges.</p>
|
||||
<button className="mt-3 px-3 py-1.5 text-xs border rounded hover:bg-accent">Open Drawer</button>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h3 className="font-medium mb-2">Dropdown Menu</h3>
|
||||
<p className="text-muted-foreground text-xs">Context menus and action lists.</p>
|
||||
<button className="mt-3 px-3 py-1.5 text-xs border rounded hover:bg-accent">▼ Open Menu</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
326
docs/.vitepress/demos/ComponentGalleryZh.tsx
Normal file
326
docs/.vitepress/demos/ComponentGalleryZh.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* 组件库展示演示 (中文版)
|
||||
* 所有 UI 组件的交互式展示
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function ComponentGalleryZh() {
|
||||
const [selectedCategory, setSelectedCategory] = useState('all')
|
||||
const [buttonVariant, setButtonVariant] = useState('default')
|
||||
const [switchState, setSwitchState] = useState(false)
|
||||
const [checkboxState, setCheckboxState] = useState(false)
|
||||
const [selectedTab, setSelectedTab] = useState('variants')
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', label: '全部组件' },
|
||||
{ id: 'buttons', label: '按钮' },
|
||||
{ id: 'forms', label: '表单' },
|
||||
{ id: 'feedback', label: '反馈' },
|
||||
{ id: 'navigation', label: '导航' },
|
||||
{ id: 'overlays', label: '叠加层' },
|
||||
]
|
||||
|
||||
const buttonVariants = ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-8">
|
||||
{/* 标题 */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">UI 组件库</h1>
|
||||
<p className="text-muted-foreground">所有可用 UI 组件的交互式展示</p>
|
||||
</div>
|
||||
|
||||
{/* 分类筛选 */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategory(cat.id)}
|
||||
className={`px-4 py-2 rounded-md text-sm transition-colors ${
|
||||
selectedCategory === cat.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'border hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 按钮部分 */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'buttons') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">按钮</h2>
|
||||
<div className="space-y-6">
|
||||
{/* 变体选择器 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">变体</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{buttonVariants.map((variant) => (
|
||||
<button
|
||||
key={variant}
|
||||
onClick={() => setButtonVariant(variant)}
|
||||
className={`px-4 py-2 rounded-md text-sm capitalize transition-colors ${
|
||||
buttonVariant === variant
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-ring'
|
||||
: 'border hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{variant}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 按钮尺寸 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">尺寸</label>
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<button className={`h-8 rounded-md px-3 text-sm ${buttonVariant === 'default' ? 'bg-primary text-primary-foreground' : 'border'}`}>
|
||||
小
|
||||
</button>
|
||||
<button className={`h-10 px-4 py-2 rounded-md text-sm ${buttonVariant === 'default' ? 'bg-primary text-primary-foreground' : 'border'}`}>
|
||||
默认
|
||||
</button>
|
||||
<button className={`h-11 rounded-md px-8 text-sm ${buttonVariant === 'default' ? 'bg-primary text-primary-foreground' : 'border'}`}>
|
||||
大
|
||||
</button>
|
||||
<button className={`h-10 w-10 rounded-md flex items-center justify-center ${buttonVariant === 'default' ? 'bg-primary text-primary-foreground' : 'border'}`}>
|
||||
⚙
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 所有按钮变体 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">所有变体</label>
|
||||
<div className="flex flex-wrap gap-3 p-4 border rounded-lg bg-muted/20">
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-primary text-primary-foreground hover:opacity-90">默认</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-destructive text-destructive-foreground hover:opacity-90">危险</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm border bg-background hover:bg-accent">轮廓</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-secondary text-secondary-foreground hover:opacity-80">次要</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm hover:bg-accent">幽灵</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm text-primary underline-offset-4 hover:underline">链接</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:opacity-90">渐变</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 表单部分 */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'forms') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">表单组件</h2>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* 输入框 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">输入框</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="输入文本..."
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="错误状态"
|
||||
className="flex h-10 w-full rounded-md border border-destructive bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-destructive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 文本域 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">文本域</label>
|
||||
<textarea
|
||||
placeholder="输入多行文本..."
|
||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 复选框 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">复选框</label>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input type="checkbox" className="h-4 w-4 rounded border border-primary" checked={checkboxState} onChange={(e) => setCheckboxState(e.target.checked)} />
|
||||
<span>接受条款和条件</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer opacity-50">
|
||||
<input type="checkbox" className="h-4 w-4 rounded border border-primary" />
|
||||
<span>订阅新闻通讯</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 开关 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">开关</label>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<div className="relative">
|
||||
<input type="checkbox" className="sr-only peer" checked={switchState} onChange={(e) => setSwitchState(e.target.checked)} />
|
||||
<div className="w-9 h-5 bg-input rounded-full peer peer-focus:ring-2 peer-focus:ring-ring peer-checked:bg-primary after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-background after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full" />
|
||||
</div>
|
||||
<span className="text-sm">启用通知 {switchState ? '(开)' : '(关)'}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 选择器 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">选择器</label>
|
||||
<select className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<option value="">选择一个选项</option>
|
||||
<option value="1">选项 1</option>
|
||||
<option value="2">选项 2</option>
|
||||
<option value="3">选项 3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 表单操作 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">表单操作</label>
|
||||
<div className="flex gap-2">
|
||||
<button className="px-4 py-2 rounded-md text-sm bg-primary text-primary-foreground hover:opacity-90">提交</button>
|
||||
<button className="px-4 py-2 rounded-md text-sm border hover:bg-accent">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 反馈部分 */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'feedback') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">反馈组件</h2>
|
||||
|
||||
{/* 徽标 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">徽标</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-primary text-primary-foreground">默认</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-secondary text-secondary-foreground">次要</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-destructive text-destructive-foreground">危险</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-success text-white">成功</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-warning text-white">警告</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-info text-white">信息</span>
|
||||
<span className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold text-foreground">轮廓</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">进度条</label>
|
||||
<div className="space-y-3 max-w-md">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>处理中...</span>
|
||||
<span>65%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-primary rounded-full transition-all" style={{ width: '65%' }}/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1">
|
||||
<span>上传中...</span>
|
||||
<span>30%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-500 rounded-full transition-all" style={{ width: '30%' }}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 提示 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">提示</label>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 p-4 border rounded-lg bg-destructive/10 border-destructive/20 text-destructive">
|
||||
<span className="text-lg">⚠</span>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">发生错误</div>
|
||||
<div className="text-xs mt-1 opacity-80">出错了,请重试。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 border rounded-lg bg-success/10 border-success/20 text-success">
|
||||
<span className="text-lg">✓</span>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">成功!</div>
|
||||
<div className="text-xs mt-1 opacity-80">您的更改已保存。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 导航部分 */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'navigation') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">导航组件</h2>
|
||||
|
||||
{/* 标签页 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">标签页</label>
|
||||
<div className="border-b">
|
||||
<div className="flex gap-4">
|
||||
{['概览', '文档', 'API 参考', '示例'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setSelectedTab(tab.toLowerCase().replace(' ', '-'))}
|
||||
className={`pb-3 px-1 text-sm border-b-2 transition-colors ${
|
||||
selectedTab === tab.toLowerCase().replace(' ', '-')
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 面包屑 */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">面包屑</label>
|
||||
<nav className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<a href="#" className="hover:text-foreground">首页</a>
|
||||
<span>/</span>
|
||||
<a href="#" className="hover:text-foreground">组件</a>
|
||||
<span>/</span>
|
||||
<span className="text-foreground">库</span>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 叠加层部分 */}
|
||||
{(selectedCategory === 'all' || selectedCategory === 'overlays') && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">叠加层组件</h2>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4 text-sm">
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h3 className="font-medium mb-2">对话框</h3>
|
||||
<p className="text-muted-foreground text-xs">用于专注用户交互的模态对话框。</p>
|
||||
<button className="mt-3 px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded">打开对话框</button>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h3 className="font-medium mb-2">抽屉</h3>
|
||||
<p className="text-muted-foreground text-xs">从屏幕边缘滑入的侧边面板。</p>
|
||||
<button className="mt-3 px-3 py-1.5 text-xs border rounded hover:bg-accent">打开抽屉</button>
|
||||
</div>
|
||||
<div className="p-4 border rounded-lg">
|
||||
<h3 className="font-medium mb-2">下拉菜单</h3>
|
||||
<p className="text-muted-foreground text-xs">上下文菜单和操作列表。</p>
|
||||
<button className="mt-3 px-3 py-1.5 text-xs border rounded hover:bg-accent">▼ 打开菜单</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
137
docs/.vitepress/demos/DashboardOverview.tsx
Normal file
137
docs/.vitepress/demos/DashboardOverview.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Dashboard Overview Demo
|
||||
* Shows the main dashboard layout with widgets
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
export default function DashboardOverview() {
|
||||
return (
|
||||
<div className="space-y-6 p-6 bg-background min-h-[600px]">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Dashboard</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Project overview and activity monitoring
|
||||
</p>
|
||||
</div>
|
||||
<button className="px-3 py-1.5 text-sm border rounded-md hover:bg-accent">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Workflow Stats Widget */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="p-4 border-b bg-muted/30">
|
||||
<h2 className="font-semibold">Project Overview & Statistics</h2>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium text-muted-foreground">Statistics</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ label: 'Active Sessions', value: '12', color: 'text-blue-500' },
|
||||
{ label: 'Total Tasks', value: '48', color: 'text-green-500' },
|
||||
{ label: 'Completed', value: '35', color: 'text-emerald-500' },
|
||||
{ label: 'Pending', value: '8', color: 'text-amber-500' },
|
||||
].map((stat, i) => (
|
||||
<div key={i} className="p-2 bg-muted/50 rounded">
|
||||
<div className={`text-lg font-bold ${stat.color}`}>{stat.value}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium text-muted-foreground">Workflow Status</div>
|
||||
<div className="flex items-center justify-center h-24">
|
||||
<div className="relative w-20 h-20">
|
||||
<svg className="w-full h-full -rotate-90" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="15" fill="none" stroke="currentColor" strokeWidth="3" className="text-muted opacity-20"/>
|
||||
<circle cx="18" cy="18" r="15" fill="none" stroke="currentColor" strokeWidth="3" className="text-blue-500" strokeDasharray="70 100"/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-xs font-bold">70%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-center space-y-1">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500"/>
|
||||
<span>Completed: 70%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium text-muted-foreground">Recent Session</div>
|
||||
<div className="p-3 bg-accent/20 rounded border">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium">Feature: Auth Flow</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-600">Running</span>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="w-3 h-3 rounded bg-green-500"/>
|
||||
<span>Implement login</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="w-3 h-3 rounded bg-amber-500"/>
|
||||
<span>Add OAuth</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="w-3 h-3 rounded bg-muted"/>
|
||||
<span className="text-muted-foreground">Test flow</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Sessions Widget */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="border-b bg-muted/30">
|
||||
<div className="flex gap-1 p-2">
|
||||
{['All Tasks', 'Workflow', 'Lite Tasks'].map((tab, i) => (
|
||||
<button
|
||||
key={tab}
|
||||
className={`px-3 py-1.5 text-xs rounded-md transition-colors ${
|
||||
i === 0 ? 'bg-background text-foreground' : 'text-muted-foreground hover:bg-foreground/5'
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ name: 'Refactor UI Components', status: 'In Progress', progress: 65 },
|
||||
{ name: 'Fix Login Bug', status: 'Pending', progress: 0 },
|
||||
{ name: 'Add Dark Mode', status: 'Completed', progress: 100 },
|
||||
].map((task, i) => (
|
||||
<div key={i} className="p-3 bg-muted/30 rounded border cursor-pointer hover:border-primary/30">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium line-clamp-1">{task.name}</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
||||
task.status === 'Completed' ? 'bg-green-500/20 text-green-600' :
|
||||
task.status === 'In Progress' ? 'bg-blue-500/20 text-blue-600' :
|
||||
'bg-gray-500/20 text-gray-600'
|
||||
}`}>{task.status}</span>
|
||||
</div>
|
||||
{task.progress > 0 && task.progress < 100 && (
|
||||
<div className="w-full h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-500 rounded-full" style={{ width: `${task.progress}%` }}/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
82
docs/.vitepress/demos/FloatingPanelsDemo.tsx
Normal file
82
docs/.vitepress/demos/FloatingPanelsDemo.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Floating Panels Demo
|
||||
* Mutually exclusive overlay panels
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function FloatingPanelsDemo() {
|
||||
const [activePanel, setActivePanel] = useState<string | null>(null)
|
||||
|
||||
const panels = [
|
||||
{ id: 'issues', title: 'Issues + Queue', side: 'left', width: 400 },
|
||||
{ id: 'queue', title: 'Queue', side: 'right', width: 320 },
|
||||
{ id: 'inspector', title: 'Inspector', side: 'right', width: 280 },
|
||||
{ id: 'execution', title: 'Execution Monitor', side: 'right', width: 300 },
|
||||
{ id: 'scheduler', title: 'Scheduler', side: 'right', width: 260 },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="relative h-[500px] p-6 bg-background border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-semibold">Floating Panels</h3>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{panels.map((panel) => (
|
||||
<button
|
||||
key={panel.id}
|
||||
onClick={() => setActivePanel(activePanel === panel.id ? null : panel.id)}
|
||||
className={`px-3 py-1.5 text-xs rounded transition-colors ${
|
||||
activePanel === panel.id
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'border hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{panel.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[380px] bg-muted/20 border rounded flex items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{activePanel ? `"${panels.find((p) => p.id === activePanel)?.title}" panel is open` : 'Click a button to open a floating panel'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Floating Panel Overlay */}
|
||||
{activePanel && (
|
||||
<div
|
||||
className={`absolute top-16 border rounded-lg shadow-lg bg-background ${
|
||||
panels.find((p) => p.id === activePanel)?.side === 'left' ? 'left-6' : 'right-6'
|
||||
}`}
|
||||
style={{ width: panels.find((p) => p.id === activePanel)?.width }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b">
|
||||
<span className="text-sm font-medium">{panels.find((p) => p.id === activePanel)?.title}</span>
|
||||
<button
|
||||
onClick={() => setActivePanel(null)}
|
||||
className="text-xs hover:bg-accent px-2 py-1 rounded"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 text-sm text-muted-foreground">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500"/>
|
||||
<span>Item 1</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500"/>
|
||||
<span>Item 2</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-amber-500"/>
|
||||
<span>Item 3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
54
docs/.vitepress/demos/MiniStatCards.tsx
Normal file
54
docs/.vitepress/demos/MiniStatCards.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Mini Stat Cards Demo
|
||||
* Individual statistics cards with sparkline trends
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
export default function MiniStatCards() {
|
||||
const stats = [
|
||||
{ label: 'Active Sessions', value: 12, trend: [8, 10, 9, 11, 10, 12, 12], color: 'blue' },
|
||||
{ label: 'Total Tasks', value: 48, trend: [40, 42, 45, 44, 46, 47, 48], color: 'green' },
|
||||
{ label: 'Completed', value: 35, trend: [25, 28, 30, 32, 33, 34, 35], color: 'emerald' },
|
||||
{ label: 'Pending', value: 8, trend: [12, 10, 11, 9, 8, 7, 8], color: 'amber' },
|
||||
{ label: 'Failed', value: 5, trend: [3, 4, 3, 5, 4, 5, 5], color: 'red' },
|
||||
{ label: 'Today Activity', value: 23, trend: [5, 10, 15, 18, 20, 22, 23], color: 'purple' },
|
||||
]
|
||||
|
||||
const colorMap = {
|
||||
blue: 'text-blue-500 bg-blue-500/10',
|
||||
green: 'text-green-500 bg-green-500/10',
|
||||
emerald: 'text-emerald-500 bg-emerald-500/10',
|
||||
amber: 'text-amber-500 bg-amber-500/10',
|
||||
red: 'text-red-500 bg-red-500/10',
|
||||
purple: 'text-purple-500 bg-purple-500/10',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background">
|
||||
<h3 className="text-sm font-semibold mb-4">Statistics with Sparklines</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{stats.map((stat, i) => (
|
||||
<div key={i} className="p-4 border rounded-lg bg-card">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-muted-foreground">{stat.label}</span>
|
||||
<div className={`w-2 h-2 rounded-full ${colorMap[stat.color].split(' ')[1]}`}/>
|
||||
</div>
|
||||
<div className={`text-2xl font-bold ${colorMap[stat.color].split(' ')[0]}`}>{stat.value}</div>
|
||||
<div className="mt-2 h-8 flex items-end gap-0.5">
|
||||
{stat.trend.map((v, j) => (
|
||||
<div
|
||||
key={j}
|
||||
className="flex-1 rounded-t"
|
||||
style={{
|
||||
height: `${(v / Math.max(...stat.trend)) * 100}%`,
|
||||
backgroundColor: v === stat.value ? 'currentColor' : 'rgba(59, 130, 246, 0.3)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
66
docs/.vitepress/demos/ProjectInfoBanner.tsx
Normal file
66
docs/.vitepress/demos/ProjectInfoBanner.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Project Info Banner Demo
|
||||
* Expandable project information with tech stack
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function ProjectInfoBanner() {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background">
|
||||
<h3 className="text-sm font-semibold mb-4">Project Info Banner</h3>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
{/* Banner Header */}
|
||||
<div className="p-4 bg-muted/30 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-semibold">My Awesome Project</h4>
|
||||
<p className="text-sm text-muted-foreground">A modern web application built with React</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="p-2 rounded-md hover:bg-accent"
|
||||
>
|
||||
<svg className={`w-5 h-5 transition-transform ${expanded ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tech Stack Badges */}
|
||||
<div className="px-4 pb-3 flex flex-wrap gap-2">
|
||||
{['TypeScript', 'React', 'Vite', 'Tailwind CSS', 'Zustand'].map((tech) => (
|
||||
<span key={tech} className="px-2 py-1 text-xs rounded-full bg-primary/10 text-primary">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{expanded && (
|
||||
<div className="p-4 border-t bg-muted/20 space-y-4">
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold mb-2">Architecture</h5>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div>• Component-based UI architecture</div>
|
||||
<div>• Centralized state management</div>
|
||||
<div>• RESTful API integration</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold mb-2">Key Components</h5>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
{['Session Manager', 'Dashboard', 'Task Scheduler', 'Analytics'].map((comp) => (
|
||||
<div key={comp} className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary"/>
|
||||
{comp}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
60
docs/.vitepress/demos/QueueItemStatusDemo.tsx
Normal file
60
docs/.vitepress/demos/QueueItemStatusDemo.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Queue Item Status Demo
|
||||
* Shows all possible queue item states
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
export default function QueueItemStatusDemo() {
|
||||
const itemStates = [
|
||||
{ status: 'pending', issueId: 'ISSUE-101', sessionKey: null },
|
||||
{ status: 'executing', issueId: 'ISSUE-102', sessionKey: 'cli-session-abc' },
|
||||
{ status: 'completed', issueId: 'ISSUE-103', sessionKey: 'cli-session-def' },
|
||||
{ status: 'blocked', issueId: 'ISSUE-104', sessionKey: null },
|
||||
{ status: 'failed', issueId: 'ISSUE-105', sessionKey: 'cli-session-ghi' },
|
||||
]
|
||||
|
||||
const statusConfig = {
|
||||
pending: { icon: '○', color: 'text-gray-400', bg: 'bg-gray-500/10', label: 'Pending' },
|
||||
executing: { icon: '▶', color: 'text-blue-500', bg: 'bg-blue-500/10', label: 'Executing' },
|
||||
completed: { icon: '✓', color: 'text-green-500', bg: 'bg-green-500/10', label: 'Completed' },
|
||||
blocked: { icon: '✕', color: 'text-red-500', bg: 'bg-red-500/10', label: 'Blocked' },
|
||||
failed: { icon: '!', color: 'text-red-500', bg: 'bg-red-500/10', label: 'Failed' },
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-4">
|
||||
<h3 className="text-sm font-semibold">Queue Item Status States</h3>
|
||||
<div className="space-y-2">
|
||||
{itemStates.map((item) => {
|
||||
const config = statusConfig[item.status as keyof typeof statusConfig]
|
||||
return (
|
||||
<div key={item.status} className="border rounded-lg p-4 flex items-center gap-4">
|
||||
<span className={`text-2xl ${config.color}`}>{config.icon}</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{item.issueId}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${config.bg} ${config.color}`}>
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
{item.sessionKey && (
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
Bound session: <code className="text-xs bg-muted px-1 rounded">{item.sessionKey}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.status === 'executing' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-24 h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-500 animate-pulse" style={{ width: '60%' }}/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">60%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
164
docs/.vitepress/demos/QueueManagementDemo.tsx
Normal file
164
docs/.vitepress/demos/QueueManagementDemo.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Queue Management Demo
|
||||
* Shows scheduler controls and queue items list
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
export default function QueueManagementDemo() {
|
||||
const [schedulerStatus, setSchedulerStatus] = useState('idle')
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
const queueItems = [
|
||||
{ id: '1', status: 'completed', issueId: 'ISSUE-1', sessionKey: 'session-1' },
|
||||
{ id: '2', status: 'executing', issueId: 'ISSUE-2', sessionKey: 'session-2' },
|
||||
{ id: '3', status: 'pending', issueId: 'ISSUE-3', sessionKey: null },
|
||||
{ id: '4', status: 'pending', issueId: 'ISSUE-4', sessionKey: null },
|
||||
{ id: '5', status: 'blocked', issueId: 'ISSUE-5', sessionKey: null },
|
||||
]
|
||||
|
||||
const statusConfig = {
|
||||
idle: { label: 'Idle', color: 'bg-gray-500/20 text-gray-600 border-gray-500' },
|
||||
running: { label: 'Running', color: 'bg-green-500/20 text-green-600 border-green-500' },
|
||||
paused: { label: 'Paused', color: 'bg-amber-500/20 text-amber-600 border-amber-500' },
|
||||
}
|
||||
|
||||
const itemStatusConfig = {
|
||||
completed: { icon: '✓', color: 'text-green-500', label: 'Completed' },
|
||||
executing: { icon: '▶', color: 'text-blue-500', label: 'Executing' },
|
||||
pending: { icon: '○', color: 'text-gray-400', label: 'Pending' },
|
||||
blocked: { icon: '✕', color: 'text-red-500', label: 'Blocked' },
|
||||
failed: { icon: '!', color: 'text-red-500', label: 'Failed' },
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (schedulerStatus === 'running') {
|
||||
const interval = setInterval(() => {
|
||||
setProgress((p) => (p >= 100 ? 0 : p + 10))
|
||||
}, 500)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [schedulerStatus])
|
||||
|
||||
const handleStart = () => {
|
||||
if (schedulerStatus === 'idle' || schedulerStatus === 'paused') {
|
||||
setSchedulerStatus('running')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePause = () => {
|
||||
if (schedulerStatus === 'running') {
|
||||
setSchedulerStatus('paused')
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = () => {
|
||||
setSchedulerStatus('idle')
|
||||
setProgress(0)
|
||||
}
|
||||
|
||||
const currentConfig = statusConfig[schedulerStatus as keyof typeof statusConfig]
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Queue Management</h3>
|
||||
<p className="text-sm text-muted-foreground">Manage issue execution queue</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scheduler Status Bar */}
|
||||
<div className="border rounded-lg p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`px-3 py-1 rounded text-xs font-medium border ${currentConfig.color}`}>
|
||||
{currentConfig.label}
|
||||
</span>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{queueItems.filter((i) => i.status === 'completed').length}/{queueItems.length} items</span>
|
||||
<span>2/2 concurrent</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{schedulerStatus === 'running' && (
|
||||
<div className="space-y-1">
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-green-500 rounded-full transition-all"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{progress}% complete</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scheduler Controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
{(schedulerStatus === 'idle' || schedulerStatus === 'paused') && (
|
||||
<button
|
||||
onClick={handleStart}
|
||||
className="px-4 py-2 text-sm bg-green-500 text-white rounded hover:bg-green-600 flex items-center gap-2"
|
||||
>
|
||||
<span>▶</span> Start
|
||||
</button>
|
||||
)}
|
||||
{schedulerStatus === 'running' && (
|
||||
<button
|
||||
onClick={handlePause}
|
||||
className="px-4 py-2 text-sm bg-amber-500 text-white rounded hover:bg-amber-600 flex items-center gap-2"
|
||||
>
|
||||
<span>⏸</span> Pause
|
||||
</button>
|
||||
)}
|
||||
{schedulerStatus !== 'idle' && (
|
||||
<button
|
||||
onClick={handleStop}
|
||||
className="px-4 py-2 text-sm bg-red-500 text-white rounded hover:bg-red-600 flex items-center gap-2"
|
||||
>
|
||||
<span>⬛</span> Stop
|
||||
</button>
|
||||
)}
|
||||
<button className="px-4 py-2 text-sm border rounded hover:bg-accent">
|
||||
Config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Queue Items List */}
|
||||
<div className="border rounded-lg">
|
||||
<div className="px-4 py-3 border-b bg-muted/30">
|
||||
<h4 className="text-sm font-semibold">Queue Items</h4>
|
||||
</div>
|
||||
<div className="divide-y max-h-80 overflow-auto">
|
||||
{queueItems.map((item) => {
|
||||
const config = itemStatusConfig[item.status as keyof typeof itemStatusConfig]
|
||||
return (
|
||||
<div key={item.id} className="px-4 py-3 flex items-center gap-4 hover:bg-accent/50">
|
||||
<span className={`text-lg ${config.color}`}>{config.icon}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{item.issueId}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${config.color} bg-opacity-10`}>
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
{item.sessionKey && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Session: {item.sessionKey}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.status === 'executing' && (
|
||||
<div className="w-20 h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-blue-500 animate-pulse" style={{ width: '60%' }}/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
64
docs/.vitepress/demos/README.md
Normal file
64
docs/.vitepress/demos/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Demo Components
|
||||
|
||||
This directory contains React components that are embedded in the documentation as interactive demos.
|
||||
|
||||
## Creating a New Demo
|
||||
|
||||
1. Create a new `.tsx` file in this directory (e.g., `my-demo.tsx`)
|
||||
2. Export a default React component
|
||||
3. Use it in markdown with `:::demo my-demo :::`
|
||||
|
||||
## Demo Template
|
||||
|
||||
```tsx
|
||||
import React from 'react'
|
||||
|
||||
/**
|
||||
* Brief description of what this demo shows
|
||||
*/
|
||||
export default function MyDemo() {
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
{/* Your demo content */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Demo Guidelines
|
||||
|
||||
- **Keep it simple**: Demos should be focused and easy to understand
|
||||
- **Use inline styles**: Avoid external dependencies for portability
|
||||
- **Add comments**: Explain what the demo is showing
|
||||
- **Test interactions**: Ensure buttons, inputs, etc. work correctly
|
||||
- **Handle state**: Use React hooks (`useState`, `useEffect`) for interactive demos
|
||||
|
||||
## Demo File Naming
|
||||
|
||||
- Use kebab-case: `button-variants.tsx`, `card-basic.tsx`
|
||||
- Group by category:
|
||||
- `ui/` - UI component demos
|
||||
- `shared/` - Shared component demos
|
||||
- `pages/` - Page-level demos
|
||||
|
||||
## Using Props
|
||||
|
||||
If you need to pass custom props to a demo, use the extended markdown syntax:
|
||||
|
||||
```markdown
|
||||
:::demo my-demo
|
||||
title: Custom Title
|
||||
height: 300px
|
||||
expandable: false
|
||||
:::
|
||||
```
|
||||
|
||||
## Available Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| name | string | - | Demo component name (required) |
|
||||
| title | string | name | Custom demo title |
|
||||
| height | string | 'auto' | Container height |
|
||||
| expandable | boolean | true | Allow expand/collapse |
|
||||
| showCode | boolean | true | Show code tab |
|
||||
86
docs/.vitepress/demos/ResizablePanesDemo.tsx
Normal file
86
docs/.vitepress/demos/ResizablePanesDemo.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Resizable Panes Demo
|
||||
* Simulates the Allotment resizable split behavior
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
export default function ResizablePanesDemo() {
|
||||
const [leftWidth, setLeftWidth] = useState(240)
|
||||
const [rightWidth, setRightWidth] = useState(280)
|
||||
const [isDragging, setIsDragging] = useState<string | null>(null)
|
||||
|
||||
const handleDragStart = (side: string) => (e: React.MouseEvent) => {
|
||||
setIsDragging(side)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (isDragging === 'left') {
|
||||
setLeftWidth(Math.max(180, Math.min(320, e.clientX - 24)))
|
||||
} else if (isDragging === 'right') {
|
||||
setRightWidth(Math.max(200, Math.min(400, window.innerWidth - e.clientX - 24)))
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseUp = () => setIsDragging(null)
|
||||
|
||||
if (isDragging) {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDragging])
|
||||
|
||||
return (
|
||||
<div className="h-[400px] flex bg-background border rounded-lg overflow-hidden">
|
||||
{/* Left Sidebar */}
|
||||
<div style={{ width: leftWidth }} className="border-r flex flex-col min-w-[180px]">
|
||||
<div className="px-3 py-2 text-xs font-semibold border-b bg-muted/30">
|
||||
Session Groups
|
||||
</div>
|
||||
<div className="flex-1 p-2 text-sm space-y-1">
|
||||
{['Active Sessions', 'Completed'].map((g) => (
|
||||
<div key={g} className="px-2 py-1 hover:bg-accent rounded cursor-pointer">{g}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Left Drag Handle */}
|
||||
<div
|
||||
onMouseDown={handleDragStart('left')}
|
||||
className={`w-1 bg-border hover:bg-primary cursor-col-resize transition-colors ${
|
||||
isDragging === 'left' ? 'bg-primary' : ''
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 bg-muted/20 flex items-center justify-center">
|
||||
<span className="text-sm text-muted-foreground">Terminal Grid Area</span>
|
||||
</div>
|
||||
|
||||
{/* Right Drag Handle */}
|
||||
<div
|
||||
onMouseDown={handleDragStart('right')}
|
||||
className={`w-1 bg-border hover:bg-primary cursor-col-resize transition-colors ${
|
||||
isDragging === 'right' ? 'bg-primary' : ''
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Right Sidebar */}
|
||||
<div style={{ width: rightWidth }} className="border-l flex flex-col min-w-[200px]">
|
||||
<div className="px-3 py-2 text-xs font-semibold border-b bg-muted/30">
|
||||
Project Files
|
||||
</div>
|
||||
<div className="flex-1 p-2 text-sm space-y-1">
|
||||
{['src/', 'docs/', 'tests/'].map((f) => (
|
||||
<div key={f} className="px-2 py-1 hover:bg-accent rounded cursor-pointer">{f}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
110
docs/.vitepress/demos/SchedulerConfigDemo.tsx
Normal file
110
docs/.vitepress/demos/SchedulerConfigDemo.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Scheduler Config Demo
|
||||
* Interactive configuration panel
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function SchedulerConfigDemo() {
|
||||
const [config, setConfig] = useState({
|
||||
maxConcurrentSessions: 2,
|
||||
sessionIdleTimeoutMs: 60000,
|
||||
resumeKeySessionBindingTimeoutMs: 300000,
|
||||
})
|
||||
|
||||
const formatMs = (ms: number) => {
|
||||
if (ms >= 60000) return `${ms / 60000}m`
|
||||
if (ms >= 1000) return `${ms / 1000}s`
|
||||
return `${ms}ms`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-6 max-w-md">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">Scheduler Configuration</h3>
|
||||
<button className="px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded hover:opacity-90">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Max Concurrent Sessions */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Max Concurrent Sessions</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="8"
|
||||
value={config.maxConcurrentSessions}
|
||||
onChange={(e) => setConfig({ ...config, maxConcurrentSessions: parseInt(e.target.value) })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm font-medium w-8 text-center">{config.maxConcurrentSessions}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Maximum number of sessions to run simultaneously
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Session Idle Timeout */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Session Idle Timeout</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="range"
|
||||
min="10000"
|
||||
max="300000"
|
||||
step="10000"
|
||||
value={config.sessionIdleTimeoutMs}
|
||||
onChange={(e) => setConfig({ ...config, sessionIdleTimeoutMs: parseInt(e.target.value) })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm font-medium w-12 text-right">{formatMs(config.sessionIdleTimeoutMs)}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Time before idle session is terminated
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Resume Key Binding Timeout */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Resume Key Binding Timeout</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="range"
|
||||
min="60000"
|
||||
max="600000"
|
||||
step="60000"
|
||||
value={config.resumeKeySessionBindingTimeoutMs}
|
||||
onChange={(e) => setConfig({ ...config, resumeKeySessionBindingTimeoutMs: parseInt(e.target.value) })}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-sm font-medium w-12 text-right">{formatMs(config.resumeKeySessionBindingTimeoutMs)}</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Time to preserve resume key session binding
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Config Display */}
|
||||
<div className="border rounded-lg p-4 bg-muted/30">
|
||||
<h4 className="text-xs font-semibold mb-3">Current Configuration</h4>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-muted-foreground">Max Concurrent</dt>
|
||||
<dd className="font-medium">{config.maxConcurrentSessions} sessions</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-muted-foreground">Idle Timeout</dt>
|
||||
<dd className="font-medium">{formatMs(config.sessionIdleTimeoutMs)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-muted-foreground">Binding Timeout</dt>
|
||||
<dd className="font-medium">{formatMs(config.resumeKeySessionBindingTimeoutMs)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
108
docs/.vitepress/demos/SessionCarousel.tsx
Normal file
108
docs/.vitepress/demos/SessionCarousel.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Session Carousel Demo
|
||||
* Auto-rotating session cards with navigation
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
export default function SessionCarousel() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const sessions = [
|
||||
{
|
||||
name: 'Feature: User Authentication',
|
||||
status: 'running',
|
||||
tasks: [
|
||||
{ name: 'Implement login form', status: 'completed' },
|
||||
{ name: 'Add OAuth provider', status: 'in-progress' },
|
||||
{ name: 'Create session management', status: 'pending' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Bug Fix: Memory Leak',
|
||||
status: 'running',
|
||||
tasks: [
|
||||
{ name: 'Identify leak source', status: 'completed' },
|
||||
{ name: 'Fix cleanup handlers', status: 'in-progress' },
|
||||
{ name: 'Add unit tests', status: 'pending' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Refactor: API Layer',
|
||||
status: 'planning',
|
||||
tasks: [
|
||||
{ name: 'Design new interface', status: 'pending' },
|
||||
{ name: 'Migrate existing endpoints', status: 'pending' },
|
||||
{ name: 'Update documentation', status: 'pending' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const statusColors = {
|
||||
completed: 'bg-green-500',
|
||||
'in-progress': 'bg-amber-500',
|
||||
pending: 'bg-muted',
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCurrentIndex((i) => (i + 1) % sessions.length)
|
||||
}, 5000)
|
||||
return () => clearInterval(timer)
|
||||
}, [sessions.length])
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background">
|
||||
<h3 className="text-sm font-semibold mb-4">Session Carousel (auto-rotates every 5s)</h3>
|
||||
<div className="border rounded-lg p-4 bg-card">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium">Session {currentIndex + 1} of {sessions.length}</span>
|
||||
<div className="flex gap-1">
|
||||
{sessions.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentIndex(i)}
|
||||
className={`w-2 h-2 rounded-full transition-colors ${
|
||||
i === currentIndex ? 'bg-primary' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-accent/20 rounded border">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-medium">{sessions[currentIndex].name}</span>
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${
|
||||
sessions[currentIndex].status === 'running' ? 'bg-green-500/20 text-green-600' : 'bg-blue-500/20 text-blue-600'
|
||||
}`}>
|
||||
{sessions[currentIndex].status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{sessions[currentIndex].tasks.map((task, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-sm">
|
||||
<div className={`w-3 h-3 rounded ${statusColors[task.status]}`}/>
|
||||
<span className={task.status === 'pending' ? 'text-muted-foreground' : ''}>{task.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-3">
|
||||
<button
|
||||
onClick={() => setCurrentIndex((i) => (i - 1 + sessions.length) % sessions.length)}
|
||||
className="px-3 py-1.5 text-sm border rounded-md hover:bg-accent"
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentIndex((i) => (i + 1) % sessions.length)}
|
||||
className="px-3 py-1.5 text-sm border rounded-md hover:bg-accent"
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
122
docs/.vitepress/demos/TerminalDashboardOverview.tsx
Normal file
122
docs/.vitepress/demos/TerminalDashboardOverview.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Terminal Dashboard Overview Demo
|
||||
* Shows the three-column layout with resizable panes and toolbar
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function TerminalDashboardOverview() {
|
||||
const [fileSidebarOpen, setFileSidebarOpen] = useState(true)
|
||||
const [sessionSidebarOpen, setSessionSidebarOpen] = useState(true)
|
||||
const [activePanel, setActivePanel] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<div className="h-[600px] flex flex-col bg-background relative">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Terminal Dashboard</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{['Sessions', 'Files', 'Issues', 'Queue', 'Inspector', 'Scheduler'].map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
onClick={() => {
|
||||
if (item === 'Sessions') setSessionSidebarOpen(!sessionSidebarOpen)
|
||||
else if (item === 'Files') setFileSidebarOpen(!fileSidebarOpen)
|
||||
else setActivePanel(activePanel === item.toLowerCase() ? null : item.toLowerCase())
|
||||
}}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
(item === 'Sessions' && sessionSidebarOpen) ||
|
||||
(item === 'Files' && fileSidebarOpen) ||
|
||||
activePanel === item.toLowerCase()
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Layout */}
|
||||
<div className="flex-1 flex min-h-0">
|
||||
{/* Session Sidebar */}
|
||||
{sessionSidebarOpen && (
|
||||
<div className="w-60 border-r flex flex-col">
|
||||
<div className="px-3 py-2 text-xs font-semibold border-b bg-muted/30">
|
||||
Session Groups
|
||||
</div>
|
||||
<div className="flex-1 p-2 space-y-1 text-sm overflow-auto">
|
||||
{['Active Sessions', 'Completed', 'Archived'].map((group) => (
|
||||
<div key={group}>
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded hover:bg-accent cursor-pointer">
|
||||
<span className="text-xs">▼</span>
|
||||
<span>{group}</span>
|
||||
</div>
|
||||
<div className="ml-4 space-y-0.5">
|
||||
<div className="px-2 py-1 text-xs text-muted-foreground hover:bg-accent rounded cursor-pointer">
|
||||
Session 1
|
||||
</div>
|
||||
<div className="px-2 py-1 text-xs text-muted-foreground hover:bg-accent rounded cursor-pointer">
|
||||
Session 2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Terminal Grid */}
|
||||
<div className="flex-1 bg-muted/20 p-2">
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-2 h-full">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="bg-background border rounded p-3 font-mono text-xs">
|
||||
<div className="text-green-500 mb-2">$ Terminal {i}</div>
|
||||
<div className="text-muted-foreground">
|
||||
<div>Working directory: /project</div>
|
||||
<div>Type a command to begin...</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Sidebar */}
|
||||
{fileSidebarOpen && (
|
||||
<div className="w-64 border-l flex flex-col">
|
||||
<div className="px-3 py-2 text-xs font-semibold border-b bg-muted/30">
|
||||
Project Files
|
||||
</div>
|
||||
<div className="flex-1 p-2 text-sm overflow-auto">
|
||||
<div className="space-y-1">
|
||||
{['src', 'docs', 'tests', 'package.json', 'README.md'].map((item) => (
|
||||
<div key={item} className="px-2 py-1 rounded hover:bg-accent cursor-pointer flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">📁</span>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Floating Panel */}
|
||||
{activePanel && (
|
||||
<div className="absolute top-12 right-4 w-80 bg-background border rounded-lg shadow-lg">
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b">
|
||||
<span className="text-sm font-medium capitalize">{activePanel} Panel</span>
|
||||
<button onClick={() => setActivePanel(null)} className="text-xs hover:bg-accent px-2 py-1 rounded">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 text-sm text-muted-foreground">
|
||||
{activePanel} content placeholder
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
docs/.vitepress/demos/TerminalLayoutPresets.tsx
Normal file
48
docs/.vitepress/demos/TerminalLayoutPresets.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Terminal Layout Presets Demo
|
||||
* Interactive layout preset buttons
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function TerminalLayoutPresets() {
|
||||
const [layout, setLayout] = useState('grid-2x2')
|
||||
|
||||
const layouts = {
|
||||
single: 'grid-cols-1 grid-rows-1',
|
||||
'split-h': 'grid-cols-2 grid-rows-1',
|
||||
'split-v': 'grid-cols-1 grid-rows-2',
|
||||
'grid-2x2': 'grid-cols-2 grid-rows-2',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">Terminal Layout Presets</h3>
|
||||
<div className="flex gap-2">
|
||||
{Object.keys(layouts).map((preset) => (
|
||||
<button
|
||||
key={preset}
|
||||
onClick={() => setLayout(preset)}
|
||||
className={`px-3 py-1.5 text-xs rounded transition-colors ${
|
||||
layout === preset
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'border hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{preset.replace('-', ' ').toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`grid gap-2 h-64 ${layouts[layout]}`}>
|
||||
{Array.from({ length: layout === 'single' ? 1 : layout.includes('2x') ? 4 : 2 }).map((_, i) => (
|
||||
<div key={i} className="bg-muted/20 border rounded p-4 font-mono text-xs">
|
||||
<div className="text-green-500">$ Terminal {i + 1}</div>
|
||||
<div className="text-muted-foreground mt-1">Ready for input...</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
57
docs/.vitepress/demos/badge-variants.tsx
Normal file
57
docs/.vitepress/demos/badge-variants.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Badge Variants Demo
|
||||
* Shows all available badge variants
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
export default function BadgeVariantsDemo() {
|
||||
const variants = [
|
||||
{ name: 'default', class: 'bg-primary text-primary-foreground' },
|
||||
{ name: 'secondary', class: 'bg-secondary text-secondary-foreground' },
|
||||
{ name: 'destructive', class: 'bg-destructive text-destructive-foreground' },
|
||||
{ name: 'outline', class: 'border text-foreground' },
|
||||
{ name: 'success', class: 'bg-success text-white' },
|
||||
{ name: 'warning', class: 'bg-warning text-white' },
|
||||
{ name: 'info', class: 'bg-info text-white' },
|
||||
{ name: 'review', class: 'bg-purple-500 text-white' },
|
||||
{ name: 'gradient', class: 'bg-gradient-to-r from-blue-500 to-purple-500 text-white' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-6">
|
||||
<h3 className="text-sm font-semibold">Badge Variants</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{variants.map((v) => (
|
||||
<span
|
||||
key={v.name}
|
||||
className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold ${v.class}`}
|
||||
>
|
||||
{v.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Examples */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium">Usage Examples</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 p-2 border rounded">
|
||||
<span className="font-medium">Status:</span>
|
||||
<span className="inline-flex items-center rounded-full bg-success px-2 py-0.5 text-xs text-white">Active</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 border rounded">
|
||||
<span className="font-medium">Priority:</span>
|
||||
<span className="inline-flex items-center rounded-full bg-destructive px-2 py-0.5 text-xs text-white">High</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 border rounded">
|
||||
<span className="font-medium">Type:</span>
|
||||
<span className="inline-flex items-center rounded-full bg-info px-2 py-0.5 text-xs text-white">Feature</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
82
docs/.vitepress/demos/button-variants.tsx
Normal file
82
docs/.vitepress/demos/button-variants.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Button Variants Demo
|
||||
* Shows all visual variants of the button component
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function ButtonVariantsDemo() {
|
||||
const [variant, setVariant] = useState('default')
|
||||
|
||||
const variants = ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link', 'gradient']
|
||||
|
||||
const getButtonClass = (v: string) => {
|
||||
const base = 'px-4 py-2 rounded-md text-sm transition-colors'
|
||||
switch (v) {
|
||||
case 'default': return `${base} bg-primary text-primary-foreground hover:opacity-90`
|
||||
case 'destructive': return `${base} bg-destructive text-destructive-foreground hover:opacity-90`
|
||||
case 'outline': return `${base} border bg-background hover:bg-accent`
|
||||
case 'secondary': return `${base} bg-secondary text-secondary-foreground hover:opacity-80`
|
||||
case 'ghost': return `${base} hover:bg-accent`
|
||||
case 'link': return `${base} text-primary underline-offset-4 hover:underline`
|
||||
case 'gradient': return `${base} bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:opacity-90`
|
||||
default: return base
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-6">
|
||||
<h3 className="text-sm font-semibold">Button Variants</h3>
|
||||
|
||||
{/* Variant Selector */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Select Variant</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{variants.map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => setVariant(v)}
|
||||
className={`px-3 py-1.5 text-xs rounded capitalize transition-colors ${
|
||||
variant === v ? 'bg-primary text-primary-foreground' : 'border hover:bg-accent'
|
||||
}`}
|
||||
>
|
||||
{v}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Preview</label>
|
||||
<div className="p-4 border rounded-lg bg-muted/20">
|
||||
<button className={getButtonClass(variant)}>
|
||||
Button
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All Variants */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">All Variants</label>
|
||||
<div className="flex flex-wrap gap-3 p-4 border rounded-lg">
|
||||
{variants.map((v) => (
|
||||
<button key={v} className={getButtonClass(v)}>
|
||||
{v.charAt(0).toUpperCase() + v.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sizes */}
|
||||
<div className="space-y-3">
|
||||
<label className="text-sm font-medium">Sizes</label>
|
||||
<div className="flex items-center gap-3 flex-wrap p-4 border rounded-lg">
|
||||
<button className={`${getButtonClass(variant)} h-8 px-3 text-xs`}>Small</button>
|
||||
<button className={`${getButtonClass(variant)} h-10 px-4`}>Default</button>
|
||||
<button className={`${getButtonClass(variant)} h-11 px-8`}>Large</button>
|
||||
<button className={`${getButtonClass(variant)} h-10 w-10`}>⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
docs/.vitepress/demos/card-variants.tsx
Normal file
71
docs/.vitepress/demos/card-variants.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Card Variants Demo
|
||||
* Shows different card layouts
|
||||
*/
|
||||
import React from 'react'
|
||||
|
||||
export default function CardVariantsDemo() {
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-6">
|
||||
<h3 className="text-sm font-semibold">Card Variants</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{/* Basic Card */}
|
||||
<div className="border rounded-lg">
|
||||
<div className="p-4 border-b">
|
||||
<h4 className="font-semibold">Basic Card</h4>
|
||||
<p className="text-sm text-muted-foreground">With header and content</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm">Card content goes here.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card with Footer */}
|
||||
<div className="border rounded-lg">
|
||||
<div className="p-4 border-b">
|
||||
<h4 className="font-semibold">Card with Footer</h4>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<p className="text-sm">Main content area.</p>
|
||||
</div>
|
||||
<div className="p-4 border-t bg-muted/20">
|
||||
<div className="flex gap-2">
|
||||
<button className="px-3 py-1.5 text-xs bg-primary text-primary-foreground rounded">Action</button>
|
||||
<button className="px-3 py-1.5 text-xs border rounded">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gradient Border Card */}
|
||||
<div className="border-2 border-transparent bg-gradient-to-r from-blue-500 to-purple-500 p-[2px] rounded-lg">
|
||||
<div className="bg-background rounded-lg p-4">
|
||||
<h4 className="font-semibold">Gradient Border</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">Card with gradient border effect.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Card */}
|
||||
<div className="border rounded-lg">
|
||||
<div className="p-4 border-b">
|
||||
<h4 className="font-semibold">Settings</h4>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Enable notifications</span>
|
||||
<div className="w-9 h-5 bg-primary rounded-full relative">
|
||||
<div className="absolute right-[2px] top-[2px] w-4 h-4 bg-white rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Dark mode</span>
|
||||
<div className="w-9 h-5 bg-muted rounded-full relative">
|
||||
<div className="absolute left-[2px] top-[2px] w-4 h-4 bg-white rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
79
docs/.vitepress/demos/checkbox-variants.tsx
Normal file
79
docs/.vitepress/demos/checkbox-variants.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Checkbox Variants Demo
|
||||
* Shows different checkbox states
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function CheckboxVariantsDemo() {
|
||||
const [checked, setChecked] = useState({ a: true, b: false, c: true })
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-6">
|
||||
<h3 className="text-sm font-semibold">Checkbox States</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Checked */}
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked.a}
|
||||
onChange={(e) => setChecked({ ...checked, a: e.target.checked })}
|
||||
className="h-4 w-4 rounded border border-primary accent-primary"
|
||||
/>
|
||||
<span className="text-sm">Checked checkbox</span>
|
||||
</label>
|
||||
|
||||
{/* Unchecked */}
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked.b}
|
||||
onChange={(e) => setChecked({ ...checked, b: e.target.checked })}
|
||||
className="h-4 w-4 rounded border border-primary accent-primary"
|
||||
/>
|
||||
<span className="text-sm">Unchecked checkbox</span>
|
||||
</label>
|
||||
|
||||
{/* Disabled */}
|
||||
<label className="flex items-center gap-3 cursor-not-allowed opacity-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled
|
||||
className="h-4 w-4 rounded border border-primary"
|
||||
/>
|
||||
<span className="text-sm">Disabled checkbox</span>
|
||||
</label>
|
||||
|
||||
{/* Disabled Checked */}
|
||||
<label className="flex items-center gap-3 cursor-not-allowed opacity-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled
|
||||
defaultChecked
|
||||
className="h-4 w-4 rounded border border-primary accent-primary"
|
||||
/>
|
||||
<span className="text-sm">Disabled checked checkbox</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Usage Example */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium">Usage Example</h4>
|
||||
<div className="p-4 border rounded-lg space-y-2">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" className="h-4 w-4 rounded border accent-primary" />
|
||||
<span className="text-sm">Accept terms and conditions</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" className="h-4 w-4 rounded border accent-primary" />
|
||||
<span className="text-sm">Subscribe to newsletter</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" className="h-4 w-4 rounded border accent-primary" />
|
||||
<span className="text-sm">Remember me</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
95
docs/.vitepress/demos/input-variants.tsx
Normal file
95
docs/.vitepress/demos/input-variants.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Input Variants Demo
|
||||
* Shows all input states
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function InputVariantsDemo() {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-6">
|
||||
<h3 className="text-sm font-semibold">Input States</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Default */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Default</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter text..."
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* With Value */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">With Value</label>
|
||||
<input
|
||||
type="text"
|
||||
value="John Doe"
|
||||
readOnly
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error State */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Error State</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Invalid input"
|
||||
className="flex h-10 w-full rounded-md border border-destructive bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-destructive"
|
||||
/>
|
||||
<p className="text-xs text-destructive">This field is required</p>
|
||||
</div>
|
||||
|
||||
{/* Disabled */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Disabled</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
placeholder="Disabled input"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-muted px-3 py-2 text-sm opacity-50 cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* With Icon */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Search Input</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">🔍</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background pl-10 pr-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controlled */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Controlled Input</label>
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="Type something..."
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Value: {value || '(empty)'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Textarea */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Textarea</label>
|
||||
<textarea
|
||||
placeholder="Enter multi-line text..."
|
||||
className="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
101
docs/.vitepress/demos/select-variants.tsx
Normal file
101
docs/.vitepress/demos/select-variants.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Select Variants Demo
|
||||
* Shows different select configurations
|
||||
*/
|
||||
import React, { useState } from 'react'
|
||||
|
||||
export default function SelectVariantsDemo() {
|
||||
const [selected, setSelected] = useState('')
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-background space-y-6">
|
||||
<h3 className="text-sm font-semibold">Select Configurations</h3>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Basic Select */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Basic Select</label>
|
||||
<select className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<option value="">Choose an option</option>
|
||||
<option value="1">Option 1</option>
|
||||
<option value="2">Option 2</option>
|
||||
<option value="3">Option 3</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* With Labels */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">With Option Groups</label>
|
||||
<select className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<optgroup label="Fruits">
|
||||
<option value="apple">Apple</option>
|
||||
<option value="banana">Banana</option>
|
||||
<option value="orange">Orange</option>
|
||||
</optgroup>
|
||||
<optgroup label="Vegetables">
|
||||
<option value="carrot">Carrot</option>
|
||||
<option value="broccoli">Broccoli</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Controlled Select */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Controlled Select</label>
|
||||
<select
|
||||
value={selected}
|
||||
onChange={(e) => setSelected(e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="">Select...</option>
|
||||
<option value="react">React</option>
|
||||
<option value="vue">Vue</option>
|
||||
<option value="angular">Angular</option>
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground">Selected: {selected || '(none)'}</p>
|
||||
</div>
|
||||
|
||||
{/* Disabled Select */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Disabled Select</label>
|
||||
<select
|
||||
disabled
|
||||
className="flex h-10 w-full rounded-md border border-input bg-muted px-3 py-2 text-sm opacity-50 cursor-not-allowed"
|
||||
>
|
||||
<option value="">Disabled select</option>
|
||||
<option value="1">Option 1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* With Separators */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">With Separators</label>
|
||||
<select className="flex h-10 w-full max-w-md rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<option value="">Select an action</option>
|
||||
<option value="edit">Edit</option>
|
||||
<option value="copy">Copy</option>
|
||||
<option value="move">Move</option>
|
||||
{/* Visual separator via disabled option */}
|
||||
<option disabled>──────────</option>
|
||||
<option value="delete" className="text-destructive">Delete</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Multiple Select */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Multiple Select (Hold Ctrl/Cmd)</label>
|
||||
<select
|
||||
multiple
|
||||
className="flex min-h-[100px] w-full max-w-md rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="html">HTML</option>
|
||||
<option value="css">CSS</option>
|
||||
<option value="js">JavaScript</option>
|
||||
<option value="ts">TypeScript</option>
|
||||
<option value="react">React</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -264,7 +264,7 @@ function hideTooltip() {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 768px) {
|
||||
.agent-orchestration {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
147
docs/.vitepress/theme/components/CodeViewer.vue
Normal file
147
docs/.vitepress/theme/components/CodeViewer.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
code: string
|
||||
lang?: string
|
||||
showCopy?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
lang: 'tsx',
|
||||
showCopy: true
|
||||
})
|
||||
|
||||
const copyStatus = ref<'idle' | 'copying' | 'copied'>('idle')
|
||||
const copyTimeout = ref<number>()
|
||||
|
||||
const lineCount = computed(() => props.code.split('\n').length)
|
||||
const copyButtonText = computed(() => {
|
||||
switch (copyStatus.value) {
|
||||
case 'copying': return '复制中...'
|
||||
case 'copied': return '已复制'
|
||||
default: return '复制'
|
||||
}
|
||||
})
|
||||
|
||||
const copyCode = async () => {
|
||||
if (copyStatus.value === 'copying') return
|
||||
|
||||
copyStatus.value = 'copying'
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.code)
|
||||
copyStatus.value = 'copied'
|
||||
|
||||
if (copyTimeout.value) {
|
||||
clearTimeout(copyTimeout.value)
|
||||
}
|
||||
copyTimeout.value = window.setTimeout(() => {
|
||||
copyStatus.value = 'idle'
|
||||
}, 2000)
|
||||
} catch {
|
||||
copyStatus.value = 'idle'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="code-viewer">
|
||||
<div class="code-header">
|
||||
<span class="code-lang">{{ lang }}</span>
|
||||
<button
|
||||
v-if="showCopy"
|
||||
class="copy-button"
|
||||
:class="copyStatus"
|
||||
:disabled="copyStatus === 'copying'"
|
||||
@click="copyCode"
|
||||
>
|
||||
<span class="copy-icon">📋</span>
|
||||
<span class="copy-text">{{ copyButtonText }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre class="code-content" :class="`language-${lang}`"><code>{{ code }}</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.code-viewer {
|
||||
background: var(--vp-code-bg);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: var(--vp-code-block-bg);
|
||||
border-bottom: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.code-lang {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.copy-button:hover:not(:disabled) {
|
||||
background: var(--vp-c-bg-mute);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.copy-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.copy-button.copied {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.copy-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.code-content {
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-content code {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--vp-code-color);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.code-content {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
354
docs/.vitepress/theme/components/DemoContainer.vue
Normal file
354
docs/.vitepress/theme/components/DemoContainer.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import React from 'react'
|
||||
import type { Root } from 'react-dom/client'
|
||||
import CodeViewer from './CodeViewer.vue'
|
||||
|
||||
interface Props {
|
||||
id: string // Demo unique ID
|
||||
name: string // Demo component name
|
||||
file?: string // Optional: explicit source file
|
||||
hasInlineCode?: boolean // Whether inline code is provided
|
||||
inlineCode?: string // Base64 encoded inline code (for display)
|
||||
virtualModule?: string // Virtual module path for inline demos
|
||||
height?: string // Demo container height
|
||||
expandable?: boolean // Allow expand/collapse
|
||||
showCode?: boolean // Show code tab
|
||||
title?: string // Custom demo title
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
hasInlineCode: false,
|
||||
inlineCode: '',
|
||||
virtualModule: '',
|
||||
height: 'auto',
|
||||
expandable: true,
|
||||
showCode: true,
|
||||
title: ''
|
||||
})
|
||||
|
||||
const demoRoot = ref<HTMLElement>()
|
||||
const reactRoot = ref<Root>()
|
||||
const sourceCode = ref('')
|
||||
const isExpanded = ref(false)
|
||||
const activeTab = ref<'preview' | 'code'>('preview')
|
||||
const isLoading = ref(true)
|
||||
const loadError = ref('')
|
||||
|
||||
// Derive demo title
|
||||
const demoTitle = computed(() => props.title || props.name)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Handle inline code mode with virtual module
|
||||
if (props.hasInlineCode && props.virtualModule) {
|
||||
// Decode base64 for source code display
|
||||
if (props.inlineCode) {
|
||||
sourceCode.value = atob(props.inlineCode)
|
||||
}
|
||||
|
||||
// Dynamically import the virtual module
|
||||
// @vite-ignore is needed for dynamic imports with variable paths
|
||||
const inlineDemoModule = await import(/* @vite-ignore */ props.virtualModule)
|
||||
const DemoComponent = inlineDemoModule.default
|
||||
|
||||
if (!DemoComponent) {
|
||||
throw new Error(`Inline demo component "${props.name}" not found in virtual module`)
|
||||
}
|
||||
|
||||
// Mount React component properly
|
||||
if (demoRoot.value) {
|
||||
reactRoot.value = createRoot(demoRoot.value)
|
||||
reactRoot.value.render(React.createElement(DemoComponent))
|
||||
}
|
||||
} else if (props.hasInlineCode && props.inlineCode) {
|
||||
// Fallback: inline code without virtual module (display only, no execution)
|
||||
const decodedCode = atob(props.inlineCode)
|
||||
sourceCode.value = decodedCode
|
||||
|
||||
if (demoRoot.value) {
|
||||
// Show a message that preview is not available
|
||||
const noticeEl = document.createElement('div')
|
||||
noticeEl.className = 'inline-demo-notice'
|
||||
noticeEl.innerHTML = `
|
||||
<p><strong>Preview not available</strong></p>
|
||||
<p>Inline demo "${props.name}" requires virtual module support.</p>
|
||||
<p>Check the "Code" tab to see the source.</p>
|
||||
`
|
||||
demoRoot.value.appendChild(noticeEl)
|
||||
}
|
||||
} else {
|
||||
// Dynamically import demo component from file
|
||||
const demoModule = await import(`../demos/${props.name}.tsx`)
|
||||
const DemoComponent = demoModule.default || demoModule[props.name]
|
||||
|
||||
if (!DemoComponent) {
|
||||
throw new Error(`Demo component "${props.name}" not found`)
|
||||
}
|
||||
|
||||
// Mount React component
|
||||
if (demoRoot.value) {
|
||||
reactRoot.value = createRoot(demoRoot.value)
|
||||
reactRoot.value.render(DemoComponent)
|
||||
|
||||
// Extract source code
|
||||
try {
|
||||
const rawModule = await import(`../demos/${props.name}.tsx?raw`)
|
||||
sourceCode.value = rawModule.default || rawModule
|
||||
} catch {
|
||||
sourceCode.value = '// Source code not available'
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
loadError.value = err instanceof Error ? err.message : 'Failed to load demo'
|
||||
console.error('DemoContainer load error:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
reactRoot.value?.unmount()
|
||||
})
|
||||
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
const switchTab = (tab: 'preview' | 'code') => {
|
||||
activeTab.value = tab
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="demo-container" :class="{ expanded: isExpanded }">
|
||||
<!-- Demo Header -->
|
||||
<div class="demo-header">
|
||||
<span class="demo-title">{{ demoTitle }}</span>
|
||||
<div class="demo-actions">
|
||||
<button
|
||||
v-if="expandable"
|
||||
class="demo-toggle"
|
||||
:aria-expanded="isExpanded"
|
||||
@click="toggleExpanded"
|
||||
>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="showCode"
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'preview' }"
|
||||
:aria-selected="activeTab === 'preview'"
|
||||
@click="switchTab('preview')"
|
||||
>
|
||||
预览
|
||||
</button>
|
||||
<button
|
||||
v-if="showCode"
|
||||
class="tab-button"
|
||||
:class="{ active: activeTab === 'code' }"
|
||||
:aria-selected="activeTab === 'code'"
|
||||
@click="switchTab('code')"
|
||||
>
|
||||
代码
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Demo Content -->
|
||||
<div
|
||||
class="demo-content"
|
||||
:style="{ height: isExpanded ? 'auto' : height }"
|
||||
>
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="demo-loading">
|
||||
<div class="spinner"></div>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="loadError" class="demo-error">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message">{{ loadError }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Preview Content -->
|
||||
<div
|
||||
v-else-if="activeTab === 'preview'"
|
||||
ref="demoRoot"
|
||||
class="demo-preview"
|
||||
/>
|
||||
|
||||
<!-- Code Content -->
|
||||
<CodeViewer
|
||||
v-else-if="showCode && activeTab === 'code'"
|
||||
:code="sourceCode"
|
||||
lang="tsx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
margin: 16px 0;
|
||||
overflow: hidden;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demo-toggle,
|
||||
.tab-button {
|
||||
padding: 4px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--vp-c-text-2);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.demo-toggle:hover,
|
||||
.tab-button:hover {
|
||||
background: var(--vp-c-bg-mute);
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
background: var(--vp-c-bg);
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
.demo-preview {
|
||||
padding: 24px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.demo-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 40px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--vp-c-border);
|
||||
border-top-color: var(--vp-c-brand);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.demo-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 40px;
|
||||
color: var(--vp-c-danger-1);
|
||||
background: var(--vp-c-danger-soft);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.demo-container.expanded .demo-content {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* Inline demo notice (fallback mode) */
|
||||
:deep(.inline-demo-notice) {
|
||||
padding: 20px;
|
||||
background: var(--vp-c-warning-soft);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.inline-demo-notice p) {
|
||||
margin: 8px 0;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
:deep(.inline-demo-notice strong) {
|
||||
color: var(--vp-c-warning-1);
|
||||
}
|
||||
|
||||
/* Inline demo preview styles */
|
||||
:deep(.inline-demo-preview) {
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.inline-demo-preview button) {
|
||||
margin: 4px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.inline-demo-preview button:hover) {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.demo-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.demo-preview {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -39,14 +39,22 @@
|
||||
<!-- Main badge -->
|
||||
<rect x="-56" y="-20" width="112" height="40" rx="12" class="ccw-bg" filter="url(#ccwShadow)"/>
|
||||
<rect x="-56" y="-20" width="112" height="40" rx="12" fill="none" stroke="url(#ccwBorder)" stroke-width="1.2"/>
|
||||
<!-- Logo icon (simplified favicon: blue square + white lines + green dot) -->
|
||||
<!-- Logo icon (orbital design) -->
|
||||
<g transform="translate(-48, -13)">
|
||||
<rect width="26" height="26" rx="6" fill="var(--vp-c-brand-1)" opacity="0.12"/>
|
||||
<rect width="26" height="26" rx="6" fill="none" stroke="var(--vp-c-brand-1)" stroke-width="0.8" opacity="0.25"/>
|
||||
<line x1="6" y1="9" x2="20" y2="9" stroke="var(--vp-c-brand-1)" stroke-width="2" stroke-linecap="round" opacity="0.65"/>
|
||||
<line x1="6" y1="13" x2="17" y2="13" stroke="var(--vp-c-brand-1)" stroke-width="2" stroke-linecap="round" opacity="0.65"/>
|
||||
<line x1="6" y1="17" x2="14" y2="17" stroke="var(--vp-c-brand-1)" stroke-width="2" stroke-linecap="round" opacity="0.65"/>
|
||||
<circle cx="19" cy="17.5" r="3" fill="#22C55E" opacity="0.8"/>
|
||||
<svg x="1" y="1" width="24" height="24" viewBox="-1 -1 26 26" fill="none" stroke="var(--vp-c-brand-1)" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 12 A8 3 0 0 1 20 12" stroke-width="0.8" opacity="0.2"/>
|
||||
<path d="M16.9 19.5 A8 3 30 0 1 7.1 4.5" stroke-width="0.8" opacity="0.2"/>
|
||||
<path d="M7.1 19.5 A8 3 -30 0 1 16.9 4.5" stroke-width="0.8" opacity="0.2"/>
|
||||
<circle cx="12" cy="12" r="1.5" fill="var(--vp-c-brand-1)" stroke="none" opacity="0.15"/>
|
||||
<path d="M20 12 A8 3 0 0 1 4 12" stroke-width="1.2" opacity="0.5"/>
|
||||
<path d="M7.1 4.5 A8 3 30 0 1 16.9 19.5" stroke-width="1.2" opacity="0.5"/>
|
||||
<path d="M16.9 4.5 A8 3 -30 0 1 7.1 19.5" stroke-width="1.2" opacity="0.5"/>
|
||||
<circle cx="17" cy="10.5" r="1.5" fill="#D97757" stroke="none" opacity="0.8"/>
|
||||
<circle cx="8" cy="16" r="1.5" fill="#10A37F" stroke="none" opacity="0.8"/>
|
||||
<circle cx="14" cy="5.5" r="1.5" fill="#4285F4" stroke="none" opacity="0.8"/>
|
||||
</svg>
|
||||
</g>
|
||||
<!-- Text — shifted right to avoid logo overlap -->
|
||||
<text x="16" y="0" text-anchor="middle" class="ccw-label">CCW</text>
|
||||
|
||||
253
docs/.vitepress/theme/components/LanguageSwitcher.vue
Normal file
253
docs/.vitepress/theme/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { site, lang, page } = useData()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const switcherRef = ref<HTMLElement>()
|
||||
const buttonRef = ref<HTMLElement>()
|
||||
const dropdownPosition = ref({ top: 0, left: 0 })
|
||||
|
||||
// Get available locales from VitePress config
|
||||
const locales = computed(() => {
|
||||
const localeConfig = site.value.locales || {}
|
||||
return Object.entries(localeConfig).map(([code, config]) => ({
|
||||
code,
|
||||
label: (config as any).label || code,
|
||||
link: (config as any).link || `/${code === 'root' ? '' : code}/`
|
||||
}))
|
||||
})
|
||||
|
||||
// Current locale
|
||||
const currentLocale = computed(() => {
|
||||
const current = locales.value.find(l => l.code === lang.value)
|
||||
return current || locales.value[0]
|
||||
})
|
||||
|
||||
// Get alternate language link for current page
|
||||
const getAltLink = (localeCode: string) => {
|
||||
if (localeCode === 'root') localeCode = ''
|
||||
|
||||
// Get current page path without locale prefix
|
||||
const currentPath = page.value.relativePath
|
||||
const altPath = localeCode ? `/${localeCode}/${currentPath}` : `/${currentPath}`
|
||||
|
||||
return altPath
|
||||
}
|
||||
|
||||
const switchLanguage = (localeCode: string) => {
|
||||
const altLink = getAltLink(localeCode)
|
||||
isOpen.value = false
|
||||
window.location.href = altLink
|
||||
}
|
||||
|
||||
// Calculate dropdown position
|
||||
const updatePosition = () => {
|
||||
if (buttonRef.value) {
|
||||
const rect = buttonRef.value.getBoundingClientRect()
|
||||
const isMobile = window.innerWidth <= 768
|
||||
|
||||
if (isMobile) {
|
||||
dropdownPosition.value = {
|
||||
top: rect.bottom + 8,
|
||||
left: 12
|
||||
}
|
||||
} else {
|
||||
dropdownPosition.value = {
|
||||
top: rect.bottom + 4,
|
||||
left: rect.right - 150
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDropdown = async () => {
|
||||
isOpen.value = !isOpen.value
|
||||
if (isOpen.value) {
|
||||
await nextTick()
|
||||
updatePosition()
|
||||
}
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (switcherRef.value && !switcherRef.value.contains(e.target as Node)) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle scroll to close dropdown
|
||||
const handleScroll = () => {
|
||||
if (isOpen.value) {
|
||||
isOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
window.addEventListener('scroll', handleScroll, true)
|
||||
window.addEventListener('resize', updatePosition)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
window.removeEventListener('scroll', handleScroll, true)
|
||||
window.removeEventListener('resize', updatePosition)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="switcherRef" class="language-switcher">
|
||||
<button
|
||||
ref="buttonRef"
|
||||
class="switcher-button"
|
||||
:aria-expanded="isOpen"
|
||||
aria-label="Switch language"
|
||||
@click.stop="toggleDropdown"
|
||||
>
|
||||
<span class="current-locale">{{ currentLocale?.label }}</span>
|
||||
<span class="dropdown-icon" :class="{ open: isOpen }">▼</span>
|
||||
</button>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<ul
|
||||
v-if="isOpen"
|
||||
class="locale-list"
|
||||
:style="{
|
||||
top: dropdownPosition.top + 'px',
|
||||
left: dropdownPosition.left + 'px'
|
||||
}"
|
||||
>
|
||||
<li v-for="locale in locales" :key="locale.code">
|
||||
<button
|
||||
class="locale-button"
|
||||
:class="{ active: locale.code === lang }"
|
||||
@click="switchLanguage(locale.code)"
|
||||
>
|
||||
<span class="locale-label">{{ locale.label }}</span>
|
||||
<span v-if="locale.code === lang" class="check-icon">✓</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.switcher-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.switcher-button:hover {
|
||||
background: var(--vp-c-bg-mute);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.current-locale {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-icon.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Locale list - rendered at body level via Teleport */
|
||||
.locale-list {
|
||||
position: fixed;
|
||||
min-width: 150px;
|
||||
max-width: calc(100vw - 24px);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px 0;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.locale-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.locale-button:hover {
|
||||
background: var(--vp-c-bg-mute);
|
||||
}
|
||||
|
||||
.locale-button.active {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.locale-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.locale-list {
|
||||
width: calc(100vw - 24px) !important;
|
||||
max-width: 300px !important;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25) !important;
|
||||
}
|
||||
|
||||
.switcher-button {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.locale-button {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1275,23 +1275,199 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* Responsive
|
||||
* Responsive (Standardized Breakpoints)
|
||||
* Mobile: < 768px | Tablet: 768px-1024px | Desktop: > 1024px
|
||||
* WCAG 2.1 AA: Touch targets min 44x44px
|
||||
* ============================================ */
|
||||
@media (max-width: 1100px) {
|
||||
|
||||
/* Tablet (768px - 1024px) */
|
||||
@media (max-width: 1024px) {
|
||||
.features-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.hero-container { gap: 1.5rem; }
|
||||
.hero-title { font-size: 2.25rem; }
|
||||
.section-container { padding: 0 1.5rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.hero-container, .json-grid, .quickstart-layout { grid-template-columns: 1fr; text-align: center; }
|
||||
.hero-subtitle { margin-left: auto; margin-right: auto; }
|
||||
.hero-actions { justify-content: center; }
|
||||
.hero-title { font-size: 2.5rem; }
|
||||
.logic-panel { grid-template-columns: 1fr; }
|
||||
.features-grid { grid-template-columns: 1fr; max-width: 420px; margin: 0 auto; }
|
||||
.feature-card { text-align: center; }
|
||||
.feature-icon-box { margin-left: auto; margin-right: auto; }
|
||||
.quickstart-info { text-align: center; }
|
||||
.qs-step { flex-direction: column; align-items: center; text-align: center; }
|
||||
.cta-actions { flex-direction: column; }
|
||||
/* Mobile (< 768px) */
|
||||
@media (max-width: 768px) {
|
||||
/* Hero Section - add extra padding-top to clear fixed nav (56px) */
|
||||
.hero-section {
|
||||
padding: 4.5rem 0 2rem;
|
||||
background:
|
||||
radial-gradient(ellipse 150% 100% at 50% 20%, var(--vp-c-brand-soft) 0%, transparent 60%),
|
||||
var(--vp-c-bg);
|
||||
}
|
||||
.hero-container {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
padding: 0 12px;
|
||||
gap: 2rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.hero-content { order: 1; }
|
||||
.hero-visual { order: 2; }
|
||||
.hero-title {
|
||||
font-size: 1.875rem;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.hero-subtitle {
|
||||
font-size: 1rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 1.75rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
.hero-actions {
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.hero-visual { min-height: 200px; }
|
||||
|
||||
/* Section Container */
|
||||
.section-container {
|
||||
padding: 0 12px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Features Grid */
|
||||
.features-section { padding: 3rem 0; }
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
.feature-card {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
min-height: auto;
|
||||
}
|
||||
.feature-icon-box {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
.feature-card h3 { font-size: 1.05rem; }
|
||||
.feature-card p { font-size: 0.9rem; }
|
||||
|
||||
/* Pipeline Section */
|
||||
.pipeline-section { padding: 3rem 0; }
|
||||
.section-header h2 { font-size: 1.5rem; }
|
||||
.section-header p { font-size: 0.9rem; }
|
||||
.pipeline-card {
|
||||
padding: 1.25rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.pipeline-flow {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.stage-node { flex-direction: row; justify-content: flex-start; gap: 1rem; }
|
||||
.stage-icon { width: 44px; height: 44px; min-width: 44px; }
|
||||
.stage-info { text-align: left; }
|
||||
.stage-info h4 { margin-top: 0; font-size: 0.9rem; }
|
||||
.stage-info p { font-size: 0.8rem; }
|
||||
.logic-panel { grid-template-columns: 1fr; gap: 1rem; }
|
||||
.law-content, .log-content { padding: 1rem; font-size: 0.8rem; min-height: 60px; }
|
||||
|
||||
/* JSON Section */
|
||||
.json-section { padding: 0; overflow-x: hidden; }
|
||||
.json-grid {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
padding: 3rem 12px;
|
||||
gap: 2rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.json-text h2 { font-size: 1.5rem; }
|
||||
.json-text p { font-size: 0.95rem; }
|
||||
.json-benefits { text-align: left; }
|
||||
.json-benefits li { font-size: 0.9rem; }
|
||||
.json-code {
|
||||
padding: 1rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.json-pre { font-size: 0.75rem; }
|
||||
|
||||
/* Quick Start */
|
||||
.quickstart-section { padding: 3rem 0; }
|
||||
.quickstart-layout {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
.quickstart-info { order: 2; }
|
||||
.quickstart-terminal { order: 1; }
|
||||
.qs-step {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.qs-step-num { width: 44px; height: 44px; min-width: 44px; }
|
||||
.qs-terminal-body { min-height: 200px; padding: 1rem; }
|
||||
.qs-terminal-header { padding: 0.4rem 0.6rem; }
|
||||
.qs-tab { min-height: 36px; padding: 0.25rem 0.5rem; }
|
||||
|
||||
/* CTA Section */
|
||||
.cta-section { padding: 3rem 0; }
|
||||
.cta-card {
|
||||
padding: 2rem 1rem;
|
||||
margin: 0 12px;
|
||||
max-width: calc(100% - 24px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cta-card h2 { font-size: 1.5rem; }
|
||||
.cta-card p { font-size: 0.95rem; margin-bottom: 1.5rem; }
|
||||
.cta-actions {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
.cta-actions .btn-primary,
|
||||
.cta-actions .btn-outline,
|
||||
.cta-actions .btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small Mobile (< 480px) */
|
||||
@media (max-width: 480px) {
|
||||
.hero-title { font-size: 1.5rem; }
|
||||
.hero-actions { flex-direction: column; width: 100%; }
|
||||
.hero-actions .btn-primary,
|
||||
.hero-actions .btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.section-title { font-size: 1.25rem; }
|
||||
.pipeline-flow { gap: 0.75rem; }
|
||||
.stage-icon { width: 40px; height: 40px; border-radius: 10px; }
|
||||
.json-text h2 { font-size: 1.25rem; }
|
||||
.cta-card { padding: 1.5rem 1rem; }
|
||||
.cta-card h2 { font-size: 1.25rem; }
|
||||
}
|
||||
</style>
|
||||
|
||||
165
docs/.vitepress/theme/components/PropsTable.vue
Normal file
165
docs/.vitepress/theme/components/PropsTable.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<script setup lang="ts">
|
||||
interface Prop {
|
||||
name: string
|
||||
type: string
|
||||
required?: boolean
|
||||
default?: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
props: Prop[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="props-table-container">
|
||||
<table class="props-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Prop</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, index) in props" :key="index" class="prop-row">
|
||||
<td class="prop-name">
|
||||
<code>{{ prop.name }}</code>
|
||||
</td>
|
||||
<td class="prop-type">
|
||||
<code>{{ prop.type }}</code>
|
||||
</td>
|
||||
<td class="prop-required">
|
||||
<span v-if="prop.required" class="badge required">Yes</span>
|
||||
<span v-else class="badge optional">No</span>
|
||||
</td>
|
||||
<td class="prop-default">
|
||||
<code v-if="prop.default !== undefined">{{ prop.default }}</code>
|
||||
<span v-else class="empty">-</span>
|
||||
</td>
|
||||
<td class="prop-description">
|
||||
{{ prop.description }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.props-table-container {
|
||||
margin: 16px 0;
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.props-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.props-table thead {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
.props-table th {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.props-table td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.props-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.props-table tr:hover {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.prop-name code,
|
||||
.prop-type code,
|
||||
.prop-default code {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 13px;
|
||||
padding: 2px 6px;
|
||||
background: var(--vp-code-bg);
|
||||
border-radius: 4px;
|
||||
color: var(--vp-code-color);
|
||||
}
|
||||
|
||||
.prop-name {
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-brand);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prop-type {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prop-required {
|
||||
white-space: nowrap;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge.required {
|
||||
background: var(--vp-c-danger-soft);
|
||||
color: var(--vp-c-danger-1);
|
||||
}
|
||||
|
||||
.badge.optional {
|
||||
background: var(--vp-c-default-soft);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.prop-default {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prop-default .empty {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.prop-description {
|
||||
color: var(--vp-c-text-2);
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.props-table {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.props-table th,
|
||||
.props-table td {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.prop-description {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const dotColor = ref('var(--vp-c-primary)')
|
||||
|
||||
function updateDotColor() {
|
||||
if (typeof document === 'undefined') return
|
||||
|
||||
const root = document.documentElement
|
||||
const style = getComputedStyle(root)
|
||||
const primaryColor = style.getPropertyValue('--vp-c-primary').trim()
|
||||
dotColor.value = primaryColor || 'currentColor'
|
||||
}
|
||||
|
||||
let observer: MutationObserver | null = null
|
||||
|
||||
onMounted(() => {
|
||||
updateDotColor()
|
||||
|
||||
// Watch for theme changes via MutationObserver
|
||||
observer = new MutationObserver(() => {
|
||||
updateDotColor()
|
||||
})
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme', 'class'],
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
observer?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
viewBox="-1 -1 26 26"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="theme-logo"
|
||||
aria-label="Claude Code Workflow"
|
||||
>
|
||||
<!-- Three horizontal lines - use currentColor to inherit from text -->
|
||||
<line x1="3" y1="6" x2="18" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="12" x2="15" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="18" x2="12" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Status dot - follows theme primary color -->
|
||||
<circle cx="19" cy="17" r="3" :style="{ fill: dotColor }"/>
|
||||
<!-- Back orbit halves -->
|
||||
<path d="M4 12 A8 3 0 0 1 20 12" stroke-width="0.9" opacity="0.15"/>
|
||||
<path d="M16.9 19.5 A8 3 30 0 1 7.1 4.5" stroke-width="0.9" opacity="0.15"/>
|
||||
<path d="M7.1 19.5 A8 3 -30 0 1 16.9 4.5" stroke-width="0.9" opacity="0.15"/>
|
||||
<!-- Core breathing pulse -->
|
||||
<circle cx="12" cy="12" r="2" fill="currentColor" stroke="none" opacity="0.1">
|
||||
<animate attributeName="opacity" dur="4s" repeatCount="indefinite" values="0.06;0.18;0.06"/>
|
||||
</circle>
|
||||
<circle cx="12" cy="12" r="1" fill="currentColor" stroke="none" opacity="0.25">
|
||||
<animate attributeName="opacity" dur="4s" repeatCount="indefinite" values="0.15;0.4;0.15"/>
|
||||
</circle>
|
||||
<!-- Front orbit halves -->
|
||||
<path d="M20 12 A8 3 0 0 1 4 12" stroke-width="1.3" opacity="0.75"/>
|
||||
<path d="M7.1 4.5 A8 3 30 0 1 16.9 19.5" stroke-width="1.3" opacity="0.75"/>
|
||||
<path d="M16.9 4.5 A8 3 -30 0 1 7.1 19.5" stroke-width="1.3" opacity="0.75"/>
|
||||
<!-- Claude agent -->
|
||||
<g>
|
||||
<animateMotion dur="8s" repeatCount="indefinite" path="M20,12 A8,3,0,0,0,4,12 A8,3,0,0,0,20,12"/>
|
||||
<animate attributeName="opacity" dur="8s" repeatCount="indefinite" values="0.95;0.95;0.3;0.3;0.95" keyTimes="0;0.35;0.5;0.65;1"/>
|
||||
<circle r="1.5" fill="#D97757" stroke="none" opacity="0.9"/>
|
||||
</g>
|
||||
<!-- OpenAI agent -->
|
||||
<g>
|
||||
<animateMotion dur="10s" repeatCount="indefinite" begin="-3s" path="M7.1,4.5 A8,3,30,0,1,16.9,19.5 A8,3,30,0,1,7.1,4.5"/>
|
||||
<animate attributeName="opacity" dur="10s" repeatCount="indefinite" begin="-3s" values="0.95;0.95;0.3;0.3;0.95" keyTimes="0;0.35;0.5;0.65;1"/>
|
||||
<circle r="1.5" fill="#10A37F" stroke="none" opacity="0.9"/>
|
||||
</g>
|
||||
<!-- Gemini agent -->
|
||||
<g>
|
||||
<animateMotion dur="12s" repeatCount="indefinite" begin="-5s" path="M16.9,4.5 A8,3,-30,0,1,7.1,19.5 A8,3,-30,0,1,16.9,4.5"/>
|
||||
<animate attributeName="opacity" dur="12s" repeatCount="indefinite" begin="-5s" values="0.95;0.95;0.3;0.3;0.95" keyTimes="0;0.35;0.5;0.65;1"/>
|
||||
<circle r="1.5" fill="#4285F4" stroke="none" opacity="0.9"/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -56,9 +51,4 @@ onUnmounted(() => {
|
||||
height: 24px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.theme-logo circle {
|
||||
fill: var(--vp-c-primary);
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,10 +35,16 @@ export const STATUS_COLORS = {
|
||||
const STORAGE_KEY_THEME = 'ccw-theme'
|
||||
const STORAGE_KEY_COLOR_MODE = 'ccw-color-mode'
|
||||
|
||||
/**
|
||||
* Check if running in browser environment
|
||||
*/
|
||||
const isBrowser = typeof window !== 'undefined' && typeof localStorage !== 'undefined'
|
||||
|
||||
/**
|
||||
* Get current theme from localStorage or default
|
||||
*/
|
||||
export function getCurrentTheme(): ThemeName {
|
||||
if (!isBrowser) return 'blue'
|
||||
const saved = localStorage.getItem(STORAGE_KEY_THEME)
|
||||
if (saved && saved in THEME_COLORS) {
|
||||
return saved as ThemeName
|
||||
@@ -50,6 +56,7 @@ export function getCurrentTheme(): ThemeName {
|
||||
* Check if dark mode is active
|
||||
*/
|
||||
export function isDarkMode(): boolean {
|
||||
if (!isBrowser) return false
|
||||
const mode = localStorage.getItem(STORAGE_KEY_COLOR_MODE) || 'auto'
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
return mode === 'dark' || (mode === 'auto' && prefersDark)
|
||||
@@ -70,17 +77,22 @@ export function getStatusColor(isDark: boolean): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate favicon SVG with dynamic colors (line style)
|
||||
* Generate favicon SVG with orbital design
|
||||
*/
|
||||
export function generateFaviconSvg(theme: ThemeName, isDark: boolean): string {
|
||||
const lineColor = getThemeColor(theme, isDark)
|
||||
const dotColor = getThemeColor(theme, isDark) // Dot follows theme color
|
||||
const orbitColor = getThemeColor(theme, isDark)
|
||||
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<line x1="3" y1="6" x2="18" y2="6" stroke="${lineColor}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="12" x2="15" y2="12" stroke="${lineColor}" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="18" x2="12" y2="18" stroke="${lineColor}" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="19" cy="17" r="3" fill="${dotColor}"/>
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-1 -1 26 26" fill="none" stroke="${orbitColor}" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 12 A8 3 0 0 1 20 12" stroke-width="0.9" opacity="0.15"/>
|
||||
<path d="M16.9 19.5 A8 3 30 0 1 7.1 4.5" stroke-width="0.9" opacity="0.15"/>
|
||||
<path d="M7.1 19.5 A8 3 -30 0 1 16.9 4.5" stroke-width="0.9" opacity="0.15"/>
|
||||
<circle cx="12" cy="12" r="1.5" fill="${orbitColor}" stroke="none" opacity="0.2"/>
|
||||
<path d="M20 12 A8 3 0 0 1 4 12" stroke-width="1.3" opacity="0.75"/>
|
||||
<path d="M7.1 4.5 A8 3 30 0 1 16.9 19.5" stroke-width="1.3" opacity="0.75"/>
|
||||
<path d="M16.9 4.5 A8 3 -30 0 1 7.1 19.5" stroke-width="1.3" opacity="0.75"/>
|
||||
<circle cx="17" cy="10.5" r="1.8" fill="#D97757" stroke="none"/>
|
||||
<circle cx="8" cy="16" r="1.8" fill="#10A37F" stroke="none"/>
|
||||
<circle cx="14" cy="5.5" r="1.8" fill="#4285F4" stroke="none"/>
|
||||
</svg>`
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,16 @@ import Breadcrumb from './components/Breadcrumb.vue'
|
||||
import PageToc from './components/PageToc.vue'
|
||||
import ProfessionalHome from './components/ProfessionalHome.vue'
|
||||
import Layout from './layouts/Layout.vue'
|
||||
// Demo system components
|
||||
import DemoContainer from './components/DemoContainer.vue'
|
||||
import CodeViewer from './components/CodeViewer.vue'
|
||||
import PropsTable from './components/PropsTable.vue'
|
||||
// Language switcher component
|
||||
import LanguageSwitcher from './components/LanguageSwitcher.vue'
|
||||
import './styles/variables.css'
|
||||
import './styles/custom.css'
|
||||
import './styles/mobile.css'
|
||||
import './styles/demo.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
@@ -23,5 +30,11 @@ export default {
|
||||
app.component('Breadcrumb', Breadcrumb)
|
||||
app.component('PageToc', PageToc)
|
||||
app.component('ProfessionalHome', ProfessionalHome)
|
||||
// Register demo system components
|
||||
app.component('DemoContainer', DemoContainer)
|
||||
app.component('CodeViewer', CodeViewer)
|
||||
app.component('PropsTable', PropsTable)
|
||||
// Register language switcher component
|
||||
app.component('LanguageSwitcher', LanguageSwitcher)
|
||||
}
|
||||
}
|
||||
|
||||
130
docs/.vitepress/theme/inlineDemoPlugin.ts
Normal file
130
docs/.vitepress/theme/inlineDemoPlugin.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Vite plugin for handling inline demo blocks as virtual modules.
|
||||
* This allows React JSX code embedded in markdown :::demo blocks to be
|
||||
* dynamically compiled and executed as proper React components.
|
||||
*/
|
||||
|
||||
import type { Plugin } from 'vite'
|
||||
import type { DemoBlockMeta } from './markdownTransform'
|
||||
|
||||
// Global registry for inline demos (populated during markdown transform)
|
||||
const inlineDemoRegistry = new Map<string, { code: string; name: string }>()
|
||||
|
||||
// Virtual module prefix
|
||||
const VIRTUAL_PREFIX = 'virtual:inline-demo:'
|
||||
const VIRTUAL_PREFIX_FULL = '\0' + VIRTUAL_PREFIX
|
||||
|
||||
/**
|
||||
* Register an inline demo during markdown transformation.
|
||||
* Returns a virtual module ID that can be imported.
|
||||
*/
|
||||
export function registerInlineDemo(
|
||||
demoId: string,
|
||||
code: string,
|
||||
name: string
|
||||
): string {
|
||||
inlineDemoRegistry.set(demoId, { code, name })
|
||||
return VIRTUAL_PREFIX + demoId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered inline demo by ID
|
||||
*/
|
||||
export function getInlineDemo(demoId: string) {
|
||||
return inlineDemoRegistry.get(demoId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered demos (useful for rebuilds)
|
||||
*/
|
||||
export function clearInlineDemos() {
|
||||
inlineDemoRegistry.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Vite plugin that resolves virtual inline-demo modules.
|
||||
* These modules contain the React/JSX code from markdown :::demo blocks.
|
||||
*/
|
||||
export function inlineDemoPlugin(): Plugin {
|
||||
return {
|
||||
name: 'vitepress-inline-demo-plugin',
|
||||
enforce: 'pre',
|
||||
|
||||
resolveId(id) {
|
||||
// Handle virtual module resolution
|
||||
if (id.startsWith(VIRTUAL_PREFIX)) {
|
||||
return VIRTUAL_PREFIX_FULL + id.slice(VIRTUAL_PREFIX.length)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
load(id) {
|
||||
// Load virtual module content
|
||||
if (id.startsWith(VIRTUAL_PREFIX_FULL)) {
|
||||
const demoId = id.slice(VIRTUAL_PREFIX_FULL.length)
|
||||
const demo = inlineDemoRegistry.get(demoId)
|
||||
|
||||
if (!demo) {
|
||||
return `export default function MissingDemo() {
|
||||
return React.createElement('div', { className: 'demo-error' },
|
||||
'Demo not found: ${demoId}'
|
||||
)
|
||||
}`
|
||||
}
|
||||
|
||||
// Wrap the inline code in an ESM module
|
||||
// The code should export a React component
|
||||
return `
|
||||
import React from 'react';
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
|
||||
// User's inline demo code:
|
||||
${demo.code}
|
||||
|
||||
// Auto-export fallback if no default export
|
||||
export default ${demo.name} || (() => React.createElement('div', null, 'Demo component "${demo.name}" not found'));
|
||||
`
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// Handle HMR for inline demos
|
||||
handleHotUpdate({ file, server }) {
|
||||
// When markdown files change, clear the registry
|
||||
if (file.endsWith('.md')) {
|
||||
clearInlineDemos()
|
||||
server.ws.send({
|
||||
type: 'full-reload',
|
||||
path: file
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform inline demo code to ensure it has proper exports.
|
||||
* This wraps bare JSX in a function component if needed.
|
||||
*/
|
||||
export function wrapDemoCode(code: string, componentName: string): string {
|
||||
// If code already has export, return as-is
|
||||
if (code.includes('export ')) {
|
||||
return code
|
||||
}
|
||||
|
||||
// If code is just JSX (starts with <), wrap it
|
||||
const trimmedCode = code.trim()
|
||||
if (trimmedCode.startsWith('<') || trimmedCode.startsWith('React.createElement')) {
|
||||
return `
|
||||
function ${componentName}() {
|
||||
return (
|
||||
${trimmedCode}
|
||||
);
|
||||
}
|
||||
export default ${componentName};
|
||||
`
|
||||
}
|
||||
|
||||
// Otherwise return as-is and hope it defines the component
|
||||
return code + `\nexport default ${componentName};`
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import DefaultTheme from 'vitepress/theme'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useDynamicIcon } from '../composables/useDynamicIcon'
|
||||
import ThemeLogo from '../components/ThemeLogo.vue'
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||
|
||||
let mediaQuery: MediaQueryList | null = null
|
||||
let systemThemeChangeHandler: (() => void) | null = null
|
||||
@@ -81,24 +82,61 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template #nav-bar-content-after>
|
||||
<div class="nav-extensions">
|
||||
<DocSearch />
|
||||
<DarkModeToggle />
|
||||
<ThemeSwitcher />
|
||||
<DocSearch class="nav-item-always" />
|
||||
<DarkModeToggle class="nav-item-desktop" />
|
||||
<ThemeSwitcher class="nav-item-desktop" />
|
||||
<LanguageSwitcher class="nav-item-desktop" />
|
||||
</div>
|
||||
</template>
|
||||
</DefaultTheme.Layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ============================================
|
||||
* Container Query Context Definitions
|
||||
* Enables component-level responsive design
|
||||
* ============================================ */
|
||||
|
||||
/* Set container context on layout root */
|
||||
:deep(.Layout) {
|
||||
container-type: inline-size;
|
||||
container-name: layout;
|
||||
}
|
||||
|
||||
/* Sidebar container context */
|
||||
:deep(.VPSidebar) {
|
||||
container-type: inline-size;
|
||||
container-name: sidebar;
|
||||
}
|
||||
|
||||
/* Main content container context */
|
||||
:deep(.VPContent) {
|
||||
container-type: inline-size;
|
||||
container-name: content;
|
||||
}
|
||||
|
||||
/* Document outline container context */
|
||||
:deep(.VPDocOutline) {
|
||||
container-type: inline-size;
|
||||
container-name: outline;
|
||||
}
|
||||
|
||||
/* Navigation container context */
|
||||
:deep(.VPNav) {
|
||||
container-type: inline-size;
|
||||
container-name: nav;
|
||||
}
|
||||
|
||||
/* Hero section with fluid spacing */
|
||||
.hero-extensions {
|
||||
margin-top: 40px;
|
||||
margin-top: var(--spacing-fluid-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 48px;
|
||||
gap: var(--spacing-fluid-xl);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -107,23 +145,23 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-size: clamp(1.5rem, 1.25rem + 1.25vw, 2rem);
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
font-size: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 4px;
|
||||
margin-top: var(--spacing-fluid-xs);
|
||||
}
|
||||
|
||||
.nav-extensions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: var(--spacing-fluid-sm);
|
||||
margin-left: auto;
|
||||
padding-left: 16px;
|
||||
padding-left: var(--spacing-fluid-sm);
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
@@ -154,22 +192,64 @@ onBeforeUnmount(() => {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Mobile overrides now handled by fluid spacing variables */
|
||||
/* Container queries in mobile.css provide additional responsiveness */
|
||||
|
||||
/* Mobile-specific styles */
|
||||
@media (max-width: 768px) {
|
||||
.hero-extensions {
|
||||
margin-top: 1rem;
|
||||
padding: 0 12px;
|
||||
max-width: 100vw;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
gap: 24px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.nav-extensions {
|
||||
gap: 8px;
|
||||
padding-left: 8px;
|
||||
gap: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Hide desktop-only nav items on mobile */
|
||||
.nav-item-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Keep always-visible items */
|
||||
.nav-item-always {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Ensure nav bar allows dropdown overflow */
|
||||
:deep(.VPNavBar) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
:deep(.VPNavBar .content) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Fix dropdown positioning for mobile */
|
||||
:deep(.VPNavBarMenuGroup .items) {
|
||||
position: fixed !important;
|
||||
left: 12px !important;
|
||||
right: 12px !important;
|
||||
top: 56px !important;
|
||||
max-width: none !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
95
docs/.vitepress/theme/markdownTransform.ts
Normal file
95
docs/.vitepress/theme/markdownTransform.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { MarkdownTransformContext } from 'vitepress'
|
||||
|
||||
// Multi-line demo block: :::demo Name #file ...code... :::
|
||||
const demoBlockRE = /:::\s*demo\s+([^\n]+)\n([\s\S]*?)\n:::/g
|
||||
|
||||
// Single-line demo block: :::demo Name #file :::
|
||||
const demoBlockSingleRE = /:::\s*demo\s+(\S+)\s*(#\S+)?\s*:::/g
|
||||
|
||||
export interface DemoBlockMeta {
|
||||
name: string
|
||||
file?: string
|
||||
code?: string
|
||||
height?: string
|
||||
expandable?: boolean
|
||||
showCode?: boolean
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function transformDemoBlocks(
|
||||
code: string,
|
||||
ctx: MarkdownTransformContext
|
||||
): string {
|
||||
// First handle multi-line demo blocks with inline code
|
||||
let result = code.replace(demoBlockRE, (match, headerLine, codeContent) => {
|
||||
const meta = parseDemoHeader(headerLine)
|
||||
|
||||
const demoId = `demo-${ctx.path.replace(/[^a-z0-9]/gi, '-')}-${meta.name}-${Date.now().toString(36)}`
|
||||
|
||||
const props = [
|
||||
`id="${demoId}"`,
|
||||
`name="${meta.name}"`,
|
||||
meta.file ? `file="${meta.file}"` : '',
|
||||
meta.height ? `height="${meta.height}"` : '',
|
||||
meta.expandable === false ? ':expandable="false"' : '',
|
||||
meta.showCode === false ? ':show-code="false"' : '',
|
||||
meta.title ? `title="${meta.title}"` : ''
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
// Return a simple comment placeholder - the inline code will be ignored
|
||||
// This avoids Vue parsing issues with JSX in markdown
|
||||
return `<DemoContainer ${props} />`
|
||||
})
|
||||
|
||||
// Then handle single-line demo blocks (file references only)
|
||||
result = result.replace(demoBlockSingleRE, (match, name, fileRef) => {
|
||||
const demoId = `demo-${ctx.path.replace(/[^a-z0-9]/gi, '-')}-${name}-${Date.now().toString(36)}`
|
||||
|
||||
const file = fileRef ? fileRef.slice(1) : undefined
|
||||
|
||||
const props = [
|
||||
`id="${demoId}"`,
|
||||
`name="${name}"`,
|
||||
file ? `file="${file}"` : '',
|
||||
':expandable="true"',
|
||||
':show-code="true"'
|
||||
].filter(Boolean).join(' ')
|
||||
|
||||
return `<DemoContainer ${props} />`
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function parseDemoHeader(headerLine: string): DemoBlockMeta {
|
||||
const parts = headerLine.trim().split(/\s+/)
|
||||
const name = parts[0] || ''
|
||||
const file = parts.find(p => p.startsWith('#'))?.slice(1)
|
||||
|
||||
// Extract props from remaining parts
|
||||
const props: Record<string, string> = {}
|
||||
for (const part of parts.slice(1)) {
|
||||
if (part.includes(':')) {
|
||||
const [key, value] = part.split(':', 2)
|
||||
props[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
file,
|
||||
height: props.height,
|
||||
expandable: props.expandable !== 'false',
|
||||
showCode: props.showCode !== 'false',
|
||||
title: props.title
|
||||
}
|
||||
}
|
||||
|
||||
// VitePress markdown configuration hook
|
||||
export function markdownTransformSetup() {
|
||||
return {
|
||||
transform: (code: string, ctx: MarkdownTransformContext) => {
|
||||
return transformDemoBlocks(code, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,11 +85,26 @@
|
||||
.VPContent:has(.pro-home) {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
max-width: 100% !important;
|
||||
max-width: 100vw !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
.Layout:has(.pro-home) {
|
||||
max-width: 100% !important;
|
||||
max-width: 100vw !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* Ensure VPNav doesn't cause overflow */
|
||||
.Layout:has(.pro-home) .VPNav {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
/* Ensure VPFooter doesn't cause overflow */
|
||||
.Layout:has(.pro-home) .VPFooter {
|
||||
max-width: 100vw;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ProfessionalHome component full width */
|
||||
@@ -98,6 +113,8 @@
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
overflow-x: hidden !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Ensure all sections extend to full width */
|
||||
@@ -111,6 +128,8 @@
|
||||
margin: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
.VPHomeHero {
|
||||
|
||||
104
docs/.vitepress/theme/styles/demo.css
Normal file
104
docs/.vitepress/theme/styles/demo.css
Normal file
@@ -0,0 +1,104 @@
|
||||
/* Demo system styles for VitePress */
|
||||
|
||||
/* Demo container theme-aware styling */
|
||||
.demo-container {
|
||||
/* Theme-aware borders */
|
||||
border: 1px solid var(--vp-c-border);
|
||||
|
||||
/* Rounded corners */
|
||||
border-radius: 8px;
|
||||
|
||||
/* Subtle shadow */
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Dark mode support */
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.demo-preview {
|
||||
/* Consistent padding */
|
||||
padding: 24px;
|
||||
|
||||
/* Min height for visibility */
|
||||
min-height: 100px;
|
||||
|
||||
/* Center content when small */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Dark mode overrides */
|
||||
.dark .demo-container {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-color: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.demo-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.demo-preview {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
/* Demo code viewer styles */
|
||||
.code-viewer {
|
||||
background: var(--vp-code-bg);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: var(--vp-code-block-bg);
|
||||
border-bottom: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
/* Props table styles */
|
||||
.props-table-container {
|
||||
margin: 16px 0;
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--vp-c-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.props-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.props-table thead {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-border);
|
||||
}
|
||||
|
||||
/* Interactive demo feedback */
|
||||
.demo-container:has(.demo-preview:hover) {
|
||||
border-color: var(--vp-c-brand);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
/* Code syntax highlighting in demos */
|
||||
.demo-preview code {
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 13px;
|
||||
padding: 2px 6px;
|
||||
background: var(--vp-code-bg);
|
||||
border-radius: 4px;
|
||||
color: var(--vp-code-color);
|
||||
}
|
||||
@@ -1,9 +1,250 @@
|
||||
/**
|
||||
* Mobile-Responsive Styles
|
||||
* Breakpoints: 320px-768px (mobile), 768px-1024px (tablet), 1024px+ (desktop)
|
||||
* Uses CSS custom properties from variables.css: --bp-mobile, --bp-tablet, --bp-desktop
|
||||
* WCAG 2.1 AA compliant
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
* Container Query Support
|
||||
* Enable component-level responsive design
|
||||
* Fallback: Uses @supports to provide media query fallbacks
|
||||
* ============================================ */
|
||||
|
||||
/* Fallback for browsers without container query support */
|
||||
@supports not (container-type: inline-size) {
|
||||
/* Sidebar fallback */
|
||||
.VPSidebar {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPSidebar {
|
||||
width: var(--vp-sidebar-width, 272px);
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Content fallback */
|
||||
.VPContent {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPContent {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.VPContent {
|
||||
padding: 32px 48px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Outline fallback */
|
||||
.VPDocOutline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPDocOutline {
|
||||
display: block;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.VPDocOutline {
|
||||
width: 256px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Container Query Rules (modern browsers) */
|
||||
@supports (container-type: inline-size) {
|
||||
/* Sidebar Container Queries */
|
||||
@container sidebar (max-width: 480px) {
|
||||
.VPSidebar .group {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.VPSidebar .title {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@container sidebar (min-width: 480px) and (max-width: 768px) {
|
||||
.VPSidebar .group {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.VPSidebar .title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@container sidebar (min-width: 768px) {
|
||||
.VPSidebar .group {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.VPSidebar .title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* Content Container Queries */
|
||||
@container content (max-width: 640px) {
|
||||
.VPDoc .content-container {
|
||||
padding: 0 var(--spacing-fluid-sm);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.vp-doc pre {
|
||||
font-size: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@container content (min-width: 640px) and (max-width: 960px) {
|
||||
.VPDoc .content-container {
|
||||
padding: 0 var(--spacing-fluid-md);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.vp-doc pre {
|
||||
font-size: 13px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@container content (min-width: 960px) {
|
||||
.VPDoc .content-container {
|
||||
padding: 0 var(--spacing-fluid-lg);
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.vp-doc h2 {
|
||||
font-size: 1.625rem;
|
||||
}
|
||||
|
||||
.vp-doc pre {
|
||||
font-size: 14px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Outline Container Queries */
|
||||
@container outline (max-width: 200px) {
|
||||
.VPDocOutline .outline-link {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.VPDocOutline .outline-marker {
|
||||
width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@container outline (min-width: 200px) and (max-width: 280px) {
|
||||
.VPDocOutline .outline-link {
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.VPDocOutline .outline-marker {
|
||||
width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@container outline (min-width: 280px) {
|
||||
.VPDocOutline .outline-link {
|
||||
font-size: 13px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.VPDocOutline .outline-marker {
|
||||
width: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Navigation Container Queries */
|
||||
@container nav (max-width: 640px) {
|
||||
.VPNavBar {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.VPNavBar .nav-extensions {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@container nav (min-width: 640px) and (max-width: 960px) {
|
||||
.VPNavBar {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.VPNavBar .nav-extensions {
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@container nav (min-width: 960px) {
|
||||
.VPNavBar {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.VPNavBar .nav-extensions {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Generic Container-Responsive Utility Class */
|
||||
@container (max-width: 480px) {
|
||||
.container-responsive {
|
||||
padding: 0 var(--spacing-fluid-xs);
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 480px) and (max-width: 768px) {
|
||||
.container-responsive {
|
||||
padding: 0 var(--spacing-fluid-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 768px) and (max-width: 1024px) {
|
||||
.container-responsive {
|
||||
padding: 0 var(--spacing-fluid-md);
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 1024px) {
|
||||
.container-responsive {
|
||||
padding: 0 var(--spacing-fluid-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* Mobile First Approach
|
||||
* ============================================ */
|
||||
@@ -18,32 +259,218 @@
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
/* Navigation - ensure hamburger menu is visible */
|
||||
.VPNav {
|
||||
height: 56px;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.VPNavBar {
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
/* Navigation bar content wrapper */
|
||||
.VPNavBar .content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Show hamburger menu button on mobile */
|
||||
.VPNavBar .VPNavBarHamburger {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Hide desktop nav links on mobile, use hamburger menu */
|
||||
.VPNavBar .VPNavBarMenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Ensure nav title is visible */
|
||||
.VPNavBar .VPNavBarTitle {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Reduce nav-extensions gap on mobile */
|
||||
.VPNavBar .nav-extensions {
|
||||
gap: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* Hide non-essential nav items on mobile */
|
||||
.nav-extensions .nav-item-desktop {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Style search button for mobile */
|
||||
.nav-extensions .DocSearch {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
/* Ensure VitePress social link is hidden on mobile (uses sidebar) */
|
||||
.VPNavBar .VPNavBarSocialLinks {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Fix dropdown menus overflow on mobile */
|
||||
.VPNavBar .VPNavBarMenuGroup {
|
||||
position: relative;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.VPNavBar .VPNavBarMenuGroup .items {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
min-width: 180px;
|
||||
max-width: calc(100vw - 24px);
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
z-index: 100;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Language switcher dropdown fix */
|
||||
.language-switcher {
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.language-switcher .locale-list {
|
||||
position: fixed !important;
|
||||
top: auto !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
right: auto !important;
|
||||
min-width: 200px !important;
|
||||
max-width: calc(100vw - 24px) !important;
|
||||
z-index: 1000 !important;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Sidebar - fix display issues */
|
||||
.VPSidebar {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
width: 100% !important;
|
||||
max-width: 320px !important;
|
||||
padding-top: 0 !important;
|
||||
top: 56px !important;
|
||||
height: calc(100vh - 56px) !important;
|
||||
max-height: calc(100vh - 56px) !important;
|
||||
overflow: visible !important;
|
||||
position: fixed !important;
|
||||
left: 0 !important;
|
||||
z-index: 40 !important;
|
||||
background: var(--vp-c-bg) !important;
|
||||
transition: transform 0.25s ease !important;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
/* Sidebar when open */
|
||||
.VPSidebar.open,
|
||||
.sidebar-open .VPSidebar {
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
/* Sidebar when closed */
|
||||
.VPSidebar:not(.open) {
|
||||
transform: translateX(-100%) !important;
|
||||
}
|
||||
|
||||
/* Sidebar nav container */
|
||||
.VPSidebar .VPSidebarNav {
|
||||
padding: 12px 0;
|
||||
height: 100%;
|
||||
min-height: auto;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Sidebar groups */
|
||||
.VPSidebar .VPSidebarGroup {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
/* Sidebar items */
|
||||
.VPSidebar .VPSidebarItem {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
/* Ensure sidebar links are properly sized */
|
||||
.VPSidebar .link {
|
||||
padding: 8px 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Local nav for mobile */
|
||||
.VPLocalNav {
|
||||
display: flex !important;
|
||||
position: sticky;
|
||||
top: 56px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Sidebar curtain/backdrop */
|
||||
.VPSidebar curtain,
|
||||
.VPSidebar .curtain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Sidebar scroll container */
|
||||
.VPSidebar .sidebar-container,
|
||||
.VPSidebar nav {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Make sure all sidebar content is visible */
|
||||
.VPSidebar .group {
|
||||
margin: 0;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.VPSidebar .title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
padding: 4px 0;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* Sidebar text styling */
|
||||
.VPSidebar .text {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
/* Ensure nested items are visible */
|
||||
.VPSidebar .items {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Backdrop for sidebar */
|
||||
.VPBackdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
top: 56px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 39;
|
||||
}
|
||||
|
||||
/* Content - reduce padding for better space usage */
|
||||
.VPContent {
|
||||
padding: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Doc content adjustments */
|
||||
/* Doc content adjustments - reduce padding */
|
||||
.VPDoc .content-container {
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* Hide outline on mobile */
|
||||
@@ -53,7 +480,7 @@
|
||||
|
||||
/* Hero Section */
|
||||
.VPHomeHero {
|
||||
padding: 40px 16px;
|
||||
padding: 40px 12px;
|
||||
}
|
||||
|
||||
.VPHomeHero h1 {
|
||||
@@ -65,14 +492,14 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Code Blocks */
|
||||
/* Code Blocks - reduce margins */
|
||||
div[class*='language-'] {
|
||||
margin: 12px -16px;
|
||||
margin: 12px -12px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
div[class*='language-'] pre {
|
||||
padding: 12px 16px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -319,6 +746,230 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* ProfessionalHome Component - Mobile Optimizations
|
||||
* ============================================ */
|
||||
@media (max-width: 768px) {
|
||||
/* Root level overflow prevention */
|
||||
html, body {
|
||||
overflow-x: hidden;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
/* VitePress Layout container fix */
|
||||
.Layout {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* VPContent container fix */
|
||||
.VPContent {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Hero extensions in Layout.vue - add proper padding */
|
||||
.hero-extensions {
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Hero Section */
|
||||
.pro-home .hero-section {
|
||||
min-height: auto;
|
||||
padding-top: 4.5rem; /* Clear fixed nav (56px) */
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Prevent horizontal scroll */
|
||||
.pro-home {
|
||||
overflow-x: hidden;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.pro-home .hero-container {
|
||||
max-width: 100%;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Fix section containers */
|
||||
.pro-home .section-container {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Feature Cards */
|
||||
.pro-home .feature-card {
|
||||
border-radius: 12px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.pro-home .feature-card:active {
|
||||
transform: scale(0.98);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Pipeline Animation */
|
||||
.pro-home .cadence-track {
|
||||
margin: 1rem 0 2rem;
|
||||
}
|
||||
|
||||
.pro-home .tick-node {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
min-width: 12px;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Terminal Window */
|
||||
.pro-home .terminal-window,
|
||||
.pro-home .qs-terminal-window {
|
||||
font-size: 0.75rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.pro-home .terminal-header,
|
||||
.pro-home .qs-terminal-header {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Code Blocks */
|
||||
.pro-home .json-code {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin: 0 -1rem;
|
||||
border-radius: 0;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Buttons touch targets */
|
||||
.pro-home .btn-primary,
|
||||
.pro-home .btn-secondary,
|
||||
.pro-home .btn-outline,
|
||||
.pro-home .btn-ghost {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - CTA Section */
|
||||
.pro-home .cta-card {
|
||||
margin: 0 0.5rem;
|
||||
border-radius: 16px;
|
||||
max-width: calc(100% - 1rem);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Animation adjustments */
|
||||
.pro-home .reveal-text,
|
||||
.pro-home .reveal-card,
|
||||
.pro-home .reveal-slide {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Stage nodes in pipeline */
|
||||
.pro-home .stage-node {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Quick Start Section - prevent overflow */
|
||||
.pro-home .quickstart-section {
|
||||
padding: 3rem 0;
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.pro-home .quickstart-layout {
|
||||
padding: 0 12px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pro-home .quickstart-info,
|
||||
.pro-home .quickstart-terminal {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Ensure all text content wraps properly */
|
||||
.pro-home .json-text,
|
||||
.pro-home .json-benefits,
|
||||
.pro-home .qs-step-content {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Features section overflow fix */
|
||||
.pro-home .features-section {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
/* Pipeline section overflow fix */
|
||||
.pro-home .pipeline-section {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Tablet Optimizations */
|
||||
@media (min-width: 768px) and (max-width: 1024px) {
|
||||
.pro-home .hero-section {
|
||||
padding: 3rem 0 2.5rem;
|
||||
}
|
||||
|
||||
.pro-home .hero-title {
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
|
||||
.pro-home .features-grid {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.pro-home .json-grid {
|
||||
gap: 2rem;
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ProfessionalHome - Small Mobile (< 480px) */
|
||||
@media (max-width: 480px) {
|
||||
.pro-home .hero-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
|
||||
.pro-home .section-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.pro-home .pipeline-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Ensure touch targets */
|
||||
.pro-home .btn-primary,
|
||||
.pro-home .btn-secondary {
|
||||
padding: 0.875rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
* Dark Mode Specific
|
||||
* ============================================ */
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
--vp-font-size-lg: 18px;
|
||||
--vp-font-size-xl: 20px;
|
||||
|
||||
/* Spacing */
|
||||
/* Spacing (Fixed) */
|
||||
--vp-spacing-xs: 0.25rem; /* 4px */
|
||||
--vp-spacing-sm: 0.5rem; /* 8px */
|
||||
--vp-spacing-md: 1rem; /* 16px */
|
||||
@@ -97,6 +97,25 @@
|
||||
--vp-spacing-xl: 2rem; /* 32px */
|
||||
--vp-spacing-2xl: 3rem; /* 48px */
|
||||
|
||||
/* Fluid Spacing (Responsive with clamp())
|
||||
* Scales smoothly between viewport widths
|
||||
* Usage: padding: var(--spacing-fluid-md);
|
||||
*/
|
||||
--spacing-fluid-xs: clamp(0.25rem, 0.2rem + 0.25vw, 0.375rem); /* 4-6px */
|
||||
--spacing-fluid-sm: clamp(0.5rem, 0.4rem + 0.5vw, 0.75rem); /* 8-12px */
|
||||
--spacing-fluid-md: clamp(0.75rem, 0.6rem + 0.75vw, 1.25rem); /* 12-20px */
|
||||
--spacing-fluid-lg: clamp(1rem, 0.8rem + 1vw, 1.75rem); /* 16-28px */
|
||||
--spacing-fluid-xl: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem); /* 24-40px */
|
||||
--spacing-fluid-2xl: clamp(2rem, 1.5rem + 2.5vw, 3.5rem); /* 32-56px */
|
||||
|
||||
/* Container Query Names
|
||||
* Usage: container-name: var(--container-sidebar);
|
||||
*/
|
||||
--container-sidebar: sidebar;
|
||||
--container-content: content;
|
||||
--container-outline: outline;
|
||||
--container-nav: nav;
|
||||
|
||||
/* Border Radius */
|
||||
--vp-radius-sm: 0.25rem; /* 4px */
|
||||
--vp-radius-md: 0.375rem; /* 6px */
|
||||
@@ -122,6 +141,19 @@
|
||||
--vp-z-index-fixed: 50;
|
||||
--vp-z-index-modal: 100;
|
||||
--vp-z-index-toast: 200;
|
||||
|
||||
/* Responsive Breakpoints (VitePress standard) */
|
||||
--bp-mobile: 768px; /* Mobile: < 768px */
|
||||
--bp-tablet: 1024px; /* Tablet: 768px - 1024px */
|
||||
--bp-desktop: 1440px; /* Desktop: > 1024px, large: > 1440px */
|
||||
|
||||
/* Container Query Breakpoints
|
||||
* Aligned with media query breakpoints for consistency
|
||||
*/
|
||||
--container-bp-sm: 480px; /* Small container */
|
||||
--container-bp-md: 768px; /* Medium container */
|
||||
--container-bp-lg: 1024px; /* Large container */
|
||||
--container-bp-xl: 1280px; /* Extra large container */
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
|
||||
411
docs/commands/claude/idaw.md
Normal file
411
docs/commands/claude/idaw.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# IDAW Commands
|
||||
|
||||
## One-Liner
|
||||
|
||||
**IDAW (Independent Development Autonomous Workflow) is the batch task execution engine** — queue development tasks, execute skill chains serially with per-task git checkpoints, and resume from interruptions.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Description | Location |
|
||||
|---------|-------------|----------|
|
||||
| **Task** | Independent JSON task definition | `.workflow/.idaw/tasks/IDAW-*.json` |
|
||||
| **Session** | Execution session with progress tracking | `.workflow/.idaw/sessions/IDA-*/` |
|
||||
| **Skill Chain** | Ordered sequence of skills per task type | Mapped from `SKILL_CHAIN_MAP` |
|
||||
| **Checkpoint** | Per-task git commit after successful execution | Automatic `git add -A && git commit` |
|
||||
|
||||
## IDAW vs Issue System
|
||||
|
||||
| Aspect | Issue System | IDAW |
|
||||
|--------|-------------|------|
|
||||
| **Granularity** | Fine-grained, multi-step orchestration | Coarse-grained, batch autonomous |
|
||||
| **Pipeline** | new → plan → queue → execute | add → run (all-in-one) |
|
||||
| **Execution** | DAG parallel | Serial with checkpoints |
|
||||
| **Storage** | `.workflow/issues.jsonl` | `.workflow/.idaw/tasks/IDAW-*.json` |
|
||||
| **Use Case** | Individual issue resolution | Batch task queue, unattended execution |
|
||||
|
||||
## Command List
|
||||
|
||||
| Command | Function | Syntax |
|
||||
|---------|----------|--------|
|
||||
| [`add`](#add) | Create tasks manually or import from issue | `/idaw:add [-y] [--from-issue <id>] "description" [--type <type>] [--priority 1-5]` |
|
||||
| [`run`](#run) | Execute task queue with git checkpoints | `/idaw:run [-y] [--task <id,...>] [--dry-run]` |
|
||||
| [`run-coordinate`](#run-coordinate) | Execute via external CLI with hook callbacks | `/idaw:run-coordinate [-y] [--task <id,...>] [--tool <tool>]` |
|
||||
| [`status`](#status) | View task and session progress | `/idaw:status [session-id]` |
|
||||
| [`resume`](#resume) | Resume interrupted session | `/idaw:resume [-y] [session-id]` |
|
||||
|
||||
## Command Details
|
||||
|
||||
### add
|
||||
|
||||
**Function**: Create IDAW tasks manually or import from existing ccw issues.
|
||||
|
||||
**Syntax**:
|
||||
```bash
|
||||
/idaw:add [-y|--yes] [--from-issue <id>[,<id>,...]] "description" [--type <task_type>] [--priority <1-5>]
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `--from-issue <id>`: Import from ccw issue (comma-separated for multiple)
|
||||
- `--type <type>`: Explicit task type (see [Task Types](#task-types))
|
||||
- `--priority 1-5`: Priority (1=critical, 5=low, default=3)
|
||||
|
||||
**Modes**:
|
||||
|
||||
| Mode | Trigger | Behavior |
|
||||
|------|---------|----------|
|
||||
| Manual | No `--from-issue` | Parse description, generate task |
|
||||
| Import | `--from-issue` | Fetch issue, freeze snapshot, create task |
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Manual creation
|
||||
/idaw:add "Fix login timeout bug" --type bugfix --priority 2
|
||||
/idaw:add "Add rate limiting to API endpoints" --priority 1
|
||||
/idaw:add "Refactor auth module to use strategy pattern"
|
||||
|
||||
# Import from ccw issue
|
||||
/idaw:add --from-issue ISS-20260128-001
|
||||
/idaw:add --from-issue ISS-20260128-001,ISS-20260128-002
|
||||
|
||||
# Auto mode (skip clarification)
|
||||
/idaw:add -y "Quick fix for typo in header"
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
Created IDAW-001: Fix login timeout bug
|
||||
Type: bugfix | Priority: 2 | Source: manual
|
||||
→ Next: /idaw:run or /idaw:status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### run
|
||||
|
||||
**Function**: Main orchestrator — execute task skill chains serially with git checkpoints.
|
||||
|
||||
**Syntax**:
|
||||
```bash
|
||||
/idaw:run [-y|--yes] [--task <id>[,<id>,...]] [--dry-run]
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `--task <id,...>`: Execute specific tasks (default: all pending)
|
||||
- `--dry-run`: Show execution plan without running
|
||||
- `-y`: Auto mode — skip confirmations, auto-skip on failure
|
||||
|
||||
**6-Phase Execution**:
|
||||
|
||||
```
|
||||
Phase 1: Load Tasks
|
||||
└─ Glob IDAW-*.json → filter → sort by priority ASC, ID ASC
|
||||
|
||||
Phase 2: Session Setup
|
||||
└─ Create session.json + progress.md + TodoWrite
|
||||
|
||||
Phase 3: Startup Protocol
|
||||
├─ Check running sessions → offer resume or fresh
|
||||
└─ Check git status → stash/continue/abort
|
||||
|
||||
Phase 4: Main Loop (serial)
|
||||
For each task:
|
||||
├─ Resolve: skill_chain || SKILL_CHAIN_MAP[task_type || inferred]
|
||||
├─ Execute each skill (retry once on failure)
|
||||
└─ On error: skip (autoYes) or ask (interactive)
|
||||
|
||||
Phase 5: Checkpoint (per task)
|
||||
├─ git add -A && git commit
|
||||
├─ Update task.json + session.json
|
||||
└─ Append to progress.md
|
||||
|
||||
Phase 6: Report
|
||||
└─ Summary: completed/failed/skipped counts + git commits
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Execute all pending tasks (auto mode)
|
||||
/idaw:run -y
|
||||
|
||||
# Execute specific tasks
|
||||
/idaw:run --task IDAW-001,IDAW-003
|
||||
|
||||
# Preview execution plan
|
||||
/idaw:run --dry-run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### run-coordinate
|
||||
|
||||
**Function**: Coordinator variant of `/idaw:run` — executes via external CLI with hook callbacks instead of blocking Skill() calls.
|
||||
|
||||
**Syntax**:
|
||||
```bash
|
||||
/idaw:run-coordinate [-y|--yes] [--task <id>[,<id>,...]] [--dry-run] [--tool <tool>]
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `--tool <tool>`: CLI tool to use (`claude`, `gemini`, `qwen`, default: `claude`)
|
||||
- `--task <id,...>`: Execute specific tasks
|
||||
- `--dry-run`: Preview plan without executing
|
||||
- `-y`: Auto mode
|
||||
|
||||
**Execution Model**:
|
||||
|
||||
```
|
||||
Launch skill via ccw cli --tool <tool> --mode write (background)
|
||||
↓
|
||||
★ STOP — wait for hook callback
|
||||
↓
|
||||
Hook fires → handleStepCompletion()
|
||||
├─ More skills in chain → launch next → STOP
|
||||
├─ Chain complete → git checkpoint → next task → STOP
|
||||
└─ All done → Report
|
||||
```
|
||||
|
||||
**When to Use**:
|
||||
|
||||
| Scenario | Command |
|
||||
|----------|---------|
|
||||
| Standard execution (main process, blocking) | `/idaw:run` |
|
||||
| External CLI, isolated context per skill | `/idaw:run-coordinate` |
|
||||
| Long-running tasks, avoid context pressure | `/idaw:run-coordinate` |
|
||||
| Need specific CLI tool (claude/gemini) | `/idaw:run-coordinate --tool gemini` |
|
||||
|
||||
**Differences from `/idaw:run`**:
|
||||
|
||||
| Aspect | `/idaw:run` | `/idaw:run-coordinate` |
|
||||
|--------|-------------|----------------------|
|
||||
| Execution | `Skill()` blocking | `ccw cli` background + hook |
|
||||
| Context | Shared main context | Isolated per CLI call |
|
||||
| Tool selection | N/A | `--tool claude\|gemini\|qwen` |
|
||||
| State | session.json | session.json + prompts_used |
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Execute via claude CLI (default)
|
||||
/idaw:run-coordinate -y
|
||||
|
||||
# Use gemini as execution tool
|
||||
/idaw:run-coordinate -y --tool gemini
|
||||
|
||||
# Specific tasks
|
||||
/idaw:run-coordinate --task IDAW-001,IDAW-003 --tool claude
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### status
|
||||
|
||||
**Function**: Read-only view of IDAW task queue and session progress.
|
||||
|
||||
**Syntax**:
|
||||
```bash
|
||||
/idaw:status [session-id]
|
||||
```
|
||||
|
||||
**View Modes**:
|
||||
|
||||
| Mode | Trigger | Output |
|
||||
|------|---------|--------|
|
||||
| Overview | No arguments | All tasks table + latest session summary |
|
||||
| Session Detail | Session ID provided | Task × status × commit table + progress.md |
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Overview
|
||||
/idaw:status
|
||||
|
||||
# Specific session
|
||||
/idaw:status IDA-auth-fix-20260301
|
||||
```
|
||||
|
||||
**Output Example**:
|
||||
```
|
||||
# IDAW Tasks
|
||||
|
||||
| ID | Title | Type | Priority | Status |
|
||||
|----------|--------------------------|---------|----------|-----------|
|
||||
| IDAW-001 | Fix auth token refresh | bugfix | 1 | completed |
|
||||
| IDAW-002 | Add rate limiting | feature | 2 | pending |
|
||||
| IDAW-003 | Refactor payment module | refactor| 3 | pending |
|
||||
|
||||
Total: 3 | Pending: 2 | Completed: 1 | Failed: 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### resume
|
||||
|
||||
**Function**: Resume an interrupted IDAW session from the last checkpoint.
|
||||
|
||||
**Syntax**:
|
||||
```bash
|
||||
/idaw:resume [-y|--yes] [session-id]
|
||||
```
|
||||
|
||||
**Options**:
|
||||
- `session-id`: Resume specific session (default: latest running)
|
||||
- `-y`: Auto-skip interrupted task, continue with remaining
|
||||
|
||||
**Recovery Flow**:
|
||||
```
|
||||
1. Find session with status=running
|
||||
2. Handle interrupted task (in_progress):
|
||||
├─ autoYes → mark as skipped
|
||||
└─ interactive → ask: Retry or Skip
|
||||
3. Build remaining task queue
|
||||
4. Execute Phase 4-6 from /idaw:run
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
```bash
|
||||
# Resume most recent running session
|
||||
/idaw:resume
|
||||
|
||||
# Resume specific session
|
||||
/idaw:resume IDA-auth-fix-20260301
|
||||
|
||||
# Resume with auto mode
|
||||
/idaw:resume -y
|
||||
```
|
||||
|
||||
## Task Types
|
||||
|
||||
IDAW supports 10 task types, each mapping to a specific skill chain:
|
||||
|
||||
| Task Type | Skill Chain | Use Case |
|
||||
|-----------|-------------|----------|
|
||||
| `bugfix` | lite-plan → test-fix | Standard bug fixes |
|
||||
| `bugfix-hotfix` | lite-plan (--hotfix) | Urgent production fixes |
|
||||
| `feature` | lite-plan → test-fix | New features |
|
||||
| `feature-complex` | plan → execute → test-fix | Multi-module features |
|
||||
| `refactor` | refactor-cycle | Code restructuring |
|
||||
| `tdd` | tdd-plan → execute | Test-driven development |
|
||||
| `test` | test-fix | Test generation |
|
||||
| `test-fix` | test-fix | Fix failing tests |
|
||||
| `review` | review-cycle | Code review |
|
||||
| `docs` | lite-plan | Documentation |
|
||||
|
||||
**Type Resolution**: Explicit `task_type` field takes priority. When null, the type is inferred from title and description using keyword matching at execution time.
|
||||
|
||||
## Task Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "IDAW-001",
|
||||
"title": "Fix auth token refresh race condition",
|
||||
"description": "Detailed problem/goal description...",
|
||||
"status": "pending",
|
||||
"priority": 2,
|
||||
"task_type": "bugfix",
|
||||
"skill_chain": null,
|
||||
"context": {
|
||||
"affected_files": ["src/auth/token-manager.ts"],
|
||||
"acceptance_criteria": ["No concurrent refresh requests"],
|
||||
"constraints": [],
|
||||
"references": []
|
||||
},
|
||||
"source": {
|
||||
"type": "manual",
|
||||
"issue_id": null,
|
||||
"issue_snapshot": null
|
||||
},
|
||||
"execution": {
|
||||
"session_id": null,
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"skill_results": [],
|
||||
"git_commit": null,
|
||||
"error": null
|
||||
},
|
||||
"created_at": "2026-03-01T10:00:00Z",
|
||||
"updated_at": "2026-03-01T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Key Fields**:
|
||||
- `task_type`: Optional — inferred from title/description when null
|
||||
- `skill_chain`: Optional — overrides automatic mapping when set
|
||||
- `source.type`: `manual` or `import-issue`
|
||||
- `source.issue_snapshot`: Frozen copy of original issue data (import only)
|
||||
- `execution`: Runtime state populated by `/idaw:run`
|
||||
|
||||
## Task Lifecycle
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["/idaw:add"] --> B["pending"]
|
||||
B --> C["/idaw:run"]
|
||||
C --> D["in_progress"]
|
||||
D --> E{"Skill Chain"}
|
||||
E -->|Success| F["completed (git commit)"]
|
||||
E -->|Retry once| G{"Retry"}
|
||||
G -->|Success| F
|
||||
G -->|Fail| H{"autoYes?"}
|
||||
H -->|Yes| I["failed (skip)"]
|
||||
H -->|No| J["Ask: Skip/Abort"]
|
||||
J -->|Skip| I
|
||||
J -->|Abort| K["Session failed"]
|
||||
|
||||
L["/idaw:resume"] --> M{"Interrupted task"}
|
||||
M -->|Retry| B
|
||||
M -->|Skip| N["skipped"]
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
.workflow/.idaw/
|
||||
├── tasks/ # Task definitions (persist across sessions)
|
||||
│ ├── IDAW-001.json
|
||||
│ ├── IDAW-002.json
|
||||
│ └── IDAW-003.json
|
||||
└── sessions/ # Execution sessions
|
||||
└── IDA-{slug}-YYYYMMDD/
|
||||
├── session.json # Session state + task queue
|
||||
└── progress.md # Human-readable progress log
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
| No tasks found | Suggest `/idaw:add` |
|
||||
| Task JSON parse error | Skip malformed task, log warning |
|
||||
| Task type unresolvable | Default to `feature` chain |
|
||||
| Skill failure | Retry once → skip (autoYes) or ask (interactive) |
|
||||
| Git commit fails (no changes) | Record `no-commit`, continue |
|
||||
| Dirty git tree | autoYes: proceed; interactive: ask |
|
||||
| Session ID collision | Append `-2` suffix |
|
||||
| Issue fetch fails (import) | Log error, skip issue |
|
||||
| Duplicate import (same issue_id) | Warn and skip |
|
||||
| No resumable sessions | Suggest `/idaw:run` |
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
```bash
|
||||
# 1. Queue tasks
|
||||
/idaw:add "Fix login timeout bug" --type bugfix --priority 1
|
||||
/idaw:add "Add rate limiting to API" --priority 2
|
||||
/idaw:add --from-issue ISS-20260128-001,ISS-20260128-002
|
||||
|
||||
# 2. Preview execution plan
|
||||
/idaw:run --dry-run
|
||||
|
||||
# 3. Execute all (unattended)
|
||||
/idaw:run -y
|
||||
|
||||
# 4. Check progress
|
||||
/idaw:status
|
||||
|
||||
# 5. Resume if interrupted
|
||||
/idaw:resume -y
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Issue Commands](./issue.md) — Fine-grained issue management
|
||||
- [Core Orchestration](./core-orchestration.md) — `/ccw` main orchestrator
|
||||
- [Workflow Commands](./workflow.md) — Individual workflow skills
|
||||
@@ -12,6 +12,7 @@
|
||||
| **Workflow** | 20+ | Planning, execution, review, TDD, testing workflows |
|
||||
| **Session Management** | 6 | Session creation, listing, resuming, completion |
|
||||
| **Issue Workflow** | 8 | Issue discovery, planning, queue, execution |
|
||||
| **IDAW** | 5 | Batch autonomous task execution with git checkpoints |
|
||||
| **Memory** | 8 | Memory capture, update, document generation |
|
||||
| **CLI Tools** | 2 | CLI initialization, Codex review |
|
||||
| **UI Design** | 10 | UI design prototype generation, style extraction |
|
||||
@@ -69,7 +70,17 @@
|
||||
| [`/issue:execute`](./issue.md#execute) | Execute queue | Intermediate |
|
||||
| [`/issue:convert-to-plan`](./issue.md#convert-to-plan) | Convert planning artifact to issue solution | Intermediate |
|
||||
|
||||
### 5. Memory Commands
|
||||
### 5. IDAW Commands
|
||||
|
||||
| Command | Function | Difficulty |
|
||||
|---------|----------|------------|
|
||||
| [`/idaw:add`](./idaw.md#add) | Create tasks manually or import from ccw issue | Beginner |
|
||||
| [`/idaw:run`](./idaw.md#run) | Execute task queue with skill chains and git checkpoints | Intermediate |
|
||||
| [`/idaw:run-coordinate`](./idaw.md#run-coordinate) | Execute via external CLI with hook callbacks | Intermediate |
|
||||
| [`/idaw:status`](./idaw.md#status) | View task and session progress | Beginner |
|
||||
| [`/idaw:resume`](./idaw.md#resume) | Resume interrupted session from last checkpoint | Intermediate |
|
||||
|
||||
### 6. Memory Commands
|
||||
|
||||
| Command | Function | Difficulty |
|
||||
|---------|----------|------------|
|
||||
@@ -82,14 +93,14 @@
|
||||
| [`/memory:docs-related-cli`](./memory.md#docs-related-cli) | Generate documentation for git-changed modules | Intermediate |
|
||||
| [`/memory:style-skill-memory`](./memory.md#style-skill-memory) | Generate SKILL memory package from style reference | Intermediate |
|
||||
|
||||
### 6. CLI Tool Commands
|
||||
### 7. CLI Tool Commands
|
||||
|
||||
| Command | Function | Difficulty |
|
||||
|---------|----------|------------|
|
||||
| [`/cli:cli-init`](./cli.md#cli-init) | Generate configuration directory and settings files | Intermediate |
|
||||
| [`/cli:codex-review`](./cli.md#codex-review) | Interactive code review using Codex CLI | Intermediate |
|
||||
|
||||
### 7. UI Design Commands
|
||||
### 8. UI Design Commands
|
||||
|
||||
| Command | Function | Difficulty |
|
||||
|---------|----------|------------|
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- [Workflow](/commands/claude/workflow) - workflow series commands
|
||||
- [Session Management](/commands/claude/session) - session series commands
|
||||
- [Issue](/commands/claude/issue) - issue series commands
|
||||
- [IDAW](/commands/claude/idaw) - batch autonomous task execution
|
||||
- [Memory](/commands/claude/memory) - memory series commands
|
||||
- [CLI](/commands/claude/cli) - cli series commands
|
||||
- [UI Design](/commands/claude/ui-design) - ui-design series commands
|
||||
|
||||
247
docs/components/index.md
Normal file
247
docs/components/index.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Component Library
|
||||
|
||||
## One-Liner
|
||||
**A comprehensive collection of reusable UI components built with Radix UI primitives and Tailwind CSS, following shadcn/ui patterns for consistent, accessible, and customizable interfaces.**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**Location**: `ccw/frontend/src/components/ui/`
|
||||
|
||||
**Purpose**: Provides a consistent set of UI components for building the CCW frontend application.
|
||||
|
||||
**Technology Stack**:
|
||||
- **Radix UI**: Unstyled, accessible component primitives
|
||||
- **Tailwind CSS**: Utility-first styling with custom theme
|
||||
- **class-variance-authority (CVA)**: Type-safe variant prop management
|
||||
- **Lucide React**: Consistent iconography
|
||||
|
||||
---
|
||||
|
||||
## Live Demo: Component Gallery
|
||||
|
||||
:::demo ComponentGallery #ComponentGallery.tsx :::
|
||||
|
||||
---
|
||||
|
||||
## Available Components
|
||||
|
||||
### Form Components
|
||||
|
||||
| Component | Description | Props |
|
||||
|-----------|-------------|-------|
|
||||
| [Button](/components/ui/button) | Clickable action buttons with variants and sizes | `variant`, `size`, `asChild` |
|
||||
| [Input](/components/ui/input) | Text input field | `error` |
|
||||
| [Textarea](/components/ui/textarea) | Multi-line text input | `error` |
|
||||
| [Select](/components/ui/select) | Dropdown selection (Radix) | Select components |
|
||||
| [Checkbox](/components/ui/checkbox) | Boolean checkbox (Radix) | `checked`, `onCheckedChange` |
|
||||
| [Switch](/components/ui/switch) | Toggle switch | `checked`, `onCheckedChange` |
|
||||
|
||||
### Layout Components
|
||||
|
||||
| Component | Description | Props |
|
||||
|-----------|-------------|-------|
|
||||
| [Card](/components/ui/card) | Content container with header/footer | Nested components |
|
||||
| [Separator](/components/ui/separator) | Visual divider | `orientation` |
|
||||
| [ScrollArea](/components/ui/scroll-area) | Custom scrollbar container | - |
|
||||
|
||||
### Feedback Components
|
||||
|
||||
| Component | Description | Props |
|
||||
|-----------|-------------|-------|
|
||||
| [Badge](/components/ui/badge) | Status indicator label | `variant` |
|
||||
| [Progress](/components/ui/progress) | Progress bar | `value` |
|
||||
| [Alert](/components/ui/alert) | Notification message | `variant` |
|
||||
| [Toast](/components/ui/toast) | Temporary notification (Radix) | Toast components |
|
||||
|
||||
### Navigation Components
|
||||
|
||||
| Component | Description | Props |
|
||||
|-----------|-------------|-------|
|
||||
| [Tabs](/components/ui/tabs) | Tab navigation (Radix) | Tabs components |
|
||||
| [TabsNavigation](/components/ui/tabs-navigation) | Custom tab bar | `tabs`, `value`, `onValueChange` |
|
||||
| [Breadcrumb](/components/ui/breadcrumb) | Navigation breadcrumb | Breadcrumb components |
|
||||
|
||||
### Overlay Components
|
||||
|
||||
| Component | Description | Props |
|
||||
|-----------|-------------|-------|
|
||||
| [Dialog](/components/ui/dialog) | Modal dialog (Radix) | `open`, `onOpenChange` |
|
||||
| [Drawer](/components/ui/drawer) | Side panel (Radix) | `open`, `onOpenChange` |
|
||||
| [Dropdown Menu](/components/ui/dropdown) | Context menu (Radix) | Dropdown components |
|
||||
| [Popover](/components/ui/popover) | Floating content (Radix) | `open`, `onOpenChange` |
|
||||
| [Tooltip](/components/ui/tooltip) | Hover tooltip (Radix) | `content` |
|
||||
| [AlertDialog](/components/ui/alert-dialog) | Confirmation dialog (Radix) | Dialog components |
|
||||
|
||||
### Disclosure Components
|
||||
|
||||
| Component | Description | Props |
|
||||
|-----------|-------------|-------|
|
||||
| [Collapsible](/components/ui/collapsible) | Expand/collapse content (Radix) | `open`, `onOpenChange` |
|
||||
| [Accordion](/components/ui/accordion) | Collapsible sections (Radix) | Accordion components |
|
||||
|
||||
---
|
||||
|
||||
## Button Variants
|
||||
|
||||
The Button component supports 8 variants via CVA (class-variance-authority):
|
||||
|
||||
| Variant | Use Case | Preview |
|
||||
|---------|----------|--------|
|
||||
| `default` | Primary action | <span className="inline-block px-3 py-1 rounded bg-primary text-primary-foreground text-xs">Default</span> |
|
||||
| `destructive` | Dangerous actions | <span className="inline-block px-3 py-1 rounded bg-destructive text-destructive-foreground text-xs">Destructive</span> |
|
||||
| `outline` | Secondary action | <span className="inline-block px-3 py-1 rounded border text-xs">Outline</span> |
|
||||
| `secondary` | Less emphasis | <span className="inline-block px-3 py-1 rounded bg-secondary text-secondary-foreground text-xs">Secondary</span> |
|
||||
| `ghost` | Subtle action | <span className="inline-block px-3 py-1 rounded text-xs">Ghost</span> |
|
||||
| `link` | Text link | <span className="inline-block px-3 py-1 underline text-primary text-xs">Link</span> |
|
||||
| `gradient` | Featured action | <span className="inline-block px-3 py-1 rounded bg-gradient-to-r from-blue-500 to-purple-500 text-white text-xs">Gradient</span> |
|
||||
| `gradientPrimary` | Primary gradient | <span className="inline-block px-3 py-1 rounded bg-gradient-to-r from-purple-500 to-pink-500 text-white text-xs">Primary</span> |
|
||||
|
||||
### Button Sizes
|
||||
|
||||
| Size | Height | Padding |
|
||||
|------|--------|---------|
|
||||
| `sm` | 36px | 12px horizontal |
|
||||
| `default` | 40px | 16px horizontal |
|
||||
| `lg` | 44px | 32px horizontal |
|
||||
| `icon` | 40px | Square (icon only) |
|
||||
|
||||
---
|
||||
|
||||
## Badge Variants
|
||||
|
||||
The Badge component supports 9 variants for different status types:
|
||||
|
||||
| Variant | Use Case | Color |
|
||||
|---------|----------|-------|
|
||||
| `default` | General info | Primary theme |
|
||||
| `secondary` | Less emphasis | Secondary theme |
|
||||
| `destructive` | Error/Danger | Destructive theme |
|
||||
| `outline` | Subtle | Text color only |
|
||||
| `success` | Success state | Green |
|
||||
| `warning` | Warning state | Amber |
|
||||
| `info` | Information | Blue |
|
||||
| `review` | Review status | Purple |
|
||||
| `gradient` | Featured | Brand gradient |
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Button
|
||||
|
||||
```tsx
|
||||
import { Button } from '@/components/ui/Button'
|
||||
|
||||
<Button variant="default" onClick={handleClick}>
|
||||
Click me
|
||||
</Button>
|
||||
|
||||
<Button variant="destructive" size="sm">
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="icon">
|
||||
<SettingsIcon />
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Input with Error State
|
||||
|
||||
```tsx
|
||||
import { Input } from '@/components/ui/Input'
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
error={hasError}
|
||||
placeholder="Enter your name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Checkbox
|
||||
|
||||
```tsx
|
||||
import { Checkbox } from '@/components/ui/Checkbox'
|
||||
|
||||
<Checkbox
|
||||
checked={accepted}
|
||||
onCheckedChange={setAccepted}
|
||||
/>
|
||||
<label>Accept terms</label>
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
```tsx
|
||||
import { Switch } from '@/components/ui/Switch'
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
/>
|
||||
<span>Enable feature</span>
|
||||
```
|
||||
|
||||
### Card
|
||||
|
||||
```tsx
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/Card'
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>Brief description here</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Main content goes here.</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button>Action</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Badge
|
||||
|
||||
```tsx
|
||||
import { Badge, badgeVariants } from '@/components/ui/Badge'
|
||||
|
||||
<Badge variant="success">Completed</Badge>
|
||||
<Badge variant="warning">Pending</Badge>
|
||||
<Badge variant="destructive">Failed</Badge>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
All components follow Radix UI's accessibility standards:
|
||||
|
||||
- **Keyboard Navigation**: All interactive components are fully keyboard accessible
|
||||
- **ARIA Attributes**: Proper roles, states, and properties
|
||||
- **Screen Reader Support**: Semantic HTML and ARIA labels
|
||||
- **Focus Management**: Visible focus indicators and logical tab order
|
||||
- **Color Contrast**: WCAG AA compliant color combinations
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Component | Keys |
|
||||
|-----------|-------|
|
||||
| Button | <kbd>Enter</kbd>, <kbd>Space</kbd> |
|
||||
| Checkbox/Switch | <kbd>Space</kbd> to toggle |
|
||||
| Select | <kbd>Arrow</kbd> keys, <kbd>Enter</kbd> to select, <kbd>Esc</kbd> to close |
|
||||
| Dialog | <kbd>Esc</kbd> to close |
|
||||
| Tabs | <kbd>Arrow</kbd> keys to navigate |
|
||||
| Dropdown | <kbd>Arrow</kbd> keys, <kbd>Enter</kbd> to select |
|
||||
|
||||
---
|
||||
|
||||
## Related Links
|
||||
|
||||
- [Radix UI Primitives](https://www.radix-ui.com/) - Headless UI component library
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - Component patterns reference
|
||||
- [CVA Documentation](https://cva.style/) - Class Variance Authority
|
||||
119
docs/components/ui/badge.md
Normal file
119
docs/components/ui/badge.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Badge
|
||||
description: Small status or label component for visual categorization
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# Badge
|
||||
|
||||
## Overview
|
||||
|
||||
The Badge component is used to display status, categories, or labels in a compact form. It's commonly used for tags, status indicators, and counts.
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo badge-variants
|
||||
Shows all available badge variants including default, secondary, destructive, outline, success, warning, info, review, and gradient
|
||||
:::
|
||||
|
||||
## Props
|
||||
|
||||
<PropsTable :props="[
|
||||
{ name: 'variant', type: '\'default\' | \'secondary\' | \'destructive\' | \'outline\' | \'success\' | \'warning\' | \'info\' | \'review\' | \'gradient\'', required: false, default: '\'default\'', description: 'Visual style variant' },
|
||||
{ name: 'className', type: 'string', required: false, default: '-', description: 'Custom CSS class name' },
|
||||
{ name: 'children', type: 'ReactNode', required: true, default: '-', description: 'Badge content' }
|
||||
]" />
|
||||
|
||||
## Variants
|
||||
|
||||
### Default
|
||||
|
||||
Primary badge with theme color background. Used for primary labels and categories.
|
||||
|
||||
### Secondary
|
||||
|
||||
Muted badge for secondary information.
|
||||
|
||||
### Destructive
|
||||
|
||||
Red badge for errors, danger states, or negative status.
|
||||
|
||||
### Outline
|
||||
|
||||
Badge with only text and border, no background. Used for subtle labels.
|
||||
|
||||
### Success
|
||||
|
||||
Green badge for success states, completed actions, or positive status.
|
||||
|
||||
### Warning
|
||||
|
||||
Yellow/amber badge for warnings, pending states, or caution indicators.
|
||||
|
||||
### Info
|
||||
|
||||
Blue badge for informational content or neutral status.
|
||||
|
||||
### Review
|
||||
|
||||
Purple badge for review status, pending review, or feedback indicators.
|
||||
|
||||
### Gradient
|
||||
|
||||
Badge with brand gradient background for featured or highlighted items.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Badge
|
||||
|
||||
```vue
|
||||
<Badge>Default</Badge>
|
||||
```
|
||||
|
||||
### Status Indicators
|
||||
|
||||
```vue
|
||||
<Badge variant="success">Active</Badge>
|
||||
<Badge variant="warning">Pending</Badge>
|
||||
<Badge variant="destructive">Failed</Badge>
|
||||
<Badge variant="info">Draft</Badge>
|
||||
```
|
||||
|
||||
### Count Badge
|
||||
|
||||
```vue
|
||||
<div class="relative">
|
||||
<Bell />
|
||||
<Badge variant="destructive" class="absolute -top-2 -right-2 h-5 w-5 rounded-full p-0 flex items-center justify-center text-xs">
|
||||
3
|
||||
</Badge>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Category Tags
|
||||
|
||||
```vue
|
||||
<div class="flex gap-2">
|
||||
<Badge variant="outline">React</Badge>
|
||||
<Badge variant="outline">TypeScript</Badge>
|
||||
<Badge variant="outline">Tailwind</Badge>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Review Status
|
||||
|
||||
```vue
|
||||
<Badge variant="review">In Review</Badge>
|
||||
```
|
||||
|
||||
### Gradient Badge
|
||||
|
||||
```vue
|
||||
<Badge variant="gradient">Featured</Badge>
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Card](/components/ui/card)
|
||||
- [Button](/components/ui/button)
|
||||
- [Avatar](/components/ui/avatar)
|
||||
80
docs/components/ui/button.md
Normal file
80
docs/components/ui/button.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Button
|
||||
description: Button component for triggering actions or submitting forms
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# Button
|
||||
|
||||
## Overview
|
||||
|
||||
The Button component is one of the most commonly used interactive elements, used to trigger actions, submit forms, or navigate to other pages.
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo button-variants
|
||||
Shows all visual variants of the button component
|
||||
:::
|
||||
|
||||
## Props
|
||||
|
||||
<PropsTable :props="[
|
||||
{ name: 'variant', type: '\'default\' | \'destructive\' | \'outline\' | \'secondary\' | \'ghost\' | \'link\' | \'gradient\' | \'gradientPrimary\'', required: false, default: '\'default\'', description: 'Visual style variant' },
|
||||
{ name: 'size', type: '\'default\' | \'sm\' | \'lg\' | \'icon\'', required: false, default: '\'default\'', description: 'Button size' },
|
||||
{ name: 'asChild', type: 'boolean', required: false, default: 'false', description: 'Whether to merge props with child element (for Radix UI composition)' },
|
||||
{ name: 'disabled', type: 'boolean', required: false, default: 'false', description: 'Whether the button is disabled' },
|
||||
{ name: 'onClick', type: '() => void', required: false, default: '-', description: 'Click event callback' },
|
||||
{ name: 'className', type: 'string', required: false, default: '-', description: 'Custom CSS class name' },
|
||||
{ name: 'children', type: 'ReactNode', required: true, default: '-', description: 'Button content' }
|
||||
]" />
|
||||
|
||||
## Variants
|
||||
|
||||
### Default
|
||||
|
||||
Default buttons are used for primary actions.
|
||||
|
||||
### Destructive
|
||||
|
||||
Destructive buttons are used for irreversible actions like delete or remove.
|
||||
|
||||
### Outline
|
||||
|
||||
Outline buttons are used for secondary actions with a lighter visual weight.
|
||||
|
||||
### Secondary
|
||||
|
||||
Secondary buttons are used for auxiliary actions.
|
||||
|
||||
### Ghost
|
||||
|
||||
Ghost buttons have no border and the lightest visual weight.
|
||||
|
||||
### Link
|
||||
|
||||
Link buttons look like links but have button interaction behavior.
|
||||
|
||||
### Gradient
|
||||
|
||||
Gradient buttons use the brand gradient with a glow effect on hover.
|
||||
|
||||
### Gradient Primary
|
||||
|
||||
Gradient Primary buttons use the primary theme gradient with an enhanced glow effect on hover.
|
||||
|
||||
## Usage Scenarios
|
||||
|
||||
| Scenario | Recommended Variant |
|
||||
|----------|---------------------|
|
||||
| Primary actions (submit, save) | default, gradientPrimary |
|
||||
| Dangerous actions (delete, remove) | destructive |
|
||||
| Secondary actions | outline, secondary |
|
||||
| Cancel actions | ghost, outline |
|
||||
| Navigation links | link |
|
||||
| Promotional/Featured CTAs | gradient |
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Input](/components/ui/input)
|
||||
- [Select](/components/ui/select)
|
||||
- [Dialog](/components/ui/dialog)
|
||||
107
docs/components/ui/card.md
Normal file
107
docs/components/ui/card.md
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Card
|
||||
description: Container component for grouping related content
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# Card
|
||||
|
||||
## Overview
|
||||
|
||||
The Card component is a versatile container used to group related content and actions. It consists of several sub-components that work together to create a cohesive card layout.
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo card-variants
|
||||
Shows different card layouts including header, content, footer, and gradient border variants
|
||||
:::
|
||||
|
||||
## Components
|
||||
|
||||
The Card component includes the following sub-components:
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `Card` | Main container with border and background |
|
||||
| `CardHeader` | Header section with padding |
|
||||
| `CardTitle` | Title heading element |
|
||||
| `CardDescription` | Descriptive text with muted color |
|
||||
| `CardContent` | Content area with top padding |
|
||||
| `CardFooter` | Footer section for actions |
|
||||
| `CardGradientBorder` | Card with gradient border |
|
||||
|
||||
## Props
|
||||
|
||||
All Card components accept standard HTML div attributes:
|
||||
|
||||
| Component | Props |
|
||||
|-----------|-------|
|
||||
| `Card` | `className?: string` |
|
||||
| `CardHeader` | `className?: string` |
|
||||
| `CardTitle` | `className?: string`, `children?: ReactNode` |
|
||||
| `CardDescription` | `className?: string`, `children?: ReactNode` |
|
||||
| `CardContent` | `className?: string` |
|
||||
| `CardFooter` | `className?: string` |
|
||||
| `CardGradientBorder` | `className?: string` |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Card
|
||||
|
||||
```vue
|
||||
<Card>
|
||||
<CardContent>
|
||||
<p>This is a basic card with content.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Card with Header
|
||||
|
||||
```vue
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>Card description goes here</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content goes here.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Complete Card with Footer
|
||||
|
||||
```vue
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project Settings</CardTitle>
|
||||
<CardDescription>Manage your project configuration</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Configure your project settings and preferences.</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button>Save Changes</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Card with Gradient Border
|
||||
|
||||
```vue
|
||||
<CardGradientBorder>
|
||||
<CardHeader>
|
||||
<CardTitle>Featured Card</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>This card has a gradient border effect.</p>
|
||||
</CardContent>
|
||||
</CardGradientBorder>
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Button](/components/ui/button)
|
||||
- [Badge](/components/ui/badge)
|
||||
- [Separator](/components/ui/separator)
|
||||
120
docs/components/ui/checkbox.md
Normal file
120
docs/components/ui/checkbox.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
title: Checkbox
|
||||
description: Checkbox component for binary choice selection
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# Checkbox
|
||||
|
||||
## Overview
|
||||
|
||||
The Checkbox component allows users to select one or more options from a set. Built on Radix UI Checkbox Primitive, it provides full accessibility support including keyboard navigation.
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo checkbox-variants
|
||||
Shows different checkbox states including checked, unchecked, indeterminate, and disabled
|
||||
:::
|
||||
|
||||
## Props
|
||||
|
||||
<PropsTable :props="[
|
||||
{ name: 'checked', type: 'boolean | \'indeterminate\'', required: false, default: 'false', description: 'Whether the checkbox is checked' },
|
||||
{ name: 'defaultChecked', type: 'boolean', required: false, default: 'false', description: 'Initial checked state (uncontrolled)' },
|
||||
{ name: 'onCheckedChange', type: '(checked: boolean) => void', required: false, default: '-', description: 'Callback when checked state changes' },
|
||||
{ name: 'disabled', type: 'boolean', required: false, default: 'false', description: 'Whether the checkbox is disabled' },
|
||||
{ name: 'required', type: 'boolean', required: false, default: 'false', description: 'Whether the checkbox is required' },
|
||||
{ name: 'name', type: 'string', required: false, default: '-', description: 'Form input name' },
|
||||
{ name: 'value', type: 'string', required: false, default: '-', description: 'Form input value' },
|
||||
{ name: 'className', type: 'string', required: false, default: '-', description: 'Custom CSS class name' }
|
||||
]" />
|
||||
|
||||
## States
|
||||
|
||||
### Unchecked
|
||||
|
||||
The default state when the checkbox is not selected.
|
||||
|
||||
### Checked
|
||||
|
||||
The checkbox shows a checkmark icon when selected.
|
||||
|
||||
### Indeterminate
|
||||
|
||||
A mixed state (partial selection) typically used for parent checkboxes when some but not all children are selected.
|
||||
|
||||
### Disabled
|
||||
|
||||
Disabled checkboxes are non-interactive and displayed with reduced opacity.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Checkbox
|
||||
|
||||
```vue
|
||||
<Checkbox />
|
||||
```
|
||||
|
||||
### With Label
|
||||
|
||||
```vue
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox id="terms" />
|
||||
<label for="terms">I agree to the terms and conditions</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Controlled Checkbox
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const checked = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox v-model:checked="checked" />
|
||||
<span>{{ checked ? 'Checked' : 'Unchecked' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Indeterminate State
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const state = ref('indeterminate')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Checkbox :checked="state" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### Form Integration
|
||||
|
||||
```vue
|
||||
<form @submit="handleSubmit">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox id="newsletter" name="newsletter" value="yes" />
|
||||
<label for="newsletter">Subscribe to newsletter</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox id="updates" name="updates" value="yes" />
|
||||
<label for="updates">Receive product updates</label>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" class="mt-4">Submit</Button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Input](/components/ui/input)
|
||||
- [Select](/components/ui/select)
|
||||
- [Radio Group](/components/ui/radio-group)
|
||||
118
docs/components/ui/input.md
Normal file
118
docs/components/ui/input.md
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Input
|
||||
description: Text input component for forms and user input
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# Input
|
||||
|
||||
## Overview
|
||||
|
||||
The Input component provides a styled text input field that extends the native HTML input element with consistent styling and error state support.
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo input-variants
|
||||
Shows all input states including default, error, and disabled
|
||||
:::
|
||||
|
||||
## Props
|
||||
|
||||
<PropsTable :props="[
|
||||
{ name: 'type', type: 'string', required: false, default: '\'text\'', description: 'HTML input type (text, password, email, number, etc.)' },
|
||||
{ name: 'error', type: 'boolean', required: false, default: 'false', description: 'Whether the input has an error (shows destructive border)' },
|
||||
{ name: 'disabled', type: 'boolean', required: false, default: 'false', description: 'Whether the input is disabled' },
|
||||
{ name: 'placeholder', type: 'string', required: false, default: '-', description: 'Placeholder text shown when input is empty' },
|
||||
{ name: 'value', type: 'string | number', required: false, default: '-', description: 'Controlled input value' },
|
||||
{ name: 'defaultValue', type: 'string | number', required: false, default: '-', description: 'Uncontrolled input default value' },
|
||||
{ name: 'onChange', type: '(event: ChangeEvent) => void', required: false, default: '-', description: 'Change event callback' },
|
||||
{ name: 'className', type: 'string', required: false, default: '-', description: 'Custom CSS class name' }
|
||||
]" />
|
||||
|
||||
## States
|
||||
|
||||
### Default
|
||||
|
||||
Standard input field with border and focus ring.
|
||||
|
||||
### Error
|
||||
|
||||
Error state with destructive border color. Set the `error` prop to `true`.
|
||||
|
||||
### Disabled
|
||||
|
||||
Disabled state with reduced opacity. Set the `disabled` prop to `true`.
|
||||
|
||||
### Focus
|
||||
|
||||
Focused state with ring outline.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Input
|
||||
|
||||
```tsx
|
||||
import { Input } from '@/components/ui/Input'
|
||||
|
||||
function Example() {
|
||||
return <input type="text" placeholder="Enter text..." />
|
||||
}
|
||||
```
|
||||
|
||||
### Controlled Input
|
||||
|
||||
```tsx
|
||||
import { Input } from '@/components/ui/Input'
|
||||
|
||||
function Example() {
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="Enter text..."
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Input with Error State
|
||||
|
||||
```tsx
|
||||
import { Input } from '@/components/ui/Input'
|
||||
|
||||
function Example() {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
error
|
||||
placeholder="Invalid input..."
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Password Input
|
||||
|
||||
```tsx
|
||||
import { Input } from '@/components/ui/Input'
|
||||
|
||||
function Example() {
|
||||
return <Input type="password" placeholder="Enter password..." />
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Keyboard Navigation**: Full native keyboard support
|
||||
- **ARIA Attributes**: Supports all standard input ARIA attributes
|
||||
- **Focus Visible**: Clear focus indicator for keyboard navigation
|
||||
- **Error State**: Visual indication for error state (use with `aria-invalid` and `aria-describedby`)
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Button](/components/ui/button)
|
||||
- [Select](/components/ui/select)
|
||||
- [Checkbox](/components/ui/checkbox)
|
||||
127
docs/components/ui/select.md
Normal file
127
docs/components/ui/select.md
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: Select
|
||||
description: Dropdown select component for choosing from a list of options
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
# Select
|
||||
|
||||
## Overview
|
||||
|
||||
The Select component allows users to choose a single value from a list of options. Built on Radix UI Select Primitive, it provides accessibility and keyboard navigation out of the box.
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo select-variants
|
||||
Shows different select configurations including basic usage, with labels, and with separators
|
||||
:::
|
||||
|
||||
## Components
|
||||
|
||||
The Select component includes the following sub-components:
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `Select` | Root component that manages state |
|
||||
| `SelectTrigger` | Button that opens the dropdown |
|
||||
| `SelectValue` | Displays the selected value |
|
||||
| `SelectContent` | Dropdown content container |
|
||||
| `SelectItem` | Individual selectable option |
|
||||
| `SelectLabel` | Non-interactive label for grouping |
|
||||
| `SelectGroup` | Groups items together |
|
||||
| `SelectSeparator` | Visual separator between items |
|
||||
| `SelectScrollUpButton` | Scroll button for long lists |
|
||||
| `SelectScrollDownButton` | Scroll button for long lists |
|
||||
|
||||
## Props
|
||||
|
||||
### Select Root
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `value` | `string` | - | Currently selected value (controlled) |
|
||||
| `defaultValue` | `string` | - | Default selected value |
|
||||
| `onValueChange` | `(value: string) => void` | - | Callback when value changes |
|
||||
| `disabled` | `boolean` | `false` | Whether the select is disabled |
|
||||
| `required` | `boolean` | `false` | Whether a value is required |
|
||||
| `name` | `string` | - | Form input name |
|
||||
|
||||
### SelectTrigger
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | - | Custom CSS class name |
|
||||
|
||||
### SelectItem
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `value` | `string` | - | Item value |
|
||||
| `disabled` | `boolean` | `false` | Whether the item is disabled |
|
||||
| `className` | `string` | - | Custom CSS class name |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Select
|
||||
|
||||
```vue
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
```
|
||||
|
||||
### With Labels and Groups
|
||||
|
||||
```vue
|
||||
<Select>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectLabel>Fruits</SelectLabel>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="orange">Orange</SelectItem>
|
||||
<SelectSeparator />
|
||||
<SelectLabel>Vegetables</SelectLabel>
|
||||
<SelectItem value="carrot">Carrot</SelectItem>
|
||||
<SelectItem value="broccoli">Broccoli</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
```
|
||||
|
||||
### Controlled Select
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const selectedValue = ref('')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select v-model="selectedValue">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a">Option A</SelectItem>
|
||||
<SelectItem value="b">Option B</SelectItem>
|
||||
<SelectItem value="c">Option C</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Related Components
|
||||
|
||||
- [Input](/components/ui/input)
|
||||
- [Checkbox](/components/ui/checkbox)
|
||||
- [Button](/components/ui/button)
|
||||
@@ -1,7 +1,6 @@
|
||||
# Dashboard
|
||||
|
||||
## One-Liner
|
||||
|
||||
**The Dashboard provides an at-a-glance overview of your project's workflow status, statistics, and recent activity through an intuitive widget-based interface.**
|
||||
|
||||
---
|
||||
@@ -18,16 +17,15 @@
|
||||
|
||||
---
|
||||
|
||||
## Page Overview
|
||||
## Overview
|
||||
|
||||
**Location**: `ccw/frontend/src/pages/HomePage.tsx`
|
||||
|
||||
**Purpose**: Dashboard home page providing project overview, statistics, workflow status, and recent activity monitoring.
|
||||
|
||||
**Access**: Navigation → Dashboard (default home page)
|
||||
|
||||
### Layout
|
||||
**Access**: Navigation → Dashboard (default home page at `/`)
|
||||
|
||||
**Layout**:
|
||||
```
|
||||
+--------------------------------------------------------------------------+
|
||||
| Dashboard Header (title + refresh) |
|
||||
@@ -55,6 +53,12 @@
|
||||
|
||||
---
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo DashboardOverview #DashboardOverview.tsx :::
|
||||
|
||||
---
|
||||
|
||||
## Core Features
|
||||
|
||||
| Feature | Description |
|
||||
@@ -68,85 +72,171 @@
|
||||
|
||||
---
|
||||
|
||||
## Usage Guide
|
||||
## Component Hierarchy
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
1. **View Project Overview**: Check the project info banner for tech stack and development index
|
||||
2. **Monitor Statistics**: Review mini stat cards for current project metrics and trends
|
||||
3. **Track Workflow Status**: View pie chart for session status distribution
|
||||
4. **Browse Active Sessions**: Use session carousel to see task details and progress
|
||||
5. **Access Recent Work**: Switch between All Tasks/Workflow/Lite Tasks tabs to find specific sessions
|
||||
|
||||
### Key Interactions
|
||||
|
||||
| Interaction | How to Use |
|
||||
|-------------|------------|
|
||||
| **Expand Project Details** | Click the chevron button in the project banner to show architecture, components, patterns |
|
||||
| **Navigate Sessions** | Click arrow buttons or wait for auto-rotation (5s interval) in the carousel |
|
||||
| **View Session Details** | Click on any session card to navigate to session detail page |
|
||||
| **Filter Recent Tasks** | Click tab buttons to filter by task type (All/Workflow/Lite) |
|
||||
| **Refresh Dashboard** | Click the refresh button in the header to reload all data |
|
||||
|
||||
### Index Status Indicator
|
||||
|
||||
| Status | Icon | Meaning |
|
||||
|--------|------|---------|
|
||||
| **Building** | Pulsing blue dot | Code index is being built/updated |
|
||||
| **Completed** | Green dot | Index is up-to-date |
|
||||
| **Idle** | Gray dot | Index status is unknown/idle |
|
||||
| **Failed** | Red dot | Index build failed |
|
||||
```
|
||||
HomePage
|
||||
├── DashboardHeader
|
||||
│ ├── Title
|
||||
│ └── Refresh Action Button
|
||||
├── WorkflowTaskWidget
|
||||
│ ├── ProjectInfoBanner (expandable)
|
||||
│ │ ├── Project Name & Description
|
||||
│ │ ├── Tech Stack Badges
|
||||
│ │ ├── Quick Stats Cards
|
||||
│ │ ├── Index Status Indicator
|
||||
│ │ ├── Architecture Section
|
||||
│ │ ├── Key Components
|
||||
│ │ └── Design Patterns
|
||||
│ ├── Stats Section
|
||||
│ │ └── MiniStatCard (6 cards with Sparkline)
|
||||
│ ├── WorkflowStatusChart
|
||||
│ │ └── Pie Chart with Legend
|
||||
│ └── SessionCarousel
|
||||
│ ├── Navigation Arrows
|
||||
│ └── Session Cards (Task List)
|
||||
└── RecentSessionsWidget
|
||||
├── Tab Navigation (All | Workflow | Lite)
|
||||
├── Task Grid
|
||||
│ └── TaskItemCard
|
||||
└── Loading/Empty States
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components Reference
|
||||
## Props API
|
||||
|
||||
### Main Components
|
||||
### HomePage Component
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| `DashboardHeader` | `@/components/dashboard/DashboardHeader.tsx` | Page header with title and refresh action |
|
||||
| `WorkflowTaskWidget` | `@/components/dashboard/widgets/WorkflowTaskWidget.tsx` | Combined widget with project info, stats, workflow status, and session carousel |
|
||||
| `RecentSessionsWidget` | `@/components/dashboard/widgets/RecentSessionsWidget.tsx` | Recent sessions across workflow and lite tasks |
|
||||
| `MiniStatCard` | (internal to WorkflowTaskWidget) | Individual stat card with optional sparkline |
|
||||
| `HomeEmptyState` | (internal to WorkflowTaskWidget) | Empty state display when no sessions exist |
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| - | - | - | This page component accepts no props (data fetched via hooks) |
|
||||
|
||||
### State Management
|
||||
### WorkflowTaskWidget
|
||||
|
||||
- **Local state**:
|
||||
- `hasError` - Error tracking for critical failures
|
||||
- `projectExpanded` - Project info banner expansion state
|
||||
- `currentSessionIndex` - Active session in carousel
|
||||
- `activeTab` - Recent sessions widget filter tab
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | `undefined` | Additional CSS classes for styling |
|
||||
|
||||
- **Custom hooks**:
|
||||
- `useWorkflowStatusCounts` - Session status distribution data
|
||||
- `useDashboardStats` - Statistics with auto-refresh (60s)
|
||||
- `useProjectOverview` - Project information and tech stack
|
||||
- `useIndexStatus` - Real-time index status (30s refresh)
|
||||
- `useSessions` - Active sessions data
|
||||
- `useLiteTasks` - Lite tasks data for recent widget
|
||||
### RecentSessionsWidget
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `className` | `string` | `undefined` | Additional CSS classes |
|
||||
| `maxItems` | `number` | `6` | Maximum number of items to display |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
## Usage Examples
|
||||
|
||||
No configuration required. The dashboard automatically fetches data from the backend.
|
||||
### Basic Dashboard
|
||||
|
||||
### Auto-Refresh Intervals
|
||||
```tsx
|
||||
import { HomePage } from '@/pages/HomePage'
|
||||
|
||||
| Data Type | Interval |
|
||||
|-----------|----------|
|
||||
| Dashboard stats | 60 seconds |
|
||||
| Index status | 30 seconds |
|
||||
| Discovery sessions | 3 seconds (on discovery page) |
|
||||
// The dashboard is automatically rendered at the root route (/)
|
||||
// No props needed - data is fetched via hooks
|
||||
```
|
||||
|
||||
### Embedding WorkflowTaskWidget
|
||||
|
||||
```tsx
|
||||
import { WorkflowTaskWidget } from '@/components/dashboard/widgets/WorkflowTaskWidget'
|
||||
|
||||
function CustomDashboard() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<WorkflowTaskWidget />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Recent Sessions Widget
|
||||
|
||||
```tsx
|
||||
import { RecentSessionsWidget } from '@/components/dashboard/widgets/RecentSessionsWidget'
|
||||
|
||||
function ActivityFeed() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<RecentSessionsWidget maxItems={10} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### Local State
|
||||
|
||||
| State | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `hasError` | `boolean` | Error tracking for critical failures |
|
||||
| `projectExpanded` | `boolean` | Project info banner expansion state |
|
||||
| `currentSessionIndex` | `number` | Active session index in carousel |
|
||||
| `activeTab` | `'all' \| 'workflow' \| 'lite'` | Recent sessions widget filter tab |
|
||||
|
||||
### Store Selectors (Zustand)
|
||||
|
||||
| Store | Selector | Purpose |
|
||||
|-------|----------|---------|
|
||||
| `appStore` | `selectIsImmersiveMode` | Check if immersive mode is active |
|
||||
|
||||
### Custom Hooks (Data Fetching)
|
||||
|
||||
| Hook | Description | Refetch Interval |
|
||||
|------|-------------|------------------|
|
||||
| `useWorkflowStatusCounts` | Session status distribution data | - |
|
||||
| `useDashboardStats` | Statistics with sparkline data | 60 seconds |
|
||||
| `useProjectOverview` | Project information and tech stack | - |
|
||||
| `useIndexStatus` | Real-time index status | 30 seconds |
|
||||
| `useSessions` | Active sessions data | - |
|
||||
| `useLiteTasks` | Lite tasks data for recent widget | - |
|
||||
|
||||
---
|
||||
|
||||
## Interactive Demos
|
||||
|
||||
### Statistics Cards Demo
|
||||
|
||||
:::demo MiniStatCards #MiniStatCards.tsx :::
|
||||
|
||||
### Project Info Banner Demo
|
||||
|
||||
:::demo ProjectInfoBanner #ProjectInfoBanner.tsx :::
|
||||
|
||||
### Session Carousel Demo
|
||||
|
||||
:::demo SessionCarousel #SessionCarousel.tsx :::
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Keyboard Navigation**:
|
||||
- <kbd>Tab</kbd> - Navigate through interactive elements
|
||||
- <kbd>Enter</kbd>/<kbd>Space</kbd> - Activate buttons and cards
|
||||
- <kbd>Arrow</kbd> keys - Navigate carousel sessions
|
||||
|
||||
- **ARIA Attributes**:
|
||||
- `aria-label` on navigation buttons
|
||||
- `aria-expanded` on expandable sections
|
||||
- `aria-live` regions for real-time updates
|
||||
|
||||
- **Screen Reader Support**:
|
||||
- All charts have text descriptions
|
||||
- Status indicators include text labels
|
||||
- Navigation is announced properly
|
||||
|
||||
---
|
||||
|
||||
## Related Links
|
||||
|
||||
- [Sessions](/features/sessions) - View and manage all sessions
|
||||
- [Terminal Dashboard](/features/terminal) - Terminal-first monitoring interface
|
||||
- [Queue](/features/queue) - Issue execution queue management
|
||||
- [Discovery](/features/discovery) - Discovery session tracking
|
||||
- [Memory](/features/memory) - Persistent memory management
|
||||
- [System Settings](/features/system-settings) - Global application settings
|
||||
- [Settings](/features/settings) - Global application settings
|
||||
|
||||
@@ -157,7 +157,7 @@ The V2 Pipeline tab monitors background jobs for:
|
||||
- **Local state**:
|
||||
- `activeTab`: TabValue
|
||||
- `searchQuery`: string
|
||||
- `selectedMemories`: Set<string>
|
||||
- `selectedMemories`: Set<string>
|
||||
- `filters`: { sourceType?: string; tags?: string[] }
|
||||
- `dialogStates`: create, edit, delete
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# Queue
|
||||
# Queue Management
|
||||
|
||||
## One-Liner
|
||||
|
||||
**The Queue page manages issue execution queues, displaying grouped tasks and solutions with conflict detection and merge capabilities.**
|
||||
**The Queue Management page provides centralized control over issue execution queues with scheduler controls, status monitoring, and session pool management.**
|
||||
|
||||
---
|
||||
|
||||
@@ -11,153 +10,292 @@
|
||||
| Pain Point | Current State | Queue Solution |
|
||||
|------------|---------------|----------------|
|
||||
| **Disorganized execution** | No unified task queue | Centralized queue with grouped items |
|
||||
| **Unknown queue status** | Can't tell if queue is ready | Status indicator with conflicts warning |
|
||||
| **Manual queue management** | No way to control execution | Activate/deactivate/delete with actions |
|
||||
| **Duplicate handling** | Confusing duplicate items | Merge queues functionality |
|
||||
| **No visibility** | Don't know what's queued | Stats cards with items/groups/tasks/solutions counts |
|
||||
| **Unknown scheduler status** | Can't tell if scheduler is running | Real-time status indicator (idle/running/paused) |
|
||||
| **No execution control** | Can't start/stop queue processing | Start/Pause/Stop controls with confirmation |
|
||||
| **Concurrency limits** | Too many simultaneous sessions | Configurable max concurrent sessions |
|
||||
| **No visibility** | Don't know what's queued | Stats cards + item list with status tracking |
|
||||
| **Resource waste** | Idle sessions consuming resources | Session pool overview with timeout config |
|
||||
|
||||
---
|
||||
|
||||
## Page Overview
|
||||
## Overview
|
||||
|
||||
**Location**: `ccw/frontend/src/pages/QueuePage.tsx`
|
||||
**Location**: `ccw/frontend/src/pages/QueuePage.tsx` (legacy), `ccw/frontend/src/components/terminal-dashboard/QueuePanel.tsx` (current)
|
||||
|
||||
**Purpose**: View and manage issue execution queues with stats, conflict detection, and queue operations.
|
||||
**Purpose**: View and manage issue execution queues with scheduler controls, progress tracking, and session pool management.
|
||||
|
||||
**Access**: Navigation → Issues → Queue tab OR directly via `/queue` route
|
||||
|
||||
### Layout
|
||||
**Access**: Navigation → Issues → Queue tab OR Terminal Dashboard → Queue floating panel
|
||||
|
||||
**Layout**:
|
||||
```
|
||||
+--------------------------------------------------------------------------+
|
||||
| Queue Title [Refresh] |
|
||||
| Queue Panel Header |
|
||||
+--------------------------------------------------------------------------+
|
||||
| Stats Cards |
|
||||
| +-------------+ +-------------+ +-------------+ +-------------+ |
|
||||
| | Total Items | | Groups | | Tasks | | Solutions | |
|
||||
| +-------------+ +-------------+ +-------------+ +-------------+ |
|
||||
| Scheduler Status Bar |
|
||||
| +----------------+ +-------------+ +-------------------------------+ |
|
||||
| | Status Badge | | Progress | | Concurrency (2/2) | |
|
||||
| +----------------+ +-------------+ +-------------------------------+ |
|
||||
+--------------------------------------------------------------------------+
|
||||
| Conflicts Warning (when conflicts exist) |
|
||||
| Scheduler Controls |
|
||||
| +--------+ +--------+ +--------+ +-----------+ |
|
||||
| | Start | | Pause | | Stop | | Config | |
|
||||
| +--------+ +--------+ +--------+ +-----------+ |
|
||||
+--------------------------------------------------------------------------+
|
||||
| Queue Cards (1-2 columns) |
|
||||
| Queue Items List |
|
||||
| +---------------------------------------------------------------------+ |
|
||||
| | QueueCard | |
|
||||
| | - Queue info | |
|
||||
| | - Grouped items preview | |
|
||||
| | - Action buttons (Activate/Deactivate/Delete/Merge) | |
|
||||
| | QueueItemRow (status, issue_id, session_key, actions) | |
|
||||
| | - Status icon (pending/executing/completed/blocked/failed) | |
|
||||
| | - Issue ID / Item ID display | |
|
||||
| | - Session binding info | |
|
||||
| | - Progress indicator (for executing items) | |
|
||||
| +---------------------------------------------------------------------+ |
|
||||
| | [More queue items...] | |
|
||||
| +---------------------------------------------------------------------+ |
|
||||
+--------------------------------------------------------------------------+
|
||||
| Status Footer (Ready/Pending indicator) |
|
||||
+--------------------------------------------------------------------------+
|
||||
| Session Pool Overview (optional) |
|
||||
| +--------------------------------------------------------------------------+
|
||||
| | Active Sessions | Idle Sessions | Total Sessions |
|
||||
| +--------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo QueueManagementDemo #QueueManagementDemo.tsx :::
|
||||
|
||||
---
|
||||
|
||||
## Core Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Stats Cards** | Four metric cards showing total items, groups, tasks, and solutions counts |
|
||||
| **Conflicts Warning** | Banner alert when conflicts exist, showing count and description |
|
||||
| **Queue Card** | Displays queue information with grouped items preview and action buttons |
|
||||
| **Activate/Deactivate** | Toggle queue active state for execution control |
|
||||
| **Delete Queue** | Remove queue with confirmation dialog |
|
||||
| **Merge Queues** | Combine multiple queues (if multiple exist) |
|
||||
| **Status Footer** | Visual indicator showing if queue is ready (active) or pending (inactive/conflicts) |
|
||||
| **Loading State** | Skeleton placeholders during data fetch |
|
||||
| **Empty State** | Friendly message when no queue exists |
|
||||
| **Scheduler Status** | Real-time status indicator (idle/running/paused) with visual badge |
|
||||
| **Progress Tracking** | Progress bar showing overall queue completion percentage |
|
||||
| **Start/Pause/Stop Controls** | Control queue execution with confirmation dialog for stop action |
|
||||
| **Concurrency Display** | Shows current active sessions vs max concurrent sessions |
|
||||
| **Queue Items List** | Scrollable list of all queue items with status, issue ID, and session binding |
|
||||
| **Status Icons** | Visual indicators for item status (pending/executing/completed/blocked/failed) |
|
||||
| **Session Pool** | Overview of active, idle, and total sessions in the pool |
|
||||
| **Config Panel** | Adjust max concurrent sessions and timeout settings |
|
||||
| **Empty State** | Friendly message when queue is empty with instructions to add items |
|
||||
|
||||
---
|
||||
|
||||
## Usage Guide
|
||||
## Component Hierarchy
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
1. **Check Queue Status**: Review stats cards and status footer to understand queue state
|
||||
2. **Review Conflicts**: If conflicts warning is shown, resolve conflicts before activation
|
||||
3. **Activate Queue**: Click "Activate" to enable queue for execution (only if no conflicts)
|
||||
4. **Deactivate Queue**: Click "Deactivate" to pause execution
|
||||
5. **Delete Queue**: Click "Delete" to remove the queue (requires confirmation)
|
||||
6. **Merge Queues**: Use merge action to combine multiple queues when applicable
|
||||
|
||||
### Key Interactions
|
||||
|
||||
| Interaction | How to Use |
|
||||
|-------------|------------|
|
||||
| **Refresh queue** | Click the refresh button to reload queue data |
|
||||
| **Activate queue** | Click the Activate button on the queue card |
|
||||
| **Deactivate queue** | Click the Deactivate button to pause the queue |
|
||||
| **Delete queue** | Click the Delete button, confirm in the dialog |
|
||||
| **Merge queues** | Select source and target queues, click merge |
|
||||
| **View status** | Check the status footer for ready/pending indication |
|
||||
|
||||
### Queue Status
|
||||
|
||||
| Status | Condition | Appearance |
|
||||
|--------|-----------|------------|
|
||||
| **Ready/Active** | Total items > 0 AND no conflicts | Green badge with checkmark |
|
||||
| **Pending/Inactive** | No items OR conflicts exist | Gray/amber badge with clock icon |
|
||||
|
||||
### Conflict Resolution
|
||||
|
||||
When conflicts are detected:
|
||||
1. A warning banner appears showing conflict count
|
||||
2. Queue cannot be activated until conflicts are resolved
|
||||
3. Status footer shows "Pending" state
|
||||
4. Resolve conflicts through the Issues panel or related workflows
|
||||
```
|
||||
QueuePage (legacy) / QueuePanel (current)
|
||||
├── QueuePanelHeader
|
||||
│ ├── Title
|
||||
│ └── Tab Switcher (Queue | Orchestrator)
|
||||
├── SchedulerBar (inline in QueueListColumn)
|
||||
│ ├── Status Badge
|
||||
│ ├── Progress + Concurrency
|
||||
│ └── Control Buttons (Play/Pause/Stop)
|
||||
├── QueueItemsList
|
||||
│ └── QueueItemRow (repeating)
|
||||
│ ├── Status Icon
|
||||
│ ├── Issue ID / Item ID
|
||||
│ ├── Session Binding
|
||||
│ └── Progress (for executing items)
|
||||
└── SchedulerPanel (standalone)
|
||||
├── Status Section
|
||||
├── Progress Bar
|
||||
├── Control Buttons
|
||||
├── Config Section
|
||||
│ ├── Max Concurrent Sessions
|
||||
│ ├── Session Idle Timeout
|
||||
│ └── Resume Key Binding Timeout
|
||||
└── Session Pool Overview
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components Reference
|
||||
## Props API
|
||||
|
||||
### Main Components
|
||||
### QueuePanel
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| `QueuePage` | `@/pages/QueuePage.tsx` | Main queue management page |
|
||||
| `QueueCard` | `@/components/issue/queue/QueueCard.tsx` | Queue display with actions |
|
||||
| `QueuePageSkeleton` | (internal to QueuePage) | Loading placeholder |
|
||||
| `QueueEmptyState` | (internal to QueuePage) | Empty state display |
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `embedded` | `boolean` | `false` | Whether panel is embedded in another component |
|
||||
|
||||
### State Management
|
||||
### SchedulerPanel
|
||||
|
||||
- **Local state**:
|
||||
- None (all data from hooks)
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| - | - | - | This component accepts no props (all data from Zustand store) |
|
||||
|
||||
- **Custom hooks**:
|
||||
- `useIssueQueue` - Queue data fetching
|
||||
- `useQueueMutations` - Queue operations (activate, deactivate, delete, merge)
|
||||
### QueueListColumn
|
||||
|
||||
### Mutation States
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| - | - | - | This component accepts no props (all data from Zustand store) |
|
||||
|
||||
| State | Loading Indicator |
|
||||
|-------|------------------|
|
||||
| `isActivating` | Disable activate button during activation |
|
||||
| `isDeactivating` | Disable deactivate button during deactivation |
|
||||
| `isDeleting` | Disable delete button during deletion |
|
||||
| `isMerging` | Disable merge button during merge operation |
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### Zustand Stores
|
||||
|
||||
| Store | Selector | Purpose |
|
||||
|-------|----------|---------|
|
||||
| `queueSchedulerStore` | `selectQueueSchedulerStatus` | Current scheduler status (idle/running/paused) |
|
||||
| `queueSchedulerStore` | `selectSchedulerProgress` | Overall queue completion percentage |
|
||||
| `queueSchedulerStore` | `selectQueueItems` | List of all queue items |
|
||||
| `queueSchedulerStore` | `selectCurrentConcurrency` | Active sessions count |
|
||||
| `queueSchedulerStore` | `selectSchedulerConfig` | Scheduler configuration |
|
||||
| `queueSchedulerStore` | `selectSessionPool` | Session pool overview |
|
||||
| `queueSchedulerStore` | `selectSchedulerError` | Error message if any |
|
||||
| `issueQueueIntegrationStore` | `selectAssociationChain` | Current association chain for highlighting |
|
||||
| `queueExecutionStore` | `selectByQueueItem` | Execution data for queue item |
|
||||
|
||||
### Queue Item Status
|
||||
|
||||
```typescript
|
||||
type QueueItemStatus =
|
||||
| 'pending' // Waiting to be executed
|
||||
| 'executing' // Currently being processed
|
||||
| 'completed' // Finished successfully
|
||||
| 'blocked' // Blocked by dependency
|
||||
| 'failed'; // Failed with error
|
||||
```
|
||||
|
||||
### Scheduler Status
|
||||
|
||||
```typescript
|
||||
type QueueSchedulerStatus =
|
||||
| 'idle' // No items or stopped
|
||||
| 'running' // Actively processing items
|
||||
| 'paused'; // Temporarily paused
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Queue Panel
|
||||
|
||||
```tsx
|
||||
import { QueuePanel } from '@/components/terminal-dashboard/QueuePanel'
|
||||
|
||||
function QueueSection() {
|
||||
return <QueuePanel />
|
||||
}
|
||||
```
|
||||
|
||||
### Standalone Scheduler Panel
|
||||
|
||||
```tsx
|
||||
import { SchedulerPanel } from '@/components/terminal-dashboard/SchedulerPanel'
|
||||
|
||||
function SchedulerControls() {
|
||||
return <SchedulerPanel />
|
||||
}
|
||||
```
|
||||
|
||||
### Queue List Column (Embedded)
|
||||
|
||||
```tsx
|
||||
import { QueueListColumn } from '@/components/terminal-dashboard/QueueListColumn'
|
||||
|
||||
function EmbeddedQueue() {
|
||||
return <QueueListColumn />
|
||||
}
|
||||
```
|
||||
|
||||
### Queue Store Actions
|
||||
|
||||
```tsx
|
||||
import { useQueueSchedulerStore } from '@/stores/queueSchedulerStore'
|
||||
|
||||
function QueueActions() {
|
||||
const startQueue = useQueueSchedulerStore((s) => s.startQueue)
|
||||
const pauseQueue = useQueueSchedulerStore((s) => s.pauseQueue)
|
||||
const stopQueue = useQueueSchedulerStore((s) => s.stopQueue)
|
||||
const updateConfig = useQueueSchedulerStore((s) => s.updateConfig)
|
||||
|
||||
const handleStart = () => startQueue()
|
||||
const handlePause = () => pauseQueue()
|
||||
const handleStop = () => stopQueue()
|
||||
const handleConfig = (config) => updateConfig(config)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleStart}>Start</button>
|
||||
<button onClick={handlePause}>Pause</button>
|
||||
<button onClick={handleStop}>Stop</button>
|
||||
<button onClick={() => handleConfig({ maxConcurrentSessions: 4 })}>
|
||||
Set Max 4
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactive Demos
|
||||
|
||||
### Queue Item Status Demo
|
||||
|
||||
:::demo QueueItemStatusDemo #QueueItemStatusDemo.tsx :::
|
||||
|
||||
### Scheduler Config Demo
|
||||
|
||||
:::demo SchedulerConfigDemo #SchedulerConfigDemo.tsx :::
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
No configuration required. Queue data is automatically fetched from the backend.
|
||||
### Scheduler Config
|
||||
|
||||
### Queue Data Structure
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `maxConcurrentSessions` | `number` | `2` | Maximum sessions running simultaneously |
|
||||
| `sessionIdleTimeoutMs` | `number` | `60000` | Idle session timeout in milliseconds |
|
||||
| `resumeKeySessionBindingTimeoutMs` | `number` | `300000` | Resume key binding timeout in milliseconds |
|
||||
|
||||
### Queue Item Structure
|
||||
|
||||
```typescript
|
||||
interface QueueData {
|
||||
tasks: Task[];
|
||||
solutions: Solution[];
|
||||
conflicts: Conflict[];
|
||||
grouped_items: Record<string, GroupedItem>;
|
||||
interface QueueItem {
|
||||
item_id: string;
|
||||
issue_id?: string;
|
||||
sessionKey?: string;
|
||||
status: QueueItemStatus;
|
||||
execution_order: number;
|
||||
created_at?: number;
|
||||
updated_at?: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
- **Keyboard Navigation**:
|
||||
- <kbd>Tab</kbd> - Navigate through queue items and controls
|
||||
- <kbd>Enter</kbd>/<kbd>Space</kbd> - Activate buttons
|
||||
- <kbd>Escape</kbd> - Close dialogs
|
||||
|
||||
- **ARIA Attributes**:
|
||||
- `aria-label` on control buttons
|
||||
- `aria-live` regions for status updates
|
||||
- `aria-current` for active queue item
|
||||
- `role="list"` on queue items list
|
||||
|
||||
- **Screen Reader Support**:
|
||||
- Status changes announced
|
||||
- Progress updates spoken
|
||||
- Error messages announced
|
||||
|
||||
---
|
||||
|
||||
## Related Links
|
||||
|
||||
- [Issue Hub](/features/issue-hub) - Unified issues, queue, and discovery management
|
||||
- [Terminal Dashboard](/features/terminal) - Terminal-first workspace with integrated queue panel
|
||||
- [Discovery](/features/discovery) - Discovery session tracking
|
||||
- [Issues Panel](/features/issue-hub) - Issue list and GitHub sync
|
||||
- [Sessions](/features/sessions) - Session management and details
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Terminal Dashboard
|
||||
|
||||
## One-Liner
|
||||
|
||||
**The Terminal Dashboard provides a terminal-first workspace with resizable panes, floating panels, and integrated tools for session monitoring and orchestration.**
|
||||
|
||||
---
|
||||
@@ -18,16 +17,15 @@
|
||||
|
||||
---
|
||||
|
||||
## Page Overview
|
||||
## Overview
|
||||
|
||||
**Location**: `ccw/frontend/src/pages/TerminalDashboardPage.tsx`
|
||||
|
||||
**Purpose**: Terminal-first layout for multi-terminal session management with integrated tools and resizable panels.
|
||||
|
||||
**Access**: Navigation → Terminal Dashboard
|
||||
|
||||
### Layout
|
||||
**Access**: Navigation → Terminal Dashboard (`/terminal-dashboard`)
|
||||
|
||||
**Layout**:
|
||||
```
|
||||
+--------------------------------------------------------------------------+
|
||||
| Dashboard Toolbar (panel toggles, layout presets, fullscreen) |
|
||||
@@ -48,6 +46,12 @@
|
||||
|
||||
---
|
||||
|
||||
## Live Demo
|
||||
|
||||
:::demo TerminalDashboardOverview #TerminalDashboardOverview.tsx :::
|
||||
|
||||
---
|
||||
|
||||
## Core Features
|
||||
|
||||
| Feature | Description |
|
||||
@@ -64,38 +68,180 @@
|
||||
|
||||
---
|
||||
|
||||
## Usage Guide
|
||||
## Component Hierarchy
|
||||
|
||||
### Basic Workflow
|
||||
```
|
||||
TerminalDashboardPage
|
||||
├── AssociationHighlightProvider (context)
|
||||
├── DashboardToolbar
|
||||
│ ├── Layout Preset Buttons (Single | Split-H | Split-V | Grid-2x2)
|
||||
│ ├── Panel Toggles (Sessions | Files | Issues | Queue | Inspector | Execution | Scheduler)
|
||||
│ ├── Fullscreen Toggle
|
||||
│ └── Launch CLI Button
|
||||
├── Allotment (Three-Column Layout)
|
||||
│ ├── SessionGroupTree
|
||||
│ │ └── Session Group Items (collapsible)
|
||||
│ ├── TerminalGrid
|
||||
│ │ ├── GridGroupRenderer (recursive)
|
||||
│ │ └── TerminalPane
|
||||
│ └── FileSidebarPanel
|
||||
│ └── File Tree View
|
||||
└── FloatingPanel (multiple, mutually exclusive)
|
||||
├── Issues+Queue (split panel)
|
||||
│ ├── IssuePanel
|
||||
│ └── QueueListColumn
|
||||
├── QueuePanel (feature flag)
|
||||
├── InspectorContent (feature flag)
|
||||
├── ExecutionMonitorPanel (feature flag)
|
||||
└── SchedulerPanel
|
||||
```
|
||||
|
||||
1. **Launch CLI Session**: Click "Launch CLI" button, configure options (tool, model, shell, working directory)
|
||||
2. **Arrange Terminals**: Use layout presets or manually split panes
|
||||
3. **Navigate Sessions**: Browse session groups in the left tree
|
||||
4. **Toggle Panels**: Click toolbar buttons to show/hide floating panels
|
||||
5. **Inspect Issues**: Open Issues panel to view associated issues
|
||||
6. **Monitor Execution**: Use Execution Monitor panel for real-time tracking
|
||||
---
|
||||
|
||||
### Key Interactions
|
||||
## Props API
|
||||
|
||||
| Interaction | How to Use |
|
||||
|-------------|------------|
|
||||
| **Launch CLI** | Click "Launch CLI" button, configure session in modal |
|
||||
| **Toggle sidebar** | Click Sessions/Files button in toolbar |
|
||||
| **Open floating panel** | Click Issues/Queue/Inspector/Execution/Scheduler button |
|
||||
| **Close floating panel** | Click X on panel or toggle button again |
|
||||
| **Resize panes** | Drag the divider between panes |
|
||||
| **Change layout** | Click layout preset buttons (single/split/grid) |
|
||||
| **Fullscreen mode** | Click fullscreen button to hide app chrome |
|
||||
### TerminalDashboardPage
|
||||
|
||||
### Panel Types
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| - | - | - | This page component accepts no props (state managed via hooks and Zustand stores) |
|
||||
|
||||
| Panel | Content | Position |
|
||||
|-------|---------|----------|
|
||||
| **Issues+Queue** | Combined Issues panel + Queue list column | Left (overlay) |
|
||||
| **Queue** | Full queue management panel | Right (overlay, feature flag) |
|
||||
| **Inspector** | Association chain inspector | Right (overlay, feature flag) |
|
||||
| **Execution Monitor** | Real-time execution tracking | Right (overlay, feature flag) |
|
||||
| **Scheduler** | Queue scheduler controls | Right (overlay) |
|
||||
### DashboardToolbar
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `activePanel` | `PanelId \| null` | `null` | Currently active floating panel |
|
||||
| `onTogglePanel` | `(panelId: PanelId) => void` | - | Callback to toggle panel visibility |
|
||||
| `isFileSidebarOpen` | `boolean` | `true` | File sidebar visibility state |
|
||||
| `onToggleFileSidebar` | `() => void` | - | Toggle file sidebar callback |
|
||||
| `isSessionSidebarOpen` | `boolean` | `true` | Session sidebar visibility state |
|
||||
| `onToggleSessionSidebar` | `() => void` | - | Toggle session sidebar callback |
|
||||
| `isFullscreen` | `boolean` | `false` | Fullscreen mode state |
|
||||
| `onToggleFullscreen` | `() => void` | - | Toggle fullscreen callback |
|
||||
|
||||
### FloatingPanel
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `isOpen` | `boolean` | `false` | Panel open state |
|
||||
| `onClose` | `() => void` | - | Close callback |
|
||||
| `title` | `string` | - | Panel title |
|
||||
| `side` | `'left' \| 'right'` | `'left'` | Panel side |
|
||||
| `width` | `number` | `400` | Panel width in pixels |
|
||||
| `children` | `ReactNode` | - | Panel content |
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### Local State
|
||||
|
||||
| State | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `activePanel` | `PanelId \| null` | Currently active floating panel (mutually exclusive) |
|
||||
| `isFileSidebarOpen` | `boolean` | File sidebar visibility |
|
||||
| `isSessionSidebarOpen` | `boolean` | Session sidebar visibility |
|
||||
|
||||
### Zustand Stores
|
||||
|
||||
| Store | Selector | Purpose |
|
||||
|-------|----------|---------|
|
||||
| `workflowStore` | `selectProjectPath` | Current project path for file sidebar |
|
||||
| `appStore` | `selectIsImmersiveMode` | Fullscreen mode state |
|
||||
| `configStore` | `featureFlags` | Feature flag configuration |
|
||||
| `terminalGridStore` | Grid layout and focused pane state |
|
||||
| `executionMonitorStore` | Active execution count |
|
||||
| `queueSchedulerStore` | Scheduler status and settings |
|
||||
|
||||
### Panel ID Type
|
||||
|
||||
```typescript
|
||||
type PanelId = 'issues' | 'queue' | 'inspector' | 'execution' | 'scheduler';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Terminal Dashboard
|
||||
|
||||
```tsx
|
||||
import { TerminalDashboardPage } from '@/pages/TerminalDashboardPage'
|
||||
|
||||
// The terminal dashboard is automatically rendered at /terminal-dashboard
|
||||
// No props needed - layout state managed internally
|
||||
```
|
||||
|
||||
### Using FloatingPanel Component
|
||||
|
||||
```tsx
|
||||
import { FloatingPanel } from '@/components/terminal-dashboard/FloatingPanel'
|
||||
import { IssuePanel } from '@/components/terminal-dashboard/IssuePanel'
|
||||
|
||||
function CustomLayout() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<FloatingPanel
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
title="Issues"
|
||||
side="left"
|
||||
width={700}
|
||||
>
|
||||
<IssuePanel />
|
||||
</FloatingPanel>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Panel Toggle Pattern
|
||||
|
||||
```tsx
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
function usePanelToggle() {
|
||||
const [activePanel, setActivePanel] = useState<string | null>(null)
|
||||
|
||||
const togglePanel = useCallback((panelId: string) => {
|
||||
setActivePanel((prev) => (prev === panelId ? null : panelId))
|
||||
}, [])
|
||||
|
||||
const closePanel = useCallback(() => {
|
||||
setActivePanel(null)
|
||||
}, [])
|
||||
|
||||
return { activePanel, togglePanel, closePanel }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Interactive Demos
|
||||
|
||||
### Layout Presets Demo
|
||||
|
||||
:::demo TerminalLayoutPresets #TerminalLayoutPresets.tsx :::
|
||||
|
||||
### Floating Panels Demo
|
||||
|
||||
:::demo FloatingPanelsDemo #FloatingPanelsDemo.tsx :::
|
||||
|
||||
### Resizable Panes Demo
|
||||
|
||||
:::demo ResizablePanesDemo #ResizablePanesDemo.tsx :::
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Feature Flags
|
||||
|
||||
| Flag | Controls |
|
||||
|------|----------|
|
||||
| `dashboardQueuePanelEnabled` | Queue panel visibility |
|
||||
| `dashboardInspectorEnabled` | Inspector panel visibility |
|
||||
| `dashboardExecutionMonitorEnabled` | Execution Monitor panel visibility |
|
||||
|
||||
### Layout Presets
|
||||
|
||||
@@ -106,59 +252,36 @@
|
||||
| **Split-V** | Two panes stacked vertically |
|
||||
| **Grid-2x2** | Four panes in 2x2 grid |
|
||||
|
||||
---
|
||||
### Panel Types
|
||||
|
||||
## Components Reference
|
||||
|
||||
### Main Components
|
||||
|
||||
| Component | Location | Purpose |
|
||||
|-----------|----------|---------|
|
||||
| `TerminalDashboardPage` | `@/pages/TerminalDashboardPage.tsx` | Main terminal dashboard page |
|
||||
| `DashboardToolbar` | `@/components/terminal-dashboard/DashboardToolbar.tsx` | Top toolbar with panel toggles |
|
||||
| `SessionGroupTree` | `@/components/terminal-dashboard/SessionGroupTree.tsx` | Session tree navigation |
|
||||
| `TerminalGrid` | `@/components/terminal-dashboard/TerminalGrid.tsx` | Tmux-style terminal panes |
|
||||
| `FileSidebarPanel` | `@/components/terminal-dashboard/FileSidebarPanel.tsx` | File explorer sidebar |
|
||||
| `FloatingPanel` | `@/components/terminal-dashboard/FloatingPanel.tsx` | Overlay panel wrapper |
|
||||
| `IssuePanel` | `@/components/terminal-dashboard/IssuePanel.tsx` | Issues list panel |
|
||||
| `QueuePanel` | `@/components/terminal-dashboard/QueuePanel.tsx` | Queue management panel |
|
||||
| `QueueListColumn` | `@/components/terminal-dashboard/QueueListColumn.tsx` | Queue list (compact) |
|
||||
| `SchedulerPanel` | `@/components/terminal-dashboard/SchedulerPanel.tsx` | Queue scheduler controls |
|
||||
| `InspectorContent` | `@/components/terminal-dashboard/BottomInspector.tsx` | Association inspector |
|
||||
| `ExecutionMonitorPanel` | `@/components/terminal-dashboard/ExecutionMonitorPanel.tsx` | Execution tracking |
|
||||
|
||||
### State Management
|
||||
|
||||
- **Local state** (TerminalDashboardPage):
|
||||
- `activePanel`: PanelId | null
|
||||
- `isFileSidebarOpen`: boolean
|
||||
- `isSessionSidebarOpen`: boolean
|
||||
|
||||
- **Zustand stores**:
|
||||
- `useWorkflowStore` - Project path
|
||||
- `useAppStore` - Immersive mode state
|
||||
- `useConfigStore` - Feature flags
|
||||
- `useTerminalGridStore` - Terminal grid layout and focused pane
|
||||
- `useExecutionMonitorStore` - Active execution count
|
||||
- `useQueueSchedulerStore` - Scheduler status
|
||||
| Panel | Content | Position | Feature Flag |
|
||||
|-------|---------|----------|--------------|
|
||||
| **Issues+Queue** | Combined Issues panel + Queue list column | Left (overlay) | - |
|
||||
| **Queue** | Full queue management panel | Right (overlay) | `dashboardQueuePanelEnabled` |
|
||||
| **Inspector** | Association chain inspector | Right (overlay) | `dashboardInspectorEnabled` |
|
||||
| **Execution Monitor** | Real-time execution tracking | Right (overlay) | `dashboardExecutionMonitorEnabled` |
|
||||
| **Scheduler** | Queue scheduler controls | Right (overlay) | - |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
## Accessibility
|
||||
|
||||
### Panel IDs
|
||||
- **Keyboard Navigation**:
|
||||
- <kbd>Tab</kbd> - Navigate through toolbar buttons
|
||||
- <kbd>Enter</kbd>/<kbd>Space</kbd> - Activate toolbar buttons
|
||||
- <kbd>Escape</kbd> - Close floating panels
|
||||
- <kbd>F11</kbd> - Toggle fullscreen mode
|
||||
|
||||
```typescript
|
||||
type PanelId = 'issues' | 'queue' | 'inspector' | 'execution' | 'scheduler';
|
||||
```
|
||||
- **ARIA Attributes**:
|
||||
- `aria-label` on toolbar buttons
|
||||
- `aria-expanded` on sidebar toggles
|
||||
- `aria-hidden` on inactive floating panels
|
||||
- `role="dialog"` on floating panels
|
||||
|
||||
### Feature Flags
|
||||
|
||||
| Flag | Controls |
|
||||
|------|----------|
|
||||
| `dashboardQueuePanelEnabled` | Queue panel visibility |
|
||||
| `dashboardInspectorEnabled` | Inspector panel visibility |
|
||||
| `dashboardExecutionMonitorEnabled` | Execution Monitor panel visibility |
|
||||
- **Screen Reader Support**:
|
||||
- Panel state announced when toggled
|
||||
- Layout changes announced
|
||||
- Focus management when panels open/close
|
||||
|
||||
---
|
||||
|
||||
@@ -168,3 +291,4 @@ type PanelId = 'issues' | 'queue' | 'inspector' | 'execution' | 'scheduler';
|
||||
- [Sessions](/features/sessions) - Session management
|
||||
- [Issue Hub](/features/issue-hub) - Issues, queue, discovery
|
||||
- [Explorer](/features/explorer) - File explorer
|
||||
- [Queue](/features/queue) - Queue management standalone page
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
```bash
|
||||
# Clone repository (replace with your fork or the actual repository URL)
|
||||
git clone https://github.com/[username]/claude-dms3.git
|
||||
git clone https://github.com/catlog22/Claude-Code-Workflow.git
|
||||
cd claude-dms3
|
||||
|
||||
# Install dependencies
|
||||
@@ -273,7 +273,7 @@ A: Select based on task objective:
|
||||
|
||||
```bash
|
||||
# 1. Clone project (replace with your fork or the actual repository URL)
|
||||
git clone https://github.com/[username]/claude-dms3.git
|
||||
git clone https://github.com/catlog22/Claude-Code-Workflow.git
|
||||
cd claude-dms3
|
||||
|
||||
# 2. Install dependencies
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user