From 60eab98782ad54084b4e984d28f2e0010ed68f17 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Thu, 22 Jan 2026 10:13:00 +0800 Subject: [PATCH] feat: Add comprehensive tests for CCW Loop System flow state - Implemented loop control tasks in JSON format for testing. - Created comprehensive test scripts for loop flow and standalone tests. - Developed a shell script to automate the testing of the entire loop system flow, including mock endpoints and state transitions. - Added error handling and execution history tests to ensure robustness. - Established variable substitution and success condition evaluations in tests. - Set up cleanup and workspace management for test environments. --- .claude/skills/ccw-loop/README.md | 303 +++ .claude/skills/ccw-loop/SKILL.md | 259 +++ .../phases/actions/action-complete.md | 320 +++ .../phases/actions/action-debug-with-file.md | 485 +++++ .../actions/action-develop-with-file.md | 365 ++++ .../ccw-loop/phases/actions/action-init.md | 200 ++ .../ccw-loop/phases/actions/action-menu.md | 192 ++ .../actions/action-validate-with-file.md | 307 +++ .../skills/ccw-loop/phases/orchestrator.md | 486 +++++ .../skills/ccw-loop/phases/state-schema.md | 474 +++++ .../skills/ccw-loop/specs/action-catalog.md | 300 +++ .../ccw-loop/specs/loop-requirements.md | 192 ++ .../ccw-loop/templates/progress-template.md | 175 ++ .../templates/understanding-template.md | 303 +++ .../ccw-loop/templates/validation-template.md | 258 +++ .../.task/E2E-TASK-1769007254162.json | 29 + ccw/src/core/dashboard-generator.ts | 5 +- ccw/src/core/routes/cli-routes.ts | 62 +- ccw/src/core/routes/loop-v2-routes.ts | 1332 ++++++++++++ ccw/src/core/routes/task-routes.ts | 42 + ccw/src/core/server.ts | 18 +- .../templates/dashboard-css/12-cli-legacy.css | 35 + .../dashboard-css/33-cli-stream-viewer.css | 55 + .../dashboard-css/36-loop-monitor.css | 1240 +++++------ .../dashboard-css/36-loop-monitor.css.backup | 1877 +++++++++++++++++ .../components/cli-stream-viewer.js | 363 +++- .../dashboard-js/components/notifications.js | 16 + ccw/src/templates/dashboard-js/i18n.js | 312 ++- .../dashboard-js/views/cli-manager.js | 5 +- .../dashboard-js/views/loop-monitor.js | 1586 ++++++++++++-- ccw/src/templates/dashboard.html | 6 +- ccw/src/tools/loop-state-manager.ts | 4 +- ccw/src/tools/loop-task-manager.ts | 380 ++++ ccw/src/types/loop.ts | 123 ++ tests/loop-flow-test.js | 329 +++ tests/loop-standalone-test.js | 565 +++++ tests/run-loop-flow-test.sh | 261 +++ 37 files changed, 12347 insertions(+), 917 deletions(-) create mode 100644 .claude/skills/ccw-loop/README.md create mode 100644 .claude/skills/ccw-loop/SKILL.md create mode 100644 .claude/skills/ccw-loop/phases/actions/action-complete.md create mode 100644 .claude/skills/ccw-loop/phases/actions/action-debug-with-file.md create mode 100644 .claude/skills/ccw-loop/phases/actions/action-develop-with-file.md create mode 100644 .claude/skills/ccw-loop/phases/actions/action-init.md create mode 100644 .claude/skills/ccw-loop/phases/actions/action-menu.md create mode 100644 .claude/skills/ccw-loop/phases/actions/action-validate-with-file.md create mode 100644 .claude/skills/ccw-loop/phases/orchestrator.md create mode 100644 .claude/skills/ccw-loop/phases/state-schema.md create mode 100644 .claude/skills/ccw-loop/specs/action-catalog.md create mode 100644 .claude/skills/ccw-loop/specs/loop-requirements.md create mode 100644 .claude/skills/ccw-loop/templates/progress-template.md create mode 100644 .claude/skills/ccw-loop/templates/understanding-template.md create mode 100644 .claude/skills/ccw-loop/templates/validation-template.md create mode 100644 .test-loop-comprehensive/.task/E2E-TASK-1769007254162.json create mode 100644 ccw/src/core/routes/loop-v2-routes.ts create mode 100644 ccw/src/templates/dashboard-css/36-loop-monitor.css.backup create mode 100644 ccw/src/tools/loop-task-manager.ts create mode 100644 tests/loop-flow-test.js create mode 100644 tests/loop-standalone-test.js create mode 100644 tests/run-loop-flow-test.sh diff --git a/.claude/skills/ccw-loop/README.md b/.claude/skills/ccw-loop/README.md new file mode 100644 index 00000000..f4786cb4 --- /dev/null +++ b/.claude/skills/ccw-loop/README.md @@ -0,0 +1,303 @@ +# CCW Loop Skill + +无状态迭代开发循环工作流,支持开发 (Develop)、调试 (Debug)、验证 (Validate) 三个阶段,每个阶段都有独立的文件记录进展。 + +## Overview + +CCW Loop 是一个自主模式 (Autonomous) 的 Skill,通过文件驱动的无状态循环,帮助开发者系统化地完成开发任务。 + +### 核心特性 + +1. **无状态循环**: 每次执行从文件读取状态,不依赖内存 +2. **文件驱动**: 所有进度记录在 Markdown 文件中,可审计、可回顾 +3. **Gemini 辅助**: 关键决策点使用 CLI 工具进行深度分析 +4. **可恢复**: 任何时候中断后可继续 +5. **双模式**: 支持交互式和自动循环 + +### 三大阶段 + +- **Develop**: 任务分解 → 代码实现 → 进度记录 +- **Debug**: 假设生成 → 证据收集 → 根因分析 → 修复验证 +- **Validate**: 测试执行 → 覆盖率检查 → 质量评估 + +## Installation + +已包含在 `.claude/skills/ccw-loop/`,无需额外安装。 + +## Usage + +### 基本用法 + +```bash +# 启动新循环 +/ccw-loop "实现用户认证功能" + +# 继续现有循环 +/ccw-loop --resume LOOP-auth-2026-01-22 + +# 自动循环模式 +/ccw-loop --auto "修复登录bug并添加测试" +``` + +### 交互式流程 + +``` +1. 启动: /ccw-loop "任务描述" +2. 初始化: 自动分析任务并生成子任务列表 +3. 显示菜单: + - 📝 继续开发 (Develop) + - 🔍 开始调试 (Debug) + - ✅ 运行验证 (Validate) + - 📊 查看详情 (Status) + - 🏁 完成循环 (Complete) + - 🚪 退出 (Exit) +4. 执行选择的动作 +5. 重复步骤 3-4 直到完成 +``` + +### 自动循环流程 + +``` +Develop (所有任务) → Debug (如有需要) → Validate → 完成 +``` + +## Directory Structure + +``` +.workflow/.loop/{session-id}/ +├── meta.json # 会话元数据 (不可修改) +├── state.json # 当前状态 (每次更新) +├── summary.md # 完成报告 (结束时生成) +├── develop/ +│ ├── progress.md # 开发进度时间线 +│ ├── tasks.json # 任务列表 +│ └── changes.log # 代码变更日志 (NDJSON) +├── debug/ +│ ├── understanding.md # 理解演变文档 +│ ├── hypotheses.json # 假设历史 +│ └── debug.log # 调试日志 (NDJSON) +└── validate/ + ├── validation.md # 验证报告 + ├── test-results.json # 测试结果 + └── coverage.json # 覆盖率数据 +``` + +## Action Reference + +| Action | 描述 | 触发条件 | +|--------|------|----------| +| action-init | 初始化会话 | 首次启动 | +| action-menu | 显示操作菜单 | 交互模式下每次循环 | +| action-develop-with-file | 执行开发任务 | 有待处理任务 | +| action-debug-with-file | 假设驱动调试 | 需要调试 | +| action-validate-with-file | 运行测试验证 | 需要验证 | +| action-complete | 完成并生成报告 | 所有任务完成 | + +详细说明见 [specs/action-catalog.md](specs/action-catalog.md) + +## CLI Integration + +CCW Loop 在关键决策点集成 CLI 工具: + +### 任务分解 (action-init) +```bash +ccw cli -p "PURPOSE: 分解开发任务..." + --tool gemini + --mode analysis + --rule planning-breakdown-task-steps +``` + +### 代码实现 (action-develop) +```bash +ccw cli -p "PURPOSE: 实现功能代码..." + --tool gemini + --mode write + --rule development-implement-feature +``` + +### 假设生成 (action-debug - 探索) +```bash +ccw cli -p "PURPOSE: Generate debugging hypotheses..." + --tool gemini + --mode analysis + --rule analysis-diagnose-bug-root-cause +``` + +### 证据分析 (action-debug - 分析) +```bash +ccw cli -p "PURPOSE: Analyze debug log evidence..." + --tool gemini + --mode analysis + --rule analysis-diagnose-bug-root-cause +``` + +### 质量评估 (action-validate) +```bash +ccw cli -p "PURPOSE: Analyze test results and coverage..." + --tool gemini + --mode analysis + --rule analysis-review-code-quality +``` + +## State Management + +### State Schema + +参见 [phases/state-schema.md](phases/state-schema.md) + +### State Transitions + +``` +pending → running → completed + ↓ + user_exit + ↓ + failed +``` + +### State Recovery + +如果 `state.json` 损坏,可从其他文件重建: +- develop/tasks.json → develop.* +- debug/hypotheses.json → debug.* +- validate/test-results.json → validate.* + +## Examples + +### Example 1: 功能开发 + +```bash +# 1. 启动循环 +/ccw-loop "Add user profile page" + +# 2. 系统初始化,生成任务: +# - task-001: Create profile component +# - task-002: Add API endpoints +# - task-003: Implement tests + +# 3. 选择 "继续开发" +# → 执行 task-001 (Gemini 辅助实现) +# → 更新 progress.md + +# 4. 重复开发直到所有任务完成 + +# 5. 选择 "运行验证" +# → 运行测试 +# → 检查覆盖率 +# → 生成 validation.md + +# 6. 选择 "完成循环" +# → 生成 summary.md +# → 询问是否扩展为 Issue +``` + +### Example 2: Bug 修复 + +```bash +# 1. 启动循环 +/ccw-loop "Fix login timeout issue" + +# 2. 选择 "开始调试" +# → 输入 bug 描述: "Login times out after 30s" +# → Gemini 生成假设 (H1, H2, H3) +# → 添加 NDJSON 日志 +# → 提示复现 bug + +# 3. 复现 bug (在应用中操作) + +# 4. 再次选择 "开始调试" +# → 解析 debug.log +# → Gemini 分析证据 +# → H2 确认为根因 +# → 生成修复代码 +# → 更新 understanding.md + +# 5. 选择 "运行验证" +# → 测试通过 + +# 6. 完成 +``` + +## Templates + +- [progress-template.md](templates/progress-template.md): 开发进度文档模板 +- [understanding-template.md](templates/understanding-template.md): 调试理解文档模板 +- [validation-template.md](templates/validation-template.md): 验证报告模板 + +## Specifications + +- [loop-requirements.md](specs/loop-requirements.md): 循环需求规范 +- [action-catalog.md](specs/action-catalog.md): 动作目录 + +## Integration + +### Dashboard Integration + +CCW Loop 与 Dashboard Loop Monitor 集成: +- Dashboard 创建 Loop → 触发此 Skill +- state.json → Dashboard 实时显示 +- 任务列表双向同步 +- 控制按钮映射到 actions + +### Issue System Integration + +完成后可扩展为 Issue: +- 维度: test, enhance, refactor, doc +- 自动调用 `/issue:new` +- 上下文自动填充 + +## Error Handling + +| 情况 | 处理 | +|------|------| +| Session 不存在 | 创建新会话 | +| state.json 损坏 | 从文件重建 | +| CLI 工具失败 | 回退到手动模式 | +| 测试失败 | 循环回到 develop/debug | +| >10 迭代 | 警告用户,建议拆分 | + +## Limitations + +1. **单会话限制**: 同一时间只能有一个活跃会话 +2. **迭代限制**: 建议不超过 10 次迭代 +3. **CLI 依赖**: 部分功能依赖 Gemini CLI 可用性 +4. **测试框架**: 需要 package.json 中定义测试脚本 + +## Troubleshooting + +### Q: 如何查看当前会话状态? + +A: 在菜单中选择 "查看详情 (Status)" + +### Q: 如何恢复中断的会话? + +A: 使用 `--resume` 参数: +```bash +/ccw-loop --resume LOOP-xxx-2026-01-22 +``` + +### Q: 如果 CLI 工具失败怎么办? + +A: Skill 会自动降级到手动模式,提示用户手动输入 + +### Q: 如何添加自定义 action? + +A: 参见 [specs/action-catalog.md](specs/action-catalog.md) 的 "Action Extensions" 部分 + +## Contributing + +添加新功能: +1. 创建 action 文件在 `phases/actions/` +2. 更新 orchestrator 决策逻辑 +3. 添加到 action-catalog.md +4. 更新 action-menu.md + +## License + +MIT + +--- + +**Version**: 1.0.0 +**Last Updated**: 2026-01-22 +**Author**: CCW Team diff --git a/.claude/skills/ccw-loop/SKILL.md b/.claude/skills/ccw-loop/SKILL.md new file mode 100644 index 00000000..2a58cf4a --- /dev/null +++ b/.claude/skills/ccw-loop/SKILL.md @@ -0,0 +1,259 @@ +--- +name: ccw-loop +description: Stateless iterative development loop workflow with documented progress. Supports develop, debug, and validate phases with file-based state tracking. Triggers on "ccw-loop", "dev loop", "development loop", "开发循环", "迭代开发". +allowed-tools: Task(*), AskUserQuestion(*), Read(*), Grep(*), Glob(*), Bash(*), Edit(*), Write(*), TodoWrite(*) +--- + +# CCW Loop - Stateless Iterative Development Workflow + +无状态迭代开发循环工作流,支持开发 (develop)、调试 (debug)、验证 (validate) 三个阶段,每个阶段都有独立的文件记录进展。 + +## Arguments + +| Arg | Required | Description | +|-----|----------|-------------| +| task | No | Task description (for new loop, mutually exclusive with --loop-id) | +| --loop-id | No | Existing loop ID to continue (from API or previous session) | +| --auto | No | Auto-cycle mode (develop → debug → validate → complete) | + +## Unified Architecture (API + Skill Integration) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Dashboard (UI) │ +│ [Create] [Start] [Pause] [Resume] [Stop] [View Progress] │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ loop-v2-routes.ts (Control Plane) │ +│ │ +│ State: .loop/{loopId}.json (MASTER) │ +│ Tasks: .loop/{loopId}.tasks.jsonl │ +│ │ +│ /start → Trigger ccw-loop skill with --loop-id │ +│ /pause → Set status='paused' (skill checks before action) │ +│ /stop → Set status='failed' (skill terminates) │ +│ /resume → Set status='running' (skill continues) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ ccw-loop Skill (Execution Plane) │ +│ │ +│ Reads/Writes: .loop/{loopId}.json (unified state) │ +│ Writes: .loop/{loopId}.progress/* (progress files) │ +│ │ +│ BEFORE each action: │ +│ → Check status: paused/stopped → exit gracefully │ +│ → running → continue with action │ +│ │ +│ Actions: init → develop → debug → validate → complete │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Key Design Principles + +1. **统一状态**: API 和 Skill 共享 `.loop/{loopId}.json` 状态文件 +2. **控制信号**: Skill 每个 Action 前检查 status 字段 (paused/stopped) +3. **文件驱动**: 所有进度、理解、结果都记录在 `.loop/{loopId}.progress/` +4. **可恢复**: 任何时候可以继续之前的循环 (`--loop-id`) +5. **双触发**: 支持 API 触发 (`--loop-id`) 和直接调用 (task description) +6. **Gemini 辅助**: 使用 CLI 工具进行深度分析和假设验证 + +## Execution Modes + +### Mode 1: Interactive (交互式) + +用户手动选择每个动作,适合复杂任务。 + +``` +用户 → 选择动作 → 执行 → 查看结果 → 选择下一动作 +``` + +### Mode 2: Auto-Loop (自动循环) + +按预设顺序自动执行,适合标准开发流程。 + +``` +Develop → Debug → Validate → (如有问题) → Develop → ... +``` + +## Session Structure (Unified Location) + +``` +.loop/ +├── {loopId}.json # 主状态文件 (API + Skill 共享) +├── {loopId}.tasks.jsonl # 任务列表 (API 管理) +└── {loopId}.progress/ # Skill 进度文件 + ├── develop.md # 开发进度记录 + ├── debug.md # 理解演变文档 + ├── validate.md # 验证报告 + ├── changes.log # 代码变更日志 (NDJSON) + └── debug.log # 调试日志 (NDJSON) +``` + +## Directory Setup + +```javascript +// loopId 来源: +// 1. API 触发时: 从 --loop-id 参数获取 +// 2. 直接调用时: 生成新的 loop-v2-{timestamp}-{random} + +const loopId = args['--loop-id'] || generateLoopId() +const loopFile = `.loop/${loopId}.json` +const progressDir = `.loop/${loopId}.progress` + +// 创建进度目录 +Bash(`mkdir -p "${progressDir}"`) +``` + +## Action Catalog + +| Action | Purpose | Output Files | CLI Integration | +|--------|---------|--------------|-----------------| +| [action-init](phases/actions/action-init.md) | 初始化循环会话 | meta.json, state.json | - | +| [action-develop-with-file](phases/actions/action-develop-with-file.md) | 开发任务执行 | progress.md, tasks.json | gemini --mode write | +| [action-debug-with-file](phases/actions/action-debug-with-file.md) | 假设驱动调试 | understanding.md, hypotheses.json | gemini --mode analysis | +| [action-validate-with-file](phases/actions/action-validate-with-file.md) | 测试与验证 | validation.md, test-results.json | gemini --mode analysis | +| [action-complete](phases/actions/action-complete.md) | 完成循环 | summary.md | - | +| [action-menu](phases/actions/action-menu.md) | 显示操作菜单 | - | - | + +## Usage + +```bash +# 启动新循环 (直接调用) +/ccw-loop "实现用户认证功能" + +# 继续现有循环 (API 触发或手动恢复) +/ccw-loop --loop-id loop-v2-20260122-abc123 + +# 自动循环模式 +/ccw-loop --auto "修复登录bug并添加测试" + +# API 触发自动循环 +/ccw-loop --loop-id loop-v2-20260122-abc123 --auto +``` + +## Execution Flow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ /ccw-loop [ | --loop-id ] [--auto] │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 1. Parameter Detection: │ +│ ├─ IF --loop-id provided: │ +│ │ ├─ Read .loop/{loopId}.json │ +│ │ ├─ Validate status === 'running' │ +│ │ └─ Continue from skill_state.current_action │ +│ └─ ELSE (task description): │ +│ ├─ Generate new loopId │ +│ ├─ Create .loop/{loopId}.json │ +│ └─ Initialize with action-init │ +│ │ +│ 2. Orchestrator Loop: │ +│ ├─ Read state from .loop/{loopId}.json │ +│ ├─ Check control signals: │ +│ │ ├─ status === 'paused' → Exit (wait for resume) │ +│ │ ├─ status === 'failed' → Exit with error │ +│ │ └─ status === 'running' → Continue │ +│ ├─ Show menu / auto-select next action │ +│ ├─ Execute action │ +│ ├─ Update .loop/{loopId}.progress/{action}.md │ +│ ├─ Update .loop/{loopId}.json (skill_state) │ +│ └─ Loop or exit based on user choice / completion │ +│ │ +│ 3. Action Execution: │ +│ ├─ BEFORE: checkControlSignals() → exit if paused/stopped │ +│ ├─ Develop: Plan → Implement → Document progress │ +│ ├─ Debug: Hypothesize → Instrument → Analyze → Fix │ +│ ├─ Validate: Test → Check → Report │ +│ └─ AFTER: Update skill_state in .loop/{loopId}.json │ +│ │ +│ 4. Termination: │ +│ ├─ Control signal: paused (graceful exit, wait resume) │ +│ ├─ Control signal: stopped (failed state) │ +│ ├─ User exits (interactive mode) │ +│ ├─ All tasks completed (status → completed) │ +│ └─ Max iterations reached │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Reference Documents + +| Document | Purpose | +|----------|---------| +| [phases/orchestrator.md](phases/orchestrator.md) | 编排器:状态读取 + 动作选择 | +| [phases/state-schema.md](phases/state-schema.md) | 状态结构定义 | +| [specs/loop-requirements.md](specs/loop-requirements.md) | 循环需求规范 | +| [specs/action-catalog.md](specs/action-catalog.md) | 动作目录 | +| [templates/progress-template.md](templates/progress-template.md) | 进度文档模板 | +| [templates/understanding-template.md](templates/understanding-template.md) | 理解文档模板 | + +## Integration with Loop Monitor (Dashboard) + +此 Skill 与 CCW Dashboard 的 Loop Monitor 实现 **控制平面 + 执行平面** 分离架构: + +### Control Plane (Dashboard/API → loop-v2-routes.ts) + +1. **创建循环**: `POST /api/loops/v2` → 创建 `.loop/{loopId}.json` +2. **启动执行**: `POST /api/loops/v2/:loopId/start` → 触发 `/ccw-loop --loop-id {loopId} --auto` +3. **暂停执行**: `POST /api/loops/v2/:loopId/pause` → 设置 `status='paused'` (Skill 下次检查时退出) +4. **恢复执行**: `POST /api/loops/v2/:loopId/resume` → 设置 `status='running'` → 重新触发 Skill +5. **停止执行**: `POST /api/loops/v2/:loopId/stop` → 设置 `status='failed'` + +### Execution Plane (ccw-loop Skill) + +1. **读取状态**: 从 `.loop/{loopId}.json` 读取 API 设置的状态 +2. **检查控制**: 每个 Action 前检查 `status` 字段 +3. **执行动作**: develop → debug → validate → complete +4. **更新进度**: 写入 `.loop/{loopId}.progress/*.md` 和更新 `skill_state` +5. **状态同步**: Dashboard 通过读取 `.loop/{loopId}.json` 获取进度 + +## CLI Integration Points + +### Develop Phase +```bash +ccw cli -p "PURPOSE: Implement {task}... +TASK: • Analyze requirements • Write code • Update progress +MODE: write +CONTEXT: @progress.md @tasks.json +EXPECTED: Implementation + updated progress.md +" --tool gemini --mode write --rule development-implement-feature +``` + +### Debug Phase +```bash +ccw cli -p "PURPOSE: Generate debugging hypotheses... +TASK: • Analyze error • Generate hypotheses • Add instrumentation +MODE: analysis +CONTEXT: @understanding.md @debug.log +EXPECTED: Hypotheses + instrumentation plan +" --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause +``` + +### Validate Phase +```bash +ccw cli -p "PURPOSE: Validate implementation... +TASK: • Run tests • Check coverage • Verify requirements +MODE: analysis +CONTEXT: @validation.md @test-results.json +EXPECTED: Validation report +" --tool gemini --mode analysis --rule analysis-review-code-quality +``` + +## Error Handling + +| Situation | Action | +|-----------|--------| +| Session not found | Create new session | +| State file corrupted | Rebuild from file contents | +| CLI tool fails | Fallback to manual analysis | +| Tests fail | Loop back to develop/debug | +| >10 iterations | Warn user, suggest break | + +## Post-Completion Expansion + +完成后询问用户是否扩展为 issue (test/enhance/refactor/doc),选中项调用 `/issue:new "{summary} - {dimension}"` diff --git a/.claude/skills/ccw-loop/phases/actions/action-complete.md b/.claude/skills/ccw-loop/phases/actions/action-complete.md new file mode 100644 index 00000000..5fe12829 --- /dev/null +++ b/.claude/skills/ccw-loop/phases/actions/action-complete.md @@ -0,0 +1,320 @@ +# Action: Complete + +完成 CCW Loop 会话,生成总结报告。 + +## Purpose + +- 生成完成报告 +- 汇总所有阶段成果 +- 提供后续建议 +- 询问是否扩展为 Issue + +## Preconditions + +- [ ] state.initialized === true +- [ ] state.status === 'running' + +## Execution + +### Step 1: 汇总统计 + +```javascript +const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() + +const sessionFolder = `.workflow/.loop/${state.session_id}` + +const stats = { + // 时间统计 + duration: Date.now() - new Date(state.created_at).getTime(), + iterations: state.iteration_count, + + // 开发统计 + develop: { + total_tasks: state.develop.total_count, + completed_tasks: state.develop.completed_count, + completion_rate: state.develop.total_count > 0 + ? (state.develop.completed_count / state.develop.total_count * 100).toFixed(1) + : 0 + }, + + // 调试统计 + debug: { + iterations: state.debug.iteration, + hypotheses_tested: state.debug.hypotheses.length, + root_cause_found: state.debug.confirmed_hypothesis !== null + }, + + // 验证统计 + validate: { + runs: state.validate.test_results.length, + passed: state.validate.passed, + coverage: state.validate.coverage, + failed_tests: state.validate.failed_tests.length + } +} + +console.log('\n生成完成报告...') +``` + +### Step 2: 生成总结报告 + +```javascript +const summaryReport = `# CCW Loop Session Summary + +**Session ID**: ${state.session_id} +**Task**: ${state.task_description} +**Started**: ${state.created_at} +**Completed**: ${getUtc8ISOString()} +**Duration**: ${formatDuration(stats.duration)} + +--- + +## Executive Summary + +${state.validate.passed + ? '✅ **任务成功完成** - 所有测试通过,验证成功' + : state.develop.completed_count === state.develop.total_count + ? '⚠️ **开发完成,验证未通过** - 需要进一步调试' + : '⏸️ **任务部分完成** - 仍有待处理项'} + +--- + +## Development Phase + +| Metric | Value | +|--------|-------| +| Total Tasks | ${stats.develop.total_tasks} | +| Completed | ${stats.develop.completed_tasks} | +| Completion Rate | ${stats.develop.completion_rate}% | + +### Completed Tasks + +${state.develop.tasks.filter(t => t.status === 'completed').map(t => ` +- ✅ ${t.description} + - Files: ${t.files_changed?.join(', ') || 'N/A'} + - Completed: ${t.completed_at} +`).join('\n')} + +### Pending Tasks + +${state.develop.tasks.filter(t => t.status !== 'completed').map(t => ` +- ⏳ ${t.description} +`).join('\n') || '_None_'} + +--- + +## Debug Phase + +| Metric | Value | +|--------|-------| +| Iterations | ${stats.debug.iterations} | +| Hypotheses Tested | ${stats.debug.hypotheses_tested} | +| Root Cause Found | ${stats.debug.root_cause_found ? 'Yes' : 'No'} | + +${stats.debug.root_cause_found ? ` +### Confirmed Root Cause + +**${state.debug.confirmed_hypothesis}**: ${state.debug.hypotheses.find(h => h.id === state.debug.confirmed_hypothesis)?.description || 'N/A'} +` : ''} + +### Hypothesis Summary + +${state.debug.hypotheses.map(h => ` +- **${h.id}**: ${h.status.toUpperCase()} + - ${h.description} +`).join('\n') || '_No hypotheses tested_'} + +--- + +## Validation Phase + +| Metric | Value | +|--------|-------| +| Test Runs | ${stats.validate.runs} | +| Status | ${stats.validate.passed ? 'PASSED' : 'FAILED'} | +| Coverage | ${stats.validate.coverage || 'N/A'}% | +| Failed Tests | ${stats.validate.failed_tests} | + +${stats.validate.failed_tests > 0 ? ` +### Failed Tests + +${state.validate.failed_tests.map(t => `- ❌ ${t}`).join('\n')} +` : ''} + +--- + +## Files Modified + +${listModifiedFiles(sessionFolder)} + +--- + +## Key Learnings + +${state.debug.iteration > 0 ? ` +### From Debugging + +${extractLearnings(state.debug.hypotheses)} +` : ''} + +--- + +## Recommendations + +${generateRecommendations(stats, state)} + +--- + +## Session Artifacts + +| File | Description | +|------|-------------| +| \`develop/progress.md\` | Development progress timeline | +| \`develop/tasks.json\` | Task list with status | +| \`debug/understanding.md\` | Debug exploration and learnings | +| \`debug/hypotheses.json\` | Hypothesis history | +| \`validate/validation.md\` | Validation report | +| \`validate/test-results.json\` | Test execution results | + +--- + +*Generated by CCW Loop at ${getUtc8ISOString()}* +` + +Write(`${sessionFolder}/summary.md`, summaryReport) +console.log(`\n报告已保存: ${sessionFolder}/summary.md`) +``` + +### Step 3: 询问后续扩展 + +```javascript +console.log('\n' + '═'.repeat(60)) +console.log(' 任务已完成') +console.log('═'.repeat(60)) + +const expansionResponse = await AskUserQuestion({ + questions: [{ + question: "是否将发现扩展为 Issue?", + header: "扩展选项", + multiSelect: true, + options: [ + { label: "测试 (Test)", description: "添加更多测试用例" }, + { label: "增强 (Enhance)", description: "功能增强建议" }, + { label: "重构 (Refactor)", description: "代码重构建议" }, + { label: "文档 (Doc)", description: "文档更新需求" }, + { label: "否,直接完成", description: "不创建 Issue" } + ] + }] +}) + +const selectedExpansions = expansionResponse["扩展选项"] + +if (selectedExpansions && !selectedExpansions.includes("否,直接完成")) { + for (const expansion of selectedExpansions) { + const dimension = expansion.split(' ')[0].toLowerCase() + const issueSummary = `${state.task_description} - ${dimension}` + + console.log(`\n创建 Issue: ${issueSummary}`) + + // 调用 /issue:new 创建 issue + await Bash({ + command: `/issue:new "${issueSummary}"`, + run_in_background: false + }) + } +} +``` + +### Step 4: 最终输出 + +```javascript +console.log(` +═══════════════════════════════════════════════════════════ + ✅ CCW Loop 会话完成 +═══════════════════════════════════════════════════════════ + + 会话 ID: ${state.session_id} + 用时: ${formatDuration(stats.duration)} + 迭代: ${stats.iterations} + + 开发: ${stats.develop.completed_tasks}/${stats.develop.total_tasks} 任务完成 + 调试: ${stats.debug.iterations} 次迭代 + 验证: ${stats.validate.passed ? '通过 ✅' : '未通过 ❌'} + + 报告: ${sessionFolder}/summary.md + +═══════════════════════════════════════════════════════════ +`) +``` + +## State Updates + +```javascript +return { + stateUpdates: { + status: 'completed', + completed_at: getUtc8ISOString(), + summary: stats + }, + continue: false, + message: `会话 ${state.session_id} 已完成` +} +``` + +## Helper Functions + +```javascript +function formatDuration(ms) { + const seconds = Math.floor(ms / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + + if (hours > 0) { + return `${hours}h ${minutes % 60}m` + } else if (minutes > 0) { + return `${minutes}m ${seconds % 60}s` + } else { + return `${seconds}s` + } +} + +function generateRecommendations(stats, state) { + const recommendations = [] + + if (stats.develop.completion_rate < 100) { + recommendations.push('- 完成剩余开发任务') + } + + if (!stats.validate.passed) { + recommendations.push('- 修复失败的测试') + } + + if (stats.validate.coverage && stats.validate.coverage < 80) { + recommendations.push(`- 提高测试覆盖率 (当前: ${stats.validate.coverage}%)`) + } + + if (stats.debug.iterations > 3 && !stats.debug.root_cause_found) { + recommendations.push('- 考虑代码重构以简化调试') + } + + if (recommendations.length === 0) { + recommendations.push('- 考虑代码审查') + recommendations.push('- 更新相关文档') + recommendations.push('- 准备部署') + } + + return recommendations.join('\n') +} +``` + +## Error Handling + +| Error Type | Recovery | +|------------|----------| +| 报告生成失败 | 显示基本统计,跳过文件写入 | +| Issue 创建失败 | 记录错误,继续完成 | + +## Next Actions + +- 无 (终止状态) +- 如需继续: 使用 `ccw-loop --resume {session-id}` 重新打开会话 diff --git a/.claude/skills/ccw-loop/phases/actions/action-debug-with-file.md b/.claude/skills/ccw-loop/phases/actions/action-debug-with-file.md new file mode 100644 index 00000000..0e7ae626 --- /dev/null +++ b/.claude/skills/ccw-loop/phases/actions/action-debug-with-file.md @@ -0,0 +1,485 @@ +# Action: Debug With File + +假设驱动调试,记录理解演变到 understanding.md,支持 Gemini 辅助分析和假设生成。 + +## Purpose + +执行假设驱动的调试流程,包括: +- 定位错误源 +- 生成可测试假设 +- 添加 NDJSON 日志 +- 分析日志证据 +- 纠正错误理解 +- 应用修复 + +## Preconditions + +- [ ] state.initialized === true +- [ ] state.status === 'running' + +## Session Setup + +```javascript +const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() + +const sessionFolder = `.workflow/.loop/${state.session_id}` +const debugFolder = `${sessionFolder}/debug` +const understandingPath = `${debugFolder}/understanding.md` +const hypothesesPath = `${debugFolder}/hypotheses.json` +const debugLogPath = `${debugFolder}/debug.log` +``` + +--- + +## Mode Detection + +```javascript +// 自动检测模式 +const understandingExists = fs.existsSync(understandingPath) +const logHasContent = fs.existsSync(debugLogPath) && fs.statSync(debugLogPath).size > 0 + +const debugMode = logHasContent ? 'analyze' : (understandingExists ? 'continue' : 'explore') + +console.log(`Debug mode: ${debugMode}`) +``` + +--- + +## Explore Mode (首次调试) + +### Step 1.1: 定位错误源 + +```javascript +if (debugMode === 'explore') { + // 询问用户 bug 描述 + const bugInput = await AskUserQuestion({ + questions: [{ + question: "请描述遇到的 bug 或错误信息:", + header: "Bug 描述", + multiSelect: false, + options: [ + { label: "手动输入", description: "输入错误描述或堆栈" }, + { label: "从测试失败", description: "从验证阶段的失败测试中获取" } + ] + }] + }) + + const bugDescription = bugInput["Bug 描述"] + + // 提取关键词并搜索 + const searchResults = await Task({ + subagent_type: 'Explore', + run_in_background: false, + prompt: `Search codebase for error patterns related to: ${bugDescription}` + }) + + // 分析搜索结果,识别受影响的位置 + const affectedLocations = analyzeSearchResults(searchResults) +} +``` + +### Step 1.2: 记录初始理解 + +```javascript +// 创建 understanding.md +const initialUnderstanding = `# Understanding Document + +**Session ID**: ${state.session_id} +**Bug Description**: ${bugDescription} +**Started**: ${getUtc8ISOString()} + +--- + +## Exploration Timeline + +### Iteration 1 - Initial Exploration (${getUtc8ISOString()}) + +#### Current Understanding + +Based on bug description and initial code search: + +- Error pattern: ${errorPattern} +- Affected areas: ${affectedLocations.map(l => l.file).join(', ')} +- Initial hypothesis: ${initialThoughts} + +#### Evidence from Code Search + +${searchResults.map(r => ` +**Keyword: "${r.keyword}"** +- Found in: ${r.files.join(', ')} +- Key findings: ${r.insights} +`).join('\n')} + +#### Next Steps + +- Generate testable hypotheses +- Add instrumentation +- Await reproduction + +--- + +## Current Consolidated Understanding + +${initialConsolidatedUnderstanding} +` + +Write(understandingPath, initialUnderstanding) +``` + +### Step 1.3: Gemini 辅助假设生成 + +```bash +ccw cli -p " +PURPOSE: Generate debugging hypotheses for: ${bugDescription} +Success criteria: Testable hypotheses with clear evidence criteria + +TASK: +• Analyze error pattern and code search results +• Identify 3-5 most likely root causes +• For each hypothesis, specify: + - What might be wrong + - What evidence would confirm/reject it + - Where to add instrumentation +• Rank by likelihood + +MODE: analysis + +CONTEXT: @${understandingPath} | Search results in understanding.md + +EXPECTED: +- Structured hypothesis list (JSON format) +- Each hypothesis with: id, description, testable_condition, logging_point, evidence_criteria +- Likelihood ranking (1=most likely) + +CONSTRAINTS: Focus on testable conditions +" --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause +``` + +### Step 1.4: 保存假设 + +```javascript +const hypotheses = { + iteration: 1, + timestamp: getUtc8ISOString(), + bug_description: bugDescription, + hypotheses: [ + { + id: "H1", + description: "...", + testable_condition: "...", + logging_point: "file.ts:func:42", + evidence_criteria: { + confirm: "...", + reject: "..." + }, + likelihood: 1, + status: "pending" + } + // ... + ], + gemini_insights: "...", + corrected_assumptions: [] +} + +Write(hypothesesPath, JSON.stringify(hypotheses, null, 2)) +``` + +### Step 1.5: 添加 NDJSON 日志 + +```javascript +// 为每个假设添加日志点 +for (const hypothesis of hypotheses.hypotheses) { + const [file, func, line] = hypothesis.logging_point.split(':') + + const logStatement = `console.log(JSON.stringify({ + hid: "${hypothesis.id}", + ts: Date.now(), + func: "${func}", + data: { /* 相关数据 */ } + }))` + + // 使用 Edit 工具添加日志 + // ... +} +``` + +--- + +## Analyze Mode (有日志后) + +### Step 2.1: 解析调试日志 + +```javascript +if (debugMode === 'analyze') { + // 读取 NDJSON 日志 + const logContent = Read(debugLogPath) + const entries = logContent.split('\n') + .filter(l => l.trim()) + .map(l => JSON.parse(l)) + + // 按假设分组 + const byHypothesis = groupBy(entries, 'hid') +} +``` + +### Step 2.2: Gemini 辅助证据分析 + +```bash +ccw cli -p " +PURPOSE: Analyze debug log evidence to validate/correct hypotheses for: ${bugDescription} +Success criteria: Clear verdict per hypothesis + corrected understanding + +TASK: +• Parse log entries by hypothesis +• Evaluate evidence against expected criteria +• Determine verdict: confirmed | rejected | inconclusive +• Identify incorrect assumptions from previous understanding +• Suggest corrections to understanding + +MODE: analysis + +CONTEXT: +@${debugLogPath} +@${understandingPath} +@${hypothesesPath} + +EXPECTED: +- Per-hypothesis verdict with reasoning +- Evidence summary +- List of incorrect assumptions with corrections +- Updated consolidated understanding +- Root cause if confirmed, or next investigation steps + +CONSTRAINTS: Evidence-based reasoning only, no speculation +" --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause +``` + +### Step 2.3: 更新理解文档 + +```javascript +// 追加新迭代到 understanding.md +const iteration = state.debug.iteration + 1 + +const analysisEntry = ` +### Iteration ${iteration} - Evidence Analysis (${getUtc8ISOString()}) + +#### Log Analysis Results + +${results.map(r => ` +**${r.id}**: ${r.verdict.toUpperCase()} +- Evidence: ${JSON.stringify(r.evidence)} +- Reasoning: ${r.reason} +`).join('\n')} + +#### Corrected Understanding + +Previous misunderstandings identified and corrected: + +${corrections.map(c => ` +- ~~${c.wrong}~~ → ${c.corrected} + - Why wrong: ${c.reason} + - Evidence: ${c.evidence} +`).join('\n')} + +#### New Insights + +${newInsights.join('\n- ')} + +#### Gemini Analysis + +${geminiAnalysis} + +${confirmedHypothesis ? ` +#### Root Cause Identified + +**${confirmedHypothesis.id}**: ${confirmedHypothesis.description} + +Evidence supporting this conclusion: +${confirmedHypothesis.supportingEvidence} +` : ` +#### Next Steps + +${nextSteps} +`} + +--- + +## Current Consolidated Understanding (Updated) + +### What We Know + +- ${validUnderstanding1} +- ${validUnderstanding2} + +### What Was Disproven + +- ~~${wrongAssumption}~~ (Evidence: ${disproofEvidence}) + +### Current Investigation Focus + +${currentFocus} + +### Remaining Questions + +- ${openQuestion1} +- ${openQuestion2} +` + +const existingContent = Read(understandingPath) +Write(understandingPath, existingContent + analysisEntry) +``` + +### Step 2.4: 更新假设状态 + +```javascript +const hypothesesData = JSON.parse(Read(hypothesesPath)) + +// 更新假设状态 +hypothesesData.hypotheses = hypothesesData.hypotheses.map(h => ({ + ...h, + status: results.find(r => r.id === h.id)?.verdict || h.status, + evidence: results.find(r => r.id === h.id)?.evidence || h.evidence, + verdict_reason: results.find(r => r.id === h.id)?.reason || h.verdict_reason +})) + +hypothesesData.iteration++ +hypothesesData.timestamp = getUtc8ISOString() + +Write(hypothesesPath, JSON.stringify(hypothesesData, null, 2)) +``` + +--- + +## Fix & Verification + +### Step 3.1: 应用修复 + +```javascript +if (confirmedHypothesis) { + console.log(`\n根因确认: ${confirmedHypothesis.description}`) + console.log('准备应用修复...') + + // 使用 Gemini 生成修复代码 + const fixPrompt = ` +PURPOSE: Fix the identified root cause +Root Cause: ${confirmedHypothesis.description} +Evidence: ${confirmedHypothesis.supportingEvidence} + +TASK: +• Generate fix code +• Ensure backward compatibility +• Add tests if needed + +MODE: write + +CONTEXT: @${confirmedHypothesis.logging_point.split(':')[0]} + +EXPECTED: Fixed code + verification steps +` + + await Bash({ + command: `ccw cli -p "${fixPrompt}" --tool gemini --mode write --rule development-debug-runtime-issues`, + run_in_background: false + }) +} +``` + +### Step 3.2: 记录解决方案 + +```javascript +const resolutionEntry = ` +### Resolution (${getUtc8ISOString()}) + +#### Fix Applied + +- Modified files: ${modifiedFiles.join(', ')} +- Fix description: ${fixDescription} +- Root cause addressed: ${rootCause} + +#### Verification Results + +${verificationResults} + +#### Lessons Learned + +1. ${lesson1} +2. ${lesson2} + +#### Key Insights for Future + +- ${insight1} +- ${insight2} +` + +const existingContent = Read(understandingPath) +Write(understandingPath, existingContent + resolutionEntry) +``` + +### Step 3.3: 清理日志 + +```javascript +// 移除调试日志 +// (可选,根据用户选择) +``` + +--- + +## State Updates + +```javascript +return { + stateUpdates: { + debug: { + current_bug: bugDescription, + hypotheses: hypothesesData.hypotheses, + confirmed_hypothesis: confirmedHypothesis?.id || null, + iteration: hypothesesData.iteration, + last_analysis_at: getUtc8ISOString(), + understanding_updated: true + }, + last_action: 'action-debug-with-file' + }, + continue: true, + message: confirmedHypothesis + ? `根因确认: ${confirmedHypothesis.description}\n修复已应用,请验证` + : `分析完成,需要更多证据\n请复现 bug 后再次执行` +} +``` + +## Error Handling + +| Error Type | Recovery | +|------------|----------| +| 空 debug.log | 提示用户复现 bug | +| 所有假设被否定 | 使用 Gemini 生成新假设 | +| 修复无效 | 记录失败尝试,迭代 | +| >5 迭代 | 建议升级到 /workflow:lite-fix | +| Gemini 不可用 | 回退到手动分析 | + +## Understanding Document Template + +参考 [templates/understanding-template.md](../../templates/understanding-template.md) + +## CLI Integration + +### 假设生成 +```bash +ccw cli -p "PURPOSE: Generate debugging hypotheses..." --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause +``` + +### 证据分析 +```bash +ccw cli -p "PURPOSE: Analyze debug log evidence..." --tool gemini --mode analysis --rule analysis-diagnose-bug-root-cause +``` + +### 生成修复 +```bash +ccw cli -p "PURPOSE: Fix the identified root cause..." --tool gemini --mode write --rule development-debug-runtime-issues +``` + +## Next Actions (Hints) + +- 根因确认: `action-validate-with-file` (验证修复) +- 需要更多证据: 等待用户复现,再次执行此动作 +- 所有假设否定: 重新执行此动作生成新假设 +- 用户选择: `action-menu` (返回菜单) diff --git a/.claude/skills/ccw-loop/phases/actions/action-develop-with-file.md b/.claude/skills/ccw-loop/phases/actions/action-develop-with-file.md new file mode 100644 index 00000000..0846a0fb --- /dev/null +++ b/.claude/skills/ccw-loop/phases/actions/action-develop-with-file.md @@ -0,0 +1,365 @@ +# Action: Develop With File + +增量开发任务执行,记录进度到 progress.md,支持 Gemini 辅助实现。 + +## Purpose + +执行开发任务并记录进度,包括: +- 分析任务需求 +- 使用 Gemini/CLI 实现代码 +- 记录代码变更 +- 更新进度文档 + +## Preconditions + +- [ ] state.status === 'running' +- [ ] state.skill_state !== null +- [ ] state.skill_state.develop.tasks.some(t => t.status === 'pending') + +## Session Setup (Unified Location) + +```javascript +const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() + +// 统一位置: .loop/{loopId} +const loopId = state.loop_id +const loopFile = `.loop/${loopId}.json` +const progressDir = `.loop/${loopId}.progress` +const progressPath = `${progressDir}/develop.md` +const changesLogPath = `${progressDir}/changes.log` +``` + +--- + +## Execution + +### Step 0: Check Control Signals (CRITICAL) + +```javascript +/** + * CRITICAL: 每个 Action 必须在开始时检查控制信号 + * 如果 API 设置了 paused/stopped,Skill 应立即退出 + */ +function checkControlSignals(loopId) { + const state = JSON.parse(Read(`.loop/${loopId}.json`)) + + switch (state.status) { + case 'paused': + console.log('⏸️ Loop paused by API. Exiting action.') + return { continue: false, reason: 'paused' } + + case 'failed': + console.log('⏹️ Loop stopped by API. Exiting action.') + return { continue: false, reason: 'stopped' } + + case 'running': + return { continue: true, reason: 'running' } + + default: + return { continue: false, reason: 'unknown_status' } + } +} + +// Execute check +const control = checkControlSignals(loopId) +if (!control.continue) { + return { + skillStateUpdates: { current_action: null }, + continue: false, + message: `Action terminated: ${control.reason}` + } +} +``` + +### Step 1: 加载任务列表 + +```javascript +// 读取任务列表 (从 skill_state) +let tasks = state.skill_state?.develop?.tasks || [] + +// 如果任务列表为空,询问用户创建 +if (tasks.length === 0) { + // 使用 Gemini 分析任务描述,生成任务列表 + const analysisPrompt = ` +PURPOSE: 分析开发任务并分解为可执行步骤 +Success: 生成 3-7 个具体、可验证的子任务 + +TASK: +• 分析任务描述: ${state.task_description} +• 识别关键功能点 +• 分解为独立子任务 +• 为每个子任务指定工具和模式 + +MODE: analysis + +CONTEXT: @package.json @src/**/*.ts | Memory: 项目结构 + +EXPECTED: +JSON 格式: +{ + "tasks": [ + { + "id": "task-001", + "description": "任务描述", + "tool": "gemini", + "mode": "write", + "files": ["src/xxx.ts"] + } + ] +} +` + + const result = await Task({ + subagent_type: 'cli-execution-agent', + run_in_background: false, + prompt: `Execute Gemini CLI with prompt: ${analysisPrompt}` + }) + + tasks = JSON.parse(result).tasks +} + +// 找到第一个待处理任务 +const currentTask = tasks.find(t => t.status === 'pending') + +if (!currentTask) { + return { + skillStateUpdates: { + develop: { ...state.skill_state.develop, current_task: null } + }, + continue: true, + message: '所有开发任务已完成' + } +} +``` + +### Step 2: 执行开发任务 + +```javascript +console.log(`\n执行任务: ${currentTask.description}`) + +// 更新任务状态 +currentTask.status = 'in_progress' + +// 使用 Gemini 实现 +const implementPrompt = ` +PURPOSE: 实现开发任务 +Task: ${currentTask.description} +Success criteria: 代码实现完成,测试通过 + +TASK: +• 分析现有代码结构 +• 实现功能代码 +• 添加必要的类型定义 +• 确保代码风格一致 + +MODE: write + +CONTEXT: @${currentTask.files?.join(' @') || 'src/**/*.ts'} + +EXPECTED: +- 完整的代码实现 +- 代码变更列表 +- 简要实现说明 + +CONSTRAINTS: 遵循现有代码风格 | 不破坏现有功能 +` + +const implementResult = await Bash({ + command: `ccw cli -p "${implementPrompt}" --tool gemini --mode write --rule development-implement-feature`, + run_in_background: false +}) + +// 记录代码变更 +const timestamp = getUtc8ISOString() +const changeEntry = { + timestamp, + task_id: currentTask.id, + description: currentTask.description, + files_changed: currentTask.files || [], + result: 'success' +} + +// 追加到 changes.log (NDJSON 格式) +const changesContent = Read(changesLogPath) || '' +Write(changesLogPath, changesContent + JSON.stringify(changeEntry) + '\n') +``` + +### Step 3: 更新进度文档 + +```javascript +const timestamp = getUtc8ISOString() +const iteration = state.develop.completed_count + 1 + +// 读取现有进度文档 +let progressContent = Read(progressPath) || '' + +// 如果是新文档,添加头部 +if (!progressContent) { + progressContent = `# Development Progress + +**Session ID**: ${state.session_id} +**Task**: ${state.task_description} +**Started**: ${timestamp} + +--- + +## Progress Timeline + +` +} + +// 追加本次进度 +const progressEntry = ` +### Iteration ${iteration} - ${currentTask.description} (${timestamp}) + +#### Task Details + +- **ID**: ${currentTask.id} +- **Tool**: ${currentTask.tool} +- **Mode**: ${currentTask.mode} + +#### Implementation Summary + +${implementResult.summary || '实现完成'} + +#### Files Changed + +${currentTask.files?.map(f => `- \`${f}\``).join('\n') || '- No files specified'} + +#### Status: COMPLETED + +--- + +` + +Write(progressPath, progressContent + progressEntry) + +// 更新任务状态 +currentTask.status = 'completed' +currentTask.completed_at = timestamp +``` + +### Step 4: 更新任务列表文件 + +```javascript +// 更新 tasks.json +const updatedTasks = tasks.map(t => + t.id === currentTask.id ? currentTask : t +) + +Write(tasksPath, JSON.stringify(updatedTasks, null, 2)) +``` + +## State Updates + +```javascript +return { + stateUpdates: { + develop: { + tasks: updatedTasks, + current_task_id: null, + completed_count: state.develop.completed_count + 1, + total_count: updatedTasks.length, + last_progress_at: getUtc8ISOString() + }, + last_action: 'action-develop-with-file' + }, + continue: true, + message: `任务完成: ${currentTask.description}\n进度: ${state.develop.completed_count + 1}/${updatedTasks.length}` +} +``` + +## Error Handling + +| Error Type | Recovery | +|------------|----------| +| Gemini CLI 失败 | 提示用户手动实现,记录到 progress.md | +| 文件写入失败 | 重试一次,失败则记录错误 | +| 任务解析失败 | 询问用户手动输入任务 | + +## Progress Document Template + +```markdown +# Development Progress + +**Session ID**: LOOP-xxx-2026-01-22 +**Task**: 实现用户认证功能 +**Started**: 2026-01-22T10:00:00+08:00 + +--- + +## Progress Timeline + +### Iteration 1 - 分析登录组件 (2026-01-22T10:05:00+08:00) + +#### Task Details + +- **ID**: task-001 +- **Tool**: gemini +- **Mode**: analysis + +#### Implementation Summary + +分析了现有登录组件结构,识别了需要修改的文件和依赖关系。 + +#### Files Changed + +- `src/components/Login.tsx` +- `src/hooks/useAuth.ts` + +#### Status: COMPLETED + +--- + +### Iteration 2 - 实现登录 API (2026-01-22T10:15:00+08:00) + +... + +--- + +## Current Statistics + +| Metric | Value | +|--------|-------| +| Total Tasks | 5 | +| Completed | 2 | +| In Progress | 1 | +| Pending | 2 | +| Progress | 40% | + +--- + +## Next Steps + +- [ ] 完成剩余任务 +- [ ] 运行测试 +- [ ] 代码审查 +``` + +## CLI Integration + +### 任务分析 +```bash +ccw cli -p "PURPOSE: 分解开发任务为子任务 +TASK: • 分析任务描述 • 识别功能点 • 生成任务列表 +MODE: analysis +CONTEXT: @package.json @src/**/* +EXPECTED: JSON 任务列表 +" --tool gemini --mode analysis --rule planning-breakdown-task-steps +``` + +### 代码实现 +```bash +ccw cli -p "PURPOSE: 实现功能代码 +TASK: • 分析需求 • 编写代码 • 添加类型 +MODE: write +CONTEXT: @src/xxx.ts +EXPECTED: 完整实现 +" --tool gemini --mode write --rule development-implement-feature +``` + +## Next Actions (Hints) + +- 所有任务完成: `action-debug-with-file` (开始调试) +- 任务失败: `action-develop-with-file` (重试或下一个任务) +- 用户选择: `action-menu` (返回菜单) diff --git a/.claude/skills/ccw-loop/phases/actions/action-init.md b/.claude/skills/ccw-loop/phases/actions/action-init.md new file mode 100644 index 00000000..ac847942 --- /dev/null +++ b/.claude/skills/ccw-loop/phases/actions/action-init.md @@ -0,0 +1,200 @@ +# Action: Initialize + +初始化 CCW Loop 会话,创建目录结构和初始状态。 + +## Purpose + +- 创建会话目录结构 +- 初始化状态文件 +- 分析任务描述生成初始任务列表 +- 准备执行环境 + +## Preconditions + +- [ ] state.status === 'pending' +- [ ] state.initialized === false + +## Execution + +### Step 1: 创建目录结构 + +```javascript +const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() + +const taskSlug = state.task_description.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 30) +const dateStr = getUtc8ISOString().substring(0, 10) +const sessionId = `LOOP-${taskSlug}-${dateStr}` +const sessionFolder = `.workflow/.loop/${sessionId}` + +Bash(`mkdir -p "${sessionFolder}/develop"`) +Bash(`mkdir -p "${sessionFolder}/debug"`) +Bash(`mkdir -p "${sessionFolder}/validate"`) + +console.log(`Session created: ${sessionId}`) +console.log(`Location: ${sessionFolder}`) +``` + +### Step 2: 创建元数据文件 + +```javascript +const meta = { + session_id: sessionId, + task_description: state.task_description, + created_at: getUtc8ISOString(), + mode: state.mode || 'interactive' +} + +Write(`${sessionFolder}/meta.json`, JSON.stringify(meta, null, 2)) +``` + +### Step 3: 分析任务生成开发任务列表 + +```javascript +// 使用 Gemini 分析任务描述 +console.log('\n分析任务描述...') + +const analysisPrompt = ` +PURPOSE: 分析开发任务并分解为可执行步骤 +Success: 生成 3-7 个具体、可验证的子任务 + +TASK: +• 分析任务描述: ${state.task_description} +• 识别关键功能点 +• 分解为独立子任务 +• 为每个子任务指定工具和模式 + +MODE: analysis + +CONTEXT: @package.json @src/**/*.ts (如存在) + +EXPECTED: +JSON 格式: +{ + "tasks": [ + { + "id": "task-001", + "description": "任务描述", + "tool": "gemini", + "mode": "write", + "priority": 1 + } + ], + "estimated_complexity": "low|medium|high", + "key_files": ["file1.ts", "file2.ts"] +} + +CONSTRAINTS: 生成实际可执行的任务 +` + +const result = await Bash({ + command: `ccw cli -p "${analysisPrompt}" --tool gemini --mode analysis --rule planning-breakdown-task-steps`, + run_in_background: false +}) + +const analysis = JSON.parse(result.stdout) +const tasks = analysis.tasks.map((t, i) => ({ + ...t, + id: t.id || `task-${String(i + 1).padStart(3, '0')}`, + status: 'pending', + created_at: getUtc8ISOString(), + completed_at: null, + files_changed: [] +})) + +// 保存任务列表 +Write(`${sessionFolder}/develop/tasks.json`, JSON.stringify(tasks, null, 2)) +``` + +### Step 4: 初始化进度文档 + +```javascript +const progressInitial = `# Development Progress + +**Session ID**: ${sessionId} +**Task**: ${state.task_description} +**Started**: ${getUtc8ISOString()} +**Estimated Complexity**: ${analysis.estimated_complexity} + +--- + +## Task List + +${tasks.map((t, i) => `${i + 1}. [ ] ${t.description}`).join('\n')} + +## Key Files + +${analysis.key_files?.map(f => `- \`${f}\``).join('\n') || '- To be determined'} + +--- + +## Progress Timeline + +` + +Write(`${sessionFolder}/develop/progress.md`, progressInitial) +``` + +### Step 5: 显示初始化结果 + +```javascript +console.log(`\n✅ 会话初始化完成`) +console.log(`\n任务列表 (${tasks.length} 项):`) +tasks.forEach((t, i) => { + console.log(` ${i + 1}. ${t.description} [${t.tool}/${t.mode}]`) +}) +console.log(`\n预估复杂度: ${analysis.estimated_complexity}`) +console.log(`\n执行 'develop' 开始开发,或 'menu' 查看更多选项`) +``` + +## State Updates + +```javascript +return { + stateUpdates: { + session_id: sessionId, + status: 'running', + initialized: true, + develop: { + tasks: tasks, + current_task_id: null, + completed_count: 0, + total_count: tasks.length, + last_progress_at: null + }, + debug: { + current_bug: null, + hypotheses: [], + confirmed_hypothesis: null, + iteration: 0, + last_analysis_at: null, + understanding_updated: false + }, + validate: { + test_results: [], + coverage: null, + passed: false, + failed_tests: [], + last_run_at: null + }, + context: { + estimated_complexity: analysis.estimated_complexity, + key_files: analysis.key_files + } + }, + continue: true, + message: `会话 ${sessionId} 已初始化\n${tasks.length} 个开发任务待执行` +} +``` + +## Error Handling + +| Error Type | Recovery | +|------------|----------| +| 目录创建失败 | 检查权限,重试 | +| Gemini 分析失败 | 提示用户手动输入任务 | +| 任务解析失败 | 使用默认任务列表 | + +## Next Actions + +- 成功: `action-menu` (显示操作菜单) 或 `action-develop-with-file` (直接开始开发) +- 失败: 报错退出 diff --git a/.claude/skills/ccw-loop/phases/actions/action-menu.md b/.claude/skills/ccw-loop/phases/actions/action-menu.md new file mode 100644 index 00000000..40e01dad --- /dev/null +++ b/.claude/skills/ccw-loop/phases/actions/action-menu.md @@ -0,0 +1,192 @@ +# Action: Menu + +显示交互式操作菜单,让用户选择下一步操作。 + +## Purpose + +- 显示当前状态摘要 +- 提供操作选项 +- 接收用户选择 +- 返回下一个动作 + +## Preconditions + +- [ ] state.initialized === true +- [ ] state.status === 'running' + +## Execution + +### Step 1: 生成状态摘要 + +```javascript +const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() + +// 开发进度 +const developProgress = state.develop.total_count > 0 + ? `${state.develop.completed_count}/${state.develop.total_count} (${(state.develop.completed_count / state.develop.total_count * 100).toFixed(0)}%)` + : '未开始' + +// 调试状态 +const debugStatus = state.debug.confirmed_hypothesis + ? `✅ 已确认根因` + : state.debug.iteration > 0 + ? `🔍 迭代 ${state.debug.iteration}` + : '未开始' + +// 验证状态 +const validateStatus = state.validate.passed + ? `✅ 通过` + : state.validate.test_results.length > 0 + ? `❌ ${state.validate.failed_tests.length} 个失败` + : '未运行' + +const statusSummary = ` +═══════════════════════════════════════════════════════════ + CCW Loop - ${state.session_id} +═══════════════════════════════════════════════════════════ + + 任务: ${state.task_description} + 迭代: ${state.iteration_count} + + ┌─────────────────────────────────────────────────────┐ + │ 开发 (Develop) │ ${developProgress.padEnd(20)} │ + │ 调试 (Debug) │ ${debugStatus.padEnd(20)} │ + │ 验证 (Validate) │ ${validateStatus.padEnd(20)} │ + └─────────────────────────────────────────────────────┘ + +═══════════════════════════════════════════════════════════ +` + +console.log(statusSummary) +``` + +### Step 2: 显示操作选项 + +```javascript +const options = [ + { + label: "📝 继续开发 (Develop)", + description: state.develop.completed_count < state.develop.total_count + ? `执行下一个开发任务` + : "所有任务已完成,可添加新任务", + action: "action-develop-with-file" + }, + { + label: "🔍 开始调试 (Debug)", + description: state.debug.iteration > 0 + ? "继续假设驱动调试" + : "开始新的调试会话", + action: "action-debug-with-file" + }, + { + label: "✅ 运行验证 (Validate)", + description: "运行测试并检查覆盖率", + action: "action-validate-with-file" + }, + { + label: "📊 查看详情 (Status)", + description: "查看详细进度和文件", + action: "action-status" + }, + { + label: "🏁 完成循环 (Complete)", + description: "结束当前循环", + action: "action-complete" + }, + { + label: "🚪 退出 (Exit)", + description: "保存状态并退出", + action: "exit" + } +] + +const response = await AskUserQuestion({ + questions: [{ + question: "选择下一步操作:", + header: "操作", + multiSelect: false, + options: options.map(o => ({ + label: o.label, + description: o.description + })) + }] +}) + +const selectedLabel = response["操作"] +const selectedOption = options.find(o => o.label === selectedLabel) +const nextAction = selectedOption?.action || 'action-menu' +``` + +### Step 3: 处理特殊选项 + +```javascript +if (nextAction === 'exit') { + console.log('\n保存状态并退出...') + return { + stateUpdates: { + status: 'user_exit' + }, + continue: false, + message: '会话已保存,使用 --resume 可继续' + } +} + +if (nextAction === 'action-status') { + // 显示详细状态 + const sessionFolder = `.workflow/.loop/${state.session_id}` + + console.log('\n=== 开发进度 ===') + const progress = Read(`${sessionFolder}/develop/progress.md`) + console.log(progress?.substring(0, 500) + '...') + + console.log('\n=== 调试状态 ===') + if (state.debug.hypotheses.length > 0) { + state.debug.hypotheses.forEach(h => { + console.log(` ${h.id}: ${h.status} - ${h.description.substring(0, 50)}...`) + }) + } else { + console.log(' 尚未开始调试') + } + + console.log('\n=== 验证结果 ===') + if (state.validate.test_results.length > 0) { + const latest = state.validate.test_results[state.validate.test_results.length - 1] + console.log(` 最近运行: ${latest.timestamp}`) + console.log(` 通过率: ${latest.summary.pass_rate}%`) + } else { + console.log(' 尚未运行验证') + } + + // 返回菜单 + return { + stateUpdates: {}, + continue: true, + nextAction: 'action-menu', + message: '' + } +} +``` + +## State Updates + +```javascript +return { + stateUpdates: { + // 不更新状态,仅返回下一个动作 + }, + continue: true, + nextAction: nextAction, + message: `执行: ${selectedOption?.label || nextAction}` +} +``` + +## Error Handling + +| Error Type | Recovery | +|------------|----------| +| 用户取消 | 返回菜单 | +| 无效选择 | 重新显示菜单 | + +## Next Actions + +根据用户选择动态决定下一个动作。 diff --git a/.claude/skills/ccw-loop/phases/actions/action-validate-with-file.md b/.claude/skills/ccw-loop/phases/actions/action-validate-with-file.md new file mode 100644 index 00000000..0705b8bb --- /dev/null +++ b/.claude/skills/ccw-loop/phases/actions/action-validate-with-file.md @@ -0,0 +1,307 @@ +# Action: Validate With File + +运行测试并验证实现,记录结果到 validation.md,支持 Gemini 辅助分析测试覆盖率和质量。 + +## Purpose + +执行测试验证流程,包括: +- 运行单元测试 +- 运行集成测试 +- 检查代码覆盖率 +- 生成验证报告 +- 分析失败原因 + +## Preconditions + +- [ ] state.initialized === true +- [ ] state.status === 'running' +- [ ] state.develop.completed_count > 0 || state.debug.confirmed_hypothesis !== null + +## Session Setup + +```javascript +const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() + +const sessionFolder = `.workflow/.loop/${state.session_id}` +const validateFolder = `${sessionFolder}/validate` +const validationPath = `${validateFolder}/validation.md` +const testResultsPath = `${validateFolder}/test-results.json` +const coveragePath = `${validateFolder}/coverage.json` +``` + +--- + +## Execution + +### Step 1: 运行测试 + +```javascript +console.log('\n运行测试...') + +// 检测测试框架 +const packageJson = JSON.parse(Read('package.json')) +const testScript = packageJson.scripts?.test || 'npm test' + +// 运行测试并捕获输出 +const testResult = await Bash({ + command: testScript, + timeout: 300000 // 5分钟 +}) + +// 解析测试输出 +const testResults = parseTestOutput(testResult.stdout) +``` + +### Step 2: 检查覆盖率 + +```javascript +// 运行覆盖率检查 +let coverageData = null + +if (packageJson.scripts?.['test:coverage']) { + const coverageResult = await Bash({ + command: 'npm run test:coverage', + timeout: 300000 + }) + + // 解析覆盖率报告 + coverageData = parseCoverageReport(coverageResult.stdout) + + Write(coveragePath, JSON.stringify(coverageData, null, 2)) +} +``` + +### Step 3: Gemini 辅助分析 + +```bash +ccw cli -p " +PURPOSE: Analyze test results and coverage +Success criteria: Identify quality issues and suggest improvements + +TASK: +• Analyze test execution results +• Review code coverage metrics +• Identify missing test cases +• Suggest quality improvements +• Verify requirements coverage + +MODE: analysis + +CONTEXT: +@${testResultsPath} +@${coveragePath} +@${sessionFolder}/develop/progress.md + +EXPECTED: +- Quality assessment report +- Failed tests analysis +- Coverage gaps identification +- Improvement recommendations +- Pass/Fail decision with rationale + +CONSTRAINTS: Evidence-based quality assessment +" --tool gemini --mode analysis --rule analysis-review-code-quality +``` + +### Step 4: 生成验证报告 + +```javascript +const timestamp = getUtc8ISOString() +const iteration = (state.validate.test_results?.length || 0) + 1 + +const validationReport = `# Validation Report + +**Session ID**: ${state.session_id} +**Task**: ${state.task_description} +**Validated**: ${timestamp} + +--- + +## Iteration ${iteration} - Validation Run + +### Test Execution Summary + +| Metric | Value | +|--------|-------| +| Total Tests | ${testResults.total} | +| Passed | ${testResults.passed} | +| Failed | ${testResults.failed} | +| Skipped | ${testResults.skipped} | +| Duration | ${testResults.duration_ms}ms | +| **Pass Rate** | **${(testResults.passed / testResults.total * 100).toFixed(1)}%** | + +### Coverage Report + +${coverageData ? ` +| File | Statements | Branches | Functions | Lines | +|------|------------|----------|-----------|-------| +${coverageData.files.map(f => `| ${f.path} | ${f.statements}% | ${f.branches}% | ${f.functions}% | ${f.lines}% |`).join('\n')} + +**Overall Coverage**: ${coverageData.overall.statements}% +` : '_No coverage data available_'} + +### Failed Tests + +${testResults.failed > 0 ? ` +${testResults.failures.map(f => ` +#### ${f.test_name} + +- **Suite**: ${f.suite} +- **Error**: ${f.error_message} +- **Stack**: +\`\`\` +${f.stack_trace} +\`\`\` +`).join('\n')} +` : '_All tests passed_'} + +### Gemini Quality Analysis + +${geminiAnalysis} + +### Recommendations + +${recommendations.map(r => `- ${r}`).join('\n')} + +--- + +## Validation Decision + +**Result**: ${testResults.passed === testResults.total ? '✅ PASS' : '❌ FAIL'} + +**Rationale**: ${validationDecision} + +${testResults.passed !== testResults.total ? ` +### Next Actions + +1. Review failed tests +2. Debug failures using action-debug-with-file +3. Fix issues and re-run validation +` : ` +### Next Actions + +1. Consider code review +2. Prepare for deployment +3. Update documentation +`} +` + +// 写入验证报告 +Write(validationPath, validationReport) +``` + +### Step 5: 保存测试结果 + +```javascript +const testResultsData = { + iteration, + timestamp, + summary: { + total: testResults.total, + passed: testResults.passed, + failed: testResults.failed, + skipped: testResults.skipped, + pass_rate: (testResults.passed / testResults.total * 100).toFixed(1), + duration_ms: testResults.duration_ms + }, + tests: testResults.tests, + failures: testResults.failures, + coverage: coverageData?.overall || null +} + +Write(testResultsPath, JSON.stringify(testResultsData, null, 2)) +``` + +--- + +## State Updates + +```javascript +const validationPassed = testResults.failed === 0 && testResults.passed > 0 + +return { + stateUpdates: { + validate: { + test_results: [...(state.validate.test_results || []), testResultsData], + coverage: coverageData?.overall.statements || null, + passed: validationPassed, + failed_tests: testResults.failures.map(f => f.test_name), + last_run_at: getUtc8ISOString() + }, + last_action: 'action-validate-with-file' + }, + continue: true, + message: validationPassed + ? `验证通过 ✅\n测试: ${testResults.passed}/${testResults.total}\n覆盖率: ${coverageData?.overall.statements || 'N/A'}%` + : `验证失败 ❌\n失败: ${testResults.failed}/${testResults.total}\n建议进入调试模式` +} +``` + +## Test Output Parsers + +### Jest/Vitest Parser + +```javascript +function parseJestOutput(stdout) { + const testPattern = /Tests:\s+(\d+) passed.*?(\d+) failed.*?(\d+) total/ + const match = stdout.match(testPattern) + + return { + total: parseInt(match[3]), + passed: parseInt(match[1]), + failed: parseInt(match[2]), + // ... parse individual test results + } +} +``` + +### Pytest Parser + +```javascript +function parsePytestOutput(stdout) { + const summaryPattern = /(\d+) passed.*?(\d+) failed.*?(\d+) error/ + // ... implementation +} +``` + +## Error Handling + +| Error Type | Recovery | +|------------|----------| +| Tests don't run | 检查测试脚本配置,提示用户 | +| All tests fail | 建议进入 debug 模式 | +| Coverage tool missing | 跳过覆盖率检查,仅运行测试 | +| Timeout | 增加超时时间或拆分测试 | + +## Validation Report Template + +参考 [templates/validation-template.md](../../templates/validation-template.md) + +## CLI Integration + +### 质量分析 +```bash +ccw cli -p "PURPOSE: Analyze test results and coverage... +TASK: • Review results • Identify gaps • Suggest improvements +MODE: analysis +CONTEXT: @test-results.json @coverage.json +EXPECTED: Quality assessment +" --tool gemini --mode analysis --rule analysis-review-code-quality +``` + +### 测试生成 (如覆盖率低) +```bash +ccw cli -p "PURPOSE: Generate missing test cases... +TASK: • Analyze uncovered code • Write tests +MODE: write +CONTEXT: @coverage.json @src/**/* +EXPECTED: Test code +" --tool gemini --mode write --rule development-generate-tests +``` + +## Next Actions (Hints) + +- 验证通过: `action-complete` (完成循环) +- 验证失败: `action-debug-with-file` (调试失败测试) +- 覆盖率低: `action-develop-with-file` (添加测试) +- 用户选择: `action-menu` (返回菜单) diff --git a/.claude/skills/ccw-loop/phases/orchestrator.md b/.claude/skills/ccw-loop/phases/orchestrator.md new file mode 100644 index 00000000..decf5ee9 --- /dev/null +++ b/.claude/skills/ccw-loop/phases/orchestrator.md @@ -0,0 +1,486 @@ +# Orchestrator + +根据当前状态选择并执行下一个动作,实现无状态循环工作流。与 API (loop-v2-routes.ts) 协作实现控制平面/执行平面分离。 + +## Role + +检查控制信号 → 读取文件状态 → 选择动作 → 执行 → 更新文件 → 循环,直到完成或被外部暂停/停止。 + +## State Management (Unified Location) + +### 读取状态 + +```javascript +const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString() + +/** + * 读取循环状态 (统一位置) + * @param loopId - Loop ID (e.g., "loop-v2-20260122-abc123") + */ +function readLoopState(loopId) { + const stateFile = `.loop/${loopId}.json` + + if (!fs.existsSync(stateFile)) { + return null + } + + const state = JSON.parse(Read(stateFile)) + return state +} +``` + +### 更新状态 + +```javascript +/** + * 更新循环状态 (只更新 skill_state 部分,不修改 API 字段) + * @param loopId - Loop ID + * @param updates - 更新内容 (skill_state 字段) + */ +function updateLoopState(loopId, updates) { + const stateFile = `.loop/${loopId}.json` + const currentState = readLoopState(loopId) + + if (!currentState) { + throw new Error(`Loop state not found: ${loopId}`) + } + + // 只更新 skill_state 和 updated_at + const newState = { + ...currentState, + updated_at: getUtc8ISOString(), + skill_state: { + ...currentState.skill_state, + ...updates + } + } + + Write(stateFile, JSON.stringify(newState, null, 2)) + return newState +} +``` + +### 创建新循环状态 (直接调用时) + +```javascript +/** + * 创建新的循环状态 (仅在直接调用时使用,API 触发时状态已存在) + */ +function createLoopState(loopId, taskDescription) { + const stateFile = `.loop/${loopId}.json` + const now = getUtc8ISOString() + + const state = { + // API 兼容字段 + loop_id: loopId, + title: taskDescription.substring(0, 100), + description: taskDescription, + max_iterations: 10, + status: 'running', // 直接调用时设为 running + current_iteration: 0, + created_at: now, + updated_at: now, + + // Skill 扩展字段 + skill_state: null // 由 action-init 初始化 + } + + // 确保目录存在 + Bash(`mkdir -p ".loop"`) + Bash(`mkdir -p ".loop/${loopId}.progress"`) + + Write(stateFile, JSON.stringify(state, null, 2)) + return state +} +``` + +## Control Signal Checking + +```javascript +/** + * 检查 API 控制信号 + * 必须在每个 Action 开始前调用 + * @returns { continue: boolean, reason: string } + */ +function checkControlSignals(loopId) { + const state = readLoopState(loopId) + + if (!state) { + return { continue: false, reason: 'state_not_found' } + } + + switch (state.status) { + case 'paused': + // API 暂停了循环,Skill 应退出等待 resume + console.log(`⏸️ Loop paused by API. Waiting for resume...`) + return { continue: false, reason: 'paused' } + + case 'failed': + // API 停止了循环 (用户手动停止) + console.log(`⏹️ Loop stopped by API.`) + return { continue: false, reason: 'stopped' } + + case 'completed': + // 已完成 + console.log(`✅ Loop already completed.`) + return { continue: false, reason: 'completed' } + + case 'created': + // API 创建但未启动 (不应该走到这里) + console.log(`⚠️ Loop not started by API.`) + return { continue: false, reason: 'not_started' } + + case 'running': + // 正常继续 + return { continue: true, reason: 'running' } + + default: + console.log(`⚠️ Unknown status: ${state.status}`) + return { continue: false, reason: 'unknown_status' } + } +} +``` + +## Decision Logic + +```javascript +/** + * 选择下一个 Action (基于 skill_state) + */ +function selectNextAction(state, mode = 'interactive') { + const skillState = state.skill_state + + // 1. 终止条件检查 (API status) + if (state.status === 'completed') return null + if (state.status === 'failed') return null + if (state.current_iteration >= state.max_iterations) { + console.warn(`已达到最大迭代次数 (${state.max_iterations})`) + return 'action-complete' + } + + // 2. 初始化检查 + if (!skillState || !skillState.current_action) { + return 'action-init' + } + + // 3. 模式判断 + if (mode === 'interactive') { + return 'action-menu' // 显示菜单让用户选择 + } + + // 4. 自动模式:基于状态自动选择 + if (mode === 'auto') { + // 按优先级:develop → debug → validate + + // 如果有待开发任务 + const hasPendingDevelop = skillState.develop?.tasks?.some(t => t.status === 'pending') + if (hasPendingDevelop) { + return 'action-develop-with-file' + } + + // 如果开发完成但未调试 + if (skillState.last_action === 'action-develop-with-file') { + const needsDebug = skillState.develop?.completed < skillState.develop?.total + if (needsDebug) { + return 'action-debug-with-file' + } + } + + // 如果调试完成但未验证 + if (skillState.last_action === 'action-debug-with-file' || + skillState.debug?.confirmed_hypothesis) { + return 'action-validate-with-file' + } + + // 如果验证失败,回到开发 + if (skillState.last_action === 'action-validate-with-file') { + if (!skillState.validate?.passed) { + return 'action-develop-with-file' + } + } + + // 全部通过,完成 + if (skillState.validate?.passed && !hasPendingDevelop) { + return 'action-complete' + } + + // 默认:开发 + return 'action-develop-with-file' + } + + // 5. 默认完成 + return 'action-complete' +} +``` + +## Execution Loop + +```javascript +/** + * 运行编排器 + * @param options.loopId - 现有 Loop ID (API 触发时) + * @param options.task - 任务描述 (直接调用时) + * @param options.mode - 'interactive' | 'auto' + */ +async function runOrchestrator(options = {}) { + const { loopId: existingLoopId, task, mode = 'interactive' } = options + + console.log('=== CCW Loop Orchestrator Started ===') + + // 1. 确定 loopId + let loopId + let state + + if (existingLoopId) { + // API 触发:使用现有 loopId + loopId = existingLoopId + state = readLoopState(loopId) + + if (!state) { + console.error(`Loop not found: ${loopId}`) + return { status: 'error', message: 'Loop not found' } + } + + console.log(`Resuming loop: ${loopId}`) + console.log(`Status: ${state.status}`) + + } else if (task) { + // 直接调用:创建新 loopId + const timestamp = getUtc8ISOString().replace(/[-:]/g, '').split('.')[0] + const random = Math.random().toString(36).substring(2, 10) + loopId = `loop-v2-${timestamp}-${random}` + + console.log(`Creating new loop: ${loopId}`) + console.log(`Task: ${task}`) + + state = createLoopState(loopId, task) + + } else { + console.error('Either --loop-id or task description is required') + return { status: 'error', message: 'Missing loopId or task' } + } + + const progressDir = `.loop/${loopId}.progress` + + // 2. 主循环 + let iteration = state.current_iteration || 0 + + while (iteration < state.max_iterations) { + iteration++ + + // ======================================== + // CRITICAL: Check control signals first + // ======================================== + const control = checkControlSignals(loopId) + if (!control.continue) { + console.log(`\n🛑 Loop terminated: ${control.reason}`) + break + } + + // 重新读取状态 (可能被 API 更新) + state = readLoopState(loopId) + + console.log(`\n[Iteration ${iteration}] Status: ${state.status}`) + + // 选择下一个动作 + const actionId = selectNextAction(state, mode) + + if (!actionId) { + console.log('No action selected, terminating.') + break + } + + console.log(`[Iteration ${iteration}] Executing: ${actionId}`) + + // 更新 current_iteration + state = { + ...state, + current_iteration: iteration, + updated_at: getUtc8ISOString() + } + Write(`.loop/${loopId}.json`, JSON.stringify(state, null, 2)) + + // 执行动作 + try { + const actionPromptFile = `.claude/skills/ccw-loop/phases/actions/${actionId}.md` + + if (!fs.existsSync(actionPromptFile)) { + console.error(`Action file not found: ${actionPromptFile}`) + continue + } + + const actionPrompt = Read(actionPromptFile) + + // 构建 Agent 提示 + const agentPrompt = ` +[LOOP CONTEXT] +Loop ID: ${loopId} +State File: .loop/${loopId}.json +Progress Dir: ${progressDir} + +[CURRENT STATE] +${JSON.stringify(state, null, 2)} + +[ACTION INSTRUCTIONS] +${actionPrompt} + +[TASK] +You are executing ${actionId} for loop: ${state.title || state.description} + +[CONTROL SIGNALS] +Before executing, check if status is still 'running'. +If status is 'paused' or 'failed', exit gracefully. + +[RETURN] +Return JSON with: +- skillStateUpdates: Object with skill_state fields to update +- continue: Boolean indicating if loop should continue +- message: String with user message +` + + const result = await Task({ + subagent_type: 'universal-executor', + run_in_background: false, + description: `Execute ${actionId}`, + prompt: agentPrompt + }) + + // 解析结果 + const actionResult = JSON.parse(result) + + // 更新状态 (只更新 skill_state) + updateLoopState(loopId, { + current_action: null, + last_action: actionId, + completed_actions: [ + ...(state.skill_state?.completed_actions || []), + actionId + ], + ...actionResult.skillStateUpdates + }) + + // 显示消息 + if (actionResult.message) { + console.log(`\n${actionResult.message}`) + } + + // 检查是否继续 + if (actionResult.continue === false) { + console.log('Action requested termination.') + break + } + + } catch (error) { + console.error(`Error executing ${actionId}: ${error.message}`) + + // 错误处理 + updateLoopState(loopId, { + current_action: null, + errors: [ + ...(state.skill_state?.errors || []), + { + action: actionId, + message: error.message, + timestamp: getUtc8ISOString() + } + ] + }) + } + } + + if (iteration >= state.max_iterations) { + console.log(`\n⚠️ Reached maximum iterations (${state.max_iterations})`) + console.log('Consider breaking down the task or taking a break.') + } + + console.log('\n=== CCW Loop Orchestrator Finished ===') + + // 返回最终状态 + const finalState = readLoopState(loopId) + return { + status: finalState.status, + loop_id: loopId, + iterations: iteration, + final_state: finalState + } +} +``` + +## Action Catalog + +| Action | Purpose | Preconditions | Effects | +|--------|---------|---------------|---------| +| [action-init](actions/action-init.md) | 初始化会话 | status=pending | initialized=true | +| [action-menu](actions/action-menu.md) | 显示操作菜单 | initialized=true | 用户选择下一动作 | +| [action-develop-with-file](actions/action-develop-with-file.md) | 开发任务 | initialized=true | 更新 progress.md | +| [action-debug-with-file](actions/action-debug-with-file.md) | 假设调试 | initialized=true | 更新 understanding.md | +| [action-validate-with-file](actions/action-validate-with-file.md) | 测试验证 | initialized=true | 更新 validation.md | +| [action-complete](actions/action-complete.md) | 完成循环 | validation_passed=true | status=completed | + +## Termination Conditions + +1. **API 暂停**: `state.status === 'paused'` (Skill 退出,等待 resume) +2. **API 停止**: `state.status === 'failed'` (Skill 终止) +3. **任务完成**: `state.status === 'completed'` +4. **迭代限制**: `state.current_iteration >= state.max_iterations` +5. **Action 请求终止**: `actionResult.continue === false` + +## Error Recovery + +| Error Type | Recovery Strategy | +|------------|-------------------| +| 动作执行失败 | 记录错误,增加 error_count,继续下一动作 | +| 状态文件损坏 | 从其他文件重建状态 (progress.md, understanding.md 等) | +| 用户中止 | 保存当前状态,允许 --resume 恢复 | +| CLI 工具失败 | 回退到手动分析模式 | + +## Mode Strategies + +### Interactive Mode (默认) + +每次显示菜单,让用户选择动作: + +``` +当前状态: 开发中 +可用操作: + 1. 继续开发 (develop) + 2. 开始调试 (debug) + 3. 运行验证 (validate) + 4. 查看进度 (status) + 5. 退出 (exit) + +请选择: +``` + +### Auto Mode (自动循环) + +按预设流程自动执行: + +``` +Develop → Debug → Validate → + ↓ (如验证失败) +Develop (修复) → Debug → Validate → 完成 +``` + +## State Machine (API Status) + +```mermaid +stateDiagram-v2 + [*] --> created: API creates loop + created --> running: API /start → Trigger Skill + running --> paused: API /pause → Set status + running --> completed: action-complete + running --> failed: API /stop OR error + paused --> running: API /resume → Re-trigger Skill + completed --> [*] + failed --> [*] + + note right of paused + Skill checks status before each action + If paused, Skill exits gracefully + end note + + note right of running + Skill executes: init → develop → debug → validate + end note +``` diff --git a/.claude/skills/ccw-loop/phases/state-schema.md b/.claude/skills/ccw-loop/phases/state-schema.md new file mode 100644 index 00000000..94c46cff --- /dev/null +++ b/.claude/skills/ccw-loop/phases/state-schema.md @@ -0,0 +1,474 @@ +# State Schema + +CCW Loop 的状态结构定义(统一版本)。 + +## 状态文件 + +**位置**: `.loop/{loopId}.json` (统一位置,API + Skill 共享) + +**旧版本位置** (仅向后兼容): `.workflow/.loop/{session-id}/state.json` + +## 结构定义 + +### 统一状态接口 (Unified Loop State) + +```typescript +/** + * Unified Loop State - API 和 Skill 共享的状态结构 + * API (loop-v2-routes.ts) 拥有状态的主控权 + * Skill (ccw-loop) 读取和更新此状态 + */ +interface LoopState { + // ===================================================== + // API FIELDS (from loop-v2-routes.ts) + // 这些字段由 API 管理,Skill 只读 + // ===================================================== + + loop_id: string // Loop ID, e.g., "loop-v2-20260122-abc123" + title: string // Loop 标题 + description: string // Loop 描述 + max_iterations: number // 最大迭代次数 + status: 'created' | 'running' | 'paused' | 'completed' | 'failed' + current_iteration: number // 当前迭代次数 + created_at: string // 创建时间 (ISO8601) + updated_at: string // 最后更新时间 (ISO8601) + completed_at?: string // 完成时间 (ISO8601) + failure_reason?: string // 失败原因 + + // ===================================================== + // SKILL EXTENSION FIELDS + // 这些字段由 Skill 管理,API 只读 + // ===================================================== + + skill_state?: { + // 当前执行动作 + current_action: 'init' | 'develop' | 'debug' | 'validate' | 'complete' | null + last_action: string | null + completed_actions: string[] + mode: 'interactive' | 'auto' + + // === 开发阶段 === + develop: { + total: number + completed: number + current_task?: string + tasks: DevelopTask[] + last_progress_at: string | null + } + + // === 调试阶段 === + debug: { + active_bug?: string + hypotheses_count: number + hypotheses: Hypothesis[] + confirmed_hypothesis: string | null + iteration: number + last_analysis_at: string | null + } + + // === 验证阶段 === + validate: { + pass_rate: number // 测试通过率 (0-100) + coverage: number // 覆盖率 (0-100) + test_results: TestResult[] + passed: boolean + failed_tests: string[] + last_run_at: string | null + } + + // === 错误追踪 === + errors: Array<{ + action: string + message: string + timestamp: string + }> + } +} + +interface DevelopTask { + id: string + description: string + tool: 'gemini' | 'qwen' | 'codex' | 'bash' + mode: 'analysis' | 'write' + status: 'pending' | 'in_progress' | 'completed' | 'failed' + files_changed: string[] + created_at: string + completed_at: string | null +} + +interface Hypothesis { + id: string // H1, H2, ... + description: string + testable_condition: string + logging_point: string + evidence_criteria: { + confirm: string + reject: string + } + likelihood: number // 1 = 最可能 + status: 'pending' | 'confirmed' | 'rejected' | 'inconclusive' + evidence: Record | null + verdict_reason: string | null +} + +interface TestResult { + test_name: string + suite: string + status: 'passed' | 'failed' | 'skipped' + duration_ms: number + error_message: string | null + stack_trace: string | null +} +``` + +## 初始状态 + +### 由 API 创建时 (Dashboard 触发) + +```json +{ + "loop_id": "loop-v2-20260122-abc123", + "title": "Implement user authentication", + "description": "Add login/logout functionality", + "max_iterations": 10, + "status": "created", + "current_iteration": 0, + "created_at": "2026-01-22T10:00:00+08:00", + "updated_at": "2026-01-22T10:00:00+08:00" +} +``` + +### 由 Skill 初始化后 (action-init) + +```json +{ + "loop_id": "loop-v2-20260122-abc123", + "title": "Implement user authentication", + "description": "Add login/logout functionality", + "max_iterations": 10, + "status": "running", + "current_iteration": 0, + "created_at": "2026-01-22T10:00:00+08:00", + "updated_at": "2026-01-22T10:00:05+08:00", + + "skill_state": { + "current_action": "init", + "last_action": null, + "completed_actions": [], + "mode": "auto", + + "develop": { + "total": 3, + "completed": 0, + "current_task": null, + "tasks": [ + { "id": "task-001", "description": "Create auth component", "status": "pending" } + ], + "last_progress_at": null + }, + + "debug": { + "active_bug": null, + "hypotheses_count": 0, + "hypotheses": [], + "confirmed_hypothesis": null, + "iteration": 0, + "last_analysis_at": null + }, + + "validate": { + "pass_rate": 0, + "coverage": 0, + "test_results": [], + "passed": false, + "failed_tests": [], + "last_run_at": null + }, + + "errors": [] + } +} +``` + +## 控制信号检查 (Control Signals) + +Skill 在每个 Action 开始前必须检查控制信号: + +```javascript +/** + * 检查 API 控制信号 + * @returns { continue: boolean, action: 'pause_exit' | 'stop_exit' | 'continue' } + */ +function checkControlSignals(loopId) { + const state = JSON.parse(Read(`.loop/${loopId}.json`)) + + switch (state.status) { + case 'paused': + // API 暂停了循环,Skill 应退出等待 resume + return { continue: false, action: 'pause_exit' } + + case 'failed': + // API 停止了循环 (用户手动停止) + return { continue: false, action: 'stop_exit' } + + case 'running': + // 正常继续 + return { continue: true, action: 'continue' } + + default: + // 异常状态 + return { continue: false, action: 'stop_exit' } + } +} +``` + +### 在 Action 中使用 + +```markdown +## Execution + +### Step 1: Check Control Signals + +\`\`\`javascript +const control = checkControlSignals(loopId) +if (!control.continue) { + // 输出退出原因 + console.log(`Loop ${control.action}: status = ${state.status}`) + + // 如果是 pause_exit,保存当前进度 + if (control.action === 'pause_exit') { + updateSkillState(loopId, { current_action: 'paused' }) + } + + return // 退出 Action +} +\`\`\` + +### Step 2: Execute Action Logic +... +``` + +## 状态转换规则 + +### 1. 初始化 (action-init) + +```javascript +// Skill 初始化后 +{ + // API 字段更新 + status: 'created' → 'running', // 或保持 'running' 如果 API 已设置 + updated_at: timestamp, + + // Skill 字段初始化 + skill_state: { + current_action: 'init', + mode: 'auto', + develop: { + tasks: [...parsed_tasks], + total: N, + completed: 0 + } + } +} +``` + +### 2. 开发进行中 (action-develop-with-file) + +```javascript +// 开发任务执行后 +{ + updated_at: timestamp, + current_iteration: state.current_iteration + 1, + + skill_state: { + current_action: 'develop', + last_action: 'action-develop-with-file', + completed_actions: [...state.skill_state.completed_actions, 'action-develop-with-file'], + develop: { + current_task: 'task-xxx', + completed: N+1, + last_progress_at: timestamp + } + } +} +``` + +### 3. 调试进行中 (action-debug-with-file) + +```javascript +// 调试执行后 +{ + updated_at: timestamp, + current_iteration: state.current_iteration + 1, + + skill_state: { + current_action: 'debug', + last_action: 'action-debug-with-file', + debug: { + active_bug: '...', + hypotheses_count: N, + hypotheses: [...new_hypotheses], + iteration: N+1, + last_analysis_at: timestamp + } + } +} +``` + +### 4. 验证完成 (action-validate-with-file) + +```javascript +// 验证执行后 +{ + updated_at: timestamp, + current_iteration: state.current_iteration + 1, + + skill_state: { + current_action: 'validate', + last_action: 'action-validate-with-file', + validate: { + test_results: [...results], + pass_rate: 95.5, + coverage: 85.0, + passed: true | false, + failed_tests: ['test1', 'test2'], + last_run_at: timestamp + } + } +} +``` + +### 5. 完成 (action-complete) + +```javascript +// 循环完成后 +{ + status: 'running' → 'completed', + completed_at: timestamp, + updated_at: timestamp, + + skill_state: { + current_action: 'complete', + last_action: 'action-complete' + } +} +``` + +## 状态派生字段 + +以下字段可从状态计算得出,不需要存储: + +```javascript +// 开发完成度 +const developProgress = state.develop.total_count > 0 + ? (state.develop.completed_count / state.develop.total_count) * 100 + : 0 + +// 是否有待开发任务 +const hasPendingDevelop = state.develop.tasks.some(t => t.status === 'pending') + +// 调试是否完成 +const debugCompleted = state.debug.confirmed_hypothesis !== null + +// 验证是否通过 +const validationPassed = state.validate.passed && state.validate.test_results.length > 0 + +// 整体进度 +const overallProgress = ( + (developProgress * 0.5) + + (debugCompleted ? 25 : 0) + + (validationPassed ? 25 : 0) +) +``` + +## 文件同步 + +### 统一位置 (Unified Location) + +状态与文件的对应关系: + +| 状态字段 | 同步文件 | 同步时机 | +|----------|----------|----------| +| 整个 LoopState | `.loop/{loopId}.json` | 每次状态变更 (主文件) | +| `skill_state.develop` | `.loop/{loopId}.progress/develop.md` | 每次开发操作后 | +| `skill_state.debug` | `.loop/{loopId}.progress/debug.md` | 每次调试操作后 | +| `skill_state.validate` | `.loop/{loopId}.progress/validate.md` | 每次验证操作后 | +| 代码变更日志 | `.loop/{loopId}.progress/changes.log` | 每次文件修改 (NDJSON) | +| 调试日志 | `.loop/{loopId}.progress/debug.log` | 每次调试日志 (NDJSON) | + +### 文件结构示例 + +``` +.loop/ +├── loop-v2-20260122-abc123.json # 主状态文件 (API + Skill) +├── loop-v2-20260122-abc123.tasks.jsonl # 任务列表 (API 管理) +└── loop-v2-20260122-abc123.progress/ # Skill 进度文件 + ├── develop.md # 开发进度 + ├── debug.md # 调试理解 + ├── validate.md # 验证报告 + ├── changes.log # 代码变更 (NDJSON) + └── debug.log # 调试日志 (NDJSON) +``` + +## 状态恢复 + +如果主状态文件 `.loop/{loopId}.json` 损坏,可以从进度文件重建 skill_state: + +```javascript +function rebuildSkillStateFromProgress(loopId) { + const progressDir = `.loop/${loopId}.progress` + + // 尝试从进度文件解析状态 + const skill_state = { + develop: parseProgressFile(`${progressDir}/develop.md`), + debug: parseProgressFile(`${progressDir}/debug.md`), + validate: parseProgressFile(`${progressDir}/validate.md`) + } + + return skill_state +} + +// 解析进度 Markdown 文件 +function parseProgressFile(filePath) { + const content = Read(filePath) + if (!content) return null + + // 从 Markdown 表格和结构中提取数据 + // ... implementation +} +``` + +### 恢复策略 + +1. **API 字段**: 无法恢复 - 需要从 API 重新获取或用户手动输入 +2. **skill_state 字段**: 可以从 `.progress/` 目录的 Markdown 文件解析 +3. **任务列表**: 从 `.loop/{loopId}.tasks.jsonl` 恢复 + +## 状态验证 + +```javascript +function validateState(state) { + const errors = [] + + // 必需字段 + if (!state.session_id) errors.push('Missing session_id') + if (!state.task_description) errors.push('Missing task_description') + + // 状态一致性 + if (state.initialized && state.status === 'pending') { + errors.push('Inconsistent: initialized but status is pending') + } + + if (state.status === 'completed' && !state.validate.passed) { + errors.push('Inconsistent: completed but validation not passed') + } + + // 开发任务一致性 + const completedTasks = state.develop.tasks.filter(t => t.status === 'completed').length + if (completedTasks !== state.develop.completed_count) { + errors.push('Inconsistent: completed_count mismatch') + } + + return { valid: errors.length === 0, errors } +} +``` diff --git a/.claude/skills/ccw-loop/specs/action-catalog.md b/.claude/skills/ccw-loop/specs/action-catalog.md new file mode 100644 index 00000000..cc1e5ffc --- /dev/null +++ b/.claude/skills/ccw-loop/specs/action-catalog.md @@ -0,0 +1,300 @@ +# Action Catalog + +CCW Loop 所有可用动作的目录和说明。 + +## Available Actions + +| Action | Purpose | Preconditions | Effects | CLI Integration | +|--------|---------|---------------|---------|-----------------| +| [action-init](../phases/actions/action-init.md) | 初始化会话 | status=pending, initialized=false | status→running, initialized→true, 创建目录和任务列表 | Gemini 任务分解 | +| [action-menu](../phases/actions/action-menu.md) | 显示操作菜单 | initialized=true, status=running | 返回用户选择的动作 | - | +| [action-develop-with-file](../phases/actions/action-develop-with-file.md) | 执行开发任务 | initialized=true, pending tasks > 0 | 更新 progress.md, 完成一个任务 | Gemini 代码实现 | +| [action-debug-with-file](../phases/actions/action-debug-with-file.md) | 假设驱动调试 | initialized=true | 更新 understanding.md, hypotheses.json | Gemini 假设生成和证据分析 | +| [action-validate-with-file](../phases/actions/action-validate-with-file.md) | 运行测试验证 | initialized=true, develop > 0 or debug confirmed | 更新 validation.md, test-results.json | Gemini 质量分析 | +| [action-complete](../phases/actions/action-complete.md) | 完成循环 | initialized=true | status→completed, 生成 summary.md | - | + +## Action Dependencies Graph + +```mermaid +graph TD + START([用户启动 /ccw-loop]) --> INIT[action-init] + INIT --> MENU[action-menu] + + MENU --> DEVELOP[action-develop-with-file] + MENU --> DEBUG[action-debug-with-file] + MENU --> VALIDATE[action-validate-with-file] + MENU --> STATUS[action-status] + MENU --> COMPLETE[action-complete] + MENU --> EXIT([退出]) + + DEVELOP --> MENU + DEBUG --> MENU + VALIDATE --> MENU + STATUS --> MENU + COMPLETE --> END([结束]) + EXIT --> END + + style INIT fill:#e1f5fe + style MENU fill:#fff3e0 + style DEVELOP fill:#e8f5e9 + style DEBUG fill:#fce4ec + style VALIDATE fill:#f3e5f5 + style COMPLETE fill:#c8e6c9 +``` + +## Action Execution Matrix + +### Interactive Mode + +| State | Auto-Selected Action | User Options | +|-------|---------------------|--------------| +| pending | action-init | - | +| running, !initialized | action-init | - | +| running, initialized | action-menu | All actions | + +### Auto Mode + +| Condition | Selected Action | +|-----------|----------------| +| pending_develop_tasks > 0 | action-develop-with-file | +| last_action=develop, !debug_completed | action-debug-with-file | +| last_action=debug, !validation_completed | action-validate-with-file | +| validation_failed | action-develop-with-file (fix) | +| validation_passed, no pending | action-complete | + +## Action Inputs/Outputs + +### action-init + +**Inputs**: +- state.task_description +- User input (optional) + +**Outputs**: +- meta.json +- state.json (初始化) +- develop/tasks.json +- develop/progress.md + +**State Changes**: +```javascript +{ + status: 'pending' → 'running', + initialized: false → true, + develop.tasks: [] → [task1, task2, ...] +} +``` + +### action-develop-with-file + +**Inputs**: +- state.develop.tasks +- User selection (如有多个待处理任务) + +**Outputs**: +- develop/progress.md (追加) +- develop/tasks.json (更新) +- develop/changes.log (追加) + +**State Changes**: +```javascript +{ + develop.current_task_id: null → 'task-xxx' → null, + develop.completed_count: N → N+1, + last_action: X → 'action-develop-with-file' +} +``` + +### action-debug-with-file + +**Inputs**: +- Bug description (用户输入或从测试失败获取) +- debug.log (如已有) + +**Outputs**: +- debug/understanding.md (追加) +- debug/hypotheses.json (更新) +- Code changes (添加日志或修复) + +**State Changes**: +```javascript +{ + debug.current_bug: null → 'bug description', + debug.hypotheses: [...updated], + debug.iteration: N → N+1, + debug.confirmed_hypothesis: null → 'H1' (如确认) +} +``` + +### action-validate-with-file + +**Inputs**: +- 测试脚本 (从 package.json) +- 覆盖率工具 (可选) + +**Outputs**: +- validate/validation.md (追加) +- validate/test-results.json (更新) +- validate/coverage.json (更新) + +**State Changes**: +```javascript +{ + validate.test_results: [...new results], + validate.coverage: null → 85.5, + validate.passed: false → true, + validate.failed_tests: ['test1', 'test2'] → [] +} +``` + +### action-complete + +**Inputs**: +- state (完整状态) +- User choices (扩展选项) + +**Outputs**: +- summary.md +- Issues (如选择扩展) + +**State Changes**: +```javascript +{ + status: 'running' → 'completed', + completed_at: null → timestamp +} +``` + +## Action Sequences + +### Typical Happy Path + +``` +action-init + → action-develop-with-file (task 1) + → action-develop-with-file (task 2) + → action-develop-with-file (task 3) + → action-validate-with-file + → PASS + → action-complete +``` + +### Debug Iteration Path + +``` +action-init + → action-develop-with-file (task 1) + → action-validate-with-file + → FAIL + → action-debug-with-file (探索) + → action-debug-with-file (分析) + → Root cause found + → action-validate-with-file + → PASS + → action-complete +``` + +### Multi-Iteration Path + +``` +action-init + → action-develop-with-file (task 1) + → action-debug-with-file + → action-develop-with-file (task 2) + → action-validate-with-file + → FAIL + → action-debug-with-file + → action-validate-with-file + → PASS + → action-complete +``` + +## Error Scenarios + +### CLI Tool Failure + +``` +action-develop-with-file + → Gemini CLI fails + → Fallback to manual implementation + → Prompt user for code + → Continue +``` + +### Test Failure + +``` +action-validate-with-file + → Tests fail + → Record failed tests + → Suggest action-debug-with-file + → User chooses debug or manual fix +``` + +### Max Iterations Reached + +``` +state.iteration_count >= 10 + → Warning message + → Suggest break or task split + → Allow continue or exit +``` + +## Action Extensions + +### Adding New Actions + +To add a new action: + +1. Create `phases/actions/action-{name}.md` +2. Define preconditions, execution, state updates +3. Add to this catalog +4. Update orchestrator.md decision logic +5. Add to action-menu.md options + +### Action Template + +```markdown +# Action: {Name} + +{Brief description} + +## Purpose + +{Detailed purpose} + +## Preconditions + +- [ ] condition1 +- [ ] condition2 + +## Execution + +### Step 1: {Step Name} + +\`\`\`javascript +// code +\`\`\` + +## State Updates + +\`\`\`javascript +return { + stateUpdates: { + // updates + }, + continue: true, + message: "..." +} +\`\`\` + +## Error Handling + +| Error Type | Recovery | +|------------|----------| +| ... | ... | + +## Next Actions (Hints) + +- condition: next_action +``` diff --git a/.claude/skills/ccw-loop/specs/loop-requirements.md b/.claude/skills/ccw-loop/specs/loop-requirements.md new file mode 100644 index 00000000..90719be5 --- /dev/null +++ b/.claude/skills/ccw-loop/specs/loop-requirements.md @@ -0,0 +1,192 @@ +# Loop Requirements Specification + +CCW Loop 的核心需求和约束定义。 + +## Core Requirements + +### 1. 无状态循环 + +**Requirement**: 每次执行从文件读取状态,执行后写回文件,不依赖内存状态。 + +**Rationale**: 支持随时中断和恢复,状态持久化。 + +**Validation**: +- [ ] 每个 action 开始时从文件读取状态 +- [ ] 每个 action 结束时将状态写回文件 +- [ ] 无全局变量或内存状态依赖 + +### 2. 文件驱动进度 + +**Requirement**: 所有进度、理解、验证结果都记录在专用 Markdown 文件中。 + +**Rationale**: 可审计、可回顾、团队可见。 + +**Validation**: +- [ ] develop/progress.md 记录开发进度 +- [ ] debug/understanding.md 记录理解演变 +- [ ] validate/validation.md 记录验证结果 +- [ ] 所有文件使用 Markdown 格式,易读 + +### 3. CLI 工具集成 + +**Requirement**: 关键决策点使用 Gemini/CLI 进行深度分析。 + +**Rationale**: 利用 LLM 能力提高质量。 + +**Validation**: +- [ ] 任务分解使用 Gemini +- [ ] 假设生成使用 Gemini +- [ ] 证据分析使用 Gemini +- [ ] 质量评估使用 Gemini + +### 4. 用户控制循环 + +**Requirement**: 支持交互式和自动循环两种模式,用户可随时介入。 + +**Rationale**: 灵活性,适应不同场景。 + +**Validation**: +- [ ] 交互模式:每步显示菜单 +- [ ] 自动模式:按预设流程执行 +- [ ] 用户可随时退出 +- [ ] 状态可恢复 + +### 5. 可恢复性 + +**Requirement**: 任何时候中断后,可以从上次位置继续。 + +**Rationale**: 长时间任务支持,意外中断恢复。 + +**Validation**: +- [ ] 状态保存在 state.json +- [ ] 使用 --resume 可继续 +- [ ] 历史记录完整保留 + +## Quality Standards + +### Completeness + +| Dimension | Threshold | +|-----------|-----------| +| 进度文档完整性 | 每个任务都有记录 | +| 理解文档演变 | 每次迭代都有更新 | +| 验证报告详尽 | 包含所有测试结果 | + +### Consistency + +| Dimension | Threshold | +|-----------|-----------| +| 文件格式一致 | 所有 Markdown 文件使用相同模板 | +| 状态同步一致 | state.json 与文件内容匹配 | +| 时间戳格式 | 统一使用 ISO8601 格式 | + +### Usability + +| Dimension | Threshold | +|-----------|-----------| +| 菜单易用性 | 选项清晰,描述准确 | +| 进度可见性 | 随时可查看当前状态 | +| 错误提示 | 错误消息清晰,提供恢复建议 | + +## Constraints + +### 1. 文件结构约束 + +``` +.workflow/.loop/{session-id}/ +├── meta.json # 只写一次,不再修改 +├── state.json # 每次 action 后更新 +├── develop/ +│ ├── progress.md # 只追加,不删除 +│ ├── tasks.json # 任务状态更新 +│ └── changes.log # NDJSON 格式,只追加 +├── debug/ +│ ├── understanding.md # 只追加,记录时间线 +│ ├── hypotheses.json # 更新假设状态 +│ └── debug.log # NDJSON 格式 +└── validate/ + ├── validation.md # 每次验证追加 + ├── test-results.json # 累积测试结果 + └── coverage.json # 最新覆盖率 +``` + +### 2. 命名约束 + +- Session ID: `LOOP-{slug}-{YYYY-MM-DD}` +- Task ID: `task-{NNN}` (三位数字) +- Hypothesis ID: `H{N}` (单字母+数字) + +### 3. 状态转换约束 + +``` +pending → running → completed + ↓ + user_exit + ↓ + failed +``` + +Only allow: `pending→running`, `running→completed/user_exit/failed` + +### 4. 错误限制约束 + +- 最大错误次数: 3 +- 超过 3 次错误 → 自动终止 +- 每次错误 → 记录到 state.errors[] + +### 5. 迭代限制约束 + +- 最大迭代次数: 10 (警告) +- 超过 10 次 → 警告用户,但不强制停止 +- 建议拆分任务或休息 + +## Integration Requirements + +### 1. Dashboard 集成 + +**Requirement**: 与 CCW Dashboard Loop Monitor 无缝集成。 + +**Specification**: +- Dashboard 创建 Loop → 调用此 Skill +- state.json → Dashboard 实时显示 +- 任务列表双向同步 +- 状态控制按钮映射到 actions + +### 2. Issue 系统集成 + +**Requirement**: 完成后可扩展为 Issue。 + +**Specification**: +- 支持维度: test, enhance, refactor, doc +- 调用 `/issue:new "{summary} - {dimension}"` +- 自动填充上下文 + +### 3. CLI 工具集成 + +**Requirement**: 使用 CCW CLI 工具进行分析和实现。 + +**Specification**: +- 任务分解: `--rule planning-breakdown-task-steps` +- 代码实现: `--rule development-implement-feature` +- 根因分析: `--rule analysis-diagnose-bug-root-cause` +- 质量评估: `--rule analysis-review-code-quality` + +## Non-Functional Requirements + +### Performance + +- Session 初始化: < 5s +- Action 执行: < 30s (不含 CLI 调用) +- 状态读写: < 1s + +### Reliability + +- 状态文件损坏恢复: 支持从其他文件重建 +- CLI 工具失败降级: 回退到手动模式 +- 错误重试: 支持一次自动重试 + +### Maintainability + +- 文档化: 所有 action 都有清晰说明 +- 模块化: 每个 action 独立可测 +- 可扩展: 易于添加新 action diff --git a/.claude/skills/ccw-loop/templates/progress-template.md b/.claude/skills/ccw-loop/templates/progress-template.md new file mode 100644 index 00000000..5f3b9682 --- /dev/null +++ b/.claude/skills/ccw-loop/templates/progress-template.md @@ -0,0 +1,175 @@ +# Progress Document Template + +开发进度文档的标准模板。 + +## Template Structure + +```markdown +# Development Progress + +**Session ID**: {{session_id}} +**Task**: {{task_description}} +**Started**: {{started_at}} +**Estimated Complexity**: {{complexity}} + +--- + +## Task List + +{{#each tasks}} +{{@index}}. [{{#if completed}}x{{else}} {{/if}}] {{description}} +{{/each}} + +## Key Files + +{{#each key_files}} +- `{{this}}` +{{/each}} + +--- + +## Progress Timeline + +{{#each iterations}} +### Iteration {{@index}} - {{task_name}} ({{timestamp}}) + +#### Task Details + +- **ID**: {{task_id}} +- **Tool**: {{tool}} +- **Mode**: {{mode}} + +#### Implementation Summary + +{{summary}} + +#### Files Changed + +{{#each files_changed}} +- `{{this}}` +{{/each}} + +#### Status: {{status}} + +--- +{{/each}} + +## Current Statistics + +| Metric | Value | +|--------|-------| +| Total Tasks | {{total_tasks}} | +| Completed | {{completed_tasks}} | +| In Progress | {{in_progress_tasks}} | +| Pending | {{pending_tasks}} | +| Progress | {{progress_percentage}}% | + +--- + +## Next Steps + +{{#each next_steps}} +- [ ] {{this}} +{{/each}} +``` + +## Template Variables + +| Variable | Type | Source | Description | +|----------|------|--------|-------------| +| `session_id` | string | state.session_id | 会话 ID | +| `task_description` | string | state.task_description | 任务描述 | +| `started_at` | string | state.created_at | 开始时间 | +| `complexity` | string | state.context.estimated_complexity | 预估复杂度 | +| `tasks` | array | state.develop.tasks | 任务列表 | +| `key_files` | array | state.context.key_files | 关键文件 | +| `iterations` | array | 从文件解析 | 迭代历史 | +| `total_tasks` | number | state.develop.total_count | 总任务数 | +| `completed_tasks` | number | state.develop.completed_count | 已完成数 | + +## Usage Example + +```javascript +const progressTemplate = Read('.claude/skills/ccw-loop/templates/progress-template.md') + +function renderProgress(state) { + let content = progressTemplate + + // 替换简单变量 + content = content.replace('{{session_id}}', state.session_id) + content = content.replace('{{task_description}}', state.task_description) + content = content.replace('{{started_at}}', state.created_at) + content = content.replace('{{complexity}}', state.context?.estimated_complexity || 'unknown') + + // 替换任务列表 + const taskList = state.develop.tasks.map((t, i) => { + const checkbox = t.status === 'completed' ? 'x' : ' ' + return `${i + 1}. [${checkbox}] ${t.description}` + }).join('\n') + content = content.replace('{{#each tasks}}...{{/each}}', taskList) + + // 替换统计 + content = content.replace('{{total_tasks}}', state.develop.total_count) + content = content.replace('{{completed_tasks}}', state.develop.completed_count) + // ... + + return content +} +``` + +## Section Templates + +### Task Entry + +```markdown +### Iteration {{N}} - {{task_name}} ({{timestamp}}) + +#### Task Details + +- **ID**: {{task_id}} +- **Tool**: {{tool}} +- **Mode**: {{mode}} + +#### Implementation Summary + +{{summary}} + +#### Files Changed + +{{#each files}} +- `{{this}}` +{{/each}} + +#### Status: COMPLETED + +--- +``` + +### Statistics Table + +```markdown +## Current Statistics + +| Metric | Value | +|--------|-------| +| Total Tasks | {{total}} | +| Completed | {{completed}} | +| In Progress | {{in_progress}} | +| Pending | {{pending}} | +| Progress | {{percentage}}% | +``` + +### Next Steps + +```markdown +## Next Steps + +{{#if all_completed}} +- [ ] Run validation tests +- [ ] Code review +- [ ] Update documentation +{{else}} +- [ ] Complete remaining {{pending}} tasks +- [ ] Review completed work +{{/if}} +``` diff --git a/.claude/skills/ccw-loop/templates/understanding-template.md b/.claude/skills/ccw-loop/templates/understanding-template.md new file mode 100644 index 00000000..c1069597 --- /dev/null +++ b/.claude/skills/ccw-loop/templates/understanding-template.md @@ -0,0 +1,303 @@ +# Understanding Document Template + +调试理解演变文档的标准模板。 + +## Template Structure + +```markdown +# Understanding Document + +**Session ID**: {{session_id}} +**Bug Description**: {{bug_description}} +**Started**: {{started_at}} + +--- + +## Exploration Timeline + +{{#each iterations}} +### Iteration {{number}} - {{title}} ({{timestamp}}) + +{{#if is_exploration}} +#### Current Understanding + +Based on bug description and initial code search: + +- Error pattern: {{error_pattern}} +- Affected areas: {{affected_areas}} +- Initial hypothesis: {{initial_thoughts}} + +#### Evidence from Code Search + +{{#each search_results}} +**Keyword: "{{keyword}}"** +- Found in: {{files}} +- Key findings: {{insights}} +{{/each}} +{{/if}} + +{{#if has_hypotheses}} +#### Hypotheses Generated (Gemini-Assisted) + +{{#each hypotheses}} +**{{id}}** (Likelihood: {{likelihood}}): {{description}} +- Logging at: {{logging_point}} +- Testing: {{testable_condition}} +- Evidence to confirm: {{confirm_criteria}} +- Evidence to reject: {{reject_criteria}} +{{/each}} + +**Gemini Insights**: {{gemini_insights}} +{{/if}} + +{{#if is_analysis}} +#### Log Analysis Results + +{{#each results}} +**{{id}}**: {{verdict}} +- Evidence: {{evidence}} +- Reasoning: {{reason}} +{{/each}} + +#### Corrected Understanding + +Previous misunderstandings identified and corrected: + +{{#each corrections}} +- ~~{{wrong}}~~ → {{corrected}} + - Why wrong: {{reason}} + - Evidence: {{evidence}} +{{/each}} + +#### New Insights + +{{#each insights}} +- {{this}} +{{/each}} + +#### Gemini Analysis + +{{gemini_analysis}} +{{/if}} + +{{#if root_cause_found}} +#### Root Cause Identified + +**{{hypothesis_id}}**: {{description}} + +Evidence supporting this conclusion: +{{supporting_evidence}} +{{else}} +#### Next Steps + +{{next_steps}} +{{/if}} + +--- +{{/each}} + +## Current Consolidated Understanding + +### What We Know + +{{#each valid_understandings}} +- {{this}} +{{/each}} + +### What Was Disproven + +{{#each disproven}} +- ~~{{assumption}}~~ (Evidence: {{evidence}}) +{{/each}} + +### Current Investigation Focus + +{{current_focus}} + +### Remaining Questions + +{{#each questions}} +- {{this}} +{{/each}} +``` + +## Template Variables + +| Variable | Type | Source | Description | +|----------|------|--------|-------------| +| `session_id` | string | state.session_id | 会话 ID | +| `bug_description` | string | state.debug.current_bug | Bug 描述 | +| `iterations` | array | 从文件解析 | 迭代历史 | +| `hypotheses` | array | state.debug.hypotheses | 假设列表 | +| `valid_understandings` | array | 从 Gemini 分析 | 有效理解 | +| `disproven` | array | 从假设状态 | 被否定的假设 | + +## Section Templates + +### Exploration Section + +```markdown +### Iteration {{N}} - Initial Exploration ({{timestamp}}) + +#### Current Understanding + +Based on bug description and initial code search: + +- Error pattern: {{pattern}} +- Affected areas: {{areas}} +- Initial hypothesis: {{thoughts}} + +#### Evidence from Code Search + +{{#each search_results}} +**Keyword: "{{keyword}}"** +- Found in: {{files}} +- Key findings: {{insights}} +{{/each}} + +#### Next Steps + +- Generate testable hypotheses +- Add instrumentation +- Await reproduction +``` + +### Hypothesis Section + +```markdown +#### Hypotheses Generated (Gemini-Assisted) + +| ID | Description | Likelihood | Status | +|----|-------------|------------|--------| +{{#each hypotheses}} +| {{id}} | {{description}} | {{likelihood}} | {{status}} | +{{/each}} + +**Details:** + +{{#each hypotheses}} +**{{id}}**: {{description}} +- Logging at: `{{logging_point}}` +- Testing: {{testable_condition}} +- Confirm: {{evidence_criteria.confirm}} +- Reject: {{evidence_criteria.reject}} +{{/each}} +``` + +### Analysis Section + +```markdown +### Iteration {{N}} - Evidence Analysis ({{timestamp}}) + +#### Log Analysis Results + +{{#each results}} +**{{id}}**: **{{verdict}}** +- Evidence: \`{{evidence}}\` +- Reasoning: {{reason}} +{{/each}} + +#### Corrected Understanding + +| Previous Assumption | Corrected To | Reason | +|---------------------|--------------|--------| +{{#each corrections}} +| ~~{{wrong}}~~ | {{corrected}} | {{reason}} | +{{/each}} + +#### Gemini Analysis + +{{gemini_analysis}} +``` + +### Consolidated Understanding Section + +```markdown +## Current Consolidated Understanding + +### What We Know + +{{#each valid}} +- {{this}} +{{/each}} + +### What Was Disproven + +{{#each disproven}} +- ~~{{this.assumption}}~~ (Evidence: {{this.evidence}}) +{{/each}} + +### Current Investigation Focus + +{{focus}} + +### Remaining Questions + +{{#each questions}} +- {{this}} +{{/each}} +``` + +### Resolution Section + +```markdown +### Resolution ({{timestamp}}) + +#### Fix Applied + +- Modified files: {{files}} +- Fix description: {{description}} +- Root cause addressed: {{root_cause}} + +#### Verification Results + +{{verification}} + +#### Lessons Learned + +{{#each lessons}} +{{@index}}. {{this}} +{{/each}} + +#### Key Insights for Future + +{{#each insights}} +- {{this}} +{{/each}} +``` + +## Consolidation Rules + +更新 "Current Consolidated Understanding" 时遵循以下规则: + +1. **简化被否定项**: 移到 "What Was Disproven",只保留单行摘要 +2. **保留有效见解**: 将确认的发现提升到 "What We Know" +3. **避免重复**: 不在合并部分重复时间线细节 +4. **关注当前状态**: 描述现在知道什么,而不是过程 +5. **保留关键纠正**: 保留重要的 wrong→right 转换供学习 + +## Anti-Patterns + +**错误示例 (冗余)**: +```markdown +## Current Consolidated Understanding + +In iteration 1 we thought X, but in iteration 2 we found Y, then in iteration 3... +Also we checked A and found B, and then we checked C... +``` + +**正确示例 (精简)**: +```markdown +## Current Consolidated Understanding + +### What We Know +- Error occurs during runtime update, not initialization +- Config value is None (not missing key) + +### What Was Disproven +- ~~Initialization error~~ (Timing evidence) +- ~~Missing key hypothesis~~ (Key exists) + +### Current Investigation Focus +Why is config value None during update? +``` diff --git a/.claude/skills/ccw-loop/templates/validation-template.md b/.claude/skills/ccw-loop/templates/validation-template.md new file mode 100644 index 00000000..8c06e6e1 --- /dev/null +++ b/.claude/skills/ccw-loop/templates/validation-template.md @@ -0,0 +1,258 @@ +# Validation Report Template + +验证报告的标准模板。 + +## Template Structure + +```markdown +# Validation Report + +**Session ID**: {{session_id}} +**Task**: {{task_description}} +**Validated**: {{timestamp}} + +--- + +## Iteration {{iteration}} - Validation Run + +### Test Execution Summary + +| Metric | Value | +|--------|-------| +| Total Tests | {{total_tests}} | +| Passed | {{passed_tests}} | +| Failed | {{failed_tests}} | +| Skipped | {{skipped_tests}} | +| Duration | {{duration}}ms | +| **Pass Rate** | **{{pass_rate}}%** | + +### Coverage Report + +{{#if has_coverage}} +| File | Statements | Branches | Functions | Lines | +|------|------------|----------|-----------|-------| +{{#each coverage_files}} +| {{path}} | {{statements}}% | {{branches}}% | {{functions}}% | {{lines}}% | +{{/each}} + +**Overall Coverage**: {{overall_coverage}}% +{{else}} +_No coverage data available_ +{{/if}} + +### Failed Tests + +{{#if has_failures}} +{{#each failures}} +#### {{test_name}} + +- **Suite**: {{suite}} +- **Error**: {{error_message}} +- **Stack**: +\`\`\` +{{stack_trace}} +\`\`\` +{{/each}} +{{else}} +_All tests passed_ +{{/if}} + +### Gemini Quality Analysis + +{{gemini_analysis}} + +### Recommendations + +{{#each recommendations}} +- {{this}} +{{/each}} + +--- + +## Validation Decision + +**Result**: {{#if passed}}✅ PASS{{else}}❌ FAIL{{/if}} + +**Rationale**: {{rationale}} + +{{#if not_passed}} +### Next Actions + +1. Review failed tests +2. Debug failures using action-debug-with-file +3. Fix issues and re-run validation +{{else}} +### Next Actions + +1. Consider code review +2. Prepare for deployment +3. Update documentation +{{/if}} +``` + +## Template Variables + +| Variable | Type | Source | Description | +|----------|------|--------|-------------| +| `session_id` | string | state.session_id | 会话 ID | +| `task_description` | string | state.task_description | 任务描述 | +| `timestamp` | string | 当前时间 | 验证时间 | +| `iteration` | number | 从文件计算 | 验证迭代次数 | +| `total_tests` | number | 测试输出 | 总测试数 | +| `passed_tests` | number | 测试输出 | 通过数 | +| `failed_tests` | number | 测试输出 | 失败数 | +| `pass_rate` | number | 计算得出 | 通过率 | +| `coverage_files` | array | 覆盖率报告 | 文件覆盖率 | +| `failures` | array | 测试输出 | 失败测试详情 | +| `gemini_analysis` | string | Gemini CLI | 质量分析 | +| `recommendations` | array | Gemini CLI | 建议列表 | + +## Section Templates + +### Test Summary + +```markdown +### Test Execution Summary + +| Metric | Value | +|--------|-------| +| Total Tests | {{total}} | +| Passed | {{passed}} | +| Failed | {{failed}} | +| Skipped | {{skipped}} | +| Duration | {{duration}}ms | +| **Pass Rate** | **{{rate}}%** | +``` + +### Coverage Table + +```markdown +### Coverage Report + +| File | Statements | Branches | Functions | Lines | +|------|------------|----------|-----------|-------| +{{#each files}} +| `{{path}}` | {{statements}}% | {{branches}}% | {{functions}}% | {{lines}}% | +{{/each}} + +**Overall Coverage**: {{overall}}% + +**Coverage Thresholds**: +- ✅ Good: ≥ 80% +- ⚠️ Warning: 60-79% +- ❌ Poor: < 60% +``` + +### Failed Test Details + +```markdown +### Failed Tests + +{{#each failures}} +#### ❌ {{test_name}} + +| Field | Value | +|-------|-------| +| Suite | {{suite}} | +| Error | {{error_message}} | +| Duration | {{duration}}ms | + +**Stack Trace**: +\`\`\` +{{stack_trace}} +\`\`\` + +**Possible Causes**: +{{#each possible_causes}} +- {{this}} +{{/each}} + +--- +{{/each}} +``` + +### Quality Analysis + +```markdown +### Gemini Quality Analysis + +#### Code Quality Assessment + +| Dimension | Score | Status | +|-----------|-------|--------| +| Correctness | {{correctness}}/10 | {{correctness_status}} | +| Completeness | {{completeness}}/10 | {{completeness_status}} | +| Reliability | {{reliability}}/10 | {{reliability_status}} | +| Maintainability | {{maintainability}}/10 | {{maintainability_status}} | + +#### Key Findings + +{{#each findings}} +- **{{severity}}**: {{description}} +{{/each}} + +#### Recommendations + +{{#each recommendations}} +{{@index}}. {{this}} +{{/each}} +``` + +### Decision Section + +```markdown +## Validation Decision + +**Result**: {{#if passed}}✅ PASS{{else}}❌ FAIL{{/if}} + +**Rationale**: +{{rationale}} + +**Confidence Level**: {{confidence}} + +### Decision Matrix + +| Criteria | Status | Weight | Score | +|----------|--------|--------|-------| +| All tests pass | {{tests_pass}} | 40% | {{tests_score}} | +| Coverage ≥ 80% | {{coverage_pass}} | 30% | {{coverage_score}} | +| No critical issues | {{no_critical}} | 20% | {{critical_score}} | +| Quality analysis pass | {{quality_pass}} | 10% | {{quality_score}} | +| **Total** | | 100% | **{{total_score}}** | + +**Threshold**: 70% to pass + +### Next Actions + +{{#if passed}} +1. ✅ Code review (recommended) +2. ✅ Update documentation +3. ✅ Prepare for deployment +{{else}} +1. ❌ Review failed tests +2. ❌ Debug failures +3. ❌ Fix issues and re-run +{{/if}} +``` + +## Historical Comparison + +```markdown +## Validation History + +| Iteration | Date | Pass Rate | Coverage | Status | +|-----------|------|-----------|----------|--------| +{{#each history}} +| {{iteration}} | {{date}} | {{pass_rate}}% | {{coverage}}% | {{status}} | +{{/each}} + +### Trend Analysis + +{{#if improving}} +📈 **Improving**: Pass rate increased from {{previous_rate}}% to {{current_rate}}% +{{else if declining}} +📉 **Declining**: Pass rate decreased from {{previous_rate}}% to {{current_rate}}% +{{else}} +➡️ **Stable**: Pass rate remains at {{current_rate}}% +{{/if}} +``` diff --git a/.test-loop-comprehensive/.task/E2E-TASK-1769007254162.json b/.test-loop-comprehensive/.task/E2E-TASK-1769007254162.json new file mode 100644 index 00000000..65c7ab99 --- /dev/null +++ b/.test-loop-comprehensive/.task/E2E-TASK-1769007254162.json @@ -0,0 +1,29 @@ +{ + "id": "E2E-TASK-1769007254162", + "title": "Test Task E2E-TASK-1769007254162", + "description": "Test task with loop control", + "status": "pending", + "loop_control": { + "enabled": true, + "description": "Test loop", + "max_iterations": 3, + "success_condition": "current_iteration >= 3", + "error_policy": { + "on_failure": "pause", + "max_retries": 3 + }, + "cli_sequence": [ + { + "step_id": "step1", + "tool": "bash", + "command": "echo \"iteration\"" + }, + { + "step_id": "step2", + "tool": "gemini", + "mode": "analysis", + "prompt_template": "Process output" + } + ] + } +} \ No newline at end of file diff --git a/ccw/src/core/dashboard-generator.ts b/ccw/src/core/dashboard-generator.ts index ddc31e99..f1e08531 100644 --- a/ccw/src/core/dashboard-generator.ts +++ b/ccw/src/core/dashboard-generator.ts @@ -99,7 +99,10 @@ const MODULE_CSS_FILES = [ '29-help.css', '30-core-memory.css', '31-api-settings.css', - '34-discovery.css' + '32-issue-manager.css', + '33-cli-stream-viewer.css', + '34-discovery.css', + '36-loop-monitor.css' ]; const MODULE_FILES = [ diff --git a/ccw/src/core/routes/cli-routes.ts b/ccw/src/core/routes/cli-routes.ts index 521cfba3..d15db144 100644 --- a/ccw/src/core/routes/cli-routes.ts +++ b/ccw/src/core/routes/cli-routes.ts @@ -60,9 +60,35 @@ interface ActiveExecution { startTime: number; output: string; status: 'running' | 'completed' | 'error'; + completedTimestamp?: number; // When execution completed (for 5-minute retention) } const activeExecutions = new Map(); +const EXECUTION_RETENTION_MS = 5 * 60 * 1000; // 5 minutes + +/** + * Cleanup stale completed executions older than retention period + * Runs periodically to prevent memory buildup + */ +export function cleanupStaleExecutions(): void { + const now = Date.now(); + const staleIds: string[] = []; + + for (const [id, exec] of activeExecutions.entries()) { + if (exec.completedTimestamp && (now - exec.completedTimestamp) > EXECUTION_RETENTION_MS) { + staleIds.push(id); + } + } + + staleIds.forEach(id => { + activeExecutions.delete(id); + console.log(`[ActiveExec] Cleaned up stale execution: ${id}`); + }); + + if (staleIds.length > 0) { + console.log(`[ActiveExec] Cleaned up ${staleIds.length} stale execution(s), remaining: ${activeExecutions.size}`); + } +} /** * Get all active CLI executions @@ -113,19 +139,12 @@ export function updateActiveExecution(event: { activeExec.output += output; } } else if (type === 'completed') { - // Mark as completed instead of immediately deleting - // Keep execution visible for 5 minutes to allow page refreshes to see it + // Mark as completed with timestamp for retention-based cleanup const activeExec = activeExecutions.get(executionId); if (activeExec) { activeExec.status = success ? 'completed' : 'error'; - - // Auto-cleanup after 5 minutes - setTimeout(() => { - activeExecutions.delete(executionId); - console.log(`[ActiveExec] Auto-cleaned completed execution: ${executionId}`); - }, 5 * 60 * 1000); - - console.log(`[ActiveExec] Marked as ${activeExec.status}, will auto-clean in 5 minutes`); + activeExec.completedTimestamp = Date.now(); + console.log(`[ActiveExec] Marked as ${activeExec.status}, retained for ${EXECUTION_RETENTION_MS / 1000}s`); } } } @@ -139,7 +158,10 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { // API: Get Active CLI Executions (for state recovery) if (pathname === '/api/cli/active' && req.method === 'GET') { - const executions = getActiveExecutions(); + const executions = getActiveExecutions().map(exec => ({ + ...exec, + isComplete: exec.status !== 'running' + })); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ executions })); return true; @@ -664,8 +686,13 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { }); }); - // Remove from active executions on completion - activeExecutions.delete(executionId); + // Mark as completed with timestamp for retention-based cleanup (not immediate delete) + const activeExec = activeExecutions.get(executionId); + if (activeExec) { + activeExec.status = result.success ? 'completed' : 'error'; + activeExec.completedTimestamp = Date.now(); + console.log(`[ActiveExec] Direct execution ${executionId} marked as ${activeExec.status}, retained for ${EXECUTION_RETENTION_MS / 1000}s`); + } // Broadcast completion broadcastToClients({ @@ -684,8 +711,13 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { }; } catch (error: unknown) { - // Remove from active executions on error - activeExecutions.delete(executionId); + // Mark as completed with timestamp for retention-based cleanup (not immediate delete) + const activeExec = activeExecutions.get(executionId); + if (activeExec) { + activeExec.status = 'error'; + activeExec.completedTimestamp = Date.now(); + console.log(`[ActiveExec] Direct execution ${executionId} marked as error, retained for ${EXECUTION_RETENTION_MS / 1000}s`); + } broadcastToClients({ type: 'CLI_EXECUTION_ERROR', diff --git a/ccw/src/core/routes/loop-v2-routes.ts b/ccw/src/core/routes/loop-v2-routes.ts new file mode 100644 index 00000000..db4556e8 --- /dev/null +++ b/ccw/src/core/routes/loop-v2-routes.ts @@ -0,0 +1,1332 @@ +/** + * Loop V2 Routes Module + * CCW Loop System - Simplified HTTP API endpoints for Dashboard + * Provides simplified loop CRUD operations independent of task files + * + * Loop Endpoints: + * - GET /api/loops/v2 - List all loops with pagination + * - POST /api/loops/v2 - Create loop with {title, description, max_iterations} + * - GET /api/loops/v2/:loopId - Get loop details + * - PUT /api/loops/v2/:loopId - Update loop metadata + * - DELETE /api/loops/v2/:loopId - Delete loop + * - POST /api/loops/v2/:loopId/start - Start loop execution + * - POST /api/loops/v2/:loopId/pause - Pause loop + * - POST /api/loops/v2/:loopId/resume - Resume loop + * - POST /api/loops/v2/:loopId/stop - Stop loop + * + * Task Management Endpoints: + * - POST /api/loops/v2/:loopId/tasks - Add task to loop + * - GET /api/loops/v2/:loopId/tasks - List all tasks for loop + * - PUT /api/loops/v2/tasks/:taskId - Update task (requires loop_id in body) + * - DELETE /api/loops/v2/tasks/:taskId - Delete task (requires loop_id query param) + * - PUT /api/loops/v2/:loopId/tasks/reorder - Reorder tasks with {ordered_task_ids: string[]} + * + * Advanced Task Features: + * - POST /api/loops/v2/:loopId/import - Import tasks from issue with {issue_id} + * - POST /api/loops/v2/:loopId/generate - Generate tasks via Gemini with {tool?, count?} + */ + +import { join } from 'path'; +import { randomBytes } from 'crypto'; +import type { RouteContext } from './types.js'; +import { LoopStatus } from '../../types/loop.js'; +import type { LoopState } from '../../types/loop.js'; +import { TaskStorageManager, type TaskCreateRequest, type TaskUpdateRequest, type TaskReorderRequest } from '../../tools/loop-task-manager.js'; +import { executeCliTool } from '../../tools/cli-executor.js'; + +/** + * V2 Loop Create Request + */ +interface V2LoopCreateRequest { + title: string; + description?: string; + max_iterations?: number; +} + +/** + * V2 Loop Update Request + */ +interface V2LoopUpdateRequest { + title?: string; + description?: string; + max_iterations?: number; +} + +/** + * V2 Loop Storage Format (simplified, independent of task files) + */ +interface V2LoopStorage { + loop_id: string; + title: string; + description: string; + max_iterations: number; + status: LoopStatus; + current_iteration: number; + created_at: string; + updated_at: string; + completed_at?: string; + failure_reason?: string; + // Tasks stored in separate tasks.jsonl file +} + +/** + * Handle V2 loop routes + * @returns true if route was handled, false otherwise + */ +export async function handleLoopV2Routes(ctx: RouteContext): Promise { + const { pathname, req, res, initialPath, handlePostRequest, url, broadcastToClients } = ctx; + + // Get workflow directory from initialPath + const workflowDir = initialPath || process.cwd(); + const loopDir = join(workflowDir, '.workflow', '.loop'); + + // Helper to broadcast loop state updates + const broadcastStateUpdate = (loopId: string, status: LoopStatus): void => { + try { + broadcastToClients({ + type: 'LOOP_STATE_UPDATE', + loop_id: loopId, + status: status as 'created' | 'running' | 'paused' | 'completed' | 'failed', + updated_at: new Date().toISOString() + }); + } catch (error) { + // Silently ignore broadcast errors + } + }; + + // Helper to generate loop ID + const generateLoopId = (): string => { + const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0]; + const random = randomBytes(4).toString('hex'); + return `loop-v2-${timestamp}-${random}`; + }; + + // Helper to read loop storage + const readLoopStorage = async (loopId: string): Promise => { + const { readFile } = await import('fs/promises'); + const { existsSync } = await import('fs'); + const filePath = join(loopDir, `${loopId}.json`); + + if (!existsSync(filePath)) { + return null; + } + + try { + const content = await readFile(filePath, 'utf-8'); + return JSON.parse(content) as V2LoopStorage; + } catch { + return null; + } + }; + + // Helper to write loop storage + const writeLoopStorage = async (loop: V2LoopStorage): Promise => { + const { writeFile, mkdir } = await import('fs/promises'); + const { existsSync } = await import('fs'); + + if (!existsSync(loopDir)) { + await mkdir(loopDir, { recursive: true }); + } + + const filePath = join(loopDir, `${loop.loop_id}.json`); + await writeFile(filePath, JSON.stringify(loop, null, 2), 'utf-8'); + }; + + // Helper to delete loop storage + const deleteLoopStorage = async (loopId: string): Promise => { + const { unlink } = await import('fs/promises'); + const { existsSync } = await import('fs'); + const filePath = join(loopDir, `${loopId}.json`); + + if (existsSync(filePath)) { + await unlink(filePath); + } + + // Also delete tasks.jsonl if exists + const tasksPath = join(loopDir, `${loopId}.tasks.jsonl`); + if (existsSync(tasksPath)) { + await unlink(tasksPath).catch(() => {}); + } + }; + + // Helper to list all loops + const listLoops = async (): Promise => { + const { readdir } = await import('fs/promises'); + const { existsSync } = await import('fs'); + + if (!existsSync(loopDir)) { + return []; + } + + const files = await readdir(loopDir); + const loopFiles = files.filter(f => f.startsWith('loop-v2-') && f.endsWith('.json')); + + const loops: V2LoopStorage[] = []; + for (const file of loopFiles) { + const loopId = file.replace('.json', ''); + const loop = await readLoopStorage(loopId); + if (loop) { + loops.push(loop); + } + } + + return loops; + }; + + // ==== EXACT PATH ROUTES ==== + + // POST /api/loops/v2 - Create loop with simplified fields + if (pathname === '/api/loops/v2' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { title, description, max_iterations } = body as V2LoopCreateRequest; + + // Validation + if (!title || typeof title !== 'string' || title.trim().length === 0) { + return { success: false, error: 'title is required and must be non-empty', status: 400 }; + } + + if (description !== undefined && typeof description !== 'string') { + return { success: false, error: 'description must be a string', status: 400 }; + } + + if (max_iterations !== undefined && (typeof max_iterations !== 'number' || max_iterations < 1)) { + return { success: false, error: 'max_iterations must be a positive number', status: 400 }; + } + + try { + const loopId = generateLoopId(); + const now = new Date().toISOString(); + + const loop: V2LoopStorage = { + loop_id: loopId, + title: title.trim(), + description: description?.trim() || '', + max_iterations: max_iterations || 10, + status: LoopStatus.CREATED, + current_iteration: 0, + created_at: now, + updated_at: now + }; + + await writeLoopStorage(loop); + + // Broadcast creation + broadcastStateUpdate(loopId, LoopStatus.CREATED); + + return { success: true, data: loop }; + } catch (error) { + return { success: false, error: (error as Error).message, status: 500 }; + } + }); + return true; + } + + // GET /api/loops/v2 - List all loops with pagination + if (pathname === '/api/loops/v2' && req.method === 'GET') { + try { + const loops = await listLoops(); + + // Parse query params for pagination and filtering + const searchParams = url?.searchParams; + let filteredLoops = loops; + + // Filter by status + const statusFilter = searchParams?.get('status'); + if (statusFilter && statusFilter !== 'all') { + filteredLoops = filteredLoops.filter(l => l.status === statusFilter); + } + + // Sort by updated_at (most recent first) + filteredLoops.sort((a, b) => + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() + ); + + // Parse pagination params + const limit = parseInt(searchParams?.get('limit') || '50', 10); + const offset = parseInt(searchParams?.get('offset') || '0', 10); + + // Apply pagination + const paginatedLoops = filteredLoops.slice(offset, offset + limit); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + data: paginatedLoops, + total: filteredLoops.length, + limit, + offset, + hasMore: offset + limit < filteredLoops.length, + timestamp: new Date().toISOString() + })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // ==== NESTED PATH ROUTES (more specific patterns first) ==== + + // POST /api/loops/v2/:loopId/start - Start loop execution + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/start$/) && req.method === 'POST') { + const loopId = pathname.split('/').slice(-2)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + try { + const loop = await readLoopStorage(loopId); + if (!loop) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Loop not found' })); + return true; + } + + // Can only start created or paused loops + if (!['created', 'paused'].includes(loop.status.toLowerCase())) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: `Cannot start loop with status: ${loop.status}` + })); + return true; + } + + // Update loop status + loop.status = LoopStatus.RUNNING; + loop.updated_at = new Date().toISOString(); + await writeLoopStorage(loop); + + // Broadcast state update + broadcastStateUpdate(loopId, LoopStatus.RUNNING); + + // Trigger ccw-loop skill execution (non-blocking) + // The skill will check status before each action and exit gracefully on pause/stop + executeCliTool({ + tool: 'claude', + prompt: `/ccw-loop --loop-id ${loopId} --auto`, + mode: 'write', + workingDir: workflowDir + }).catch((error) => { + // Log error but don't fail the start request + console.error(`Failed to trigger ccw-loop skill for ${loopId}:`, error); + // Update loop status to failed + readLoopStorage(loopId).then(async (failedLoop) => { + if (failedLoop) { + failedLoop.status = LoopStatus.FAILED; + failedLoop.failure_reason = `Skill execution failed: ${error.message}`; + failedLoop.completed_at = new Date().toISOString(); + await writeLoopStorage(failedLoop); + broadcastStateUpdate(loopId, LoopStatus.FAILED); + } + }); + }); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, data: loop, message: 'Loop started' })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // POST /api/loops/v2/:loopId/pause - Pause loop + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/pause$/) && req.method === 'POST') { + const loopId = pathname.split('/').slice(-2)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + try { + const loop = await readLoopStorage(loopId); + if (!loop) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Loop not found' })); + return true; + } + + // Can only pause running loops + if (loop.status !== LoopStatus.RUNNING) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: `Cannot pause loop with status: ${loop.status}` + })); + return true; + } + + loop.status = LoopStatus.PAUSED; + loop.updated_at = new Date().toISOString(); + await writeLoopStorage(loop); + + broadcastStateUpdate(loopId, LoopStatus.PAUSED); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, data: loop, message: 'Loop paused' })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // POST /api/loops/v2/:loopId/resume - Resume loop + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/resume$/) && req.method === 'POST') { + const loopId = pathname.split('/').slice(-2)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + try { + const loop = await readLoopStorage(loopId); + if (!loop) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Loop not found' })); + return true; + } + + // Can only resume paused loops + if (loop.status !== LoopStatus.PAUSED) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: `Cannot resume loop with status: ${loop.status}` + })); + return true; + } + + loop.status = LoopStatus.RUNNING; + loop.updated_at = new Date().toISOString(); + await writeLoopStorage(loop); + + broadcastStateUpdate(loopId, LoopStatus.RUNNING); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, data: loop, message: 'Loop resumed' })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // POST /api/loops/v2/:loopId/stop - Stop loop + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/stop$/) && req.method === 'POST') { + const loopId = pathname.split('/').slice(-2)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + try { + const loop = await readLoopStorage(loopId); + if (!loop) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Loop not found' })); + return true; + } + + // Can only stop running or paused loops + if (![LoopStatus.RUNNING, LoopStatus.PAUSED, LoopStatus.CREATED].includes(loop.status)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: `Cannot stop loop with status: ${loop.status}` + })); + return true; + } + + loop.status = LoopStatus.FAILED; + loop.failure_reason = 'Manually stopped by user'; + loop.completed_at = new Date().toISOString(); + loop.updated_at = loop.completed_at; + await writeLoopStorage(loop); + + broadcastStateUpdate(loopId, LoopStatus.FAILED); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, data: loop, message: 'Loop stopped' })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // ==== SINGLE PARAM ROUTES (must come after nested routes) ==== + + // GET /api/loops/v2/:loopId - Get loop details + if (pathname.match(/^\/api\/loops\/v2\/[^/]+$/) && req.method === 'GET') { + const loopId = pathname.split('/').pop(); + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + try { + const loop = await readLoopStorage(loopId); + if (!loop) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Loop not found' })); + return true; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, data: loop })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // PUT /api/loops/v2/:loopId - Update loop metadata + if (pathname.match(/^\/api\/loops\/v2\/[^/]+$/) && req.method === 'PUT') { + const loopId = pathname.split('/').pop(); + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + handlePostRequest(req, res, async (body) => { + const { title, description, max_iterations } = body as V2LoopUpdateRequest; + + try { + const loop = await readLoopStorage(loopId); + if (!loop) { + return { success: false, error: 'Loop not found', status: 404 }; + } + + // Can only update created or paused loops + if (![LoopStatus.CREATED, LoopStatus.PAUSED, LoopStatus.FAILED, LoopStatus.COMPLETED].includes(loop.status)) { + return { success: false, error: `Cannot update loop with status: ${loop.status}`, status: 400 }; + } + + // Validate and apply updates + if (title !== undefined) { + if (typeof title !== 'string' || title.trim().length === 0) { + return { success: false, error: 'title must be a non-empty string', status: 400 }; + } + loop.title = title.trim(); + } + + if (description !== undefined) { + if (typeof description !== 'string') { + return { success: false, error: 'description must be a string', status: 400 }; + } + loop.description = description.trim(); + } + + if (max_iterations !== undefined) { + if (typeof max_iterations !== 'number' || max_iterations < 1) { + return { success: false, error: 'max_iterations must be a positive number', status: 400 }; + } + loop.max_iterations = max_iterations; + } + + loop.updated_at = new Date().toISOString(); + await writeLoopStorage(loop); + + broadcastStateUpdate(loopId, loop.status); + + return { success: true, data: loop }; + } catch (error) { + return { success: false, error: (error as Error).message, status: 500 }; + } + }); + return true; + } + + // DELETE /api/loops/v2/:loopId - Delete loop + if (pathname.match(/^\/api\/loops\/v2\/[^/]+$/) && req.method === 'DELETE') { + const loopId = pathname.split('/').pop(); + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + try { + const loop = await readLoopStorage(loopId); + if (!loop) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Loop not found' })); + return true; + } + + // Cannot delete running loops + if (loop.status === LoopStatus.RUNNING) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: 'Cannot delete running loop. Stop it first.' + })); + return true; + } + + await deleteLoopStorage(loopId); + + // Broadcast deletion + try { + broadcastToClients({ + type: 'LOOP_DELETED', + loop_id: loopId + }); + } catch { + // Ignore broadcast errors + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, message: 'Loop deleted' })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // ==== TASK MANAGEMENT ENDPOINTS ==== + + // Helper to create TaskStorageManager instance + const createTaskManager = (): TaskStorageManager => { + return new TaskStorageManager(workflowDir); + }; + + // POST /api/loops/v2/:loopId/tasks - Add task to loop + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/tasks$/) && req.method === 'POST') { + const loopId = pathname.split('/').slice(-2)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + handlePostRequest(req, res, async (body) => { + const { description, tool, mode, prompt_template, command, on_error } = body as TaskCreateRequest; + + // Validation + if (!description || typeof description !== 'string' || description.trim().length === 0) { + return { success: false, error: 'description is required', status: 400 }; + } + + if (!tool || typeof tool !== 'string') { + return { success: false, error: 'tool is required', status: 400 }; + } + + const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude']; + if (!validTools.includes(tool)) { + return { success: false, error: `tool must be one of: ${validTools.join(', ')}`, status: 400 }; + } + + if (!mode || typeof mode !== 'string') { + return { success: false, error: 'mode is required', status: 400 }; + } + + const validModes = ['analysis', 'write', 'review']; + if (!validModes.includes(mode)) { + return { success: false, error: `mode must be one of: ${validModes.join(', ')}`, status: 400 }; + } + + if (!prompt_template || typeof prompt_template !== 'string' || prompt_template.trim().length === 0) { + return { success: false, error: 'prompt_template is required', status: 400 }; + } + + try { + const taskManager = createTaskManager(); + const task = await taskManager.addTask(loopId, { + description: description.trim(), + tool, + mode, + prompt_template: prompt_template.trim(), + command, + on_error + }); + + // Broadcast task added + try { + broadcastToClients({ + type: 'TASK_ADDED', + loop_id: loopId, + task_id: task.task_id, + task: task + }); + } catch { + // Ignore broadcast errors + } + + return { success: true, data: task }; + } catch (error) { + return { success: false, error: (error as Error).message, status: 500 }; + } + }); + return true; + } + + // GET /api/loops/v2/:loopId/tasks - List all tasks for loop + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/tasks$/) && req.method === 'GET') { + const loopId = pathname.split('/').slice(-2)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + try { + const taskManager = createTaskManager(); + const tasks = await taskManager.getTasks(loopId); + + // Sort by order + tasks.sort((a, b) => a.order - b.order); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + data: tasks, + total: tasks.length, + loop_id: loopId, + timestamp: new Date().toISOString() + })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // GET /api/loops/v2/tasks/:taskId - Get single task (taskId lookup) + if (pathname.match(/^\/api\/loops\/v2\/tasks\/[^/]+$/) && req.method === 'GET') { + const taskId = pathname.split('/').pop(); + if (!taskId || !isValidId(taskId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid task ID format' })); + return true; + } + + try { + const taskManager = createTaskManager(); + // Get all loops and search for the task + const loops = await listLoops(); + let foundTask = null; + let foundLoopId = null; + + for (const loop of loops) { + const loopId = loop.loop_id; + try { + const tasks = await taskManager.getTasks(loopId); + const task = tasks.find(t => t.task_id === taskId); + if (task) { + foundTask = task; + foundLoopId = loopId; + break; + } + } catch { + continue; + } + } + + if (!foundTask) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Task not found' })); + return true; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + data: { ...foundTask, loop_id: foundLoopId }, + timestamp: new Date().toISOString() + })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // PUT /api/loops/v2/tasks/:taskId - Update task (taskId lookup) + if (pathname.match(/^\/api\/loops\/v2\/tasks\/[^/]+$/) && req.method === 'PUT') { + const taskId = pathname.split('/').pop(); + if (!taskId || !isValidId(taskId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid task ID format' })); + return true; + } + + handlePostRequest(req, res, async (body) => { + const { loop_id, description, tool, mode, prompt_template, command, on_error } = body as TaskUpdateRequest & { loop_id?: string }; + + if (!loop_id || typeof loop_id !== 'string') { + return { success: false, error: 'loop_id is required', status: 400 }; + } + + if (!isValidId(loop_id)) { + return { success: false, error: 'Invalid loop_id format', status: 400 }; + } + + try { + const taskManager = createTaskManager(); + const updatedTask = await taskManager.updateTask(loop_id, taskId, { + description, + tool, + mode, + prompt_template, + command, + on_error + }); + + if (!updatedTask) { + return { success: false, error: 'Task not found', status: 404 }; + } + + // Broadcast task updated + try { + broadcastToClients({ + type: 'TASK_UPDATED', + loop_id: loop_id, + task_id: taskId, + task: updatedTask + }); + } catch { + // Ignore broadcast errors + } + + return { success: true, data: updatedTask }; + } catch (error) { + return { success: false, error: (error as Error).message, status: 500 }; + } + }); + return true; + } + + // DELETE /api/loops/v2/tasks/:taskId - Delete task (taskId lookup) + if (pathname.match(/^\/api\/loops\/v2\/tasks\/[^/]+$/) && req.method === 'DELETE') { + const taskId = pathname.split('/').pop(); + if (!taskId || !isValidId(taskId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid task ID format' })); + return true; + } + + // Get loop_id from query parameter + const urlObj = new URL(req.url || '', `http://localhost`); + const loopId = urlObj.searchParams.get('loop_id'); + + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'loop_id query parameter is required' })); + return true; + } + + try { + const taskManager = createTaskManager(); + const deleted = await taskManager.deleteTask(loopId, taskId); + + if (!deleted) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Task not found' })); + return true; + } + + // Broadcast task deleted + try { + broadcastToClients({ + type: 'TASK_DELETED', + loop_id: loopId, + task_id: taskId + }); + } catch { + // Ignore broadcast errors + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, message: 'Task deleted' })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: (error as Error).message })); + return true; + } + } + + // PUT /api/loops/v2/:loopId/tasks/reorder - Reorder tasks + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/tasks\/reorder$/) && req.method === 'PUT') { + const loopId = pathname.split('/').slice(-3)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + handlePostRequest(req, res, async (body) => { + const { ordered_task_ids } = body as TaskReorderRequest; + + if (!ordered_task_ids || !Array.isArray(ordered_task_ids)) { + return { success: false, error: 'ordered_task_ids must be an array', status: 400 }; + } + + if (ordered_task_ids.length === 0) { + return { success: false, error: 'ordered_task_ids cannot be empty', status: 400 }; + } + + try { + const taskManager = createTaskManager(); + const reorderedTasks = await taskManager.reorderTasks(loopId, { ordered_task_ids }); + + // Broadcast tasks reordered + try { + broadcastToClients({ + type: 'TASK_REORDERED', + loop_id: loopId, + ordered_task_ids: ordered_task_ids, + tasks: reorderedTasks + }); + } catch { + // Ignore broadcast errors + } + + return { success: true, data: reorderedTasks }; + } catch (error) { + return { success: false, error: (error as Error).message, status: 500 }; + } + }); + return true; + } + + // ==== ADVANCED TASK FEATURES ==== + + // POST /api/loops/v2/:loopId/import - Import tasks from issue + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/import$/) && req.method === 'POST') { + const loopId = pathname.split('/').slice(-2)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + handlePostRequest(req, res, async (body) => { + const { issue_id } = body as { issue_id?: string }; + + if (!issue_id || typeof issue_id !== 'string') { + return { success: false, error: 'issue_id is required', status: 400 }; + } + + try { + // Fetch issue data from issue-manager + const { readFile } = await import('fs/promises'); + const { existsSync } = await import('fs'); + const issuesDir = join(workflowDir, '.workflow', 'issues'); + const issuesPath = join(issuesDir, 'issues.jsonl'); + + let issueData: any = null; + + // Try reading from active issues + if (existsSync(issuesPath)) { + const content = await readFile(issuesPath, 'utf-8'); + const issues = content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line)); + issueData = issues.find((i: any) => i.id === issue_id); + } + + // Try reading from history if not found + if (!issueData) { + const historyPath = join(issuesDir, 'issue-history.jsonl'); + if (existsSync(historyPath)) { + const content = await readFile(historyPath, 'utf-8'); + const historyIssues = content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line)); + issueData = historyIssues.find((i: any) => i.id === issue_id); + } + } + + if (!issueData) { + return { success: false, error: `Issue ${issue_id} not found`, status: 404 }; + } + + // Load solutions to get bound solution tasks + const solutionsPath = join(issuesDir, 'solutions', `${issue_id}.jsonl`); + let tasksToImport: any[] = []; + + if (existsSync(solutionsPath)) { + const solutionsContent = await readFile(solutionsPath, 'utf-8'); + const solutions = solutionsContent.split('\n').filter(line => line.trim()).map(line => JSON.parse(line)); + + // Get tasks from bound solution + const boundSolution = solutions.find((s: any) => s.id === issueData.bound_solution_id) || + solutions.find((s: any) => s.is_bound) || + solutions[0]; + + if (boundSolution?.tasks) { + tasksToImport = boundSolution.tasks; + } + } + + if (tasksToImport.length === 0) { + return { success: false, error: 'No tasks found in issue. Bind a solution with tasks first.', status: 400 }; + } + + // Broadcast import start + broadcastToClients({ + type: 'LOOP_TASK_IMPORT_PROGRESS', + loop_id: loopId, + payload: { + stage: 'starting', + total: tasksToImport.length, + imported: 0 + } + }); + + const taskManager = createTaskManager(); + const createdTasks: any[] = []; + + // Convert issue tasks to loop tasks + for (let i = 0; i < tasksToImport.length; i++) { + const issueTask = tasksToImport[i]; + + // Map issue task fields to loop task fields + const taskRequest: TaskCreateRequest = { + description: issueTask.description || issueTask.title || `Task ${i + 1}`, + tool: mapIssueToolToLoopTool(issueTask.tool) || 'gemini', + mode: mapIssueModeToLoopMode(issueTask.mode) || 'write', + prompt_template: issueTask.prompt_template || issueTask.prompt || `Execute: ${issueTask.description || issueTask.title}`, + command: issueTask.command, + on_error: mapIssueOnError(issueTask.on_error) + }; + + const task = await taskManager.addTask(loopId, taskRequest); + createdTasks.push(task); + + // Broadcast progress + broadcastToClients({ + type: 'LOOP_TASK_IMPORT_PROGRESS', + loop_id: loopId, + payload: { + stage: 'importing', + total: tasksToImport.length, + imported: i + 1, + current_task: task + } + }); + } + + // Broadcast completion + broadcastToClients({ + type: 'LOOP_TASK_IMPORT_COMPLETE', + loop_id: loopId, + payload: { + total: tasksToImport.length, + imported: createdTasks.length, + tasks: createdTasks + } + }); + + return { + success: true, + data: createdTasks, + message: `Imported ${createdTasks.length} tasks from issue ${issue_id}` + }; + } catch (error) { + return { success: false, error: (error as Error).message, status: 500 }; + } + }); + return true; + } + + // POST /api/loops/v2/:loopId/generate - Generate tasks via Gemini + if (pathname.match(/\/api\/loops\/v2\/[^/]+\/generate$/) && req.method === 'POST') { + const loopId = pathname.split('/').slice(-2)[0]; + if (!loopId || !isValidId(loopId)) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' })); + return true; + } + + handlePostRequest(req, res, async (body) => { + const { tool = 'gemini', count } = body as { tool?: string; count?: number }; + + try { + // Get loop details for context + const loop = await readLoopStorage(loopId); + if (!loop) { + return { success: false, error: 'Loop not found', status: 404 }; + } + + // Broadcast generation start + broadcastToClients({ + type: 'LOOP_TASK_GENERATION_PROGRESS', + loop_id: loopId, + payload: { + stage: 'analyzing', + message: 'Analyzing loop description...' + } + }); + + // Build generation prompt + const generatePrompt = `PURPOSE: Generate ${count || 5} specific tasks for loop execution +TASK: Analyze the loop description and generate a list of actionable tasks that can be executed via CLI tools. Each task should have clear description, tool selection, mode, and prompt template. +MODE: analysis +CONTEXT: Loop title: ${loop.title} +Loop description: ${loop.description || 'No description provided'} +Max iterations: ${loop.max_iterations} +EXPECTED: Return a JSON array of tasks with this exact structure: +[ + { + "description": "Clear task description", + "tool": "gemini|codex|qwen|bash", + "mode": "analysis|write|review", + "prompt_template": "PURPOSE: ... TASK: ... MODE: analysis CONTEXT: @**/* EXPECTED: ...", + "on_error": "continue|pause|fail_fast" + } +] +CONSTRAINTS: Generate ${count || 5} tasks | Use gemini for AI tasks | Use bash for CLI commands | Include error handling strategy`; + + // Call CLI with gemini to generate tasks + let generatedTasks: any[] = []; + let outputBuffer = ''; + + const result = await executeCliTool({ + tool: tool === 'codex' || tool === 'qwen' || tool === 'gemini' ? tool : 'gemini', + prompt: generatePrompt, + mode: 'analysis', + format: 'plain', + cd: workflowDir, + timeout: 120000, // 2 minutes timeout + stream: true + }, (unit) => { + // Collect output + outputBuffer += unit.content; + + // Broadcast partial output for progress + broadcastToClients({ + type: 'LOOP_TASK_GENERATION_PROGRESS', + loop_id: loopId, + payload: { + stage: 'generating', + message: 'Generating tasks...', + output: unit.content + } + }); + }); + + if (!result.success) { + return { success: false, error: 'Failed to generate tasks via CLI', status: 500 }; + } + + // Parse generated tasks from CLI output + generatedTasks = parseGeneratedTasks(outputBuffer); + + if (generatedTasks.length === 0) { + return { + success: false, + error: 'No valid tasks generated. Check CLI output for details.', + status: 500, + output: outputBuffer + }; + } + + // Broadcast import start + broadcastToClients({ + type: 'LOOP_TASK_GENERATION_PROGRESS', + loop_id: loopId, + payload: { + stage: 'importing', + message: `Importing ${generatedTasks.length} generated tasks...`, + total: generatedTasks.length, + imported: 0 + } + }); + + const taskManager = createTaskManager(); + const createdTasks: any[] = []; + + // Add generated tasks to loop + for (let i = 0; i < generatedTasks.length; i++) { + const genTask = generatedTasks[i]; + + const taskRequest: TaskCreateRequest = { + description: genTask.description || `Generated Task ${i + 1}`, + tool: validateTool(genTask.tool) ? genTask.tool : 'gemini', + mode: validateMode(genTask.mode) ? genTask.mode : 'write', + prompt_template: genTask.prompt_template || `Execute task: ${genTask.description}`, + command: genTask.command, + on_error: validateOnError(genTask.on_error) ? genTask.on_error : 'continue' + }; + + const task = await taskManager.addTask(loopId, taskRequest); + createdTasks.push(task); + + // Broadcast progress + broadcastToClients({ + type: 'LOOP_TASK_GENERATION_PROGRESS', + loop_id: loopId, + payload: { + stage: 'importing', + message: `Importing task ${i + 1}/${generatedTasks.length}...`, + total: generatedTasks.length, + imported: i + 1, + current_task: task + } + }); + } + + // Broadcast completion + broadcastToClients({ + type: 'LOOP_TASK_GENERATION_COMPLETE', + loop_id: loopId, + payload: { + total: generatedTasks.length, + imported: createdTasks.length, + tasks: createdTasks + } + }); + + return { + success: true, + data: createdTasks, + message: `Generated and imported ${createdTasks.length} tasks` + }; + } catch (error) { + return { success: false, error: (error as Error).message, status: 500 }; + } + }); + return true; + } + + return false; +} + +/** + * Sanitize ID parameter to prevent path traversal attacks + * @returns true if valid, false if invalid + */ +function isValidId(id: string): boolean { + if (!id) return false; + // Block path traversal attempts and null bytes + if (id.includes('/') || id.includes('\\') || id === '..' || id === '.') return false; + if (id.includes('\0')) return false; + return true; +} + +/** + * Map issue tool to loop tool + */ +function mapIssueToolToLoopTool(tool: any): 'bash' | 'gemini' | 'codex' | 'qwen' | 'claude' | null { + const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude']; + if (validTools.includes(tool)) return tool as any; + // Map aliases + if (tool === 'ccw') return 'gemini'; + if (tool === 'ai') return 'gemini'; + return null; +} + +/** + * Map issue mode to loop mode + */ +function mapIssueModeToLoopMode(mode: any): 'analysis' | 'write' | 'review' | null { + const validModes = ['analysis', 'write', 'review']; + if (validModes.includes(mode)) return mode as any; + // Map aliases + if (mode === 'read') return 'analysis'; + if (mode === 'create' || mode === 'modify') return 'write'; + return null; +} + +/** + * Map issue on_error value + */ +function mapIssueOnError(onError: any): 'continue' | 'pause' | 'fail_fast' | undefined { + const validValues = ['continue', 'pause', 'fail_fast']; + if (validValues.includes(onError)) return onError as any; + // Map aliases + if (onError === 'stop') return 'pause'; + if (onError === 'abort') return 'fail_fast'; + return undefined; +} + +/** + * Validate tool value + */ +function validateTool(tool: any): boolean { + const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude']; + return validTools.includes(tool); +} + +/** + * Validate mode value + */ +function validateMode(mode: any): boolean { + const validModes = ['analysis', 'write', 'review']; + return validModes.includes(mode); +} + +/** + * Validate on_error value + */ +function validateOnError(onError: any): boolean { + const validValues = ['continue', 'pause', 'fail_fast']; + return validValues.includes(onError); +} + +/** + * Parse generated tasks from CLI output + * Extracts JSON array from output, handles various response formats + */ +function parseGeneratedTasks(output: string): any[] { + let tasks: any[] = []; + + // Try to find JSON array in output + const jsonMatch = output.match(/\[[\s\S]*\]/); + if (jsonMatch) { + try { + tasks = JSON.parse(jsonMatch[0]); + } catch { + // Invalid JSON, try alternative parsing + } + } + + // If no valid JSON array found, try parsing line by line + if (tasks.length === 0) { + const lines = output.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + try { + tasks.push(JSON.parse(trimmed)); + } catch { + // Skip invalid lines + } + } + } + } + + // Filter and validate task objects + return tasks.filter(t => + t && + typeof t === 'object' && + (t.description || t.title || t.task) && + (t.tool || t.mode || t.prompt_template) + ).map(t => ({ + description: t.description || t.title || t.task || 'Untitled task', + tool: t.tool || 'gemini', + mode: t.mode || 'write', + prompt_template: t.prompt_template || t.prompt || `Execute: ${t.description || t.title || t.task}`, + command: t.command, + on_error: t.on_error || 'continue' + })); +} diff --git a/ccw/src/core/routes/task-routes.ts b/ccw/src/core/routes/task-routes.ts index c75bb85e..e0c8972c 100644 --- a/ccw/src/core/routes/task-routes.ts +++ b/ccw/src/core/routes/task-routes.ts @@ -152,6 +152,48 @@ export async function handleTaskRoutes(ctx: RouteContext): Promise { return true; } + // GET /api/tasks/:taskId - Get single task + const taskDetailMatch = pathname.match(/^\/api\/tasks\/([^\/]+)$/); + if (taskDetailMatch && req.method === 'GET') { + const taskId = decodeURIComponent(taskDetailMatch[1]); + + // Sanitize taskId to prevent path traversal + if (taskId.includes('/') || taskId.includes('\\') || taskId === '..' || taskId === '.') { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid task ID format' })); + return true; + } + + try { + const taskPath = join(taskDir, taskId + '.json'); + + if (!existsSync(taskPath)) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Task not found: ' + taskId })); + return true; + } + + const content = await readFile(taskPath, 'utf-8'); + const task = JSON.parse(content) as Task; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + data: { + task: task + } + })); + return true; + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: false, + error: (error as Error).message + })); + return true; + } + } + // POST /api/tasks/validate - Validate task loop_control configuration if (pathname === '/api/tasks/validate' && req.method === 'POST') { handlePostRequest(req, res, async (body) => { diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index 342abe4c..de8de2c3 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -6,7 +6,7 @@ import { resolvePath, getRecentPaths, normalizePathForDisplay } from '../utils/p // Import route handlers import { handleStatusRoutes } from './routes/status-routes.js'; -import { handleCliRoutes } from './routes/cli-routes.js'; +import { handleCliRoutes, cleanupStaleExecutions } from './routes/cli-routes.js'; import { handleCliSettingsRoutes } from './routes/cli-settings-routes.js'; import { handleMemoryRoutes } from './routes/memory-routes.js'; import { handleCoreMemoryRoutes } from './routes/core-memory-routes.js'; @@ -29,6 +29,7 @@ import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js'; import { handleNavStatusRoutes } from './routes/nav-status-routes.js'; import { handleAuthRoutes } from './routes/auth-routes.js'; import { handleLoopRoutes } from './routes/loop-routes.js'; +import { handleLoopV2Routes } from './routes/loop-v2-routes.js'; import { handleTestLoopRoutes } from './routes/test-loop-routes.js'; import { handleTaskRoutes } from './routes/task-routes.js'; @@ -568,7 +569,12 @@ export async function startServer(options: ServerOptions = {}): Promise { + clearInterval(cleanupInterval); + console.log('[Server] Stopped CLI execution cleanup interval'); + }); + // Start health check service for all enabled providers try { const healthCheckService = getHealthCheckService(); diff --git a/ccw/src/templates/dashboard-css/12-cli-legacy.css b/ccw/src/templates/dashboard-css/12-cli-legacy.css index 58b7e34a..bbb7b47e 100644 --- a/ccw/src/templates/dashboard-css/12-cli-legacy.css +++ b/ccw/src/templates/dashboard-css/12-cli-legacy.css @@ -2,6 +2,41 @@ * Legacy Container Styles (kept for compatibility) * ======================================== */ +/* CLI Stream Recovery Badge Styles */ +.cli-stream-recovery-badge { + font-size: 0.5625rem; + font-weight: 600; + padding: 0.125rem 0.375rem; + background: hsl(38 92% 50% / 0.15); + color: hsl(38 92% 50%); + border-radius: 9999px; + text-transform: uppercase; + letter-spacing: 0.03em; + margin-left: 0.375rem; +} + +.cli-status-recovery-badge { + font-size: 0.625rem; + font-weight: 600; + padding: 0.125rem 0.5rem; + background: hsl(38 92% 50% / 0.15); + color: hsl(38 92% 50%); + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.03em; + margin-left: 0.5rem; +} + +/* Tab styling for recovered sessions */ +.cli-stream-tab.recovered { + border-color: hsl(38 92% 50% / 0.3); +} + +.cli-stream-tab.recovered .cli-stream-recovery-badge { + background: hsl(38 92% 50% / 0.2); + color: hsl(38 92% 55%); +} + /* Container */ .cli-manager-container { display: flex; diff --git a/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css b/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css index e3bfd92f..cec8431b 100644 --- a/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +++ b/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css @@ -161,6 +161,8 @@ display: flex; align-items: center; gap: 8px; + /* Isolate from parent transform to fix native tooltip positioning */ + will-change: transform; } .cli-stream-action-btn { @@ -196,6 +198,10 @@ color: hsl(var(--muted-foreground)); cursor: pointer; transition: all 0.15s; + /* Fix native tooltip positioning under transformed parent */ + position: relative; + z-index: 1; + transform: translateZ(0); } .cli-stream-close-btn:hover { @@ -203,6 +209,49 @@ color: hsl(var(--destructive)); } +/* Icon-only action buttons (cleaner style matching close button) */ +.cli-stream-icon-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + background: transparent; + border: none; + border-radius: 4px; + color: hsl(var(--muted-foreground)); + cursor: pointer; + transition: all 0.15s; + /* Fix native tooltip positioning under transformed parent */ + position: relative; + z-index: 1; + /* Create new stacking context to isolate from parent transform */ + transform: translateZ(0); +} + +.cli-stream-icon-btn svg { + width: 16px; + height: 16px; +} + +.cli-stream-icon-btn:hover { + background: hsl(var(--hover)); + color: hsl(var(--foreground)); +} + +.cli-stream-icon-btn:first-child:hover { + /* Clear completed - green/success tint */ + background: hsl(142 76% 36% / 0.1); + color: hsl(142 76% 36%); +} + +.cli-stream-icon-btn:nth-child(2):hover { + /* Clear all - orange/warning tint */ + background: hsl(38 92% 50% / 0.1); + color: hsl(38 92% 50%); +} + /* ===== Tab Bar ===== */ .cli-stream-tabs { display: flex; @@ -787,6 +836,12 @@ animation: streamBadgePulse 1.5s ease-in-out infinite; } +.cli-stream-badge.has-completed { + display: flex; + background: hsl(var(--muted) / 0.8); + color: hsl(var(--muted-foreground)); +} + @keyframes streamBadgePulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.15); } diff --git a/ccw/src/templates/dashboard-css/36-loop-monitor.css b/ccw/src/templates/dashboard-css/36-loop-monitor.css index c35d8551..e32d38db 100644 --- a/ccw/src/templates/dashboard-css/36-loop-monitor.css +++ b/ccw/src/templates/dashboard-css/36-loop-monitor.css @@ -1,8 +1,13 @@ +/* =================================== + Loop Monitor Styles + Redesigned: 2026-01-22 + Target: ~500 lines, consistent with Dashboard design + =================================== */ + /* ========================================== - LOOP MONITOR STYLES + LAYOUT ========================================== */ -/* Layout */ .loop-monitor-layout { display: grid; grid-template-columns: 350px 1fr; @@ -11,7 +16,17 @@ min-height: 500px; } -/* Loop List Panel */ +@media (max-width: 1024px) { + .loop-monitor-layout { + grid-template-columns: 1fr; + height: auto; + } +} + +/* ========================================== + LOOP LIST PANEL + ========================================== */ + .loop-list-panel { background: hsl(var(--card)); border: 1px solid hsl(var(--border)); @@ -31,17 +46,47 @@ .panel-header h3 { margin: 0; - font-size: 1rem; + font-size: 0.875rem; font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; + color: hsl(var(--foreground)); } +.panel-header h3 i, +.panel-header h3 svg { + color: hsl(var(--primary)); +} + +.header-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.filter-select, .filter-group select { padding: 0.375rem 0.75rem; border: 1px solid hsl(var(--border)); border-radius: 0.375rem; background: hsl(var(--background)); color: hsl(var(--foreground)); - font-size: 0.875rem; + font-size: 0.75rem; + cursor: pointer; + transition: border-color 0.2s; +} + +.filter-select:hover, +.filter-group select:hover { + border-color: hsl(var(--primary)); +} + +.filter-select:focus, +.filter-group select:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1); } .loop-list { @@ -50,48 +95,39 @@ padding: 0.5rem; } -/* Loop Card */ +/* ========================================== + LOOP CARDS + ========================================== */ + .loop-card { - padding: 0.75rem; + padding: 0.875rem 1rem; margin-bottom: 0.5rem; - background: hsl(var(--background)); + background: hsl(var(--card)); border: 1px solid hsl(var(--border)); - border-left-width: 4px; + border-left: 3px solid hsl(var(--muted-foreground)); border-radius: 0.5rem; cursor: pointer; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; -} - -/* Card shine effect on hover */ -.loop-card::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent); - transition: left 0.5s; -} - -.loop-card:hover::before { - left: 100%; + transition: all 0.2s ease; } .loop-card:hover { - background: hsl(var(--hover)); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); - transform: translateY(-1px); + transform: translateY(-2px); + box-shadow: 0 4px 12px hsl(var(--foreground) / 0.08); + border-color: hsl(var(--primary)); } .loop-card.selected { - background: hsl(var(--primary) / 0.12); + background: hsl(var(--accent) / 0.5); border-color: hsl(var(--primary)); - box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2); } +/* Status border colors */ +.loop-card.status-created { border-left-color: hsl(var(--muted-foreground)); } +.loop-card.status-running { border-left-color: hsl(var(--info)); } +.loop-card.status-paused { border-left-color: hsl(var(--warning)); } +.loop-card.status-completed { border-left-color: hsl(var(--success)); } +.loop-card.status-failed { border-left-color: hsl(var(--destructive)); } + .loop-card-header { display: flex; align-items: center; @@ -110,12 +146,39 @@ flex-shrink: 0; } +.loop-status-indicator.created { background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); } +.loop-status-indicator.running { background: hsl(var(--info) / 0.15); color: hsl(var(--info)); } +.loop-status-indicator.paused { background: hsl(var(--warning) / 0.15); color: hsl(var(--warning)); } +.loop-status-indicator.completed { background: hsl(var(--success) / 0.15); color: hsl(var(--success)); } +.loop-status-indicator.failed { background: hsl(var(--destructive) / 0.15); color: hsl(var(--destructive)); } + +.loop-status-indicator.running { + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + .loop-title { font-weight: 600; font-size: 0.875rem; + color: hsl(var(--foreground)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + flex: 1; +} + +.version-badge { + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + padding: 0.125rem 0.375rem; + border-radius: 9999px; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); } .loop-card-body { @@ -124,6 +187,17 @@ gap: 0.375rem; } +.loop-description { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + .loop-meta { display: flex; justify-content: space-between; @@ -131,57 +205,89 @@ color: hsl(var(--muted-foreground)); } -.loop-task-id { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 180px; -} - -.loop-status-text { - font-weight: 500; -} - .loop-progress { display: flex; align-items: center; gap: 0.5rem; } -.loop-step-info { - font-size: 0.75rem; +/* ========================================== + STATUS BADGES + ========================================== */ + +.status-badge, +.detail-status { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.status-badge.created, +.detail-status.created { + background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); } -.loop-time { - font-size: 0.7rem; - color: hsl(var(--muted-foreground)); +.status-badge.running, +.detail-status.running { + background: hsl(var(--info) / 0.15); + color: hsl(var(--info)); } -/* Progress Bar */ +.status-badge.paused, +.detail-status.paused { + background: hsl(var(--warning) / 0.15); + color: hsl(var(--warning)); +} + +.status-badge.completed, +.detail-status.completed { + background: hsl(var(--success) / 0.15); + color: hsl(var(--success)); +} + +.status-badge.failed, +.detail-status.failed { + background: hsl(var(--destructive) / 0.15); + color: hsl(var(--destructive)); +} + +/* ========================================== + PROGRESS BARS + ========================================== */ + .progress-bar { flex: 1; - height: 6px; + height: 0.5rem; background: hsl(var(--muted)); - border-radius: 3px; + border-radius: 9999px; overflow: hidden; } .progress-fill { height: 100%; - background: hsl(var(--primary)); - border-radius: 3px; - transition: width 0.3s; + background: linear-gradient(90deg, hsl(var(--primary)), hsl(var(--primary) / 0.8)); + border-radius: 9999px; + transition: width 0.3s ease; } .progress-text { font-size: 0.75rem; color: hsl(var(--muted-foreground)); - min-width: 60px; + min-width: 48px; text-align: right; } -/* Loop Detail Panel */ +/* ========================================== + DETAIL PANEL + ========================================== */ + .loop-detail-panel { background: hsl(var(--card)); border: 1px solid hsl(var(--border)); @@ -190,17 +296,12 @@ padding: 1.5rem; } -.empty-detail-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - color: hsl(var(--muted-foreground)); - gap: 1rem; +@media (max-width: 1024px) { + .loop-detail-panel { + min-height: 400px; + } } -/* Loop Detail */ .loop-detail { display: flex; flex-direction: column; @@ -215,64 +316,11 @@ border-bottom: 1px solid hsl(var(--border)); } -.detail-status { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - border-radius: 2rem; - font-weight: 600; -} - -.status-icon { - font-size: 1.25rem; -} - .detail-actions { display: flex; gap: 0.5rem; } -.btn { - display: inline-flex; - align-items: center; - gap: 0.375rem; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; - border: none; -} - -.btn-success { - background: hsl(var(--success)); - color: white; -} - -.btn-success:hover { - background: hsl(var(--success) / 0.9); -} - -.btn-warning { - background: hsl(var(--warning)); - color: white; -} - -.btn-warning:hover { - background: hsl(var(--warning) / 0.9); -} - -.btn-danger { - background: hsl(var(--destructive)); - color: white; -} - -.btn-danger:hover { - background: hsl(var(--destructive) / 0.9); -} - .detail-info { padding: 1rem; background: hsl(var(--muted) / 0.3); @@ -280,16 +328,24 @@ } .detail-title { + margin: 0 0 0.5rem 0; + font-size: 1.125rem; + font-weight: 600; + color: hsl(var(--foreground)); +} + +.detail-description { + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + line-height: 1.5; margin: 0 0 0.75rem 0; - font-size: 1.25rem; - font-weight: 700; } .detail-meta { display: flex; flex-wrap: wrap; gap: 1rem; - font-size: 0.875rem; + font-size: 0.75rem; color: hsl(var(--muted-foreground)); } @@ -308,9 +364,12 @@ .detail-section h4 { margin: 0; - font-size: 1rem; + font-size: 0.8125rem; font-weight: 600; color: hsl(var(--foreground)); + display: flex; + align-items: center; + gap: 0.5rem; } .progress-group { @@ -326,45 +385,65 @@ } .progress-item label { - font-size: 0.875rem; + font-size: 0.75rem; font-weight: 500; color: hsl(var(--muted-foreground)); } -/* CLI Sequence */ -.cli-sequence { +/* ========================================== + CLI SEQUENCE / STEPS + ========================================== */ + +.cli-sequence, +.steps-list { display: flex; flex-direction: column; gap: 0.5rem; } -.cli-step { +.cli-step, +.step-item { display: flex; gap: 0.75rem; padding: 0.75rem; background: hsl(var(--muted) / 0.3); border-radius: 0.375rem; + border-left: 2px solid hsl(var(--border)); } -.step-marker { - width: 28px; - height: 28px; +.cli-step.current, +.step-item.current { + border-left-color: hsl(var(--primary)); + background: hsl(var(--primary) / 0.05); +} + +.cli-step.completed, +.step-item.completed { + border-left-color: hsl(var(--success)); +} + +.step-marker, +.step-number { + width: 24px; + height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: hsl(var(--muted)); - font-size: 0.875rem; + font-size: 0.75rem; font-weight: 600; flex-shrink: 0; } -.cli-step.current .step-marker { +.cli-step.current .step-marker, +.step-item.current .step-number { background: hsl(var(--primary)); color: white; } -.cli-step.completed .step-marker { +.cli-step.completed .step-marker, +.step-item.completed .step-number { background: hsl(var(--success)); color: white; } @@ -374,368 +453,236 @@ min-width: 0; } -.step-name { +.step-name, +.step-id { font-weight: 600; font-size: 0.875rem; - margin-bottom: 0.25rem; -} - -.step-prompt { - font-size: 0.75rem; - color: hsl(var(--muted-foreground)); - overflow: hidden; - text-overflow: ellipsis; -} - -/* Variables Grid */ -.variables-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 0.5rem; -} - -.variable-item { - display: flex; - flex-direction: column; - gap: 0.25rem; - padding: 0.5rem; - background: hsl(var(--muted) / 0.3); - border-radius: 0.375rem; -} - -.variable-key { - font-size: 0.75rem; - font-weight: 600; - color: hsl(var(--muted-foreground)); -} - -.variable-value { - font-size: 0.875rem; - font-family: 'Consolas', 'Monaco', monospace; - overflow: hidden; - text-overflow: ellipsis; -} - -/* Execution Timeline */ -.execution-timeline { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.timeline-iteration { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding-left: 1rem; -} - -.timeline-iteration.current { - border-left: 3px solid hsl(var(--primary)); - padding-left: 0.875rem; -} - -.iteration-header { - display: flex; - align-items: center; - gap: 0.5rem; - font-weight: 600; -} - -.iteration-marker { - font-size: 1rem; -} - -.timeline-iteration.completed .iteration-marker { - color: hsl(var(--success)); -} - -.timeline-iteration.current .iteration-marker { - color: hsl(var(--primary)); -} - -.iteration-steps { - display: flex; - flex-direction: column; - gap: 0.375rem; - padding-left: 1.5rem; -} - -.timeline-step { - display: flex; - gap: 0.75rem; - padding: 0.5rem; - background: hsl(var(--muted) / 0.2); - border-radius: 0.375rem; -} - -.step-status { - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - font-size: 0.75rem; - flex-shrink: 0; -} - -.step-status.success { - background: hsl(var(--success)); - color: white; -} - -.step-status.failed { - background: hsl(var(--destructive)); - color: white; -} - -.step-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 0.125rem; -} - -.step-tool { - font-weight: 600; - font-size: 0.875rem; -} - -.step-time { - font-size: 0.75rem; - color: hsl(var(--muted-foreground)); -} - -.step-duration { - font-size: 0.75rem; - color: hsl(var(--muted-foreground)); -} - -.step-error { - font-size: 0.75rem; - color: hsl(var(--destructive)); - background: hsl(var(--destructive) / 0.1); - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - margin-top: 0.25rem; -} - -/* Error Section */ -.error-section { - padding: 1rem; - background: hsl(var(--destructive) / 0.1); - border: 1px solid hsl(var(--destructive) / 0.3); - border-radius: 0.5rem; -} - -.error-section h4 { - margin: 0 0 0.5rem 0; - color: hsl(var(--destructive)); -} - -.error-message { - font-family: 'Consolas', 'Monaco', monospace; - font-size: 0.875rem; - color: hsl(var(--destructive)); - white-space: pre-wrap; - word-break: break-word; -} - -/* Notification */ -.notification { - position: fixed; - bottom: 1rem; - right: 1rem; - padding: 0.75rem 1.5rem; - border-radius: 0.5rem; - font-weight: 500; - z-index: 1000; - animation: slideIn 0.3s ease-out; -} - -.notification.success { - background: hsl(var(--success)); - color: white; -} - -.notification.error { - background: hsl(var(--destructive)); - color: white; -} - -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* Empty State */ -.empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 3rem 1rem; - color: hsl(var(--muted-foreground)); - gap: 0.75rem; -} - -/* Loading Spinner */ -.loading-spinner { - display: flex; - align-items: center; - justify-content: center; - padding: 3rem 1rem; - color: hsl(var(--muted-foreground)); -} - -/* Color Themes */ -.text-cyan-500 { color: #06b6d4; } -.bg-cyan-100 { background: #cffafe; } -.text-amber-500 { color: #f59e0b; } -.bg-amber-100 { background: #fef3c7; } -.text-emerald-500 { color: #10b981; } -.bg-emerald-100 { background: #d1fae5; } -.text-red-500 { color: #ef4444; } -.bg-red-100 { background: #fee2e2; } -.text-gray-400 { color: #9ca3af; } -.bg-gray-100 { background: #f3f4f6; } - -.border-l-cyan-500 { border-left-color: #06b6d4; } -.border-l-amber-500 { border-left-color: #f59e0b; } -.border-l-emerald-500 { border-left-color: #10b981; } -.border-l-red-500 { border-left-color: #ef4444; } -.border-l-gray-400 { border-left-color: #9ca3af; } - -.bg-cyan-light { background: hsl(var(--info) / 0.2); } -.text-cyan { color: hsl(var(--info)); } - -/* Dark mode adjustments */ -[data-theme="dark"] .text-cyan-500 { color: #22d3ee; } -[data-theme="dark"] .bg-cyan-100 { background: #164e63; } -[data-theme="dark"] .text-amber-500 { color: #fbbf24; } -[data-theme="dark"] .bg-amber-100 { background: #78350f; } -[data-theme="dark"] .text-emerald-500 { color: #34d399; } -[data-theme="dark"] .bg-emerald-100 { background: #065f46; } -[data-theme="dark"] .text-red-500 { color: #f87171; } -[data-theme="dark"] .bg-red-100 { background: #7f1d1d; } -[data-theme="dark"] .text-gray-400 { color: #9ca3af; } -[data-theme="dark"] .bg-gray-100 { background: #374151; } - -/* Responsive */ -@media (max-width: 1024px) { - .loop-monitor-layout { - grid-template-columns: 1fr; - height: auto; - } - - .loop-detail-panel { - min-height: 400px; - } - - .loop-list-panel { - border-right: none; - border-bottom: 1px solid hsl(var(--border)); - } -} - -/* ========================================== - * TASK CREATION STYLES - * ========================================== */ - -/* Header actions */ -.header-actions { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.header-actions .btn { - padding: 0.375rem 0.75rem; - font-size: 0.875rem; -} - -/* Tasks list */ -.tasks-list-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - border-bottom: 1px solid hsl(var(--border)); - background: hsl(var(--muted) / 0.3); -} - -/* Task card */ -.task-card { - background: white; - border: 1px solid hsl(var(--border)); - border-radius: 0.5rem; - margin-bottom: 0.75rem; - overflow: hidden; - transition: box-shadow 0.2s, transform 0.2s; -} - -.task-card:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - transform: translateY(-1px); -} - -.task-card-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - background: hsl(var(--muted) / 0.3); - border-bottom: 1px solid hsl(var(--border)); -} - -.task-title { - font-weight: 600; color: hsl(var(--foreground)); } -.task-id { +.step-prompt, +.step-command { font-size: 0.75rem; color: hsl(var(--muted-foreground)); - font-family: monospace; + margin-top: 0.25rem; } -.task-card-body { - padding: 1rem; +.step-tool { + font-size: 0.6875rem; + padding: 0.125rem 0.375rem; + background: hsl(var(--info) / 0.15); + color: hsl(var(--info)); + border-radius: 9999px; } -.task-description { +/* ========================================== + TIMELINE + ========================================== */ + +.execution-timeline { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.timeline-item { + position: relative; + padding-left: 2rem; + padding-bottom: 1rem; +} + +.timeline-item::before { + content: ''; + position: absolute; + left: 0.625rem; + top: 1.5rem; + bottom: -0.5rem; + width: 2px; + background: hsl(var(--border)); +} + +.timeline-item:last-child::before { + display: none; +} + +.timeline-marker { + position: absolute; + left: 0; + top: 0.25rem; + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); +} + +.timeline-item.success .timeline-marker { + background: hsl(var(--success)); + color: white; +} + +.timeline-item.failed .timeline-marker { + background: hsl(var(--destructive)); + color: white; +} + +.timeline-item.current .timeline-marker { + background: hsl(var(--primary)); + color: white; +} + +.timeline-content { font-size: 0.875rem; - margin-bottom: 0.75rem; +} + +.timeline-title { + font-weight: 500; + color: hsl(var(--foreground)); +} + +.timeline-time { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +/* ========================================== + EMPTY STATES + ========================================== */ + +.empty-state, +.empty-detail-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 1.5rem; + color: hsl(var(--muted-foreground)); + text-align: center; +} + +.empty-state-icon, +.empty-icon-large { + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + background: hsl(var(--muted) / 0.3); + border-radius: 50%; + margin-bottom: 1rem; +} + +.empty-state-icon i, +.empty-state-icon svg, +.empty-icon-large i, +.empty-icon-large svg { + width: 28px; + height: 28px; + opacity: 0.5; + color: hsl(var(--primary)); +} + +.empty-state-title { + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--foreground)); + margin-bottom: 0.5rem; +} + +.empty-state-hint, +.empty-hint { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + max-width: 280px; line-height: 1.5; } -.task-meta { +/* ========================================== + TASK LIST + ========================================== */ + +.task-list { display: flex; - gap: 1rem; - font-size: 0.875rem; - color: hsl(var(--muted-foreground)); - margin-bottom: 0.75rem; + flex-direction: column; + gap: 0.5rem; + max-height: 400px; + overflow-y: auto; } -.task-meta span { +.task-item { display: flex; align-items: center; - gap: 0.25rem; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + cursor: grab; + transition: all 0.2s ease; } -/* Modal styles */ +.task-item:hover { + border-color: hsl(var(--primary)); + box-shadow: 0 2px 8px hsl(var(--foreground) / 0.06); +} + +.task-item-drag { + color: hsl(var(--muted-foreground)); + cursor: grab; +} + +.task-item-content { + flex: 1; + min-width: 0; +} + +.task-item-description { + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--foreground)); +} + +.task-item-meta { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.25rem; +} + +.task-status-badge, +.task-tool-badge { + font-size: 0.625rem; + font-weight: 500; + padding: 0.125rem 0.375rem; + border-radius: 9999px; +} + +.task-status-badge { background: hsl(var(--info) / 0.15); color: hsl(var(--info)); } +.task-tool-badge { background: hsl(var(--muted)); color: hsl(var(--muted-foreground)); } + +.task-item-actions { + display: flex; + gap: 0.25rem; + opacity: 0; + transition: opacity 0.2s; +} + +.task-item:hover .task-item-actions { + opacity: 1; +} + +/* ========================================== + MODALS + ========================================== */ + .modal-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.5); + background: hsl(var(--foreground) / 0.5); display: flex; align-items: center; justify-content: center; @@ -744,15 +691,15 @@ } .modal-content { - background: white; - border-radius: 0.75rem; + background: hsl(var(--card)); + border-radius: 0.5rem; width: 100%; max-width: 600px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; - box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15); + box-shadow: 0 20px 25px hsl(var(--foreground) / 0.15); } .modal-content.modal-lg { @@ -765,12 +712,11 @@ align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid hsl(var(--border)); - background: hsl(var(--muted) / 0.3); } .modal-header h3 { margin: 0; - font-size: 1.25rem; + font-size: 1rem; font-weight: 600; } @@ -780,15 +726,12 @@ cursor: pointer; padding: 0.25rem; border-radius: 0.25rem; - display: flex; - align-items: center; - justify-content: center; color: hsl(var(--muted-foreground)); transition: background 0.2s; } .modal-close:hover { - background: hsl(var(--muted) / 0.5); + background: hsl(var(--muted)); } .modal-body { @@ -800,13 +743,15 @@ .modal-footer { display: flex; justify-content: flex-end; - gap: 0.75rem; + gap: 0.5rem; padding: 1rem 1.5rem; border-top: 1px solid hsl(var(--border)); - background: hsl(var(--muted) / 0.3); } -/* Form styles */ +/* ========================================== + FORMS + ========================================== */ + .form-section { margin-bottom: 1.5rem; padding-bottom: 1.5rem; @@ -815,23 +760,31 @@ .form-section:last-child { border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; } .form-section h4 { margin: 0 0 1rem 0; - font-size: 1rem; + font-size: 0.875rem; font-weight: 600; - color: hsl(var(--foreground)); + display: flex; + align-items: center; + gap: 0.5rem; } .form-group { margin-bottom: 1rem; } +.form-group:last-child { + margin-bottom: 0; +} + .form-group label { display: block; margin-bottom: 0.375rem; - font-size: 0.875rem; + font-size: 0.75rem; font-weight: 500; color: hsl(var(--foreground)); } @@ -842,13 +795,15 @@ border: 1px solid hsl(var(--border)); border-radius: 0.375rem; font-size: 0.875rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); transition: border-color 0.2s, box-shadow 0.2s; } .form-control:focus { outline: none; border-color: hsl(var(--primary)); - box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1); + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.1); } textarea.form-control { @@ -865,61 +820,32 @@ textarea.form-control { flex: 1; } -/* CLI Step Card */ -.cli-step-card { - background: hsl(var(--muted) / 0.3); - border: 1px solid hsl(var(--border)); - border-radius: 0.5rem; - padding: 1rem; - margin-bottom: 1rem; -} - -.cli-step-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.step-number { - font-weight: 600; - color: hsl(var(--primary)); -} - -.btn-text { - background: none; - border: none; - color: hsl(var(--destructive)); - cursor: pointer; - padding: 0.25rem; -} - -.btn-text:hover { - background: hsl(var(--destructive) / 0.1); - border-radius: 0.25rem; -} - -/* Bash field visibility */ -.cli-step-card .bash-only { - display: none; -} - -.cli-step-card .bash-only:not([style*="display: none"]) { +.form-group small { display: block; + margin-top: 0.25rem; + font-size: 0.6875rem; + color: hsl(var(--muted-foreground)); } -/* Button variants */ +.required { + color: hsl(var(--destructive)); +} + +/* ========================================== + BUTTONS + ========================================== */ + .btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; padding: 0.5rem 1rem; border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; border: none; - display: inline-flex; - align-items: center; - gap: 0.375rem; - transition: background 0.2s, opacity 0.2s; + transition: all 0.2s; } .btn:disabled { @@ -927,98 +853,218 @@ textarea.form-control { cursor: not-allowed; } -.btn-primary { - background: hsl(var(--primary)); - color: white; -} +.btn-primary { background: hsl(var(--primary)); color: white; } +.btn-primary:hover:not(:disabled) { background: hsl(var(--primary) / 0.9); } -.btn-primary:hover:not(:disabled) { - background: hsl(var(--primary) / 0.9); -} +.btn-success { background: hsl(var(--success)); color: white; } +.btn-success:hover:not(:disabled) { background: hsl(var(--success) / 0.9); } + +.btn-warning { background: hsl(var(--warning)); color: white; } +.btn-warning:hover:not(:disabled) { background: hsl(var(--warning) / 0.9); } + +.btn-danger { background: hsl(var(--destructive)); color: white; } +.btn-danger:hover:not(:disabled) { background: hsl(var(--destructive) / 0.9); } .btn-secondary { - background: hsl(var(--secondary)); - color: white; + background: hsl(var(--muted)); + color: hsl(var(--foreground)); } - .btn-secondary:hover:not(:disabled) { - background: hsl(var(--secondary) / 0.9); + background: hsl(var(--muted) / 0.8); } -.btn-success { - background: hsl(var(--success)); - color: white; +.btn-outline { + background: transparent; + border: 1px solid hsl(var(--border)); + color: hsl(var(--foreground)); +} +.btn-outline:hover:not(:disabled) { + background: hsl(var(--accent)); + border-color: hsl(var(--primary)); } -.btn-success:hover:not(:disabled) { - background: hsl(var(--success) / 0.9); -} - -.btn-danger { - background: hsl(var(--destructive)); - color: white; -} - -.btn-danger:hover:not(:disabled) { - background: hsl(var(--destructive) / 0.9); +.btn-text { + background: none; + border: none; + color: hsl(var(--muted-foreground)); + padding: 0.25rem 0.5rem; } +.btn-text:hover { color: hsl(var(--foreground)); } .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; } -.w-full { - width: 100%; -} +/* ========================================== + ALERTS & ERRORS + ========================================== */ -.flex { - display: flex; -} - -.flex-1 { - flex: 1; -} - -.justify-between { - justify-content: space-between; -} - -.items-center { - align-items: center; -} - -.mb-3 { - margin-bottom: 0.75rem; -} - -.mt-3 { - margin-top: 0.75rem; -} - -.mt-4 { - margin-top: 1rem; -} - -.mr-2 { - margin-right: 0.5rem; -} - -.text-sm { +.alert { + padding: 0.75rem 1rem; + border-radius: 0.375rem; font-size: 0.875rem; + margin-bottom: 1rem; } -.text-gray-500 { +.alert-error { + background: hsl(var(--destructive) / 0.1); + border: 1px solid hsl(var(--destructive) / 0.3); + color: hsl(var(--destructive)); +} + +.alert-warning { + background: hsl(var(--warning) / 0.1); + border: 1px solid hsl(var(--warning) / 0.3); + color: hsl(var(--warning)); +} + +.alert-success { + background: hsl(var(--success) / 0.1); + border: 1px solid hsl(var(--success) / 0.3); + color: hsl(var(--success)); +} + +.error-section { + padding: 1rem; + background: hsl(var(--destructive) / 0.1); + border: 1px solid hsl(var(--destructive) / 0.3); + border-radius: 0.5rem; +} + +.error-section h4 { + margin: 0 0 0.5rem 0; + color: hsl(var(--destructive)); +} + +.error-message { + font-family: monospace; + font-size: 0.875rem; + color: hsl(var(--destructive)); + white-space: pre-wrap; + word-break: break-word; +} + +/* ========================================== + CONFIG DISPLAY + ========================================== */ + +.config-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; +} + +.config-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.config-label { + font-size: 0.6875rem; + color: hsl(var(--muted-foreground)); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.config-value { + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--foreground)); +} + +.config-code { + display: block; + padding: 0.5rem 0.75rem; + background: hsl(var(--muted)); + border-radius: 0.375rem; + font-size: 0.875rem; + font-family: monospace; + overflow-x: auto; +} + +/* ========================================== + UTILITIES + ========================================== */ + +.flex { display: flex; } +.flex-1 { flex: 1; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.w-full { width: 100%; } +.text-sm { font-size: 0.875rem; } +.small { font-size: 0.75rem; color: hsl(var(--muted-foreground)); } + +.loading-spinner { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; color: hsl(var(--muted-foreground)); } -.text-gray-400 { - color: #9ca3af; +/* Notification */ +.notification { + position: fixed; + bottom: 1rem; + right: 1rem; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 500; + z-index: 1000; + animation: slideIn 0.3s ease-out; } -.small { - font-size: 0.75rem; - color: hsl(var(--muted-foreground)); +.notification.success { background: hsl(var(--success)); color: white; } +.notification.error { background: hsl(var(--destructive)); color: white; } + +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } } + +/* Scrollbar */ +.loop-list::-webkit-scrollbar, +.task-list::-webkit-scrollbar { + width: 6px; +} + +.loop-list::-webkit-scrollbar-track, +.task-list::-webkit-scrollbar-track { + background: hsl(var(--muted) / 0.3); + border-radius: 3px; +} + +.loop-list::-webkit-scrollbar-thumb, +.task-list::-webkit-scrollbar-thumb { + background: hsl(var(--muted-foreground) / 0.3); + border-radius: 3px; +} + +.loop-list::-webkit-scrollbar-thumb:hover, +.task-list::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground) / 0.5); +} + +/* Mobile Responsive */ +@media (max-width: 768px) { + .task-item-actions { + opacity: 1; + } + + .detail-header { + flex-direction: column; + gap: 1rem; + align-items: flex-start; + } + + .detail-actions { + width: 100%; + } + + .detail-actions .btn { + flex: 1; + justify-content: center; } } diff --git a/ccw/src/templates/dashboard-css/36-loop-monitor.css.backup b/ccw/src/templates/dashboard-css/36-loop-monitor.css.backup new file mode 100644 index 00000000..d67d7d03 --- /dev/null +++ b/ccw/src/templates/dashboard-css/36-loop-monitor.css.backup @@ -0,0 +1,1877 @@ +/* ========================================== + LOOP MONITOR STYLES + ========================================== */ + +/* Layout */ +.loop-monitor-layout { + display: grid; + grid-template-columns: 350px 1fr; + gap: 1.5rem; + height: calc(100vh - 200px); + min-height: 500px; +} + +/* Loop List Panel */ +.loop-list-panel { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.75rem; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + border-bottom: 1px solid hsl(var(--border)); + background: linear-gradient(180deg, hsl(var(--muted) / 0.3) 0%, transparent 100%); +} + +.panel-header h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; + color: hsl(var(--foreground)); +} + +.panel-header h3 i, +.panel-header h3 svg { + color: hsl(var(--primary)); +} + +/* Filter Select with Icon */ +.filter-select { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 0.75rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; +} + +.filter-select:hover { + border-color: hsl(var(--primary) / 0.5); + background: hsl(var(--hover)); +} + +.filter-select:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1); +} + +.filter-group select { + padding: 0.375rem 0.75rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.875rem; +} + +.loop-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +/* Loop Card */ +.loop-card { + padding: 0.875rem 1rem; + margin-bottom: 0.625rem; + background: hsl(var(--background)); + border: 1px solid hsl(var(--border)); + border-left-width: 4px; + border-radius: 0.625rem; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +/* Card shine effect on hover */ +.loop-card::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent); + transition: left 0.6s ease; + pointer-events: none; +} + +.loop-card:hover::before { + left: 100%; +} + +.loop-card:hover { + background: hsl(var(--hover)); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); + transform: translateY(-2px); + border-color: hsl(var(--primary) / 0.3); +} + +.loop-card.selected { + background: linear-gradient(135deg, hsl(var(--primary) / 0.15) 0%, hsl(var(--primary) / 0.05) 100%); + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 3px hsl(var(--primary) / 0.15), 0 4px 12px hsl(var(--primary) / 0.1); +} + +/* Status-specific border colors */ +.loop-card.status-created { border-left-color: hsl(var(--muted-foreground)); } +.loop-card.status-running { border-left-color: hsl(var(--info)); } +.loop-card.status-paused { border-left-color: hsl(var(--warning)); } +.loop-card.status-completed { border-left-color: hsl(var(--success)); } +.loop-card.status-failed { border-left-color: hsl(var(--destructive)); } + +.loop-card-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.loop-status-indicator { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 0.875rem; + flex-shrink: 0; + background: hsl(var(--muted)); +} + +/* Status indicator colors */ +.loop-status-indicator.created { + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +.loop-status-indicator.running { + background: hsl(var(--info-light)); + color: hsl(var(--info)); + animation: pulse-ring 2s ease-in-out infinite; +} + +.loop-status-indicator.paused { + background: hsl(var(--warning-light)); + color: hsl(var(--warning)); +} + +.loop-status-indicator.completed { + background: hsl(var(--success-light)); + color: hsl(var(--success)); +} + +.loop-status-indicator.failed { + background: hsl(var(--destructive) / 0.15); + color: hsl(var(--destructive)); +} + +@keyframes pulse-ring { + 0%, 100% { + box-shadow: 0 0 0 0 hsl(var(--info) / 0.4); + } + 50% { + box-shadow: 0 0 0 6px hsl(var(--info) / 0); + } +} + +.loop-title { + font-weight: 600; + font-size: 0.875rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.loop-card-body { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.loop-meta { + display: flex; + justify-content: space-between; + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.loop-task-id { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 180px; +} + +.loop-status-text { + font-weight: 500; +} + +.loop-progress { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.loop-step-info { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.loop-time { + font-size: 0.7rem; + color: hsl(var(--muted-foreground)); +} + +/* Progress Bar */ +.progress-bar { + flex: 1; + height: 8px; + background: hsl(var(--muted)); + border-radius: 4px; + overflow: hidden; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, hsl(var(--primary)), hsl(var(--success))); + border-radius: 4px; + transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +/* Progress bar shimmer effect */ +.progress-fill::after { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + transform: translateX(-100%); + animation: progress-shimmer 2s infinite; +} + +@keyframes progress-shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } +} + +/* Running state - animated gradient */ +.loop-card.status-running .progress-fill { + background: linear-gradient(90deg, hsl(var(--info)), hsl(var(--primary))); + animation: progress-pulse 2s ease-in-out infinite; +} + +@keyframes progress-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.85; } +} + +.progress-text { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + min-width: 60px; + text-align: right; +} + +/* Loop Detail Panel */ +.loop-detail-panel { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.75rem; + overflow-y: auto; + padding: 1.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.empty-detail-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: hsl(var(--muted-foreground)); + gap: 1.5rem; + padding: 3rem 1rem; +} + +.empty-detail-state i, +.empty-detail-state svg { + width: 64px; + height: 64px; + opacity: 0.4; + color: hsl(var(--primary)); +} + +.empty-detail-state p { + font-size: 1rem; + max-width: 280px; + text-align: center; + line-height: 1.6; +} + +.empty-detail-state .empty-icon-large { + width: 80px; + height: 80px; + padding: 1.25rem; + background: hsl(var(--muted) / 0.3); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.empty-detail-state .empty-icon-large i, +.empty-detail-state .empty-icon-large svg { + width: 40px; + height: 40px; + opacity: 0.5; +} + +.empty-detail-state .empty-hint { + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + opacity: 0.8; +} + +/* Loop Detail */ +.loop-detail { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.detail-header { + display: flex; + align-items: center; + justify-content: space-between; + padding-bottom: 1rem; + border-bottom: 1px solid hsl(var(--border)); +} + +.detail-status { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.625rem 1.25rem; + border-radius: 2rem; + font-weight: 600; + font-size: 0.875rem; +} + +/* Detail status variants */ +.detail-status.created { + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); +} + +.detail-status.running { + background: hsl(var(--info-light)); + color: hsl(var(--info)); + animation: status-pulse 2s ease-in-out infinite; +} + +.detail-status.paused { + background: hsl(var(--warning-light)); + color: hsl(var(--warning)); +} + +.detail-status.completed { + background: hsl(var(--success-light)); + color: hsl(var(--success)); +} + +.detail-status.failed { + background: hsl(var(--destructive) / 0.15); + color: hsl(var(--destructive)); +} + +@keyframes status-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.8; } +} + +.status-icon { + font-size: 1.25rem; +} + +.detail-actions { + display: flex; + gap: 0.5rem; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + border: none; +} + +.btn-success { + background: hsl(var(--success)); + color: white; +} + +.btn-success:hover { + background: hsl(var(--success) / 0.9); +} + +.btn-warning { + background: hsl(var(--warning)); + color: white; +} + +.btn-warning:hover { + background: hsl(var(--warning) / 0.9); +} + +.btn-danger { + background: hsl(var(--destructive)); + color: white; +} + +.btn-danger:hover { + background: hsl(var(--destructive) / 0.9); +} + +.detail-info { + padding: 1rem; + background: hsl(var(--muted) / 0.3); + border-radius: 0.5rem; +} + +.detail-title { + margin: 0 0 0.75rem 0; + font-size: 1.25rem; + font-weight: 700; +} + +.detail-meta { + display: flex; + flex-wrap: wrap; + gap: 1rem; + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); +} + +.detail-meta span { + display: flex; + align-items: center; + gap: 0.375rem; +} + +/* Detail Section */ +.detail-section { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.detail-section h4 { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: hsl(var(--foreground)); +} + +.progress-group { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.progress-item { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.progress-item label { + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--muted-foreground)); +} + +/* CLI Sequence */ +.cli-sequence { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.cli-step { + display: flex; + gap: 0.75rem; + padding: 0.75rem; + background: hsl(var(--muted) / 0.3); + border-radius: 0.375rem; +} + +.step-marker { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: hsl(var(--muted)); + font-size: 0.875rem; + font-weight: 600; + flex-shrink: 0; +} + +.cli-step.current .step-marker { + background: hsl(var(--primary)); + color: white; +} + +.cli-step.completed .step-marker { + background: hsl(var(--success)); + color: white; +} + +.step-content { + flex: 1; + min-width: 0; +} + +.step-name { + font-weight: 600; + font-size: 0.875rem; + margin-bottom: 0.25rem; +} + +.step-prompt { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + overflow: hidden; + text-overflow: ellipsis; +} + +/* Variables Grid */ +.variables-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.5rem; +} + +.variable-item { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.5rem; + background: hsl(var(--muted) / 0.3); + border-radius: 0.375rem; +} + +.variable-key { + font-size: 0.75rem; + font-weight: 600; + color: hsl(var(--muted-foreground)); +} + +.variable-value { + font-size: 0.875rem; + font-family: 'Consolas', 'Monaco', monospace; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Execution Timeline */ +.execution-timeline { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.timeline-iteration { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding-left: 1rem; +} + +.timeline-iteration.current { + border-left: 3px solid hsl(var(--primary)); + padding-left: 0.875rem; +} + +.iteration-header { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; +} + +.iteration-marker { + font-size: 1rem; +} + +.timeline-iteration.completed .iteration-marker { + color: hsl(var(--success)); +} + +.timeline-iteration.current .iteration-marker { + color: hsl(var(--primary)); +} + +.iteration-steps { + display: flex; + flex-direction: column; + gap: 0.375rem; + padding-left: 1.5rem; +} + +.timeline-step { + display: flex; + gap: 0.75rem; + padding: 0.5rem; + background: hsl(var(--muted) / 0.2); + border-radius: 0.375rem; +} + +.step-status { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 0.75rem; + flex-shrink: 0; +} + +.step-status.success { + background: hsl(var(--success)); + color: white; +} + +.step-status.failed { + background: hsl(var(--destructive)); + color: white; +} + +.step-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.step-tool { + font-weight: 600; + font-size: 0.875rem; +} + +.step-time { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.step-duration { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.step-error { + font-size: 0.75rem; + color: hsl(var(--destructive)); + background: hsl(var(--destructive) / 0.1); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + margin-top: 0.25rem; +} + +/* Error Section */ +.error-section { + padding: 1rem; + background: hsl(var(--destructive) / 0.1); + border: 1px solid hsl(var(--destructive) / 0.3); + border-radius: 0.5rem; +} + +.error-section h4 { + margin: 0 0 0.5rem 0; + color: hsl(var(--destructive)); +} + +.error-message { + font-family: 'Consolas', 'Monaco', monospace; + font-size: 0.875rem; + color: hsl(var(--destructive)); + white-space: pre-wrap; + word-break: break-word; +} + +/* Notification */ +.notification { + position: fixed; + bottom: 1rem; + right: 1rem; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 500; + z-index: 1000; + animation: slideIn 0.3s ease-out; +} + +.notification.success { + background: hsl(var(--success)); + color: white; +} + +.notification.error { + background: hsl(var(--destructive)); + color: white; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Empty State */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + color: hsl(var(--muted-foreground)); + gap: 1rem; + text-align: center; +} + +.empty-state-icon { + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + background: hsl(var(--muted) / 0.4); + border-radius: 50%; + margin-bottom: 0.5rem; +} + +.empty-state-icon i, +.empty-state-icon svg { + width: 28px; + height: 28px; + opacity: 0.5; +} + +.empty-state-title { + font-size: 1rem; + font-weight: 500; + color: hsl(var(--foreground)); +} + +.empty-state-hint { + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + max-width: 280px; + line-height: 1.5; +} + +/* Loading Spinner */ +.loading-spinner { + display: flex; + align-items: center; + justify-content: center; + padding: 3rem 1rem; + color: hsl(var(--muted-foreground)); +} + +/* Color Themes */ +.text-cyan-500 { color: #06b6d4; } +.bg-cyan-100 { background: #cffafe; } +.text-amber-500 { color: #f59e0b; } +.bg-amber-100 { background: #fef3c7; } +.text-emerald-500 { color: #10b981; } +.bg-emerald-100 { background: #d1fae5; } +.text-red-500 { color: #ef4444; } +.bg-red-100 { background: #fee2e2; } +.text-gray-400 { color: #9ca3af; } +.bg-gray-100 { background: #f3f4f6; } + +.border-l-cyan-500 { border-left-color: #06b6d4; } +.border-l-amber-500 { border-left-color: #f59e0b; } +.border-l-emerald-500 { border-left-color: #10b981; } +.border-l-red-500 { border-left-color: #ef4444; } +.border-l-gray-400 { border-left-color: #9ca3af; } + +.bg-cyan-light { background: hsl(var(--info) / 0.2); } +.text-cyan { color: hsl(var(--info)); } + +/* Dark mode adjustments */ +[data-theme="dark"] .text-cyan-500 { color: #22d3ee; } +[data-theme="dark"] .bg-cyan-100 { background: #164e63; } +[data-theme="dark"] .text-amber-500 { color: #fbbf24; } +[data-theme="dark"] .bg-amber-100 { background: #78350f; } +[data-theme="dark"] .text-emerald-500 { color: #34d399; } +[data-theme="dark"] .bg-emerald-100 { background: #065f46; } +[data-theme="dark"] .text-red-500 { color: #f87171; } +[data-theme="dark"] .bg-red-100 { background: #7f1d1d; } +[data-theme="dark"] .text-gray-400 { color: #9ca3af; } +[data-theme="dark"] .bg-gray-100 { background: #374151; } + +/* Responsive */ +@media (max-width: 1024px) { + .loop-monitor-layout { + grid-template-columns: 1fr; + height: auto; + } + + .loop-detail-panel { + min-height: 400px; + } + + .loop-list-panel { + border-right: none; + border-bottom: 1px solid hsl(var(--border)); + } +} + +/* ========================================== + * TASK CREATION STYLES + * ========================================== */ + +/* Header actions */ +.header-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.header-actions .btn { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; +} + +/* Tasks list */ +.tasks-list-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.3); +} + +/* Task card */ +.task-card { + background: white; + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + margin-bottom: 0.75rem; + overflow: hidden; + transition: box-shadow 0.2s, transform 0.2s; +} + +.task-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} + +.task-card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: hsl(var(--muted) / 0.3); + border-bottom: 1px solid hsl(var(--border)); +} + +.task-title { + font-weight: 600; + color: hsl(var(--foreground)); +} + +.task-id { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + font-family: monospace; +} + +.task-card-body { + padding: 1rem; +} + +.task-description { + color: hsl(var(--muted-foreground)); + font-size: 0.875rem; + margin-bottom: 0.75rem; + line-height: 1.5; +} + +.task-meta { + display: flex; + gap: 1rem; + font-size: 0.875rem; + color: hsl(var(--muted-foreground)); + margin-bottom: 0.75rem; +} + +.task-meta span { + display: flex; + align-items: center; + gap: 0.25rem; +} + +/* Modal styles */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +} + +.modal-content { + background: white; + border-radius: 0.75rem; + width: 100%; + max-width: 600px; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 20px 25px rgba(0, 0, 0, 0.15); +} + +.modal-content.modal-lg { + max-width: 800px; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.5rem; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.3); +} + +.modal-header h3 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; +} + +.modal-close { + background: none; + border: none; + cursor: pointer; + padding: 0.25rem; + border-radius: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + color: hsl(var(--muted-foreground)); + transition: background 0.2s; +} + +.modal-close:hover { + background: hsl(var(--muted) / 0.5); +} + +.modal-body { + padding: 1.5rem; + overflow-y: auto; + flex: 1; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + padding: 1rem 1.5rem; + border-top: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.3); +} + +/* Form styles */ +.form-section { + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid hsl(var(--border)); +} + +.form-section:last-child { + border-bottom: none; +} + +.form-section h4 { + margin: 0 0 1rem 0; + font-size: 1rem; + font-weight: 600; + color: hsl(var(--foreground)); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.form-section h4 i, +.form-section h4 svg { + color: hsl(var(--primary)); +} + +/* Section header with action button */ +.section-header-with-action { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.section-header-with-action h4 { + margin: 0; +} + +.section-header-with-action .btn-outline { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + border: 1px solid hsl(var(--border)); + background: transparent; + color: hsl(var(--foreground)); + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.375rem; +} + +.section-header-with-action .btn-outline:hover { + background: hsl(var(--accent) / 0.1); + border-color: hsl(var(--primary)); + color: hsl(var(--primary)); +} + +/* Section-specific styling */ +.form-section.section-config { + background: linear-gradient(135deg, hsl(var(--primary) / 0.05) 0%, transparent 50%); + border-radius: 0.5rem; + padding: 1.25rem; + border: 1px solid hsl(var(--primary) / 0.2); + border-left: 4px solid hsl(var(--primary)); +} + +.form-section.section-config h4 { + color: hsl(var(--primary)); +} + +.form-section.section-config h4 i, +.form-section.section-config h4 svg { + color: hsl(var(--primary)); +} + +.form-section.section-cli { + background: linear-gradient(135deg, hsl(var(--info) / 0.05) 0%, transparent 50%); + border-radius: 0.5rem; + padding: 1.25rem; + border: 1px solid hsl(var(--info) / 0.2); + border-left: 4px solid hsl(var(--info)); +} + +.form-section.section-cli h4 { + color: hsl(var(--info)); +} + +.form-section.section-cli h4 i, +.form-section.section-cli h4 svg { + color: hsl(var(--info)); +} + +.form-section.section-basic { + background: linear-gradient(135deg, hsl(var(--success) / 0.05) 0%, transparent 50%); + border-radius: 0.5rem; + padding: 1.25rem; + border: 1px solid hsl(var(--success) / 0.2); + border-left: 4px solid hsl(var(--success)); +} + +.form-section.section-basic h4 { + color: hsl(var(--success)); +} + +.form-section.section-basic h4 i, +.form-section.section-basic h4 svg { + color: hsl(var(--success)); +} + +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: flex; + align-items: center; + gap: 0.375rem; + margin-bottom: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--foreground)); +} + +/* Label icons in config section */ +.section-config .form-group label i, +.section-config .form-group label svg { + color: hsl(var(--primary)); + opacity: 0.7; +} + +/* Label icons in CLI section */ +.section-cli .form-group label i, +.section-cli .form-group label svg { + color: hsl(var(--info)); + opacity: 0.7; +} + +/* Label icons in basic section */ +.section-basic .form-group label i, +.section-basic .form-group label svg { + color: hsl(var(--success)); + opacity: 0.7; +} + +.form-control { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.375rem; + font-size: 0.875rem; + transition: all 0.2s; + background: hsl(var(--background)); +} + +.form-control:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1); + background: hsl(var(--card)); +} + +/* Section-specific input focus colors */ +.section-config .form-control:focus { + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1); +} + +.section-cli .form-control:focus { + border-color: hsl(var(--info)); + box-shadow: 0 0 0 3px hsl(var(--info) / 0.1); +} + +.section-basic .form-control:focus { + border-color: hsl(var(--success)); + box-shadow: 0 0 0 3px hsl(var(--success) / 0.1); +} + +/* Helper text */ +.form-group small { + display: block; + margin-top: 0.375rem; + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + line-height: 1.4; +} + +.text-gray-500 { + color: hsl(var(--muted-foreground)); +} + +textarea.form-control { + resize: vertical; + min-height: 80px; +} + +.form-row { + display: flex; + gap: 1rem; +} + +.form-row .form-group { + flex: 1; +} + +/* CLI Step Card */ +.cli-step-card { + background: linear-gradient(135deg, hsl(var(--card)) 0%, hsl(var(--muted) / 0.2) 100%); + border: 1px solid hsl(var(--info) / 0.3); + border-radius: 0.625rem; + padding: 1.5rem; + margin-bottom: 1rem; + transition: all 0.25s ease; +} + +.cli-step-card:hover { + border-color: hsl(var(--info) / 0.5); + box-shadow: 0 4px 12px hsl(var(--info) / 0.1); +} + +.cli-step-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 0.75rem; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid hsl(var(--border) / 0.5); +} + +.step-number { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + font-weight: 500; + flex-shrink: 0; + padding: 0.5rem 0.75rem; + background: hsl(var(--info) / 0.08); + border-radius: 0.5rem; +} + +.step-number i { + color: hsl(var(--info)); + font-size: 0.875rem; +} + +.step-number .step-text { + font-size: 0.75rem; + font-weight: 600; + color: hsl(var(--info)); + white-space: nowrap; + line-height: 1.2; +} + +.step-number::before { + display: none; +} + +.btn-text { + background: none; + border: none; + color: hsl(var(--destructive)); + cursor: pointer; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + transition: background 0.2s; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.btn-text:hover { + background: hsl(var(--destructive) / 0.1); +} + +/* Bash field visibility */ +.cli-step-card .bash-only { + display: none; +} + +.cli-step-card .bash-only:not([style*="display: none"]) { + display: block; +} + +/* Button variants */ +.btn { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + border: none; + display: inline-flex; + align-items: center; + gap: 0.375rem; + transition: background 0.2s, opacity 0.2s; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: hsl(var(--primary)); + color: white; +} + +.btn-primary:hover:not(:disabled) { + background: hsl(var(--primary) / 0.9); +} + +.btn-secondary { + background: hsl(var(--secondary)); + color: white; +} + +.btn-secondary:hover:not(:disabled) { + background: hsl(var(--secondary) / 0.9); +} + +/* Info light button for CLI section */ +.btn-info-light { + background: hsl(var(--info) / 0.15); + color: hsl(var(--info)); + border: 1px solid hsl(var(--info) / 0.3); +} + +.btn-info-light:hover:not(:disabled) { + background: hsl(var(--info) / 0.25); + border-color: hsl(var(--info) / 0.5); +} + +.btn-success { + background: hsl(var(--success)); + color: white; +} + +.btn-success:hover:not(:disabled) { + background: hsl(var(--success) / 0.9); +} + +.btn-danger { + background: hsl(var(--destructive)); + color: white; +} + +.btn-danger:hover:not(:disabled) { + background: hsl(var(--destructive) / 0.9); +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +.w-full { + width: 100%; +} + +.flex { + display: flex; +} + +.flex-1 { + flex: 1; +} + +.justify-between { + justify-content: space-between; +} + +.items-center { + align-items: center; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.text-sm { + font-size: 0.875rem; +} + +.text-gray-400 { + color: #9ca3af; +} + +.small { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +/* ========================================== + * TASK DETAIL STYLES + * ========================================== */ + +/* Config Grid */ +.config-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; +} + +.config-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.config-item-full { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.config-label { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.config-value { + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--foreground)); +} + +.config-code { + display: block; + padding: 0.5rem 0.75rem; + background: hsl(var(--muted)); + border-radius: 0.375rem; + font-size: 0.875rem; + font-family: monospace; + color: hsl(var(--foreground)); + overflow-x: auto; +} + +/* Steps List */ +.steps-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.steps-list .step-item { + display: flex; + gap: 0.75rem; + padding: 0.75rem; + background: hsl(var(--muted) / 0.3); + border-radius: 0.5rem; + border-left: 3px solid hsl(var(--primary)); +} + +.steps-list .step-number { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: hsl(var(--primary)); + color: white; + border-radius: 50%; + font-size: 0.75rem; + font-weight: 600; + flex-shrink: 0; +} + +.steps-list .step-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.steps-list .step-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.steps-list .step-id { + font-weight: 600; + color: hsl(var(--foreground)); +} + +.steps-list .step-tool { + font-size: 0.75rem; + padding: 0.125rem 0.5rem; + background: hsl(var(--info) / 0.15); + color: hsl(var(--info)); + border-radius: 9999px; +} + +.steps-list .step-details { + display: flex; + gap: 1rem; + font-size: 0.875rem; +} + +.steps-list .step-mode { + color: hsl(var(--muted-foreground)); +} + +.steps-list .step-error { + color: hsl(var(--warning)); +} + +.steps-list .step-prompt, +.steps-list .step-command { + padding: 0.5rem; + background: hsl(var(--background)); + border-radius: 0.375rem; + font-size: 0.875rem; +} + +.steps-list .step-prompt small, +.steps-list .step-command small { + display: block; + margin-bottom: 0.25rem; + color: hsl(var(--muted-foreground)); +} + +.steps-list .step-prompt p { + margin: 0; + color: hsl(var(--foreground)); +} + +.steps-list .step-command code { + color: hsl(var(--foreground)); +} + +/* ========================================== + * V2 LOOP STYLES + * ========================================== */ + +/* Loop card description */ +.loop-card-body .loop-description { + font-size: 0.8125rem; + color: hsl(var(--muted-foreground)); + line-height: 1.4; + margin-bottom: 0.25rem; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +/* Version badge (v1/v2) */ +.version-badge { + font-size: 0.625rem; + font-weight: 700; + text-transform: uppercase; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + background: hsl(var(--muted) / 0.5); + color: hsl(var(--muted-foreground)); + letter-spacing: 0.05em; +} + +/* Detail description */ +.detail-description { + font-size: 0.9375rem; + color: hsl(var(--muted-foreground)); + line-height: 1.5; + margin: 0 0 1rem 0; +} + +/* Info box for v2 loops */ +.info-box { + padding: 1rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + line-height: 1.6; +} + +.info-box p { + margin: 0 0 1rem 0; + color: hsl(var(--muted-foreground)); +} + +.info-box p:last-child { + margin-bottom: 0; +} + +/* Required field marker */ +.required { + color: hsl(var(--destructive)); + font-weight: bold; +} + +/* Alert styles */ +.alert { + padding: 0.75rem 1rem; + border-radius: 0.375rem; + margin-bottom: 1rem; + font-size: 0.875rem; +} + +.alert-error { + background: hsl(var(--destructive) / 0.1); + border: 1px solid hsl(var(--destructive) / 0.3); + color: hsl(var(--destructive)); +} + +.alert-warning { + background: hsl(var(--warning) / 0.1); + border: 1px solid hsl(var(--warning) / 0.3); + color: hsl(var(--warning)); +} + +.alert-success { + background: hsl(var(--success) / 0.1); + border: 1px solid hsl(var(--success) / 0.3); + color: hsl(var(--success)); +} + +/* Task list placeholder */ +.task-list-placeholder { + padding: 2rem; + background: hsl(var(--muted) / 0.2); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.task-list-placeholder .empty-state i, +.task-list-placeholder .empty-state svg { + width: 48px; + height: 48px; + opacity: 0.4; +} + +/* Loop ID display in detail meta */ +.detail-meta span { + font-size: 0.8125rem; +} + +/* ========================================== + * TASK LIST STYLES + * ========================================== */ + +/* Task list container */ +.task-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: 400px; + overflow-y: auto; +} + +/* Task list empty state */ +.task-list-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 1.5rem; + background: hsl(var(--muted) / 0.2); + border-radius: 0.5rem; + color: hsl(var(--muted-foreground)); + text-align: center; +} + +.task-list-empty i, +.task-list-empty svg { + width: 48px; + height: 48px; + opacity: 0.4; + margin-bottom: 1rem; + color: hsl(var(--primary)); +} + +/* Task item */ +.task-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1rem; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + cursor: grab; + transition: all 0.2s ease; +} + +.task-item:hover { + background: hsl(var(--hover)); + border-color: hsl(var(--primary) / 0.3); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.task-item.dragging { + opacity: 0.5; + background: hsl(var(--muted) / 0.3); + border-style: dashed; +} + +/* Drag handle */ +.task-item-drag { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: hsl(var(--muted-foreground)); + flex-shrink: 0; + cursor: grab; +} + +.task-item:hover .task-item-drag { + color: hsl(var(--primary)); +} + +/* Task content */ +.task-item-content { + flex: 1; + min-width: 0; +} + +.task-item-header { + margin-bottom: 0.375rem; +} + +.task-item-description { + font-size: 0.875rem; + font-weight: 500; + color: hsl(var(--foreground)); + line-height: 1.4; +} + +/* Task meta with badges */ +.task-item-meta { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +/* Task status badge */ +.task-status-badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.task-status-badge.status-analysis { + background: hsl(var(--info) / 0.15); + color: hsl(var(--info)); +} + +.task-status-badge.status-write { + background: hsl(var(--success) / 0.15); + color: hsl(var(--success)); +} + +.task-status-badge.status-review { + background: hsl(var(--warning) / 0.15); + color: hsl(var(--warning)); +} + +/* Tool badge */ +.task-tool-badge { + display: inline-flex; + align-items: center; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 500; + background: hsl(var(--muted) / 0.5); + color: hsl(var(--muted-foreground)); +} + +/* Task actions */ +.task-item-actions { + display: flex; + align-items: center; + gap: 0.375rem; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.2s ease; +} + +.task-item:hover .task-item-actions { + opacity: 1; +} + +/* Mobile - always show actions */ +@media (max-width: 768px) { + .task-item-actions { + opacity: 1; + } +} + +/* Small buttons for task actions */ +.task-item-actions .btn { + padding: 0.375rem 0.5rem; + font-size: 0.75rem; +} + +/* Drag over state for drop indicator */ +.task-item.drag-over { + border-top: 2px solid hsl(var(--primary)); + margin-top: 2px; +} + +/* Scrollbar styling for task list */ +.task-list::-webkit-scrollbar { + width: 6px; +} + +.task-list::-webkit-scrollbar-track { + background: hsl(var(--muted) / 0.3); + border-radius: 3px; +} + +.task-list::-webkit-scrollbar-thumb { + background: hsl(var(--muted-foreground) / 0.3); + border-radius: 3px; +} + +.task-list::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground) / 0.5); +} diff --git a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js index 5b789a9b..f7fa7d23 100644 --- a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +++ b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js @@ -10,7 +10,7 @@ let streamScrollHandler = null; // Track scroll listener let streamStatusTimers = []; // Track status update timers // ===== State Management ===== -let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime } } +let cliStreamExecutions = {}; // { executionId: { tool, mode, output, status, startTime, endTime, recovered } } let activeStreamTab = null; let autoScrollEnabled = true; let isCliStreamViewerOpen = false; @@ -18,116 +18,212 @@ let searchFilter = ''; // Search filter for output content const MAX_OUTPUT_LINES = 5000; // Prevent memory issues +// ===== Sync State Management ===== +let syncPromise = null; // Track ongoing sync to prevent duplicates +let syncTimeoutId = null; // Debounce timeout ID +let lastSyncTime = 0; // Track last successful sync time +const SYNC_DEBOUNCE_MS = 300; // Debounce delay for sync calls +const SYNC_TIMEOUT_MS = 10000; // 10 second timeout for sync requests + // ===== State Synchronization ===== /** * Sync active executions from server * Called on initialization to recover state when view is opened mid-execution + * Also called on WebSocket reconnection to restore CLI viewer state + * + * Features: + * - Debouncing: Prevents rapid successive sync calls + * - Deduplication: Only one sync at a time + * - Timeout handling: 10 second timeout for sync requests + * - Recovery flag: Marks recovered sessions for visual indicator */ async function syncActiveExecutions() { // Only sync in server mode if (!window.SERVER_MODE) return; - try { - const response = await fetch('/api/cli/active'); - if (!response.ok) return; + // Deduplication: if a sync is already in progress, return that promise + if (syncPromise) { + console.log('[CLI Stream] Sync already in progress, skipping'); + return syncPromise; + } - const { executions } = await response.json(); - if (!executions || executions.length === 0) return; + // Clear any pending debounced sync + if (syncTimeoutId) { + clearTimeout(syncTimeoutId); + syncTimeoutId = null; + } - let needsUiUpdate = false; + syncPromise = (async function() { + try { + // Create timeout promise + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Sync timeout')), SYNC_TIMEOUT_MS); + }); - executions.forEach(exec => { - const existing = cliStreamExecutions[exec.id]; + // Race between fetch and timeout + const response = await Promise.race([ + fetch('/api/cli/active'), + timeoutPromise + ]); - // Parse historical output from server - const historicalLines = []; - if (exec.output) { - const lines = exec.output.split('\n'); - const startIndex = Math.max(0, lines.length - MAX_OUTPUT_LINES + 1); - lines.slice(startIndex).forEach(line => { - if (line.trim()) { - historicalLines.push({ - type: 'stdout', - content: line, - timestamp: exec.startTime || Date.now() - }); - } - }); - } - - if (existing) { - // Already tracked by WebSocket events - merge historical output - // Only prepend historical lines that are not already in the output - // (WebSocket events only add NEW output, so historical output should come before) - const existingContentSet = new Set(existing.output.map(o => o.content)); - const missingLines = historicalLines.filter(h => !existingContentSet.has(h.content)); - - if (missingLines.length > 0) { - // Find the system start message index (skip it when prepending) - const systemMsgIndex = existing.output.findIndex(o => o.type === 'system'); - const insertIndex = systemMsgIndex >= 0 ? systemMsgIndex + 1 : 0; - - // Prepend missing historical lines after system message - existing.output.splice(insertIndex, 0, ...missingLines); - - // Trim if too long - if (existing.output.length > MAX_OUTPUT_LINES) { - existing.output = existing.output.slice(-MAX_OUTPUT_LINES); - } - - needsUiUpdate = true; - console.log(`[CLI Stream] Merged ${missingLines.length} historical lines for ${exec.id}`); - } + if (!response.ok) { + console.warn('[CLI Stream] Sync response not OK:', response.status); return; } - needsUiUpdate = true; + const { executions } = await response.json(); - // New execution - rebuild full state - cliStreamExecutions[exec.id] = { - tool: exec.tool || 'cli', - mode: exec.mode || 'analysis', - output: [], - status: exec.status || 'running', - startTime: exec.startTime || Date.now(), - endTime: null - }; + // Handle empty response gracefully + if (!executions || executions.length === 0) { + console.log('[CLI Stream] No active executions to sync'); + return; + } - // Add system start message - cliStreamExecutions[exec.id].output.push({ - type: 'system', - content: `[${new Date(exec.startTime).toLocaleTimeString()}] CLI execution started: ${exec.tool} (${exec.mode} mode)`, - timestamp: exec.startTime + let needsUiUpdate = false; + const now = Date.now(); + lastSyncTime = now; + + executions.forEach(exec => { + const existing = cliStreamExecutions[exec.id]; + + // Parse historical output from server with type detection + const historicalLines = []; + if (exec.output) { + const lines = exec.output.split('\n'); + const startIndex = Math.max(0, lines.length - MAX_OUTPUT_LINES + 1); + lines.slice(startIndex).forEach(line => { + if (line.trim()) { + // Detect type from content prefix for proper formatting + const parsed = parseMessageType(line); + // Map parsed type to chunkType for rendering + const typeMap = { + system: 'system', + thinking: 'thought', + response: 'stdout', + result: 'metadata', + error: 'stderr', + warning: 'stderr', + info: 'metadata' + }; + historicalLines.push({ + type: parsed.hasPrefix ? (typeMap[parsed.type] || 'stdout') : 'stdout', + content: line, // Keep original content with prefix + timestamp: exec.startTime || Date.now() + }); + } + }); + } + + if (existing) { + // Already tracked by WebSocket events - merge historical output + // Only prepend historical lines that are not already in the output + // (WebSocket events only add NEW output, so historical output should come before) + const existingContentSet = new Set(existing.output.map(o => o.content)); + const missingLines = historicalLines.filter(h => !existingContentSet.has(h.content)); + + if (missingLines.length > 0) { + // Find the system start message index (skip it when prepending) + const systemMsgIndex = existing.output.findIndex(o => o.type === 'system'); + const insertIndex = systemMsgIndex >= 0 ? systemMsgIndex + 1 : 0; + + // Prepend missing historical lines after system message + existing.output.splice(insertIndex, 0, ...missingLines); + + // Trim if too long + if (existing.output.length > MAX_OUTPUT_LINES) { + existing.output = existing.output.slice(-MAX_OUTPUT_LINES); + } + + needsUiUpdate = true; + console.log(`[CLI Stream] Merged ${missingLines.length} historical lines for ${exec.id}`); + } + return; + } + + needsUiUpdate = true; + + // New execution - rebuild full state with recovered flag + cliStreamExecutions[exec.id] = { + tool: exec.tool || 'cli', + mode: exec.mode || 'analysis', + output: [], + status: exec.status || 'running', + startTime: exec.startTime || Date.now(), + endTime: exec.status !== 'running' ? Date.now() : null, + recovered: true // Mark as recovered for visual indicator + }; + + // Add system start message + cliStreamExecutions[exec.id].output.push({ + type: 'system', + content: `[${new Date(exec.startTime).toLocaleTimeString()}] CLI execution started: ${exec.tool} (${exec.mode} mode)`, + timestamp: exec.startTime + }); + + // Add historical output + cliStreamExecutions[exec.id].output.push(...historicalLines); + + // Add recovery notice for completed executions + if (exec.isComplete) { + cliStreamExecutions[exec.id].output.push({ + type: 'system', + content: `[Session recovered from server - ${exec.status}]`, + timestamp: now + }); + } }); - // Add historical output - cliStreamExecutions[exec.id].output.push(...historicalLines); - }); + // Update UI if we recovered or merged any executions + if (needsUiUpdate) { + // Set active tab to first running execution, or first recovered if none running + const runningExec = executions.find(e => e.status === 'running'); + if (runningExec && !activeStreamTab) { + activeStreamTab = runningExec.id; + } else if (!runningExec && executions.length > 0 && !activeStreamTab) { + // If no running executions, select the first recovered one + activeStreamTab = executions[0].id; + } - // Update UI if we recovered or merged any executions - if (needsUiUpdate) { - // Set active tab to first running execution - const runningExec = executions.find(e => e.status === 'running'); - if (runningExec && !activeStreamTab) { - activeStreamTab = runningExec.id; + renderStreamTabs(); + updateStreamBadge(); + + // If viewer is open, render content. If not, open it if we have any recovered executions. + if (isCliStreamViewerOpen) { + renderStreamContent(activeStreamTab); + } else if (executions.length > 0) { + // Automatically open the viewer if it's closed and we just synced any executions + // (running or completed - user might refresh after completion to see the output) + toggleCliStreamViewer(); + } } - renderStreamTabs(); - updateStreamBadge(); - - // If viewer is open, render content. If not, and there's a running execution, open it. - if (isCliStreamViewerOpen) { - renderStreamContent(activeStreamTab); - } else if (executions.some(e => e.status === 'running')) { - // Automatically open the viewer if it's closed and we just synced a running task - toggleCliStreamViewer(); + console.log(`[CLI Stream] Synced ${executions.length} active execution(s)`); + } catch (e) { + if (e.message === 'Sync timeout') { + console.warn('[CLI Stream] Sync request timed out after', SYNC_TIMEOUT_MS, 'ms'); + } else { + console.error('[CLI Stream] Sync failed:', e); } + } finally { + syncPromise = null; // Clear the promise to allow future syncs } + })(); - console.log(`[CLI Stream] Synced ${executions.length} active execution(s)`); - } catch (e) { - console.error('[CLI Stream] Sync failed:', e); + return syncPromise; +} + +/** + * Debounced sync function - prevents rapid successive sync calls + * Use this when multiple sync triggers may happen in quick succession + */ +function syncActiveExecutionsDebounced() { + if (syncTimeoutId) { + clearTimeout(syncTimeoutId); } + syncTimeoutId = setTimeout(function() { + syncTimeoutId = null; + syncActiveExecutions(); + }, SYNC_DEBOUNCE_MS); } // ===== Initialization ===== @@ -502,19 +598,24 @@ function renderStreamTabs() { tabsContainer.innerHTML = execIds.map(id => { const exec = cliStreamExecutions[id]; const isActive = id === activeStreamTab; - const canClose = exec.status !== 'running'; - + const isRecovered = exec.recovered === true; + + // Recovery badge HTML + const recoveryBadge = isRecovered + ? `Recovered` + : ''; + return ` -
${escapeHtml(exec.tool)} ${exec.mode} - + title="${_streamT('cliStream.close')}">×
`; }).join(''); @@ -589,29 +690,35 @@ function renderStreamContent(executionId) { function renderStreamStatus(executionId) { const statusContainer = document.getElementById('cliStreamStatus'); if (!statusContainer) return; - + const exec = executionId ? cliStreamExecutions[executionId] : null; - + if (!exec) { statusContainer.innerHTML = ''; return; } - - const duration = exec.endTime + + const duration = exec.endTime ? formatDuration(exec.endTime - exec.startTime) : formatDuration(Date.now() - exec.startTime); - - const statusLabel = exec.status === 'running' + + const statusLabel = exec.status === 'running' ? _streamT('cliStream.running') : exec.status === 'completed' ? _streamT('cliStream.completed') : _streamT('cliStream.error'); - + + // Recovery badge for status bar + const recoveryBadge = exec.recovered + ? `Recovered` + : ''; + statusContainer.innerHTML = `
${statusLabel} + ${recoveryBadge}
@@ -623,15 +730,15 @@ function renderStreamStatus(executionId) {
-
`; - + if (typeof lucide !== 'undefined') lucide.createIcons(); // Update duration periodically for running executions @@ -656,52 +763,85 @@ function switchStreamTab(executionId) { function updateStreamBadge() { const badge = document.getElementById('cliStreamBadge'); if (!badge) return; - + const runningCount = Object.values(cliStreamExecutions).filter(e => e.status === 'running').length; - + const totalCount = Object.keys(cliStreamExecutions).length; + if (runningCount > 0) { badge.textContent = runningCount; badge.classList.add('has-running'); + } else if (totalCount > 0) { + // Show badge for completed executions too (with a different style) + badge.textContent = totalCount; + badge.classList.remove('has-running'); + badge.classList.add('has-completed'); } else { badge.textContent = ''; - badge.classList.remove('has-running'); + badge.classList.remove('has-running', 'has-completed'); } } // ===== User Actions ===== function closeStream(executionId) { const exec = cliStreamExecutions[executionId]; - if (!exec || exec.status === 'running') return; - + if (!exec) return; + + // Note: We now allow closing running tasks - this just removes from view, + // the actual CLI process continues on the server delete cliStreamExecutions[executionId]; - + // Switch to another tab if this was active if (activeStreamTab === executionId) { const remaining = Object.keys(cliStreamExecutions); activeStreamTab = remaining.length > 0 ? remaining[0] : null; } - + renderStreamTabs(); renderStreamContent(activeStreamTab); updateStreamBadge(); + + // If no executions left, close the viewer + if (Object.keys(cliStreamExecutions).length === 0) { + toggleCliStreamViewer(); + } } function clearCompletedStreams() { const toRemove = Object.keys(cliStreamExecutions).filter( id => cliStreamExecutions[id].status !== 'running' ); - + toRemove.forEach(id => delete cliStreamExecutions[id]); - + // Update active tab if needed if (activeStreamTab && !cliStreamExecutions[activeStreamTab]) { const remaining = Object.keys(cliStreamExecutions); activeStreamTab = remaining.length > 0 ? remaining[0] : null; } - + renderStreamTabs(); renderStreamContent(activeStreamTab); updateStreamBadge(); + + // If no executions left, close the viewer + if (Object.keys(cliStreamExecutions).length === 0) { + toggleCliStreamViewer(); + } +} + +function clearAllStreams() { + // Clear all executions (both running and completed) + const allIds = Object.keys(cliStreamExecutions); + + allIds.forEach(id => delete cliStreamExecutions[id]); + activeStreamTab = null; + + renderStreamTabs(); + renderStreamContent(null); + updateStreamBadge(); + + // Close the viewer since there's nothing to show + toggleCliStreamViewer(); } function toggleAutoScroll() { @@ -839,6 +979,7 @@ window.handleCliStreamError = handleCliStreamError; window.switchStreamTab = switchStreamTab; window.closeStream = closeStream; window.clearCompletedStreams = clearCompletedStreams; +window.clearAllStreams = clearAllStreams; window.toggleAutoScroll = toggleAutoScroll; window.handleSearchInput = handleSearchInput; window.clearSearch = clearSearch; diff --git a/ccw/src/templates/dashboard-js/components/notifications.js b/ccw/src/templates/dashboard-js/components/notifications.js index bdbd7efb..34030d61 100644 --- a/ccw/src/templates/dashboard-js/components/notifications.js +++ b/ccw/src/templates/dashboard-js/components/notifications.js @@ -140,6 +140,22 @@ function initWebSocket() { wsConnection.onopen = () => { console.log('[WS] Connected'); + + // Trigger CLI stream sync on WebSocket reconnection + // This allows the viewer to recover after page refresh + if (typeof syncActiveExecutions === 'function') { + syncActiveExecutions().then(function() { + console.log('[WS] CLI executions synced after connection'); + }).catch(function(err) { + console.warn('[WS] Failed to sync CLI executions:', err); + }); + } + + // Emit custom event for other components to handle reconnection + const reconnectEvent = new CustomEvent('websocket-reconnected', { + detail: { timestamp: Date.now() } + }); + window.dispatchEvent(reconnectEvent); }; wsConnection.onmessage = (event) => { diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 76d48b92..ddd028a8 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -36,6 +36,7 @@ const i18n = { 'common.disabled': 'Disabled', 'common.yes': 'Yes', 'common.no': 'No', + 'common.na': 'N/A', // Header 'header.project': 'Project:', @@ -2406,6 +2407,154 @@ const i18n = { 'common.copyId': 'Copy ID', 'common.copied': 'Copied!', 'common.copyError': 'Failed to copy', + + // Loop Monitor + 'loop.title': 'Loop Monitor', + 'loop.loops': 'Loops', + 'loop.all': 'All', + 'loop.running': 'Running', + 'loop.paused': 'Paused', + 'loop.completed': 'Completed', + 'loop.failed': 'Failed', + 'loop.tasks': 'Tasks', + 'loop.newLoop': 'New Loop', + 'loop.loading': 'Loading loops...', + 'loop.noLoops': 'No loops found', + 'loop.noLoopsHint': 'Create a loop task to get started', + 'loop.selectLoop': 'Select a loop to view details', + 'loop.selectLoopHint': 'Click on a loop from the list to see its details', + 'loop.loopNotFound': 'Loop not found', + 'loop.selectAnotherLoop': 'Select another loop from the list', + 'loop.task': 'Task', + 'loop.steps': 'steps', + 'loop.taskInfo': 'Task Info', + 'loop.edit': 'Edit', + 'loop.taskId': 'Task ID', + 'loop.step': 'Step', + 'loop.updated': 'Updated', + 'loop.created': 'Created', + 'loop.progress': 'Progress', + 'loop.iteration': 'Iteration', + 'loop.currentStep': 'Current Step', + 'loop.cliSequence': 'CLI Sequence', + 'loop.stateVariables': 'State Variables', + 'loop.executionHistory': 'Execution History', + 'loop.failureReason': 'Failure Reason', + 'loop.pause': 'Pause', + 'loop.resume': 'Resume', + 'loop.stop': 'Stop', + 'loop.confirmStop': 'Stop loop {loopId}?\n\nIteration: {currentIteration}/{maxIterations}\nThis action cannot be undone.', + 'loop.loopPaused': 'Loop paused', + 'loop.loopResumed': 'Loop resumed', + 'loop.loopStopped': 'Loop stopped', + 'loop.failedToPause': 'Failed to pause', + 'loop.failedToResume': 'Failed to resume', + 'loop.failedToStop': 'Failed to stop', + 'loop.failedToLoad': 'Failed to load loops', + 'loop.justNow': 'just now', + 'loop.minutesAgo': '{m}m ago', + 'loop.hoursAgo': '{h}h ago', + 'loop.daysAgo': '{d}d ago', + 'loop.tasksCount': '{count} task(s) with loop enabled', + 'loop.noLoopTasks': 'No loop-enabled tasks found', + 'loop.createLoopTask': 'Create Loop Task', + 'loop.backToLoops': 'Back to Loops', + 'loop.startLoop': 'Start Loop', + 'loop.loopStarted': 'Loop started', + 'loop.failedToStart': 'Failed to start loop', + 'loop.createTaskFailed': 'Failed to create task', + 'loop.createLoopModal': 'Create Loop Task', + 'loop.basicInfo': 'Basic Information', + 'loop.importFromIssue': 'Import from Issue', + 'loop.selectIssue': 'Select an Issue', + 'loop.noIssuesFound': 'No issues found', + 'loop.fetchIssuesFailed': 'Failed to fetch issues', + 'loop.fetchIssueFailed': 'Failed to fetch issue', + 'loop.issueImported': 'Issue imported', + 'loop.taskTitle': 'Task Title', + 'loop.taskTitlePlaceholder': 'e.g., Auto Test Fix Loop', + 'loop.description': 'Description', + 'loop.descriptionPlaceholder': 'Describe what this loop does...', + 'loop.loopConfig': 'Loop Configuration', + 'loop.maxIterations': 'Max Iterations', + 'loop.errorPolicy': 'Error Policy', + 'loop.pauseOnError': 'Pause on error', + 'loop.retryAutomatically': 'Retry automatically', + 'loop.failImmediately': 'Fail immediately', + 'loop.maxRetries': 'Max Retries (for retry policy)', + 'loop.successCondition': 'Success Condition (JavaScript expression)', + 'loop.successConditionPlaceholder': 'e.g., state_variables.test_stdout.includes(\'passed\')', + 'loop.availableVars': 'Available: state_variables, current_iteration', + 'loop.cliSequence': 'CLI Sequence', + 'loop.addStep': 'Add Step', + 'loop.stepNumber': 'Step {number}', + 'loop.stepLabel': 'Step', + 'loop.removeStep': 'Remove step', + 'loop.stepId': 'Step ID', + 'loop.stepIdPlaceholder': 'e.g., run_tests', + 'loop.tool': 'Tool', + 'loop.mode': 'Mode', + 'loop.command': 'Command', + 'loop.commandPlaceholder': 'e.g., npm test', + 'loop.promptTemplate': 'Prompt Template (supports [variable_name] substitution)', + 'loop.promptPlaceholder': 'Enter prompt template...', + 'loop.onError': 'On Error', + 'loop.continue': 'Continue', + 'loop.pause': 'Pause', + 'loop.failFast': 'Fail Fast', + 'loop.cancel': 'Cancel', + 'loop.createAndStart': 'Create Loop', + 'loop.created': 'Created', + 'loop.createFailed': 'Create Loop Failed', + 'loop.taskCreated': 'Task created', + 'loop.taskCreatedFailedStart': 'Task created but failed to start loop', + // V2 Simplified Loop + 'loop.create': 'Create', + 'loop.loopCreated': 'Loop created successfully', + 'loop.titleRequired': 'Title is required', + 'loop.invalidMaxIterations': 'Max iterations must be between 1 and 100', + 'loop.loopInfo': 'Loop Info', + 'loop.v2LoopInfo': 'This is a simplified loop. Tasks are managed independently in the detail view.', + 'loop.manageTasks': 'Manage Tasks', + 'loop.taskManagement': 'Task Management', + 'loop.taskManagementPlaceholder': 'Task management will be available in the next update. Use the v1 loops for full task configuration.', + 'loop.noTasksYet': 'No tasks configured yet', + 'loop.back': 'Back', + 'loop.loopNotFound': 'Loop not found', + 'loop.selectAnotherLoop': 'Please select another loop from the list', + 'loop.start': 'Start', + 'loop.loopStarted': 'Loop started', + 'loop.failedToStart': 'Failed to start loop', + // Task List Management + 'loop.taskList': 'Task List', + 'loop.addTask': 'Add Task', + 'loop.taskDescription': 'Task Description', + 'loop.taskDescriptionPlaceholder': 'Describe what this task should do...', + 'loop.modeAnalysis': 'Analysis', + 'loop.modeWrite': 'Write', + 'loop.modeReview': 'Review', + 'loop.save': 'Save', + 'loop.taskAdded': 'Task added successfully', + 'loop.addTaskFailed': 'Failed to add task', + 'loop.editTask': 'Edit Task', + 'loop.taskUpdated': 'Task updated successfully', + 'loop.updateTaskFailed': 'Failed to update task', + 'loop.confirmDeleteTask': 'Are you sure you want to delete this task? This action cannot be undone.', + 'loop.taskDeleted': 'Task deleted successfully', + 'loop.deleteTaskFailed': 'Failed to delete task', + 'loop.deleteTaskError': 'Error deleting task', + 'loop.loadTasksFailed': 'Failed to load tasks', + 'loop.loadTasksError': 'Error loading tasks', + 'loop.tasksReordered': 'Tasks reordered', + 'loop.saveOrderFailed': 'Failed to save order', + 'loop.noTasksHint': 'Add your first task to get started', + 'loop.noDescription': 'No description', + 'loop.descriptionRequired': 'Description is required', + 'loop.loadTaskFailed': 'Failed to load task', + 'loop.loadTaskError': 'Error loading task', + 'loop.taskTitleHint': 'Enter a descriptive title for your loop', + 'loop.descriptionHint': 'Optional context about what this loop does', + 'loop.maxIterationsHint': 'Maximum number of iterations to run (1-100)', }, zh: { @@ -2436,6 +2585,7 @@ const i18n = { 'common.disabled': '已禁用', 'common.yes': '是', 'common.no': '否', + 'common.na': '无', // Header 'header.project': '项目:', @@ -4818,6 +4968,153 @@ const i18n = { 'common.copyId': '复制 ID', 'common.copied': '已复制!', 'common.copyError': '复制失败', + + // Loop Monitor - 循环监控 + 'loop.title': '循环监控', + 'loop.loops': '循环', + 'loop.all': '全部', + 'loop.running': '运行中', + 'loop.paused': '已暂停', + 'loop.completed': '已完成', + 'loop.failed': '失败', + 'loop.tasks': '任务', + 'loop.newLoop': '新建循环', + 'loop.loading': '加载循环中...', + 'loop.noLoops': '未找到循环', + 'loop.noLoopsHint': '创建一个循环任务开始使用', + 'loop.selectLoop': '选择一个循环查看详情', + 'loop.selectLoopHint': '点击列表中的循环查看其详细信息', + 'loop.loopNotFound': '循环未找到', + 'loop.selectAnotherLoop': '从列表中选择另一个循环', + 'loop.task': '任务', + 'loop.steps': '个步骤', + 'loop.taskInfo': '任务信息', + 'loop.edit': '编辑', + 'loop.taskId': '任务 ID', + 'loop.step': '步骤', + 'loop.updated': '更新时间', + 'loop.created': '创建时间', + 'loop.progress': '进度', + 'loop.iteration': '迭代', + 'loop.currentStep': '当前步骤', + 'loop.cliSequence': 'CLI 序列', + 'loop.stateVariables': '状态变量', + 'loop.executionHistory': '执行历史', + 'loop.failureReason': '失败原因', + 'loop.pause': '暂停', + 'loop.resume': '恢复', + 'loop.stop': '停止', + 'loop.confirmStop': '确定停止循环 {loopId}?\n\n迭代:{currentIteration}/{maxIterations}\n此操作无法撤销。', + 'loop.loopPaused': '循环已暂停', + 'loop.loopResumed': '循环已恢复', + 'loop.loopStopped': '循环已停止', + 'loop.failedToPause': '暂停失败', + 'loop.failedToResume': '恢复失败', + 'loop.failedToStop': '停止失败', + 'loop.failedToLoad': '加载循环失败', + 'loop.justNow': '刚刚', + 'loop.minutesAgo': '{m} 分钟前', + 'loop.hoursAgo': '{h} 小时前', + 'loop.daysAgo': '{d} 天前', + 'loop.tasksCount': '{count} 个启用循环的任务', + 'loop.noLoopTasks': '未找到启用循环的任务', + 'loop.createLoopTask': '创建循环任务', + 'loop.backToLoops': '返回循环列表', + 'loop.startLoop': '启动循环', + 'loop.loopStarted': '循环已启动', + 'loop.failedToStart': '启动循环失败', + 'loop.createTaskFailed': '创建任务失败', + 'loop.createLoopModal': '创建循环任务', + 'loop.basicInfo': '基本信息', + 'loop.importFromIssue': '从问题导入', + 'loop.selectIssue': '选择问题', + 'loop.noIssuesFound': '未找到问题', + 'loop.fetchIssuesFailed': '获取问题列表失败', + 'loop.fetchIssueFailed': '获取问题详情失败', + 'loop.issueImported': '已导入问题', + 'loop.taskTitle': '任务标题', + 'loop.taskTitlePlaceholder': '例如:自动测试修复循环', + 'loop.description': '描述', + 'loop.descriptionPlaceholder': '描述此循环的功能...', + 'loop.loopConfig': '循环配置', + 'loop.maxIterations': '最大迭代次数', + 'loop.errorPolicy': '错误策略', + 'loop.pauseOnError': '暂停', + 'loop.retryAutomatically': '自动重试', + 'loop.failImmediately': '立即失败', + 'loop.maxRetries': '最大重试次数(重试策略)', + 'loop.successCondition': '成功条件(JavaScript 表达式)', + 'loop.successConditionPlaceholder': '例如:state_variables.test_stdout.includes(\'passed\')', + 'loop.availableVars': '可用变量:state_variables、current_iteration', + 'loop.cliSequence': 'CLI 序列', + 'loop.addStep': '添加步骤', + 'loop.stepNumber': '步骤 {number}', + 'loop.stepLabel': '步骤', + 'loop.removeStep': '移除步骤', + 'loop.stepId': '步骤 ID', + 'loop.stepIdPlaceholder': '例如:run_tests', + 'loop.tool': '工具', + 'loop.mode': '模式', + 'loop.command': '命令', + 'loop.commandPlaceholder': '例如:npm test', + 'loop.promptTemplate': '提示模板(支持 [variable_name] 变量替换)', + 'loop.promptPlaceholder': '输入提示模板...', + 'loop.onError': '错误处理', + 'loop.continue': '继续', + 'loop.pause': '暂停', + 'loop.failFast': '立即失败', + 'loop.cancel': '取消', + 'loop.createAndStart': '创建循环', + 'loop.created': '已创建', + 'loop.createFailed': '创建循环失败', + 'loop.taskCreatedFailedStart': '任务已创建,但启动循环失败', + // V2 Simplified Loop + 'loop.create': '创建', + 'loop.loopCreated': '循环创建成功', + 'loop.titleRequired': '标题不能为空', + 'loop.invalidMaxIterations': '最大迭代次数必须在 1 到 100 之间', + 'loop.loopInfo': '循环信息', + 'loop.v2LoopInfo': '这是一个简化版循环。任务在详情视图中独立管理。', + 'loop.manageTasks': '管理任务', + 'loop.taskManagement': '任务管理', + 'loop.taskManagementPlaceholder': '任务管理将在后续更新中提供。请使用 v1 循环进行完整任务配置。', + 'loop.noTasksYet': '尚未配置任务', + 'loop.back': '返回', + 'loop.loopNotFound': '循环未找到', + 'loop.selectAnotherLoop': '请从列表中选择其他循环', + 'loop.start': '启动', + 'loop.loopStarted': '循环已启动', + 'loop.failedToStart': '启动循环失败', + // Task List Management + 'loop.taskList': '任务列表', + 'loop.addTask': '添加任务', + 'loop.taskDescription': '任务描述', + 'loop.taskDescriptionPlaceholder': '描述此任务应该做什么...', + 'loop.modeAnalysis': '分析', + 'loop.modeWrite': '编写', + 'loop.modeReview': '审查', + 'loop.save': '保存', + 'loop.taskAdded': '任务添加成功', + 'loop.addTaskFailed': '添加任务失败', + 'loop.editTask': '编辑任务', + 'loop.taskUpdated': '任务更新成功', + 'loop.updateTaskFailed': '更新任务失败', + 'loop.confirmDeleteTask': '确定要删除此任务吗?此操作无法撤销。', + 'loop.taskDeleted': '任务删除成功', + 'loop.deleteTaskFailed': '删除任务失败', + 'loop.deleteTaskError': '删除任务时出错', + 'loop.loadTasksFailed': '加载任务失败', + 'loop.loadTasksError': '加载任务时出错', + 'loop.tasksReordered': '任务已重新排序', + 'loop.saveOrderFailed': '保存排序失败', + 'loop.noTasksHint': '添加您的第一个任务开始使用', + 'loop.noDescription': '无描述', + 'loop.descriptionRequired': '描述不能为空', + 'loop.loadTaskFailed': '加载任务失败', + 'loop.loadTaskError': '加载任务时出错', + 'loop.taskTitleHint': '为循环输入描述性标题', + 'loop.descriptionHint': '关于循环功能的可选上下文', + 'loop.maxIterationsHint': '最大迭代次数 (1-100)', } }; @@ -4872,11 +5169,24 @@ function switchLang(lang) { localStorage.setItem('ccw-lang', lang); applyTranslations(); updateLangToggle(); - + // Re-render current view to update dynamic content if (typeof updateContentTitle === 'function') { updateContentTitle(); } + + // Re-render loop monitor if visible + if (typeof window.selectedLoopId !== 'undefined' && document.getElementById('loopList')) { + if (typeof updateLoopStatusLabels === 'function') { + updateLoopStatusLabels(); + } + if (typeof renderLoopList === 'function') { + renderLoopList(); + } + if (window.selectedLoopId && typeof renderLoopDetail === 'function') { + renderLoopDetail(window.selectedLoopId); + } + } } } diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index 9dbba649..d204aa0b 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -130,8 +130,9 @@ async function initCsrfToken() { /** * Sync active CLI executions from server * Called when view is opened to restore running execution state + * Note: Renamed from syncActiveExecutions to avoid conflict with cli-stream-viewer.js */ -async function syncActiveExecutions() { +async function syncActiveExecutionsForManager() { try { var response = await fetch('/api/cli/active'); if (!response.ok) return; @@ -1202,7 +1203,7 @@ async function renderCliManager() { } // 同步活动执行 - syncActiveExecutions(); + syncActiveExecutionsForManager(); } // ========== Helper Functions ========== diff --git a/ccw/src/templates/dashboard-js/views/loop-monitor.js b/ccw/src/templates/dashboard-js/views/loop-monitor.js index 7a3ec995..44b37586 100644 --- a/ccw/src/templates/dashboard-js/views/loop-monitor.js +++ b/ccw/src/templates/dashboard-js/views/loop-monitor.js @@ -9,20 +9,36 @@ window.loopWebSocket = null; window.loopReconnectAttempts = 0; window.loopMaxReconnectAttempts = 10; -// Status colors and icons +// Status icons and keys (will be updated with i18n labels dynamically) +// Colors are now handled by CSS via semantic class names (.loop-status-indicator.{status}) const loopStatusConfig = { - created: { icon: '○', label: 'Created', className: 'text-gray-400 bg-gray-100', border: 'border-l-gray-400' }, - running: { icon: '●', label: 'Running', className: 'text-cyan-500 bg-cyan-100 animate-pulse', border: 'border-l-cyan-500' }, - paused: { icon: '⏸', label: 'Paused', className: 'text-amber-500 bg-amber-100', border: 'border-l-amber-500' }, - completed: { icon: '✓', label: 'Completed', className: 'text-emerald-500 bg-emerald-100', border: 'border-l-emerald-500' }, - failed: { icon: '✗', label: 'Failed', className: 'text-red-500 bg-red-100', border: 'border-l-red-500' } + created: { icon: '○', key: 'created' }, + running: { icon: '●', key: 'running' }, + paused: { icon: '⏸', key: 'paused' }, + completed: { icon: '✓', key: 'completed' }, + failed: { icon: '✗', key: 'failed' } }; +// Get localized status label +function getLoopStatusLabel(status) { + return t('loop.' + status) || status; +} + +// Update status config with localized labels +function updateLoopStatusLabels() { + for (const status in loopStatusConfig) { + loopStatusConfig[status].label = getLoopStatusLabel(status); + } +} + /** * Render Loop Monitor view */ async function renderLoopMonitor() { try { + // Update status labels with current language + updateLoopStatusLabels(); + // Hide stats and carousel if function exists if (typeof hideStatsAndCarousel === 'function') { hideStatsAndCarousel(); @@ -39,33 +55,40 @@ async function renderLoopMonitor() {
-

Loops

-
- + +
+
-
Loading loops...
+
${t('loop.loading')}
- -

Select a loop to view details

+
+ +
+

${t('loop.selectLoop')}

+

${t('loop.selectLoopHint')}

@@ -86,7 +109,7 @@ async function renderLoopMonitor() { await loadLoops(); } catch (err) { console.error('Failed to load loops:', err); - showError('Failed to load loops: ' + (err.message || String(err))); + showError(t('loop.failedToLoad') + ': ' + (err.message || String(err))); } } catch (err) { console.error('Failed to render Loop Monitor:', err); @@ -165,24 +188,69 @@ function handleLoopUpdate(data) { } /** - * Load all loops from API + * Load all loops from API (both v1 and v2) */ async function loadLoops() { try { - const response = await fetch('/api/loops'); - const result = await response.json(); + // Fetch v2 loops (new simplified format) + const v2Response = await fetch('/api/loops/v2'); + const v2Result = await v2Response.json(); - if (result.success) { - result.data.forEach(loop => { + if (v2Result.success && v2Result.data) { + v2Result.data.forEach(loop => { window.loopStateStore[loop.loop_id] = loop; }); - renderLoopList(); + } + + // Fetch v1 loops (legacy format with task_id) + const v1Response = await fetch('/api/loops'); + const v1Result = await v1Response.json(); + + if (v1Result.success && v1Result.data) { + v1Result.data.forEach(loop => { + window.loopStateStore[loop.loop_id] = loop; + }); + } + + const loopCount = Object.keys(window.loopStateStore).length; + + // If no active loops, check for tasks and show tasks tab instead + if (loopCount === 0) { + await showTasksTabIfAny(); } else { - showError('Failed to load loops: ' + (result.error || 'Unknown error')); + renderLoopList(); } } catch (err) { console.error('Load loops error:', err); - showError('Failed to load loops: ' + err.message); + showError(t('loop.failedToLoad') + ': ' + err.message); + } +} + +/** + * Show tasks tab if there are any loop-enabled tasks + */ +async function showTasksTabIfAny() { + try { + const response = await fetch('/api/tasks'); + const result = await response.json(); + + if (result.success) { + const tasks = result.data || []; + const loopEnabledTasks = tasks.filter(t => t.loop_control && t.loop_control.enabled); + + // Only show tasks tab if there are loop-enabled tasks + if (loopEnabledTasks.length > 0) { + await showTasksTab(); + } else { + // No loops and no tasks, show empty state + renderLoopList(); + } + } else { + renderLoopList(); + } + } catch (err) { + console.error('Check tasks error:', err); + renderLoopList(); } } @@ -204,8 +272,11 @@ function renderLoopList() { if (filteredLoops.length === 0) { container.innerHTML = `
- -

No loops found

+
+ +
+

${t('loop.noLoops')}

+

${t('loop.noLoopsHint')}

`; if (typeof lucide !== 'undefined') lucide.createIcons(); @@ -227,16 +298,40 @@ function renderLoopCard(loop) { ? Math.round((loop.current_iteration / loop.max_iterations) * 100) : 0; + // Lucide icons for each status + const statusIcons = { + created: 'circle', + running: 'activity', + paused: 'pause-circle', + completed: 'check-circle-2', + failed: 'x-circle' + }; + + // Check if this is a v2 loop (has title field) or v1 loop (has task_id field) + const isV2 = loop.hasOwnProperty('title'); + const displayTitle = isV2 ? (loop.title || loop.loop_id) : loop.loop_id; + const displaySubtitle = isV2 ? (loop.description || '') : (loop.task_id || 'N/A'); + + // v1 loops have current_cli_step and cli_sequence, v2 loops don't + const hasStepInfo = loop.hasOwnProperty('current_cli_step') && loop.cli_sequence; + const stepInfo = hasStepInfo + ? `
+ ${loop.current_cli_step + 1}/${loop.cli_sequence?.length || 0} +
` + : ''; + return ` -
- ${config.icon} - ${escapeHtml(loop.loop_id)} + + + + ${escapeHtml(displayTitle)}
+ ${displaySubtitle ? `
${escapeHtml(displaySubtitle).substring(0, 60)}${displaySubtitle.length > 60 ? '...' : ''}
` : ''}
- Task: ${escapeHtml(loop.task_id || 'N/A')} ${config.label}
@@ -245,11 +340,9 @@ function renderLoopCard(loop) {
${loop.current_iteration}/${loop.max_iterations} (${progress}%)
-
- Step: ${loop.current_cli_step + 1}/${loop.cli_sequence?.length || 0} -
+ ${stepInfo}
- Updated: ${formatRelativeTime(loop.updated_at)} + ${formatRelativeTime(loop.updated_at)}
@@ -275,8 +368,11 @@ function renderLoopDetail(loopId) { if (!loop) { container.innerHTML = `
- -

Loop not found

+
+ +
+

${t('loop.loopNotFound')}

+

${t('loop.selectAnotherLoop')}

`; if (typeof lucide !== 'undefined') lucide.createIcons(); @@ -287,7 +383,12 @@ function renderLoopDetail(loopId) { const iterProgress = loop.max_iterations > 0 ? Math.round((loop.current_iteration / loop.max_iterations) * 100) : 0; - const stepProgress = loop.cli_sequence?.length > 0 + + // Check if this is a v2 loop (has title field) or v1 loop (has task_id field) + const isV2 = loop.hasOwnProperty('title'); + const displayTitle = isV2 ? (loop.title || loop.loop_id) : loop.loop_id; + const hasStepInfo = loop.hasOwnProperty('current_cli_step') && loop.cli_sequence; + const stepProgress = hasStepInfo && loop.cli_sequence.length > 0 ? Math.round(((loop.current_cli_step + 1) / loop.cli_sequence.length) * 100) : 0; @@ -295,24 +396,30 @@ function renderLoopDetail(loopId) {
-
- ${config.icon} +
+ ${config.label} + ${isV2 ? 'v2' : 'v1'}
${loop.status === 'running' ? ` ` : ''} ${loop.status === 'paused' ? ` ` : ''} - ${(loop.status === 'running' || loop.status === 'paused') ? ` + ${loop.status === 'created' ? ` + + ` : ''} + ${(loop.status === 'running' || loop.status === 'paused' || loop.status === 'created') ? ` ` : ''}
@@ -320,61 +427,78 @@ function renderLoopDetail(loopId) {
-

${escapeHtml(loop.loop_id)}

+

${escapeHtml(displayTitle)}

+ ${loop.description ? `

${escapeHtml(loop.description)}

` : ''}
- Created: ${formatDateTime(loop.created_at)} - Updated: ${formatRelativeTime(loop.updated_at)} - Task: ${escapeHtml(loop.task_id || 'N/A')} + ${t('loop.created')}: ${formatDateTime(loop.created_at)} + ${t('loop.updated')}: ${formatRelativeTime(loop.updated_at)} + ${loop.task_id ? ` ${escapeHtml(loop.task_id)}` : ''} + ${escapeHtml(loop.loop_id)}
-

Progress

+

${t('loop.progress')}

- +
${loop.current_iteration}/${loop.max_iterations} (${iterProgress}%)
-
- -
-
+ ${hasStepInfo ? ` +
+ +
+
+
+ ${loop.current_cli_step + 1}/${loop.cli_sequence?.length || 0}
- ${loop.current_cli_step + 1}/${loop.cli_sequence?.length || 0} + ` : ''} +
+
+ + ${hasStepInfo ? ` + +
+

${t('loop.cliSequence')}

+
+ ${(loop.cli_sequence || []).map((step, index) => { + const isCurrent = index === loop.current_cli_step; + const isPast = index < loop.current_cli_step; + const stepStatus = isCurrent ? 'current' : (isPast ? 'completed' : 'pending'); + + return ` +
+
${isPast ? '' : (isCurrent ? '' : index + 1)}
+
+
${escapeHtml(step.tool || 'unknown')}
+
${escapeHtml(step.prompt?.substring(0, 100) || '')}${step.prompt?.length > 100 ? '...' : ''}
+
+
+ `; + }).join('')}
-
- - -
-

CLI Sequence

-
- ${(loop.cli_sequence || []).map((step, index) => { - const isCurrent = index === loop.current_cli_step; - const isPast = index < loop.current_cli_step; - const stepStatus = isCurrent ? 'current' : (isPast ? 'completed' : 'pending'); - - return ` -
-
${isPast ? '✓' : (isCurrent ? '●' : index + 1)}
-
-
${escapeHtml(step.tool || 'unknown')}
-
${escapeHtml(step.prompt?.substring(0, 100) || '')}${step.prompt?.length > 100 ? '...' : ''}
-
-
- `; - }).join('')} + ` : ` + +
+

${t('loop.loopInfo') || 'Loop Info'}

+
+

${t('loop.v2LoopInfo') || 'This is a simplified loop. Tasks are managed independently in the detail view.'}

+ +
-
+ `} ${Object.keys(loop.state_variables || {}).length > 0 ? `
-

State Variables

+

${t('loop.stateVariables')}

${Object.entries(loop.state_variables || {}).map(([key, value]) => `
@@ -389,7 +513,7 @@ function renderLoopDetail(loopId) { ${(loop.execution_history?.length || 0) > 0 ? `
-

Execution History

+

${t('loop.executionHistory')}

${renderExecutionTimeline(loop)}
@@ -399,7 +523,7 @@ function renderLoopDetail(loopId) { ${loop.failure_reason ? `
-

Failure Reason

+

${t('loop.failureReason')}

${escapeHtml(loop.failure_reason)}
` : ''} @@ -432,21 +556,21 @@ function renderExecutionTimeline(loop) { return `
- ${isCurrent ? '●' : '✓'} - Iteration ${iter} + ${isCurrent ? '' : ''} + ${t('loop.iteration')} ${iter}
${records.map(record => `
- ${record.success ? '✓' : '✗'} + ${record.success ? '' : ''}
${escapeHtml(sequence[record.step_index]?.tool || 'unknown')}
-
${formatDateTime(record.started_at)}
-
${record.duration_ms}ms
+
${formatDateTime(record.started_at)}
+
${record.duration_ms}ms
${!record.success && record.error ? ` -
${escapeHtml(record.error)}
+
${escapeHtml(record.error)}
` : ''}
@@ -465,42 +589,50 @@ function filterLoops() { } /** - * Pause loop + * Pause loop (v1 or v2) */ async function pauseLoop(loopId) { + const loop = window.loopStateStore[loopId]; + const isV2 = loop && loop.hasOwnProperty('title'); + const endpoint = isV2 ? `/api/loops/v2/${loopId}/pause` : `/api/loops/${loopId}/pause`; + try { - const response = await fetch(`/api/loops/${loopId}/pause`, { method: 'POST' }); + const response = await fetch(endpoint, { method: 'POST' }); const result = await response.json(); if (result.success) { - showNotification('Loop paused', 'success'); + showNotification(t('loop.loopPaused'), 'success'); await loadLoops(); } else { - showError('Failed to pause: ' + (result.error || 'Unknown error')); + showError(t('loop.failedToPause') + ': ' + (result.error || t('common.error'))); } } catch (err) { console.error('Pause loop error:', err); - showError('Failed to pause: ' + err.message); + showError(t('loop.failedToPause') + ': ' + err.message); } } /** - * Resume loop + * Resume loop (v1 or v2) */ async function resumeLoop(loopId) { + const loop = window.loopStateStore[loopId]; + const isV2 = loop && loop.hasOwnProperty('title'); + const endpoint = isV2 ? `/api/loops/v2/${loopId}/resume` : `/api/loops/${loopId}/resume`; + try { - const response = await fetch(`/api/loops/${loopId}/resume`, { method: 'POST' }); + const response = await fetch(endpoint, { method: 'POST' }); const result = await response.json(); if (result.success) { - showNotification('Loop resumed', 'success'); + showNotification(t('loop.loopResumed'), 'success'); await loadLoops(); } else { - showError('Failed to resume: ' + (result.error || 'Unknown error')); + showError(t('loop.failedToResume') + ': ' + (result.error || t('common.error'))); } } catch (err) { console.error('Resume loop error:', err); - showError('Failed to resume: ' + err.message); + showError(t('loop.failedToResume') + ': ' + err.message); } } @@ -511,28 +643,658 @@ function confirmStopLoop(loopId) { const loop = window.loopStateStore[loopId]; if (!loop) return; - if (confirm(`Stop loop ${loopId}?\n\nIteration: ${loop.current_iteration}/${loop.max_iterations}\nThis action cannot be undone.`)) { + const message = t('loop.confirmStop', { + loopId: loopId, + currentIteration: loop.current_iteration, + maxIterations: loop.max_iterations + }); + + if (confirm(message)) { stopLoop(loopId); } } /** - * Stop loop + * Stop loop (v1 or v2) */ async function stopLoop(loopId) { + const loop = window.loopStateStore[loopId]; + const isV2 = loop && loop.hasOwnProperty('title'); + const endpoint = isV2 ? `/api/loops/v2/${loopId}/stop` : `/api/loops/${loopId}/stop`; + try { - const response = await fetch(`/api/loops/${loopId}/stop`, { method: 'POST' }); + const response = await fetch(endpoint, { method: 'POST' }); const result = await response.json(); if (result.success) { - showNotification('Loop stopped', 'success'); + showNotification(t('loop.loopStopped'), 'success'); await loadLoops(); } else { - showError('Failed to stop: ' + (result.error || 'Unknown error')); + showError(t('loop.failedToStop') + ': ' + (result.error || t('common.error'))); } } catch (err) { console.error('Stop loop error:', err); - showError('Failed to stop: ' + err.message); + showError(t('loop.failedToStop') + ': ' + err.message); + } +} + +/** + * Start loop (v2 only) + */ +async function startLoopV2(loopId) { + try { + const response = await fetch(`/api/loops/v2/${loopId}/start`, { method: 'POST' }); + const result = await response.json(); + + if (result.success) { + showNotification(t('loop.loopStarted') || 'Loop started', 'success'); + await loadLoops(); + } else { + showError(t('loop.failedToStart') + ': ' + (result.error || t('common.error'))); + } + } catch (err) { + console.error('Start loop error:', err); + showError(t('loop.failedToStart') + ': ' + err.message); + } +} + +// ========================================== +// TASK MANAGEMENT FOR V2 LOOPS +// ========================================== + +// Task drag state +const taskDragState = { + dragging: null, + loopId: null +}; + +/** + * Show loop tasks (v2 task management) + * Displays the task list for a v2 loop with add/edit/delete/reorder functionality + */ +async function showLoopTasks(loopId) { + const container = document.getElementById('loopDetailPanel'); + const loop = window.loopStateStore[loopId]; + + if (!loop) return; + + // Set current loop ID for task operations + window.currentLoopId = loopId; + + container.innerHTML = ` +
+
+
+ + ${t('loop.tasks') || 'Tasks'} +
+
+ + +
+
+
+
+

${t('loop.taskList') || 'Task List'}

+ ${t('loop.loading') || 'Loading...'} +
+
+
${t('loop.loading') || 'Loading...'}
+
+
+
+ `; + + if (typeof lucide !== 'undefined') lucide.createIcons(); + + // Load tasks + await loadLoopTasks(loopId); +} + +/** + * Load tasks for a loop from the v2 API + */ +async function loadLoopTasks(loopId) { + const container = document.getElementById('loopTasksList'); + const countEl = document.getElementById('taskCount'); + + if (!container) return; + + try { + const response = await fetch(`/api/loops/v2/${encodeURIComponent(loopId)}/tasks`); + const result = await response.json(); + + if (!result.success) { + container.innerHTML = ` +
+ +

${t('loop.loadTasksFailed') || 'Failed to load tasks'}

+

${result.error || ''}

+
+ `; + if (typeof lucide !== 'undefined') lucide.createIcons(); + return; + } + + const tasks = result.data || []; + + // Update count + if (countEl) { + countEl.textContent = `${tasks.length} ${t('loop.tasks') || 'tasks'}`; + } + + if (tasks.length === 0) { + container.innerHTML = ` +
+ +

${t('loop.noTasksYet') || 'No tasks yet'}

+

${t('loop.noTasksHint') || 'Add your first task to get started'}

+ +
+ `; + if (typeof lucide !== 'undefined') lucide.createIcons(); + return; + } + + // Render tasks + container.innerHTML = tasks.map(task => renderTaskItem(task)).join(''); + if (typeof lucide !== 'undefined') lucide.createIcons(); + + // Initialize drag-drop + initTaskDragDrop(); + + } catch (err) { + console.error('Load loop tasks error:', err); + container.innerHTML = ` +
+ +

${t('loop.loadTasksError') || 'Error loading tasks'}

+

${err.message}

+
+ `; + if (typeof lucide !== 'undefined') lucide.createIcons(); + } +} + +/** + * Render a single task item in the list + */ +function renderTaskItem(task) { + const statusBadges = { + analysis: 'analysis', + write: 'write', + review: 'review' + }; + + const modeBadge = statusBadges[task.mode] || `${task.mode || 'unknown'}`; + const toolBadge = `${task.tool || 'gemini'}`; + + return ` +
+
+ +
+
+
+ ${escapeHtml(task.description || t('loop.noDescription') || 'No description')} +
+
+ ${toolBadge} + ${modeBadge} +
+
+
+ + +
+
+ `; +} + +/** + * Initialize drag-drop for task list + */ +function initTaskDragDrop() { + const items = document.querySelectorAll('.task-item[draggable="true"]'); + + items.forEach(item => { + item.addEventListener('dragstart', handleTaskDragStart); + item.addEventListener('dragend', handleTaskDragEnd); + item.addEventListener('dragover', handleTaskDragOver); + item.addEventListener('drop', handleTaskDrop); + }); +} + +function handleTaskDragStart(e) { + const item = e.target.closest('.task-item'); + if (!item) return; + + taskDragState.dragging = item.dataset.taskId; + taskDragState.loopId = window.currentLoopId; + + item.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', item.dataset.taskId); +} + +function handleTaskDragEnd(e) { + const item = e.target.closest('.task-item'); + if (item) { + item.classList.remove('dragging'); + } + taskDragState.dragging = null; + taskDragState.loopId = null; +} + +function handleTaskDragOver(e) { + e.preventDefault(); + + const target = e.target.closest('.task-item'); + if (!target || target.dataset.taskId === taskDragState.dragging) return; + + e.dataTransfer.dropEffect = 'move'; +} + +function handleTaskDrop(e) { + e.preventDefault(); + + const target = e.target.closest('.task-item'); + if (!target || !taskDragState.dragging) return; + + const container = target.closest('.task-list'); + if (!container) return; + + // Get new order + const items = Array.from(container.querySelectorAll('.task-item')); + const draggedItem = items.find(i => i.dataset.taskId === taskDragState.dragging); + const targetIndex = items.indexOf(target); + const draggedIndex = items.indexOf(draggedItem); + + if (draggedIndex === targetIndex) return; + + // Reorder in DOM + if (draggedIndex < targetIndex) { + target.after(draggedItem); + } else { + target.before(draggedItem); + } + + // Get new order and save + const newOrder = Array.from(container.querySelectorAll('.task-item')).map(i => i.dataset.taskId); + saveTaskOrder(taskDragState.loopId, newOrder); +} + +/** + * Save new task order to the API + */ +async function saveTaskOrder(loopId, newOrder) { + try { + const response = await fetch(`/api/loops/v2/${encodeURIComponent(loopId)}/tasks/reorder`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ordered_task_ids: newOrder }) + }); + + if (!response.ok) { + throw new Error('Failed to save task order'); + } + + const result = await response.json(); + if (result.error) { + showNotification(result.error, 'error'); + } else { + showNotification(t('loop.tasksReordered') || 'Tasks reordered', 'success'); + // Reload to reflect changes + await loadLoopTasks(loopId); + } + } catch (err) { + console.error('Failed to save task order:', err); + showNotification(t('loop.saveOrderFailed') || 'Failed to save order', 'error'); + // Reload to restore original order + await loadLoopTasks(loopId); + } +} + +/** + * Show add task modal + */ +async function showAddTaskModal(loopId) { + const modal = document.createElement('div'); + modal.id = 'addTaskModal'; + modal.className = 'modal-overlay'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + if (typeof lucide !== 'undefined') lucide.createIcons(); + + // Focus on description field + setTimeout(() => document.getElementById('taskDescription').focus(), 100); +} + +/** + * Close task modal + */ +function closeTaskModal() { + const modal = document.getElementById('addTaskModal') || document.getElementById('editTaskModal'); + if (modal) { + modal.remove(); + } +} + +/** + * Handle add task form submission + */ +async function handleAddTask(event, loopId) { + event.preventDefault(); + + const form = event.target; + const errorDiv = document.getElementById('addTaskError'); + + // Clear previous errors + if (errorDiv) { + errorDiv.style.display = 'none'; + errorDiv.textContent = ''; + } + + // Get form values + const description = form.description.value.trim(); + const tool = form.tool.value; + const mode = form.mode.value; + + // Client-side validation + if (!description) { + if (errorDiv) { + errorDiv.textContent = t('loop.descriptionRequired') || 'Description is required'; + errorDiv.style.display = 'block'; + } + return; + } + + try { + // Call POST /api/loops/v2/:loopId/tasks + const response = await fetch(`/api/loops/v2/${encodeURIComponent(loopId)}/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + description: description, + tool: tool, + mode: mode + }) + }); + + const result = await response.json(); + + if (result.success) { + showNotification(t('loop.taskAdded') || 'Task added successfully', 'success'); + closeTaskModal(); + // Reload tasks + await loadLoopTasks(loopId); + } else { + if (errorDiv) { + errorDiv.textContent = result.error || (t('loop.addTaskFailed') || 'Failed to add task'); + errorDiv.style.display = 'block'; + } + } + } catch (err) { + console.error('Add task error:', err); + if (errorDiv) { + errorDiv.textContent = err.message || (t('loop.addTaskFailed') || 'Failed to add task'); + errorDiv.style.display = 'block'; + } + } +} + +/** + * Edit existing task + */ +async function editTask(taskId) { + const loopId = window.currentLoopId; + if (!loopId) return; + + // Fetch task details + try { + const response = await fetch(`/api/loops/v2/tasks/${encodeURIComponent(taskId)}`); + const result = await response.json(); + + if (!result.success || !result.data) { + showNotification(t('loop.loadTaskFailed') || 'Failed to load task', 'error'); + return; + } + + const task = result.data; + showEditTaskModal(loopId, task); + } catch (err) { + console.error('Load task error:', err); + showNotification(t('loop.loadTaskError') || 'Error loading task', 'error'); + } +} + +/** + * Show edit task modal + */ +function showEditTaskModal(loopId, task) { + const modal = document.createElement('div'); + modal.id = 'editTaskModal'; + modal.className = 'modal-overlay'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + if (typeof lucide !== 'undefined') lucide.createIcons(); + + // Focus on description field + setTimeout(() => document.getElementById('editTaskDescription').focus(), 100); +} + +/** + * Handle edit task form submission + */ +async function handleEditTask(event, loopId, taskId) { + event.preventDefault(); + + const form = event.target; + const errorDiv = document.getElementById('editTaskError'); + + // Clear previous errors + if (errorDiv) { + errorDiv.style.display = 'none'; + errorDiv.textContent = ''; + } + + // Get form values + const description = form.description.value.trim(); + const tool = form.tool.value; + const mode = form.mode.value; + + // Client-side validation + if (!description) { + if (errorDiv) { + errorDiv.textContent = t('loop.descriptionRequired') || 'Description is required'; + errorDiv.style.display = 'block'; + } + return; + } + + try { + // Call PUT /api/loops/v2/:loopId/tasks/:taskId + const response = await fetch(`/api/loops/v2/${encodeURIComponent(loopId)}/tasks/${encodeURIComponent(taskId)}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + description: description, + tool: tool, + mode: mode + }) + }); + + const result = await response.json(); + + if (result.success) { + showNotification(t('loop.taskUpdated') || 'Task updated successfully', 'success'); + closeTaskModal(); + // Reload tasks + await loadLoopTasks(loopId); + } else { + if (errorDiv) { + errorDiv.textContent = result.error || (t('loop.updateTaskFailed') || 'Failed to update task'); + errorDiv.style.display = 'block'; + } + } + } catch (err) { + console.error('Update task error:', err); + if (errorDiv) { + errorDiv.textContent = err.message || (t('loop.updateTaskFailed') || 'Failed to update task'); + errorDiv.style.display = 'block'; + } + } +} + +/** + * Confirm delete task + */ +function confirmDeleteTask(taskId) { + const message = t('loop.confirmDeleteTask') || 'Are you sure you want to delete this task? This action cannot be undone.'; + + if (confirm(message)) { + deleteTask(taskId); + } +} + +/** + * Delete task + */ +async function deleteTask(taskId) { + const loopId = window.currentLoopId; + if (!loopId) return; + + try { + // Call DELETE /api/loops/v2/:loopId/tasks/:taskId + const response = await fetch(`/api/loops/v2/${encodeURIComponent(loopId)}/tasks/${encodeURIComponent(taskId)}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + showNotification(t('loop.taskDeleted') || 'Task deleted successfully', 'success'); + // Reload tasks + await loadLoopTasks(loopId); + } else { + showNotification(t('loop.deleteTaskFailed') || 'Failed to delete task: ' + (result.error || ''), 'error'); + } + } catch (err) { + console.error('Delete task error:', err); + showNotification(t('loop.deleteTaskError') || 'Error deleting task: ' + err.message, 'error'); } } @@ -549,6 +1311,17 @@ window.destroyLoopMonitor = function() { }; // Helper functions +function getStatusIcon(status) { + const icons = { + created: 'circle', + running: 'activity', + paused: 'pause-circle', + completed: 'check-circle-2', + failed: 'x-circle' + }; + return icons[status] || 'circle'; +} + function escapeHtml(text) { if (text == null) return ''; const div = document.createElement('div'); @@ -568,10 +1341,10 @@ function formatRelativeTime(isoString) { const now = new Date(); const diff = Math.floor((now - date) / 1000); - if (diff < 60) return 'just now'; - if (diff < 3600) return Math.floor(diff / 60) + 'm ago'; - if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'; - return Math.floor(diff / 86400) + 'd ago'; + if (diff < 60) return t('loop.justNow'); + if (diff < 3600) return t('loop.minutesAgo', { m: Math.floor(diff / 60) }); + if (diff < 86400) return t('loop.hoursAgo', { h: Math.floor(diff / 3600) }); + return t('loop.daysAgo', { d: Math.floor(diff / 86400) }); } function showNotification(message, type) { @@ -599,7 +1372,7 @@ async function showTasksTab() { const result = await response.json(); if (!result.success) { - showError('Failed to load tasks: ' + (result.error || 'Unknown error')); + showError(t('loop.failedToLoad') + ': ' + (result.error || t('common.error'))); return; } @@ -609,7 +1382,7 @@ async function showTasksTab() { renderTasksList(loopEnabledTasks); } catch (err) { console.error('Load tasks error:', err); - showError('Failed to load tasks: ' + err.message); + showError(t('loop.failedToLoad') + ': ' + err.message); } } @@ -623,9 +1396,9 @@ function renderTasksList(tasks) { listContainer.innerHTML = `
-

No loop-enabled tasks found

+

${t('loop.noLoopTasks')}

`; @@ -635,9 +1408,9 @@ function renderTasksList(tasks) { listContainer.innerHTML = `
-

${tasks.length} task(s) with loop enabled

+

${t('loop.tasksCount', { count: tasks.length })}

@@ -656,19 +1429,19 @@ function renderTaskCard(task) { const stepCount = config.cli_sequence ? config.cli_sequence.length : 0; return ` -
+
${escapeHtml(task.title || task.id)} ${escapeHtml(task.id)}
-

${escapeHtml(config.description || 'No description')}

+

${escapeHtml(config.description || t('common.na'))}

Max: ${config.max_iterations || 10} Steps: ${stepCount}
-
@@ -688,28 +1461,198 @@ async function startLoopFromTask(taskId) { const result = await response.json(); if (result.success) { - showNotification('Loop started: ' + result.data.loopId, 'success'); + showNotification(t('loop.loopStarted') + ': ' + result.data.loopId, 'success'); await loadLoops(); // Refresh to show new loop } else { - showError('Failed to start loop: ' + (result.error || 'Unknown error')); + showError(t('loop.failedToStart') + ': ' + (result.error || t('common.error'))); } } catch (err) { console.error('Start loop error:', err); - showError('Failed to start loop: ' + err.message); + showError(t('loop.failedToStart') + ': ' + err.message); } } /** - * Show create loop modal + * Show task detail in detail panel */ -function showCreateLoopModal() { +async function showTaskDetail(taskId) { + try { + const response = await fetch(`/api/tasks/${encodeURIComponent(taskId)}`); + const result = await response.json(); + + if (!result.success) { + showError('Failed to load task: ' + (result.error || 'Unknown error')); + return; + } + + const task = result.data?.task || result.data; + renderTaskDetail(task); + } catch (err) { + console.error('Load task error:', err); + showError('Failed to load task: ' + err.message); + } +} + +/** + * Render task detail panel + */ +function renderTaskDetail(task) { + const container = document.getElementById('loopDetailPanel'); + if (!container) return; + + const config = task.loop_control || {}; + const cliSequence = config.cli_sequence || []; + const stepCount = cliSequence.length; + + container.innerHTML = ` +
+ +
+
+ + ${t('loop.task')} +
+
+ + +
+
+ + +
+

${escapeHtml(task.title || task.id)}

+

${escapeHtml(config.description || task.description || '')}

+
+ + +
+

${t('loop.loopConfig')}

+
+
+ ${t('loop.maxIterations')} + ${config.max_iterations || 10} +
+
+ ${t('loop.errorPolicy')} + ${config.error_policy?.on_failure || 'pause'} +
+
+ ${t('loop.maxRetries')} + ${config.error_policy?.max_retries || 3} +
+
+ ${t('loop.cliSequence')} + ${stepCount} ${t('loop.steps')} +
+
+ ${config.success_condition ? ` +
+ ${t('loop.successCondition')} + ${escapeHtml(config.success_condition)} +
+ ` : ''} +
+ + +
+

${t('loop.cliSequence')}

+
+ ${cliSequence.map((step, index) => ` +
+
${index + 1}
+
+
+ ${escapeHtml(step.step_id || `Step ${index + 1}`)} + ${step.tool} +
+
+ ${step.mode} + ${step.on_error ? `On error: ${step.on_error}` : ''} +
+ ${step.prompt_template ? ` +
+ ${t('loop.promptTemplate')}: +

${escapeHtml(step.prompt_template.substring(0, 100))}${step.prompt_template.length > 100 ? '...' : ''}

+
+ ` : ''} + ${step.command ? ` +
+ ${t('loop.command')}: + ${escapeHtml(step.command)} +
+ ` : ''} +
+
+ `).join('')} +
+
+ + +
+

${t('loop.taskInfo')}

+
+
+ ${t('loop.taskId')} + ${escapeHtml(task.id)} +
+
+ ${t('loop.status')} + ${task.status || 'active'} +
+ ${task.meta?.created_by ? ` +
+ ${t('loop.createdBy')} + ${task.meta.created_by} +
+ ` : ''} +
+
+
+ `; + + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +// Global store for available CLI tools +window.availableCliTools = []; + +/** + * Fetch available CLI tools from API + */ +async function fetchAvailableCliTools() { + try { + const response = await fetch('/api/cli/status'); + const data = await response.json(); + // Return only available tools (where available: true) + return Object.entries(data) + .filter(([_, status]) => status.available) + .map(([name, _]) => name); + } catch (err) { + console.error('Failed to fetch CLI tools:', err); + // Fallback to default tools + return ['gemini', 'qwen', 'codex', 'claude']; + } +} + +/** + * Show create loop modal (backup - complex version with CLI sequence) + * @deprecated Use showSimpleCreateLoopModal instead + */ +async function showCreateLoopModal_backup() { + // Fetch available CLI tools first + window.availableCliTools = await fetchAvailableCliTools(); + const modal = document.createElement('div'); modal.id = 'createLoopModal'; modal.className = 'modal-overlay'; modal.innerHTML = `