Compare commits

..

18 Commits

Author SHA1 Message Date
catlog22
4d17bb02a4 chore: release v7.1.0 2026-03-01 23:17:52 +08:00
catlog22
5cab8ae8a5 fix: CSRF token accessibility and hook installation status
- Remove HttpOnly from XSRF-TOKEN cookie for JavaScript readability
- Add hook installation status detection in system settings API
- Update InjectionControlTab to show installed hooks status
- Add brace expansion support in globToRegex utility
2026-03-01 23:17:37 +08:00
catlog22
ffe3b427ce feat(docs): 添加技能/团队命令对比表和代码审查报告 2026-03-01 21:01:26 +08:00
catlog22
8c953b287d feat(idaw): add run-coordinate command for external CLI execution with hook callbacks 2026-03-01 20:58:26 +08:00
catlog22
b1e321267e docs: fix command invocation syntax accuracy [IDAW-004]
- Fix /workflow-tdd → /workflow-tdd-plan (correct skill name)
- Fix /workflow:test-fix → /workflow-test-fix (skill, not command)
- Fix /workflow:skill-designer → /workflow-skill-designer (skill)
- Fix /workflow:plan → /workflow-plan (skill, not command)
- Remove non-existent /workflow:wave-plan reference
- Update both English and Chinese documentation
2026-03-01 20:50:30 +08:00
catlog22
d0275f14b2 feat(idaw): add CLI-assisted analysis for pre-task context and error recovery
- Pre-task context analysis via gemini for bugfix/complex tasks
- CLI-assisted error diagnosis before retry on skill failure
- Consistent implementation across run.md and resume.md
2026-03-01 20:47:19 +08:00
catlog22
ee4dc367d9 docs: fix 404 errors - add missing zh guide files and fix zh-CN config [IDAW-002]
- Add docs/zh/guide/first-workflow.md (Chinese translation)
- Add docs/zh/guide/cli-tools.md (Chinese translation)
- Fix zh-CN locale config to only show existing files (dashboard, terminal, queue)
- Remove non-existent zh-CN sidebar entries that caused 404 errors
2026-03-01 20:34:11 +08:00
catlog22
a63fb370aa docs: fix repository URLs in getting started guide [IDAW-001]
Replace placeholder URLs with actual repository URL:
https://github.com/catlog22/Claude-Code-Workflow.git
2026-03-01 20:24:20 +08:00
catlog22
da19a6ec89 feat: Implement IDAW commands and update favicon/logo SVGs
- Added IDAW (Independent Development Autonomous Workflow) commands for batch task execution, including `/idaw:add`, `/idaw:run`, `/idaw:status`, and `/idaw:resume`.
- Updated documentation for IDAW commands in both English and Chinese.
- Modified favicon and logo SVGs to reflect new orbital design with dynamic colors.
- Incremented package version from 7.0.6 to 7.0.9.
2026-03-01 20:05:44 +08:00
catlog22
bf84a157ea chore: bump version to 7.0.9
feat(idaw): Independent Development Autonomous Workflow
- /idaw:add — manual task creation + import from ccw issue
- /idaw:run — 6-phase serial orchestrator with git checkpoints
- /idaw:status — read-only progress viewer
- /idaw:resume — resume interrupted sessions from last checkpoint
2026-03-01 19:50:27 +08:00
catlog22
41f990ddd4 Enhance shell safety in skill argument assembly and add animated orbital motion demo
- Updated `assembleSkillArgs` function in `resume.md` and `run.md` to sanitize task goal for shell safety by escaping special characters.
- Introduced a new animated orbital motion demo in `icon-concepts.html`, showcasing agents orbiting with varying speeds and a breathing core effect.
2026-03-01 19:48:50 +08:00
catlog22
3463bc8e27 feat(idaw): add resume, run, and status commands for task management
- Implemented /idaw:resume to resume interrupted sessions with task handling and auto mode.
- Created /idaw:run for executing task skill chains with git checkpoints and session management.
- Added /idaw:status for viewing task and session progress, including overview and specific session details.
- Introduced helper functions for task type inference and skill argument assembly.
- Enhanced task management with session tracking, progress reporting, and error handling.
2026-03-01 19:40:05 +08:00
catlog22
9ad755e225 feat: add comprehensive analysis report for Hook templates compliance with official standards
- Introduced a detailed report outlining compliance issues and recommendations for the `ccw/frontend` implementation of Hook templates.
- Identified critical issues regarding command structure and input reading methods.
- Highlighted errors related to cross-platform compatibility of Bash scripts on Windows.
- Documented warnings regarding matcher formats and exit code usage.
- Provided a summary of supported trigger types and outlined missing triggers.
- Included a section on completed fixes and references to affected files for easier tracking.
2026-03-01 15:12:44 +08:00
catlog22
8799a9c2fd refactor(team-planex): redesign skill with inverted control and beat model
- Delete executor agent (main flow IS the executor now)
- Rewrite SKILL.md: delegated planning + inline execution
- Input accepts issues.jsonl / roadmap session from roadmap-with-file
- Single reusable planner agent via send_input (Pattern 2.3)
- Interleaved plan-execute loop with eager delegation
- Follow codex v3 conventions (decision tables, placeholders)
- Remove complexity assessment and dynamic splitting
2026-03-01 15:06:06 +08:00
catlog22
1f859ae4b9 fix: align spec paths and add missing translation keys 2026-03-01 13:42:25 +08:00
catlog22
ecf4e4d848 fix: align spec paths from .workflow/specs to .ccw/specs
- Fix path mismatch between command files and frontend/backend spec-index-builder
- Update init-specs.md, init-guidelines.md, sync.md, solidify.md to use .ccw/specs/
- Update init.md, start.md, clean.md, unified-execute-with-file.md, collaborative-plan-with-file.md
- Add scope field to architecture-constraints.md and coding-conventions.md
- Ensures specs created by commands are visible in frontend Spec Settings page
2026-03-01 13:28:54 +08:00
catlog22
8ceae6d6fd Add Chinese documentation for custom skills development and reference guide
- Created a new document for custom skills development (`custom.md`) detailing the structure, creation, implementation, and best practices for developing custom CCW skills.
- Added an index document (`index.md`) summarizing all built-in skills, their categories, and usage examples.
- Introduced a reference guide (`reference.md`) providing a quick reference for all 33 built-in CCW skills, including triggers and purposes.
2026-03-01 13:08:12 +08:00
catlog22
2fb93d20e0 feat: add queue management and terminal dashboard documentation in Chinese
- Introduced comprehensive documentation for the queue management feature, detailing its pain points, core functionalities, and component structure.
- Added terminal dashboard documentation, highlighting its layout, core features, and usage examples.
- Created an index page in Chinese for Claude Code Workflow, summarizing its purpose and core features, along with quick links to installation and guides.
2026-03-01 10:52:46 +08:00
155 changed files with 23892 additions and 1917 deletions

View File

@@ -9,6 +9,7 @@ keywords:
- pattern
readMode: required
priority: high
scope: project
---
# Architecture Constraints

View File

@@ -9,6 +9,7 @@ keywords:
- convention
readMode: required
priority: high
scope: project
---
# Coding Conventions

View 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
```

View 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
```

View 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
```

View 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
```

View 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
```

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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' : ''}
`);
```

View File

@@ -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

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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}]`
}))
})
```

View File

@@ -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):

View File

@@ -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}]`
}))
})
```

View File

@@ -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}]"}
]
```

View File

@@ -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 |

View File

@@ -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 |
|---------|--------|
| 13 | 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 |

View File

@@ -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)

View File

@@ -0,0 +1 @@
.ace-tool/

View File

@@ -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',

View File

@@ -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': {

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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;
}>;
}

View File

@@ -1,4 +1,6 @@
{
"cancel": "Cancel",
"save": "Save",
"aria": {
"toggleNavigation": "Toggle navigation menu",
"refreshWorkspace": "Refresh workspace",

View File

@@ -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."

View File

@@ -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"
}
}

View File

@@ -1,4 +1,6 @@
{
"cancel": "取消",
"save": "保存",
"aria": {
"toggleNavigation": "切换导航菜单",
"refreshWorkspace": "刷新工作区",

View File

@@ -96,6 +96,11 @@
"message": "尝试调整筛选条件或搜索查询。",
"noFixProgress": "无修复进度数据"
},
"notExecuted": {
"title": "审查尚未执行",
"message": "此审查会话已创建,但审查流程尚未启动。尚未生成任何发现结果。",
"hint": "💡 提示:请执行审查工作流以开始分析代码并生成发现结果。"
},
"notFound": {
"title": "未找到审查会话",
"message": "无法找到请求的审查会话。"

View File

@@ -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": "所有钩子安装成功"
}
}

View File

@@ -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>
) : (

View File

@@ -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;

View File

@@ -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}`,
];

View File

@@ -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;
}

View File

@@ -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

View 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

View File

@@ -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}`,
];

View File

@@ -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[] = [];

View File

@@ -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, '.*')

View File

@@ -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();
});
});

View 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)*

View File

@@ -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' }
]
}
]
}
}
}
}
}))

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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 |

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -264,7 +264,7 @@ function hideTooltip() {
}
}
@media (max-width: 640px) {
@media (max-width: 768px) {
.agent-orchestration {
padding: 2rem 1rem;
}

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>`
}

View File

@@ -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)
}
}

View 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};`
}

View File

@@ -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>

View 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)
}
}
}

View File

@@ -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 {

View 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);
}

View File

@@ -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
* ============================================ */

View File

@@ -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 */
}
/* ============================================

View 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

View File

@@ -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 |
|---------|----------|------------|

View File

@@ -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
View 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
View 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)

View 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
View 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)

View 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
View 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)

View 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)

View File

@@ -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

View File

@@ -157,7 +157,7 @@ The V2 Pipeline tab monitors background jobs for:
- **Local state**:
- `activeTab`: TabValue
- `searchQuery`: string
- `selectedMemories`: Set<string>
- `selectedMemories`: Set&lt;string&gt;
- `filters`: { sourceType?: string; tags?: string[] }
- `dialogStates`: create, edit, delete

View File

@@ -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

View File

@@ -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

View File

@@ -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