feat: add CLI Viewer Page with multi-pane layout and state management

- Implemented the CliViewerPage component for displaying CLI outputs in a configurable multi-pane layout.
- Integrated Zustand for state management, allowing for dynamic layout changes and tab management.
- Added layout options: single, split horizontal, split vertical, and 2x2 grid.
- Created viewerStore for managing layout, panes, and tabs, including actions for adding/removing panes and tabs.
- Added CoordinatorPage barrel export for easier imports.
This commit is contained in:
catlog22
2026-02-03 17:28:26 +08:00
parent b63e254f36
commit 37ba849e75
101 changed files with 10422 additions and 1145 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -21,7 +21,7 @@
"sidebar": "docs",
"previous": {
"title": "Overview",
"permalink": "/docs/docs/"
"permalink": "/docs/docs/overview"
},
"next": {
"title": "/ccw-plan",

View File

@@ -4,8 +4,8 @@
"description": "CCW is a professional workflow automation platform that combines AI-powered intelligence with structured development workflows. With 40+ commands and 15 integrated workflows, CCW transforms how you build, test, and ship software.",
"source": "@site/docs/overview.mdx",
"sourceDirName": ".",
"slug": "/",
"permalink": "/docs/docs/",
"slug": "/overview",
"permalink": "/docs/docs/overview",
"draft": false,
"unlisted": false,
"editUrl": "https://github.com/ccw/docs/tree/main/docs/overview.mdx",
@@ -15,8 +15,7 @@
"frontMatter": {
"title": "Welcome to CCW",
"sidebar_label": "Overview",
"sidebar_position": 1,
"slug": "/"
"sidebar_position": 1
},
"sidebar": "docs",
"next": {

File diff suppressed because one or more lines are too long

View File

@@ -7,10 +7,11 @@ export default {
"title": "CCW Help Documentation",
"tagline": "Professional Workflow Automation Platform",
"favicon": "img/favicon.ico",
"url": "https://ccw.dev",
"url": "http://localhost:3001",
"baseUrl": "/docs/",
"organizationName": "ccw",
"projectName": "docs",
"trailingSlash": false,
"onBrokenLinks": "throw",
"i18n": {
"defaultLocale": "en",

View File

@@ -127,7 +127,7 @@
},
{
"id": "overview",
"path": "/docs/docs/",
"path": "/docs/docs/overview",
"sidebar": "docs"
},
{
@@ -169,7 +169,7 @@
"sidebars": {
"docs": {
"link": {
"path": "/docs/docs/",
"path": "/docs/docs/overview",
"label": "Quick Start"
}
}

View File

@@ -14,7 +14,7 @@
"calendar": "gregory",
"path": "en",
"translate": false,
"url": "https://ccw.dev",
"url": "http://localhost:3001",
"baseUrl": "/docs/"
},
"zh": {
@@ -24,7 +24,7 @@
"calendar": "gregory",
"path": "zh",
"translate": true,
"url": "https://ccw.dev",
"url": "http://localhost:3001",
"baseUrl": "/docs/zh/"
}
}

View File

@@ -11,7 +11,6 @@ export default {
"__comp---theme-docs-root-5-e-9-0b6": [() => import(/* webpackChunkName: "__comp---theme-docs-root-5-e-9-0b6" */ "@theme/DocsRoot"), "@theme/DocsRoot", require.resolveWeak("@theme/DocsRoot")],
"__props---docs-docsa-20-f19": [() => import(/* webpackChunkName: "__props---docs-docsa-20-f19" */ "@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json"), "@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json", require.resolveWeak("@generated/docusaurus-plugin-content-docs/default/p/docs-docs-fbb.json")],
"__props---docs-docusaurus-debug-content-344-8d5": [() => import(/* webpackChunkName: "__props---docs-docusaurus-debug-content-344-8d5" */ "@generated/docusaurus-plugin-debug/default/p/docs-docusaurus-debug-content-a52.json"), "@generated/docusaurus-plugin-debug/default/p/docs-docusaurus-debug-content-a52.json", require.resolveWeak("@generated/docusaurus-plugin-debug/default/p/docs-docusaurus-debug-content-a52.json")],
"content---docs-docs-188-f57": [() => import(/* webpackChunkName: "content---docs-docs-188-f57" */ "@site/docs/overview.mdx"), "@site/docs/overview.mdx", require.resolveWeak("@site/docs/overview.mdx")],
"content---docs-docs-commands-cli-cli-init-056-2d3": [() => import(/* webpackChunkName: "content---docs-docs-commands-cli-cli-init-056-2d3" */ "@site/docs/commands/cli/cli-init.mdx"), "@site/docs/commands/cli/cli-init.mdx", require.resolveWeak("@site/docs/commands/cli/cli-init.mdx")],
"content---docs-docs-commands-cli-codex-reviewf-1-b-532": [() => import(/* webpackChunkName: "content---docs-docs-commands-cli-codex-reviewf-1-b-532" */ "@site/docs/commands/cli/codex-review.mdx"), "@site/docs/commands/cli/codex-review.mdx", require.resolveWeak("@site/docs/commands/cli/codex-review.mdx")],
"content---docs-docs-commands-general-ccw-coordinatord-55-a04": [() => import(/* webpackChunkName: "content---docs-docs-commands-general-ccw-coordinatord-55-a04" */ "@site/docs/commands/general/ccw-coordinator.mdx"), "@site/docs/commands/general/ccw-coordinator.mdx", require.resolveWeak("@site/docs/commands/general/ccw-coordinator.mdx")],
@@ -35,6 +34,7 @@ export default {
"content---docs-docs-commands-memory-memory-update-full-666-28e": [() => import(/* webpackChunkName: "content---docs-docs-commands-memory-memory-update-full-666-28e" */ "@site/docs/commands/memory/memory-update-full.mdx"), "@site/docs/commands/memory/memory-update-full.mdx", require.resolveWeak("@site/docs/commands/memory/memory-update-full.mdx")],
"content---docs-docs-commands-memory-memory-update-related-611-f0a": [() => import(/* webpackChunkName: "content---docs-docs-commands-memory-memory-update-related-611-f0a" */ "@site/docs/commands/memory/memory-update-related.mdx"), "@site/docs/commands/memory/memory-update-related.mdx", require.resolveWeak("@site/docs/commands/memory/memory-update-related.mdx")],
"content---docs-docs-faqea-3-29f": [() => import(/* webpackChunkName: "content---docs-docs-faqea-3-29f" */ "@site/docs/faq.mdx"), "@site/docs/faq.mdx", require.resolveWeak("@site/docs/faq.mdx")],
"content---docs-docs-overview-188-8fe": [() => import(/* webpackChunkName: "content---docs-docs-overview-188-8fe" */ "@site/docs/overview.mdx"), "@site/docs/overview.mdx", require.resolveWeak("@site/docs/overview.mdx")],
"content---docs-docs-workflows-faqbcf-a47": [() => import(/* webpackChunkName: "content---docs-docs-workflows-faqbcf-a47" */ "@site/docs/workflows/faq.mdx"), "@site/docs/workflows/faq.mdx", require.resolveWeak("@site/docs/workflows/faq.mdx")],
"content---docs-docs-workflows-introduction-9-f-4-dba": [() => import(/* webpackChunkName: "content---docs-docs-workflows-introduction-9-f-4-dba" */ "@site/docs/workflows/introduction.mdx"), "@site/docs/workflows/introduction.mdx", require.resolveWeak("@site/docs/workflows/introduction.mdx")],
"content---docs-docs-workflows-level-1-ultra-lightweightc-5-a-692": [() => import(/* webpackChunkName: "content---docs-docs-workflows-level-1-ultra-lightweightc-5-a-692" */ "@site/docs/workflows/level-1-ultra-lightweight.mdx"), "@site/docs/workflows/level-1-ultra-lightweight.mdx", require.resolveWeak("@site/docs/workflows/level-1-ultra-lightweight.mdx")],

View File

@@ -39,22 +39,16 @@ export default [
},
{
path: '/docs/docs',
component: ComponentCreator('/docs/docs', '7fe'),
component: ComponentCreator('/docs/docs', '942'),
routes: [
{
path: '/docs/docs',
component: ComponentCreator('/docs/docs', '830'),
component: ComponentCreator('/docs/docs', 'a90'),
routes: [
{
path: '/docs/docs',
component: ComponentCreator('/docs/docs', '77f'),
component: ComponentCreator('/docs/docs', 'c2e'),
routes: [
{
path: '/docs/docs/',
component: ComponentCreator('/docs/docs/', '2fa'),
exact: true,
sidebar: "docs"
},
{
path: '/docs/docs/commands/cli/cli-init',
component: ComponentCreator('/docs/docs/commands/cli/cli-init', 'c74'),
@@ -193,6 +187,12 @@ export default [
exact: true,
sidebar: "docs"
},
{
path: '/docs/docs/overview',
component: ComponentCreator('/docs/docs/overview', '7df'),
exact: true,
sidebar: "docs"
},
{
path: '/docs/docs/workflows/faq',
component: ComponentCreator('/docs/docs/workflows/faq', 'f47'),

View File

@@ -42,23 +42,19 @@
"plugin": "plugin---docs-docusaurus-debugb-38-c84"
}
},
"/docs/docs-7fe": {
"/docs/docs-942": {
"__comp": "__comp---theme-docs-root-5-e-9-0b6",
"__context": {
"plugin": "plugin---docs-docsaba-31e"
}
},
"/docs/docs-830": {
"/docs/docs-a90": {
"__comp": "__comp---theme-doc-version-roota-7-b-5de",
"__props": "__props---docs-docsa-20-f19"
},
"/docs/docs-77f": {
"/docs/docs-c2e": {
"__comp": "__comp---theme-doc-roota-94-67a"
},
"/docs/docs/-2fa": {
"__comp": "__comp---theme-doc-item-178-a40",
"content": "content---docs-docs-188-f57"
},
"/docs/docs/commands/cli/cli-init-c74": {
"__comp": "__comp---theme-doc-item-178-a40",
"content": "content---docs-docs-commands-cli-cli-init-056-2d3"
@@ -151,6 +147,10 @@
"__comp": "__comp---theme-doc-item-178-a40",
"content": "content---docs-docs-faqea-3-29f"
},
"/docs/docs/overview-7df": {
"__comp": "__comp---theme-doc-item-178-a40",
"content": "content---docs-docs-overview-188-8fe"
},
"/docs/docs/workflows/faq-f47": {
"__comp": "__comp---theme-doc-item-178-a40",
"content": "content---docs-docs-workflows-faqbcf-a47"

View File

@@ -2,7 +2,6 @@
title: Welcome to CCW
sidebar_label: Overview
sidebar_position: 1
slug: /
---
import Mermaid from '@theme/Mermaid';
@@ -29,7 +28,7 @@ CCW (Claude Code Workflows) is an advanced development environment that orchestr
<div className="row">
<div className="col col--4">
<Link to="/docs/workflows/introduction" className="card padding--lg">
<Link to="/workflows/introduction" className="card padding--lg">
<div className="card__header">
<h3>Workflows</h3>
</div>
@@ -39,7 +38,7 @@ CCW (Claude Code Workflows) is an advanced development environment that orchestr
</Link>
</div>
<div className="col col--4">
<Link to="/docs/commands/workflow/workflow-plan" className="card padding--lg">
<Link to="/commands/general/ccw" className="card padding--lg">
<div className="card__header">
<h3>Commands</h3>
</div>
@@ -49,7 +48,7 @@ CCW (Claude Code Workflows) is an advanced development environment that orchestr
</Link>
</div>
<div className="col col--4">
<Link to="/docs/faq" className="card padding--lg">
<Link to="/faq" className="card padding--lg">
<div className="card__header">
<h3>FAQ</h3>
</div>

View File

@@ -7,12 +7,14 @@ const config: Config = {
tagline: 'Professional Workflow Automation Platform',
favicon: 'img/favicon.ico',
url: 'https://ccw.dev',
url: 'http://localhost:3001',
baseUrl: '/docs/',
organizationName: 'ccw',
projectName: 'docs',
trailingSlash: false,
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',

View File

@@ -0,0 +1,331 @@
---
title: 欢迎使用 CCW
sidebar_label: 概览
sidebar_position: 1
---
import Mermaid from '@theme/Mermaid';
import Link from '@docusaurus/Link';
# 欢迎使用 CCW
CCW 是一个专业的工作流自动化平台,结合了 AI 驱动的智能与结构化开发工作流。凭借 40+ 命令和 15 个集成工作流CCW 彻底改变了您构建、测试和交付软件的方式。
## 什么是 CCW
CCW (Claude Code Workflows) 是一个高级开发环境,编排 AI 代理、工作流和工具以加速软件开发,同时保持质量标准。
**核心能力:**
- **AI 驱动开发** - 利用多种 AI 模型Gemini、Codex、Claude、Qwen进行代码分析、生成和审查
- **结构化工作流** - 从快速执行到智能编排的 15 个工作流级别
- **40+ 命令** - 覆盖规划、执行、测试、审查和维护的全面命令集
- **会话管理** - 完整的状态持久化,支持可恢复的会话
- **多代理协调** - 具有依赖感知任务分配的并行执行
- **质量关卡** - 内置的测试、验证和代码审查工作流
## 快速链接
<div className="row">
<div className="col col--4">
<Link to="/docs/workflows/introduction" className="card padding--lg">
<div className="card__header">
<h3>工作流</h3>
</div>
<div className="card__body">
<p>探索从快速执行到智能编排的 15 个工作流级别</p>
</div>
</Link>
</div>
<div className="col col--4">
<Link to="/docs/commands/general/ccw" className="card padding--lg">
<div className="card__header">
<h3>命令</h3>
</div>
<div className="card__body">
<p>工作流、问题、CLI 和内存操作的完整命令参考</p>
</div>
</Link>
</div>
<div className="col col--4">
<Link to="/docs/faq" className="card padding--lg">
<div className="card__header">
<h3>常见问题</h3>
</div>
<div className="card__body">
<p>常见问题、故障排除提示和最佳实践</p>
</div>
</Link>
</div>
</div>
## 核心功能
### 40+ 命令
CCW 提供按类别组织的全面命令集:
| 类别 | 命令数 | 用途 |
|----------|----------|---------|
| **工作流** | 13 命令 | 规划、执行、审查、清理 |
| **问题** | 7 命令 | 问题发现、规划、解决 |
| **CLI** | 2 命令 | CLI 初始化和审查 |
| **内存** | 6 命令 | 知识管理和文档 |
| **通用** | 7+ 命令 | 协调、调试、流程创建 |
### 15 个集成工作流
<Mermaid
chart={`
graph TB
subgraph Main["主工作流 (5 个级别)"]
L1["级别 1: 快速<br/>lite-lite-lite"]
L2["级别 2: 轻量级<br/>lite-plan, lite-fix, multi-cli-plan"]
L3["级别 3: 标准<br/>plan, tdd-plan, test-fix-gen"]
L4["级别 4: 头脑风暴<br/>brainstorm:auto-parallel"]
L5["级别 5: 智能化<br/>ccw-coordinator"]
L1 --> L2 --> L3 --> L4 --> L5
end
subgraph Issue["问题工作流"]
I1["阶段 1: 积累"]
I2["阶段 2: 解决"]
I1 --> I2
end
Main -.->|开发后| Issue
classDef level1 fill:#e3f2fd,stroke:#1976d2
classDef level2 fill:#bbdefb,stroke:#1976d2
classDef level3 fill:#90caf9,stroke:#1976d2
classDef level4 fill:#64b5f6,stroke:#1976d2
classDef level5 fill:#42a5f5,stroke:#1976d2
classDef issue fill:#fff3e0,stroke:#f57c00
class L1 level1,L2 level2,L3 level3,L4 level4,L5 level5,I1,I2 issue
`}
/>
**主工作流级别:**
- **级别 1**:零开销的超轻量级直接执行
- **级别 2**:轻量级规划 (lite-plan)、错误修复 (lite-fix)、多 CLI 分析 (multi-cli-plan)
- **级别 3**:标准规划 (plan)、测试驱动开发 (tdd-plan)、测试修复生成 (test-fix-gen)
- **级别 4**:多角色并行分析的头脑风暴
- **级别 5**:自动命令选择的智能编排
**问题工作流:**
- **积累阶段**discover、new
- **解决阶段**plan、queue、execute
### AI 驱动的智能
CCW 集成多种 AI 模型提供智能辅助:
| 模型 | 能力 | 使用场景 |
|-------|--------------|-----------|
| **Gemini** | 分析 + 写入 | 代码审查、调试、重构 |
| **Codex** | 分析 + 写入 + 审查 | Git 感知代码审查、实现 |
| **Claude** | 分析 + 写入 | 复杂推理、文档 |
| **Qwen** | 分析 + 写入 | 代码生成、模式匹配 |
**多 CLI 协作:**
```bash
# 比较多个 AI 视角的解决方案
/ccw multi-cli-plan "比较 Redis vs RabbitMQ 用于消息队列"
```
### 会话管理
CCW 为所有工作流会话提供完整的状态持久化:
```bash
# 启动规划会话
/ccw workflow:plan "实现用户认证"
# 恢复暂停的会话
/ccw workflow:session:resume WFS-user-auth
# 列出所有会话
/ccw workflow:session:list
# 完成并归档
/ccw workflow:session:complete WFS-user-auth
```
**会话结构:**
```
.workflow/active/WFS-{session}/
├── workflow-session.json # 会话元数据
├── IMPL_PLAN.md # 实现计划
├── TODO_LIST.md # 进度跟踪
├── .task/
│ ├── IMPL-001.json # 任务定义
│ └── ...
└── .summaries/
├── IMPL-001-summary.md # 完成摘要
└── ...
```
## 快速入门
### 选择您的起点
<div className="row">
<div className="col col--6">
<div className="card padding--md">
<div className="card__header">
<h3>CCW 新手?</h3>
</div>
<div className="card__body">
<p>从 <strong>级别 1 工作流</strong> 开始处理简单任务:</p>
<ul>
<li>快速修复和配置调整</li>
<li>简单功能实现</li>
<li>无规划开销的直接执行</li>
</ul>
<Link to="/docs/workflows/level-1-ultra-lightweight" className="button button--primary button--sm">学习级别 1</Link>
</div>
</div>
</div>
<div className="col col--6">
<div className="card padding--md">
<div className="card__header">
<h3>经验丰富的开发者?</h3>
</div>
<div className="card__body">
<p>直接跳到 <strong>级别 3 工作流</strong> 处理复杂功能:</p>
<ul>
<li>多模块变更</li>
<li>测试驱动开发</li>
<li>带验证的完整规划</li>
</ul>
<Link to="/docs/workflows/level-3-standard" className="button button--primary button--sm">学习级别 3</Link>
</div>
</div>
</div>
</div>
### 快速入门示例
**快速修复 (级别 1)**
```bash
# 简单更改的直接执行
/ccw lite-lite-lite "修复登录按钮中的拼写错误"
```
**带诊断的错误修复 (级别 2)**
```bash
# 智能错误诊断和修复
/ccw lite-fix "用户在个人资料更新时遇到 500 错误"
```
**功能开发 (级别 3)**
```bash
# 完整的规划和执行
/ccw workflow:plan "添加 OAuth2 认证"
/ccw workflow:execute --session WFS-oauth-auth
```
**新功能设计 (级别 4)**
```bash
# 多角色头脑风暴
/ccw brainstorm:auto-parallel "设计实时通知系统"
```
**不确定的命令 (级别 5)**
```bash
# 自动分析并推荐命令链
/ccw ccw-coordinator "需要重构整个 API 层"
```
## 架构概览
<Mermaid
chart={`
flowchart LR
User[开发者] --> CCW[CCW CLI]
CCW --> Workflow{工作流引擎}
Workflow --> L1[级别 1: 直接]
Workflow --> L2[级别 2: 轻量级]
Workflow --> L3[级别 3: 标准]
Workflow --> L4[级别 4: 头脑风暴]
Workflow --> L5[级别 5: 智能化]
L1 --> Agent[代理执行]
L2 --> Agent
L3 --> Agent
L4 --> MultiAgent[多代理并行]
L5 --> Coordinator[智能协调器]
Agent --> AI[AI 模型]
MultiAgent --> AI
Coordinator --> AI
AI --> Output[代码 + 工件]
Output --> Repo[Git 仓库]
Repo --> Session[会话状态]
classDef user fill:#f3f9ff,stroke:#1976d2
classDef ccw fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
classDef agent fill:#c8e6c9,stroke:#388e3c
classDef ai fill:#fff9c4,stroke:#f57c00
classDef storage fill:#f1f8e9,stroke:#33691e
class User user,CCW ccw,Workflow ccw,Agent agent,MultiAgent agent,Coordinator ccw,AI ai,Output storage,Repo storage,Session storage
`}
/>
## 下一步
### 学习基础知识
1. **[工作流简介](./workflows/introduction.mdx)** - 了解工作流级别和选择
2. **[命令参考](./commands/general/ccw.mdx)** - 完整的命令文档
3. **[常见问题](./faq.mdx)** - 常见问题和故障排除
### 按使用场景探索
| 使用场景 | 推荐工作流 | 文档 |
|----------|---------------------|---------------|
| 快速修复 | `lite-lite-lite` | [级别 1](./workflows/level-1-ultra-lightweight.mdx) |
| 错误诊断 | `lite-fix` | [级别 2](./workflows/level-2-rapid.mdx) |
| 功能开发 | `plan` → `execute` | [级别 3](./workflows/level-3-standard.mdx) |
| 测试驱动开发 | `tdd-plan` → `execute` | [级别 3](./workflows/level-3-standard.mdx) |
| 架构设计 | `brainstorm:auto-parallel` | [级别 4](./workflows/level-4-brainstorm.mdx) |
| 复杂工作流 | `ccw-coordinator` | [级别 5](./workflows/level-5-intelligent.mdx) |
| 问题跟踪 | 问题工作流 | [工作流 FAQ](./workflows/faq.mdx) |
### 试用一下
**安装:**
```bash
npm install -g @ccw/cli
```
**初始化项目:**
```bash
ccw init
```
**第一个工作流:**
```bash
# 简单任务
ccw lite-lite-lite "添加用户个人资料页面"
# 复杂功能
ccw workflow:plan "实现购物车"
```
## 支持
- **文档**: [docs.ccw.dev](https://docs.ccw.dev)
- **GitHub**: [github.com/ccw/ccw](https://github.com/ccw/ccw)
- **问题**: [github.com/ccw/ccw/issues](https://github.com/ccw/ccw/issues)
---
**准备开始了吗?** 探索 [工作流简介](./workflows/introduction.mdx) 了解所有 15 个工作流级别。

View File

@@ -0,0 +1,375 @@
---
title: 工作流常见问题
description: 工作流使用中的常见问题和解答
sidebar_position: 7
---
import Mermaid from '@theme/Mermaid';
# 工作流常见问题
CCW 工作流的常见问题和解答。
## 通用问题
### Main Workflow 和 Issue Workflow 有什么区别?
**Main Workflow** 用于主要开发Level 1-5而 **Issue Workflow** 用于开发后期的维护工作。
| 方面 | Main Workflow | Issue Workflow |
|--------|---------------|----------------|
| **用途** | 功能开发 | 开发后修复 |
| **时机** | 开发阶段 | 主工作流完成后 |
| **并行处理** | 依赖分析 | Worktree 隔离(可选) |
### 如何选择合适的工作流级别?
<Mermaid
chart={`
flowchart TD
Start([开始]) --> Q1{开发后期?}
Q1 -->|是| Issue["Issue Workflow"]
Q1 -->|否| Q2{命令不确定?}
Q2 -->|是| L5["Level 5: ccw-coordinator"]
Q2 -->|否| Q3{需求清晰?}
Q3 -->|否| L4["Level 4: brainstorm"]
Q3 -->|是| Q4{需要持久化会话?}
Q4 -->|是| Q5{开发类型?}
Q4 -->|否| Q6{多视角分析?}
Q5 -->|标准开发| L3Std["Level 3: plan"]
Q5 -->|TDD| L3TDD["Level 3: tdd-plan"]
Q5 -->|测试修复| L3Test["Level 3: test-fix-gen"]
Q6 -->|是| L2Multi["Level 2: multi-cli-plan"]
Q6 -->|否| Q7{Bug 修复?}
Q7 -->|是| L2Fix["Level 2: lite-fix"]
Q7 -->|否| Q8{需要规划?}
Q8 -->|是| L2Plan["Level 2: lite-plan"]
Q8 -->|否| L1["Level 1: lite-lite-lite"]
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef decision fill:#fff9c4,stroke:#f57c00
classDef level fill:#e3f2fd,stroke:#1976d2
class Start startend,Q1,Q2,Q3,Q4,Q6,Q7,Q8 decision,Issue,L1,L2Plan,L2Fix,L2Multi,L3Std,L3TDD,L3Test,L4,L5 level
`}
/>
### 什么是最小执行单元?
**最小执行单元**是指必须作为原子组一起执行的命令集合。拆分这些命令会破坏逻辑流程并产生不完整的状态。
**示例**:单元 `lite-plan -> lite-execute` 必须一起完成。在 `lite-plan` 之后停止会留下计划但没有实现。
## Level 1 问题
### 何时使用 Level 1
在以下情况下使用 Level 1 (`lite-lite-lite`)
- 快速修复(拼写错误、小幅调整)
- 简单功能(单个函数、小型工具)
- 配置更改(环境变量、超时值)
- 文档更新readme、注释
**不要使用**在:
- 多模块更改
- 需要持久化记录
- 复杂重构
- 测试驱动开发
## Level 2 问题
### lite-plan、lite-fix 和 multi-cli-plan 有什么区别?
| 工作流 | 用途 | 使用场景 |
|----------|---------|-------------|
| `lite-plan` | 需求清晰 | 单模块功能 |
| `lite-fix` | Bug 诊断 | Bug 修复、生产问题 |
| `multi-cli-plan` | 多视角分析 | 技术选型、方案比较 |
### 什么是热修复模式?
```bash
/workflow:lite-fix --hotfix "Production database connection failing"
```
**热修复模式**
- 跳过大部分诊断阶段
- 最小化规划(直接执行)
- 自动生成后续任务用于完整修复 + 复盘
- **仅用于生产紧急情况**
### 何时使用 multi-cli-plan vs lite-plan
在以下情况下使用 `multi-cli-plan`
- 需要多个视角Gemini、Codex、Claude
- 技术选型决策
- 方案比较
- 架构权衡
在以下情况下使用 `lite-plan`
- 需求清晰
- 单视角足够
- 需要更快迭代
## Level 3 问题
### plan、tdd-plan 和 test-fix-gen 有什么区别?
| 工作流 | 用途 | 关键特性 |
|----------|---------|-------------|
| `plan` | 标准开发 | 5 阶段规划与验证 |
| `tdd-plan` | 测试驱动开发 | 红-绿-重构循环 |
| `test-fix-gen` | 测试修复 | 渐进式测试层级L0-L3|
### 什么是 TDD测试驱动开发
**TDD** 遵循红-绿-重构循环:
1. **红Red**:编写失败的测试
2. **绿Green**:编写最小代码使测试通过
3. **重构Refactor**:在保持测试通过的同时改进代码
**铁律**
```
没有失败的测试就不写生产代码
```
### 为什么 TDD 要求先写测试?
| 方面 | 测试优先 | 测试随后 |
|--------|-----------|------------|
| **证明** | 测试在实现前失败 | 测试立即通过(无证明)|
| **发现** | 编码前发现边界情况 | 编码后发现边界情况 |
| **验证** | 验证需求 | 验证实现 |
### test-fix-gen 中有哪些测试层级?
| 层级 | 类型 | 描述 |
|-------|------|-------------|
| **L0** | 静态 | 类型检查、linting |
| **L1** | 单元 | 函数级别测试 |
| **L2** | 集成 | 组件交互 |
| **L3** | E2E | 完整系统测试 |
## Level 4 问题
### 何时使用 brainstorm:auto-parallel
在以下情况下使用 Level 4 (`brainstorm:auto-parallel`)
- 新功能设计
- 系统架构重构
- 探索性需求
- 不确定的实现方法
- 需要多维度权衡
### brainstorm 中有哪些可用角色?
| 角色 | 描述 |
|------|-------------|
| `system-architect` | 系统设计 |
| `ui-designer` | UI 设计 |
| `ux-expert` | 用户体验 |
| `product-manager` | 产品需求 |
| `product-owner` | 业务价值 |
| `data-architect` | 数据结构 |
| `scrum-master` | 流程和团队 |
| `subject-matter-expert` | 领域专业知识 |
| `test-strategist` | 测试策略 |
### 什么是 With-File 工作流?
**With-File 工作流**提供多 CLI 协作的文档化探索:
| 工作流 | 用途 | 级别 |
|----------|---------|-------|
| `brainstorm-with-file` | 多视角构思 | 4 |
| `debug-with-file` | 假设驱动调试 | 3 |
| `analyze-with-file` | 协作分析 | 3 |
## Level 5 问题
### 何时使用 ccw-coordinator
在以下情况下使用 Level 5 (`ccw-coordinator`)
- 复杂的多步骤工作流
- 不确定使用哪些命令
- 需要端到端自动化
- 需要完整的状态跟踪和可恢复性
- 团队协作统一执行流程
### ccw-coordinator 与其他级别有何不同?
| 方面 | Level 1-4 | Level 5 |
|--------|----------|--------|
| **命令选择** | 手动 | 自动 |
| **编排** | 手动 | 智能 |
| **状态跟踪** | 各异 | 完整持久化 |
## 执行问题
### 什么是 lite-execute
`lite-execute` 是 Level 2 工作流的统一执行命令:
```bash
/workflow:lite-execute --in-memory
```
**特性**
- 独立任务并行执行
- 依赖任务顺序执行
- 通过 TodoWrite 跟踪进度
- 可选代码审查
### 什么是 execute
`execute` 是 Level 3 工作流的统一执行命令:
```bash
/workflow:execute --session WFS-{session-id}
```
**特性**
- 依赖分析
- 并行/顺序任务执行
- 基于会话的进度跟踪
- 任务完成摘要
## 会话问题
### 如何恢复暂停的会话?
```bash
/workflow:session:resume # 恢复最近的会话
/workflow:session:resume WFS-{session-id} # 恢复特定会话
```
### 如何完成会话?
```bash
/workflow:session:complete --session WFS-{session-id}
```
这将使用经验教训归档会话并更新清单。
### 如何列出所有会话?
```bash
/workflow:session:list
```
## 产物问题
### 工作流产物存储在哪里?
| 级别 | 产物位置 |
|-------|-------------------|
| Level 1 | 无(无状态)|
| Level 2 | `memory://plan` 或 `.workflow/.lite-fix/`、`.workflow/.multi-cli-plan/` |
| Level 3 | `.workflow/active/WFS-{session}/` |
| Level 4 | `.workflow/active/WFS-{session}/.brainstorming/` |
| Level 5 | `.workflow/.ccw-coordinator/{session}/` |
### 会话中包含哪些文件?
```
.workflow/active/WFS-{session}/
├── workflow-session.json # 会话元数据
├── IMPL_PLAN.md # 实现计划
├── TODO_LIST.md # 进度跟踪
├── .task/
│ ├── IMPL-001.json # 任务定义
│ ├── IMPL-002.json
│ └── ...
└── .process/
├── context-package.json # 项目上下文
└── planning-notes.md
```
## 测试问题
### 如何为现有代码添加测试?
```bash
# 会话模式(从现有会话)
/workflow:test-fix-gen WFS-user-auth-v2
# 提示模式(直接描述)
/workflow:test-fix-gen "Add unit tests for the auth API"
```
### 如何修复失败的测试?
```bash
/workflow:test-fix-gen "Tests failing for user registration"
/workflow:test-cycle-execute
```
工作流将:
1. 分析测试失败
2. 识别根本原因
3. 迭代修复问题
4. 验证 >= 95% 通过率
## 故障排除
### 我的工作流失败了,该怎么办?
1. **检查错误消息** - 识别根本原因
2. **查看 state.json** - 检查 `.workflow/.ccw-coordinator/{session}/state.json`
3. **恢复会话** - 使用 `/workflow:session:resume` 继续
4. **调整并重试** - 根据错误修改方法
### 如何跳过失败的任务?
编辑任务 JSON 将状态设置为 "completed"
```bash
jq '.status = "completed"' .workflow/active/WFS-{session}/.task/IMPL-001.json
```
### 如何清理旧会话?
```bash
# 列出会话
/workflow:session:list
# 删除特定会话
rm -rf .workflow/active/WFS-{session-id}
# 清理所有已完成的会话
/workflow:clean
```
## 最佳实践
### 工作流最佳实践有哪些?
1. **从简单开始** - 使用满足需求的最低级别
2. **执行前规划** - 尽可能使用验证步骤
3. **持续测试** - 将测试集成到工作流中
4. **代码审查** - 使用内置审查工作流
5. **记录决策** - 对复杂决策使用头脑风暴工作流
### 何时使用 worktree 隔离?
**Worktree 隔离**主要用于 **Issue Workflow**
- 主开发完成后
- 已合并到 `main` 分支
- 发现需要修复的问题
- 需要在不影响当前开发的情况下修复
**Main Workflow** 不需要 worktree因为
- 依赖分析解决了并行问题
- 代理并行执行独立任务
- 不需要文件系统隔离
## 相关文档
- [简介](./introduction.mdx) - 工作流概述
- [Level 1](./level-1-ultra-lightweight.mdx) - 超轻量级工作流
- [Level 2](./level-2-rapid.mdx) - 快速工作流
- [Level 3](./level-3-standard.mdx) - 标准工作流
- [Level 4](./level-4-brainstorm.mdx) - 头脑风暴工作流
- [Level 5](./level-5-intelligent.mdx) - 智能工作流
- [命令](../commands/general/ccw.mdx) - 命令参考

View File

@@ -0,0 +1,313 @@
---
title: 工作流介绍
description: CCW 工作流全面概述 - 从急速执行到智能编排
sidebar_position: 1
---
import Mermaid from '@theme/Mermaid';
# 工作流介绍
CCW 提供两类工作流体系:**主干工作流** (Main Workflow) 和 **Issue 工作流** (Issue Workflow),它们协同覆盖软件开发的完整生命周期。
## 工作流架构概览
<Mermaid
chart={`
graph TB
subgraph Main["主干工作流 (5 个层级)"]
L1["层级 1: 急速执行<br/>lite-lite-lite"]
L2["层级 2: 轻量规划<br/>lite-plan, lite-fix, multi-cli-plan"]
L3["层级 3: 标准规划<br/>plan, tdd-plan, test-fix-gen"]
L4["层级 4: 头脑风暴<br/>brainstorm:auto-parallel"]
L5["层级 5: 智能编排<br/>ccw-coordinator"]
L1 --> L2 --> L3 --> L4 --> L5
end
subgraph Issue["Issue 工作流 (开发后维护)"]
I1["阶段 1: 积累<br/>discover, new"]
I2["阶段 2: 解决<br/>plan, queue, execute"]
I1 --> I2
end
Main -.->|开发完成后| Issue
classDef level1 fill:#e3f2fd,stroke:#1976d2
classDef level2 fill:#bbdefb,stroke:#1976d2
classDef level3 fill:#90caf9,stroke:#1976d2
classDef level4 fill:#64b5f6,stroke:#1976d2
classDef level5 fill:#42a5f5,stroke:#1976d2
classDef issue fill:#fff3e0,stroke:#f57c00
class L1 level1,L2 level2,L3 level3,L4 level4,L5 level5,I1,I2 issue
`}
/>
## 主干工作流 vs Issue 工作流
| 维度 | 主干工作流 | Issue 工作流 |
|------|-------------------|----------------|
| **定位** | 主要开发周期 | 开发后的维护补充 |
| **时机** | 功能开发阶段 | 主干开发完成后 |
| **范围** | 完整功能实现 | 针对性修复/增强 |
| **并行策略** | 依赖分析 + Agent 并行 | Worktree 隔离 (可选) |
| **分支模型** | 在当前分支工作 | 可使用独立 worktree |
### 为什么主干工作流不自动使用 Worktree
**依赖分析已解决并行问题**
1. 规划阶段 (`/workflow:plan`) 执行依赖分析
2. 自动识别任务依赖和关键路径
3. 划分**并行组** (独立任务) 和**串行链** (依赖任务)
4. Agent 并行执行独立任务,无需文件系统隔离
<Mermaid
chart={`
graph TD
subgraph Dependency["依赖分析"]
A["任务 A"]
B["任务 B"]
C["任务 C"]
D["任务 D"]
A & B --> PG["并行组 1<br/>Agent 1"]
C --> SC["串行链<br/>Agent 2"]
D --> SC
PG --> R["结果"]
SC --> R
end
classDef pg fill:#c8e6c9,stroke:#388e3c
classDef sc fill:#fff9c4,stroke:#f57c00
class PG pg,SC sc
`}
/>
### 为什么 Issue 工作流支持 Worktree
Issue 工作流作为**补充机制**,场景不同:
1. 主干开发完成,已合并到 `main`
2. 发现需要修复的问题
3. 需要在不影响当前开发的情况下修复
4. Worktree 隔离让主分支保持稳定
<Mermaid
chart={`
graph LR
Dev["开发"] --> Rel["发布"]
Rel --> Issue["发现问题"]
Issue --> Fix["Worktree 修复"]
Fix --> Merge["合并回主干"]
Merge --> NewDev["继续新功能"]
NewDev -.-> Dev
`}
/>
## 15 个工作流层级详解
### 层级 1: 急速执行
**复杂度**: 低 | **产物**: 无 | **状态**: 无状态
| 工作流 | 描述 |
|----------|-------------|
| `lite-lite-lite` | 超轻量直接执行,零开销 |
**适用于**: 快速修复、简单功能、配置调整
---
### 层级 2: 轻量规划
**复杂度**: 低-中 | **产物**: 内存/轻量文件 | **状态**: 会话内
| 工作流 | 描述 |
|----------|-------------|
| `lite-plan` | 内存规划,适用于明确需求 |
| `lite-fix` | 智能漏洞诊断和修复 |
| `multi-cli-plan` | 多 CLI 协作分析 |
**适用于**: 单模块功能、漏洞修复、技术选型
---
### 层级 2.5: 桥接工作流
**复杂度**: 低-中 | **用途**: 轻量到 Issue 工作流的过渡
| 工作流 | 描述 |
|----------|-------------|
| `rapid-to-issue` | 从快速规划桥接到 Issue 工作流 |
**适用于**: 将轻量规划转换为 Issue 跟踪
---
### 层级 3: 标准规划
**复杂度**: 中-高 | **产物**: 持久化会话文件 | **状态**: 完整会话管理
| 工作流 | 描述 |
|----------|-------------|
| `plan` | 复杂功能开发5 个阶段 |
| `tdd-plan` | 测试驱动开发Red-Green-Refactor |
| `test-fix-gen` | 测试修复生成,渐进式层级 |
**适用于**: 多模块改动、重构、TDD 开发
---
### With-File 工作流 (层级 3-4)
**复杂度**: 中-高 | **产物**: 文档化探索 | **多 CLI**: 支持
| 工作流 | 描述 | 层级 |
|----------|-------------|-------|
| `brainstorm-with-file` | 多视角创意构思 | 4 |
| `debug-with-file` | 假设驱动调试 | 3 |
| `analyze-with-file` | 协作分析 | 3 |
**适用于**: 需要多 CLI 协作的文档化探索
---
### 层级 4: 头脑风暴
**复杂度**: 高 | **产物**: 多角色分析文档 | **角色数**: 3-9
| 工作流 | 描述 |
|----------|-------------|
| `brainstorm:auto-parallel` | 多角色头脑风暴与综合 |
**适用于**: 新功能设计、架构重构、探索性需求
---
### 层级 5: 智能编排
**复杂度**: 所有层级 | **产物**: 完整状态持久化 | **自动化**: 完全自动
| 工作流 | 描述 |
|----------|-------------|
| `ccw-coordinator` | 自动分析并推荐命令链 |
**适用于**: 复杂多步骤工作流、不确定命令、端到端自动化
---
### Issue 工作流
**复杂度**: 可变 | **产物**: Issue 记录 | **隔离**: Worktree 可选
| 阶段 | 命令 |
|-------|----------|
| **积累** | `discover`, `discover-by-prompt`, `new` |
| **解决** | `plan --all-pending`, `queue`, `execute` |
**适用于**: 开发后 Issue 修复、维护主分支稳定性
## 选择合适的工作流
### 快速选择表
| 场景 | 推荐工作流 | 层级 |
|----------|---------------------|-------|
| 快速修复、配置调整 | `lite-lite-lite` | 1 |
| 明确的单模块功能 | `lite-plan -> lite-execute` | 2 |
| 漏洞诊断和修复 | `lite-fix` | 2 |
| 生产环境紧急修复 | `lite-fix --hotfix` | 2 |
| 技术选型、方案对比 | `multi-cli-plan -> lite-execute` | 2 |
| 多模块改动、重构 | `plan -> verify -> execute` | 3 |
| 测试驱动开发 | `tdd-plan -> execute -> tdd-verify` | 3 |
| 测试失败修复 | `test-fix-gen -> test-cycle-execute` | 3 |
| 新功能、架构设计 | `brainstorm:auto-parallel -> plan -> execute` | 4 |
| 复杂多步骤工作流、不确定命令 | `ccw-coordinator` | 5 |
| 开发后 Issue 修复 | Issue 工作流 | - |
### 决策流程图
<Mermaid
chart={`
flowchart TD
Start([开始新任务]) --> Q0{开发后<br/>维护?}
Q0 -->|是| IssueW["Issue 工作流<br/>discover -> plan -> queue -> execute"]
Q0 -->|否| Q1{不确定使用<br/>哪些命令?}
Q1 -->|是| L5["层级 5: ccw-coordinator<br/>自动分析并推荐"]
Q1 -->|否| Q2{需求<br/>明确?}
Q2 -->|不确定| L4["层级 4: brainstorm:auto-parallel<br/>多角色探索"]
Q2 -->|明确| Q3{需要持久化<br/>会话?}
Q3 -->|是| Q4{开发类型?}
Q3 -->|否| Q5{需要多<br/>视角?}
Q4 -->|标准| L3Std["层级 3: plan -> verify -> execute"]
Q4 -->|TDD| L3TDD["层级 3: tdd-plan -> execute -> verify"]
Q4 -->|测试修复| L3Test["层级 3: test-fix-gen -> test-cycle"]
Q5 -->|是| L2Multi["层级 2: multi-cli-plan"]
Q5 -->|否| Q6{漏洞修复?}
Q6 -->|是| L2Fix["层级 2: lite-fix"]
Q6 -->|否| Q7{需要规划?}
Q7 -->|是| L2Plan["层级 2: lite-plan -> lite-execute"]
Q7 -->|否| L1["层级 1: lite-lite-lite"]
IssueW --> End([完成])
L5 --> End
L4 --> End
L3Std --> End
L3TDD --> End
L3Test --> End
L2Multi --> End
L2Fix --> End
L2Plan --> End
L1 --> End
classDef level1 fill:#e3f2fd,stroke:#1976d2
classDef level2 fill:#bbdefb,stroke:#1976d2
classDef level3 fill:#90caf9,stroke:#1976d2
classDef level4 fill:#64b5f6,stroke:#1976d2
classDef level5 fill:#42a5f5,stroke:#1976d2
classDef issue fill:#fff3e0,stroke:#f57c00
class L1 level1,L2Plan,L2Fix,L2Multi level2,L3Std,L3TDD,L3Test level3,L4 level4,L5 level5,IssueW issue
`}
/>
### 复杂度指标
系统基于关键词自动评估复杂度:
| 权重 | 关键词 |
|--------|----------|
| +2 | refactor(重构), migrate(迁移), architect(架构), system(系统) |
| +2 | multiple(多个), across(跨), all(所有), entire(整个) |
| +1 | integrate(集成), api, database(数据库) |
| +1 | security(安全), performance(性能), scale(扩展) |
- **高复杂度** (&gt;=4): 自动选择层级 3-4
- **中等复杂度** (2-3): 自动选择层级 2
- **低复杂度** (&lt;2): 自动选择层级 1
## 最小执行单元
**定义**: 必须作为原子组一起执行以实现有意义的工作流里程碑的一组命令。
| 单元名称 | 命令 | 用途 |
|-----------|----------|---------|
| **快速实现** | lite-plan -> lite-execute | 轻量规划和执行 |
| **多 CLI 规划** | multi-cli-plan -> lite-execute | 多视角分析和执行 |
| **漏洞修复** | lite-fix -> lite-execute | 漏洞诊断和修复执行 |
| **验证式规划** | plan -> plan-verify -> execute | 带验证的规划和执行 |
| **TDD 规划** | tdd-plan -> execute | 测试驱动开发规划和执行 |
| **测试验证** | test-fix-gen -> test-cycle-execute | 生成测试任务并执行测试修复循环 |
| **代码审查** | review-session-cycle -> review-cycle-fix | 完整审查循环并应用修复 |
## 下一步
- [层级 1: 超轻量工作流](./level-1-ultra-lightweight.mdx) - 了解急速执行
- [层级 2: 快速工作流](./level-2-rapid.mdx) - 轻量规划和漏洞修复
- [层级 3: 标准工作流](./level-3-standard.mdx) - 完整规划和 TDD
- [层级 4: 头脑风暴工作流](./level-4-brainstorm.mdx) - 多角色探索
- [层级 5: 智能工作流](./level-5-intelligent.mdx) - 自动化编排
- [常见问题](./faq.mdx) - 常见问题和解答

View File

@@ -0,0 +1,250 @@
---
title: 层级 1: 超轻量工作流
description: 急速执行工作流 - 零开销直接执行
sidebar_position: 2
---
import Mermaid from '@theme/Mermaid';
# Level 1: 超轻量工作流
**复杂度**: 低 | **产物**: 无 | **状态**: 无状态 | **迭代**: 通过 AskUser 交互
最简单的工作流,用于立即处理简单任务 - 最小开销,直接执行。
## 概述
Level 1 工作流专为快速、直接的任务设计,不需要规划、文档或持久化状态。它们从输入直接执行到完成。
<Mermaid
chart={`
flowchart LR
Input([User Input]) --> Clarify[Clarification<br/>Optional]
Clarify --> Select{Auto-select<br/>CLI}
Select --> Analysis[Parallel Analysis<br/>Multi-tool exploration]
Analysis --> Results[Show Results]
Results --> Execute[Direct Execute]
Execute --> Done([Complete])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
class Input,Done startend,Clarify,Select,Analysis,Results,Execute action,Select decision
`}
/>
## 包含的工作流: lite-lite-lite
### 命令
```bash
/workflow:lite-lite-lite
# 或者 CCW 为简单任务自动选择
```
### 流程图
<Mermaid
chart={`
flowchart TD
A([User Input]) --> B{Clarification<br/>Needed?}
B -->|Yes| C[AskUserQuestion<br/>Goal/Scope/Constraints]
B -->|No| D[Task Analysis]
C --> D
D --> E{CLI Selection}
E --> F[Gemini<br/>Analysis]
E --> G[Codex<br/>Implementation]
E --> H[Claude<br/>Reasoning]
F --> I[Aggregate Results]
G --> I
H --> I
I --> J{Direct<br/>Execute?}
J -->|Yes| K[Execute Directly]
J -->|Iterate| L[Refine via AskUser]
L --> K
K --> M([Complete])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
class A,M startend,B,E,J decision,C,D,F,G,H,I,K action
`}
/>
### 特性
| 属性 | 值 |
|----------|-------|
| **复杂度** | 低 |
| **产物** | 无(无中间文件) |
| **状态** | 无状态 |
| **CLI 选择** | 自动分析任务类型 |
| **迭代** | 通过 AskUser 交互 |
### 流程阶段
1. **输入分析**
- 解析用户输入的任务意图
- 检测复杂度级别
- 识别所需的 CLI 工具
2. **可选澄清**(如果 clarity_score < 2
- 目标: 创建/修复/优化/分析
- 范围: 单文件/模块/跨模块
- 约束: 向后兼容/跳过测试/紧急热修复
3. **CLI 自动选择**
- 任务类型 -> CLI 工具映射
- 跨多个工具并行分析
- 聚合结果展示
4. **直接执行**
- 立即执行更改
- 无中间产物
- 可通过 AskUser 进行迭代
### CLI 选择逻辑
```javascript
function selectCLI(task) {
const patterns = {
'gemini': /analyze|review|understand|explore/,
'codex': /implement|generate|create|write code/,
'claude': /debug|fix|optimize|refactor/,
'qwen': /consult|discuss|compare/
};
for (const [cli, pattern] of Object.entries(patterns)) {
if (pattern.test(task)) return cli;
}
return 'gemini'; // 默认
}
```
## 使用场景
### 适用场景
- 快速修复(简单拼写错误、小幅调整)
- 简单功能添加(单个函数、小型工具)
- 配置调整(环境变量、配置文件)
- 小范围重命名(变量名、函数名)
- 文档更新readme、注释
### 不适用场景
- 多模块更改(使用 Level 2+
- 需要持久化记录(使用 Level 3+
- 复杂重构(使用 Level 3-4
- 测试驱动开发(使用 Level 3 TDD
- 架构设计(使用 Level 4-5
## 示例
### 示例 1: 快速修复
```bash
/workflow:lite-lite-lite "Fix typo in function name: getUserData"
```
**流程**:
1. 检测: 简单拼写错误修复
2. 选择: Codex 用于重构
3. 执行: 跨文件直接重命名
4. 完成: 不生成产物
### 示例 2: 简单功能
```bash
/workflow:lite-lite-lite "Add logging to user login function"
```
**流程**:
1. 检测: 单模块功能
2. 选择: Claude 用于实现
3. 澄清: 日志级别?输出目标?
4. 执行: 添加日志语句
5. 完成: 工作代码
### 示例 3: 配置调整
```bash
/workflow:lite-lite-lite "Update API timeout to 30 seconds"
```
**流程**:
1. 检测: 配置更改
2. 选择: Gemini 用于分析
3. 分析: 查找所有超时配置
4. 执行: 更新值
5. 完成: 配置已更新
## 优缺点
### 优点
| 优势 | 描述 |
|---------|-------------|
| **速度** | 最快的工作流,零开销 |
| **简单性** | 无需规划或文档 |
| **直接性** | 输入 -> 执行 -> 完成 |
| **无产物** | 清洁的工作区,无文件混乱 |
| **低认知负担** | 简单、直接的执行 |
### 缺点
| 限制 | 描述 |
|------------|-------------|
| **无追踪** | 没有更改记录 |
| **无规划** | 无法处理复杂任务 |
| **无审查** | 没有内置代码审查 |
| **范围有限** | 仅限单模块 |
| **无回滚** | 更改立即生效 |
## 与其他层级的比较
| 方面 | Level 1 | Level 2 | Level 3 |
|--------|---------|---------|---------|
| **规划** | 无 | 内存中 | 持久化 |
| **产物** | 无 | 内存文件 | 会话文件 |
| **复杂度** | 低 | 低-中 | 中-高 |
| **可追踪性** | 无 | 部分 | 完整 |
| **审查** | 无 | 可选 | 内置 |
## 何时升级到更高级别
**需要 Level 2+ 的信号**:
- 任务涉及多个模块
- 需要跟踪进度
- 需求需要澄清
- 需要代码审查
- 需要测试生成
**升级路径**:
<Mermaid
chart={`
graph LR
L1["Level 1:<br/>lite-lite-lite"] --> L2["Level 2:<br/>lite-plan"]
L2 --> L3["Level 3:<br/>plan"]
L3 --> L4["Level 4:<br/>brainstorm"]
L4 --> L5["Level 5:<br/>ccw-coordinator"]
classDef node fill:#e3f2fd,stroke:#1976d2
class L1,L2,L3,L4,L5 node
`}
/>
## 相关工作流
- [层级 2: 快速工作流](./level-2-rapid.mdx) - 带规划的下一层级
- [层级 3: 标准工作流](./level-3-standard.mdx) - 完整会话管理
- [FAQ](./faq.mdx) - 常见问题

View File

@@ -0,0 +1,453 @@
---
title: 层级 2: 快速工作流
description: 轻量规划和漏洞诊断工作流 - 适用于单模块功能
sidebar_position: 3
---
import Mermaid from '@theme/Mermaid';
# 层级 2: 快速工作流
**复杂度**: 低-中等 | **产物**: 内存/轻量文件 | **状态**: 会话作用域
Level 2 工作流提供轻量级规划或单次分析,支持快速迭代。它们专为需求相对明确、不需要完整会话持久化的任务而设计。
## 概述
<Mermaid
chart={`
flowchart TD
Start([用户输入]) --> Select{选择工作流}
Select -->|需求<br/>明确| LP[lite-plan]
Select -->|漏洞修复| LF[lite-fix]
Select -->|需要<br/>多CLI| MCP[multi-cli-plan]
LP --> LE[lite-execute]
LF --> LE
MCP --> LE
LE --> Test{需要<br/>测试?}
Test -->|是| TFG[test-fix-gen]
Test -->|否| Done([完成])
TFG --> TCE[test-cycle-execute]
TCE --> Done
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef workflow fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef execute fill:#c5e1a5,stroke:#388e3c
class Start,Done startend,Select,Test decision,LP,LF,MCP workflow,LE execute,TFG,TCE execute
`}
/>
## 包含的工作流
| 工作流 | 用途 | 产物 | 执行方式 |
|----------|---------|-----------|-----------|
| `lite-plan` | 需求明确的开发 | memory://plan | -> `lite-execute` |
| `lite-fix` | 漏洞诊断与修复 | `.workflow/.lite-fix/` | -> `lite-execute` |
| `multi-cli-plan` | 多视角任务 | `.workflow/.multi-cli-plan/` | -> `lite-execute` |
### 共同特征
| 属性 | 值 |
|----------|-------|
| **复杂度** | 低-中等 |
| **状态** | 会话作用域 / 轻量持久化 |
| **执行** | 统一通过 `lite-execute` |
| **使用场景** | 需求相对明确 |
---
## 工作流 1: lite-plan -> lite-execute
**内存规划 + 直接执行**
### 命令
```bash
/workflow:lite-plan "Add user authentication API"
/workflow:lite-execute
```
### 流程图
<Mermaid
chart={`
flowchart TD
A([开始]) --> B[阶段 1: 代码探索]
B --> C{需要<br/>代码探索?}
C -->|是| D[cli-explore-agent<br/>多角度分析]
C -->|否| E[跳过探索]
D --> F[阶段 2: 复杂度评估]
E --> F
F --> G{复杂度}
G -->|低| H[直接 Claude<br/>规划]
G -->|中| H
G -->|高| I[cli-lite-planning<br/>-agent]
H --> J[阶段 3: 规划]
I --> J
J --> K[阶段 4: 确认]
K --> L{确认?}
L -->|修改| M[调整计划]
M --> K
L -->|允许| N[阶段 5: 执行]
L -->|取消| O([中止])
N --> P[构建执行上下文]
P --> Q[/workflow:lite-execute]
Q --> R([完成])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef agent fill:#ffecb3,stroke:#ffa000
class A,R,O startend,B,D,E,J,M,P,Q action,C,G,L decision,F,H,I agent
`}
/>
### 流程阶段
**阶段 1: 代码探索** (可选)
```bash
# 如果指定了 -e 参数
/workflow:lite-plan -e "Add user authentication API"
```
- 通过 cli-explore-agent 进行多角度代码分析
- 识别模式、依赖关系、集成点
**阶段 2: 复杂度评估**
- 低: 无需代理,直接规划
- 中/高: 使用 cli-lite-planning-agent
**阶段 3: 规划**
- 加载计划 schema: `~/.claude/workflows/cli-templates/schemas/plan-json-schema.json`
- 按照生成计划 schema 生成 plan.json
**阶段 4: 确认与选择**
- 显示计划摘要(任务、复杂度、预估时间)
- 询问用户选择:
- 确认: 允许 / 修改 / 取消
- 执行: Agent / Codex / Auto
- 审核: Gemini / Agent / 跳过
**阶段 5: 执行**
- 构建执行上下文(计划 + 探索 + 澄清 + 选择)
- 通过 `/workflow:lite-execute --in-memory` 执行
### 产物
- **内存**: `memory://plan` (不持久化)
- **可选**: `.workflow/.lite-exploration/` (如果使用了代码探索)
### 使用场景
需求明确的单模块功能
**示例**:
```bash
/workflow:lite-plan "Add email validation to user registration form"
/workflow:lite-execute
```
---
## 工作流 2: lite-fix
**智能诊断 + 修复 (5 阶段)**
### 命令
```bash
/workflow:lite-fix "Login timeout after 30 seconds"
/workflow:lite-execute --in-memory --mode bugfix
# 紧急热修复模式
/workflow:lite-fix --hotfix "Production database connection failing"
```
### 流程图
<Mermaid
chart={`
flowchart TD
A([开始]) --> B[阶段 1: 漏洞分析<br/>& 诊断]
B --> C[严重程度预评估<br/>低/中/高/严重]
C --> D[并行 cli-explore<br/>-agent 诊断<br/>1-4 角度]
D --> E[阶段 2: 澄清]
E --> F{需要<br/>更多信息?}
F -->|是| G[AskUserQuestion<br/>聚合澄清]
F -->|否| H[阶段 3: 修复规划]
G --> H
H --> I{严重程度}
I -->|低/中| J[直接 Claude<br/>规划]
I -->|高/严重| K[cli-lite-planning<br/>-agent]
J --> L[阶段 4: 确认]
K --> L
L --> M[用户确认<br/>执行方法]
M --> N[阶段 5: 执行]
N --> O[/workflow:lite-execute<br/>--in-memory --mode bugfix/]
O --> P([完成])
Q[热修复模式] --> R[跳过诊断<br/>最小规划]
R --> N
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef agent fill:#ffecb3,stroke:#ffa000
classDef hotfix fill:#ffccbc,stroke:#bf360c
class A,P startend,B,E,G,H,L,M,N,O action,F,I decision,C,D,J,K agent,Q,R hotfix
`}
/>
### 流程阶段
**阶段 1: 漏洞分析与诊断**
- 智能严重程度预评估(低/中/高/严重)
- 并行 cli-explore-agent 诊断1-4 角度)
**阶段 2: 澄清** (可选)
- 聚合澄清需求
- AskUserQuestion 获取缺失信息
**阶段 3: 修复规划**
- 低/中严重程度 -> 直接 Claude 规划
- 高/严重程度 -> cli-lite-planning-agent
**阶段 4: 确认与选择**
- 显示修复计划摘要
- 用户确认执行方法
**阶段 5: 执行**
- 通过 `/workflow:lite-execute --in-memory --mode bugfix` 执行
### 产物
**位置**: `.workflow/.lite-fix/{bug-slug}-{YYYY-MM-DD}/`
```
.workflow/.lite-fix/
└── login-timeout-fix-2025-02-03/
├── diagnosis-root-cause.json
├── diagnosis-impact.json
├── diagnosis-code-flow.json
├── diagnosis-similar.json
├── diagnoses-manifest.json
├── fix-plan.json
└── README.md
```
### 严重程度级别
| 严重程度 | 描述 | 规划方法 |
|----------|-------------|-----------------|
| **低** | 简单修复,根因明确 | 直接 Claude可选理由 |
| **中** | 中等复杂度,需调查 | 直接 Claude含理由 |
| **高** | 复杂,影响多个组件 | cli-lite-planning-agent完整 schema |
| **严重** | 生产事故,紧急 | cli-lite-planning-agent + 热修复模式 |
### 热修复模式
```bash
/workflow:lite-fix --hotfix "Production API returning 500 errors"
```
**特点**:
- 跳过大部分诊断阶段
- 最小规划(直接执行)
- 自动生成后续任务用于完整修复 + 事后分析
### 使用场景
- 漏洞诊断
- 生产环境紧急情况
- 根因分析
---
## 工作流 3: multi-cli-plan -> lite-execute
**多CLI 协作分析 + 共识收敛**
### 命令
```bash
/workflow:multi-cli-plan "Compare Redis vs Memcached for caching"
/workflow:lite-execute
```
### 流程图
<Mermaid
chart={`
flowchart TD
A([开始]) --> B[阶段 1: 上下文收集]
B --> C[ACE 语义搜索<br/>构建上下文包]
C --> D[阶段 2: 多CLI 讨论<br/>迭代]
D --> E[cli-discuss-agent]
E --> F[Gemini + Codex + Claude]
F --> G{已收敛?}
G -->|否| H[交叉验证<br/>综合解决方案]
H --> D
G -->|是| I[阶段 3: 展示选项]
I --> J[展示解决方案<br/>及权衡]
J --> K[阶段 4: 用户决策]
K --> L[用户选择解决方案]
L --> M[阶段 5: 计划生成]
M --> N[cli-lite-planning<br/>-agent]
N --> O[-> lite-execute]
O --> P([完成])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef agent fill:#ffecb3,stroke:#ffa000
class A,P startend,B,C,E,H,J,L,M,O action,G,K decision,F,N agent
`}
/>
### 流程阶段
**阶段 1: 上下文收集**
- ACE 语义搜索
- 构建上下文包
**阶段 2: 多CLI 讨论** (迭代)
- cli-discuss-agent 执行 Gemini + Codex + Claude
- 交叉验证,综合解决方案
- 循环直至收敛或达到最大轮次
**阶段 3: 展示选项**
- 展示解决方案及权衡
**阶段 4: 用户决策**
- 用户选择解决方案
**阶段 5: 计划生成**
- cli-lite-planning-agent 生成计划
- -> lite-execute
### 产物
**位置**: `.workflow/.multi-cli-plan/{MCP-task-slug-date}/`
```
.workflow/.multi-cli-plan/
└── redis-vs-memcached-2025-02-03/
├── context-package.json
├── rounds/
│ ├── round-1/
│ │ ├── gemini-analysis.md
│ │ ├── codex-analysis.md
│ │ ├── claude-analysis.md
│ │ └── synthesis.json
│ ├── round-2/
│ └── ...
├── selected-solution.json
├── IMPL_PLAN.md
└── plan.json
```
### 与 lite-plan 对比
| 方面 | multi-cli-plan | lite-plan |
|--------|---------------|-----------|
| **上下文** | ACE 语义搜索 | 手动文件模式 |
| **分析** | 多CLI 交叉验证 | 单次规划 |
| **迭代** | 多轮至收敛 | 单轮 |
| **可信度** | 高(共识驱动) | 中(单视角) |
| **时间** | 较长(多轮) | 更快 |
### 使用场景
- 多视角分析
- 技术选型
- 方案对比
- 架构决策
---
## Level 2 对比表
| 方面 | lite-plan | lite-fix | multi-cli-plan |
|--------|-----------|----------|----------------|
| **用途** | 需求明确 | 漏洞诊断 | 多视角 |
| **规划** | 内存中 | 基于严重程度 | 共识驱动 |
| **产物** | memory://plan | .lite-fix/ | .multi-cli-plan/ |
| **代码探索** | 可选 (-e 参数) | 内置诊断 | ACE 搜索 |
| **多CLI** | 否 | 否 | 是 (Gemini/Codex/Claude) |
| **最适合** | 单模块功能 | 漏洞修复 | 技术决策 |
## 执行: lite-execute
所有 Level 2 工作流通过 `lite-execute` 执行:
```bash
/workflow:lite-execute --in-memory
```
### 执行流程
<Mermaid
chart={`
flowchart TD
A([开始]) --> B[初始化追踪<br/>previousExecutionResults]
B --> C[任务分组<br/>& 批量创建]
C --> D[提取显式<br/>depends_on]
D --> E[分组: 独立<br/>任务 -> 并行批次]
E --> F[分组: 依赖<br/>任务 -> 顺序阶段]
F --> G[创建 TodoWrite<br/>批次列表]
G --> H[启动执行]
H --> I[阶段 1: 所有独立<br/>任务 - 单批次]
I --> J[阶段 2+: 依赖任务<br/>按依赖顺序]
J --> K[追踪进度<br/>TodoWrite 更新]
K --> L{需要<br/>代码审核?}
L -->|是| M[审核流程]
L -->|否| N([完成])
M --> N
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
class A,N startend,B,D,E,F,G,I,J,K,M action,C,L decision
`}
/>
### 特性
- **并行执行** 独立任务
- **顺序阶段** 依赖任务
- **进度追踪** 通过 TodoWrite
- **可选代码审核**
## 相关工作流
- [Level 1: 超轻量级](./level-1-ultra-lightweight.mdx) - 更简单的工作流
- [Level 3: 标准级](./level-3-standard.mdx) - 完整会话管理
- [Level 4: 头脑风暴](./level-4-brainstorm.mdx) - 多角色探索
- [常见问题](./faq.mdx) - 常见问题解答

View File

@@ -0,0 +1,507 @@
---
title: 层级 3: 标准工作流
description: 标准规划工作流 - 完整规划和 TDD 开发
sidebar_position: 4
---
import Mermaid from '@theme/Mermaid';
# 层级 3: 标准工作流
**复杂度**: 中-高 | **产物**: 持久化会话文件 | **状态**: 完全会话管理
层级 3 工作流提供完整规划并支持持久化会话管理。专为需要可追溯性、验证和结构化执行的多模块变更而设计。
## 概述
<Mermaid
chart={`
flowchart TD
Start([用户输入]) --> Select{选择工作流}
Select -->|标准<br/>开发| Plan[plan]
Select -->|测试驱动| TDD[tdd-plan]
Select -->|测试修复| TestFix[test-fix-gen]
Plan --> Verify[plan-verify<br/>可选]
TDD --> Verify
Verify --> Execute[execute]
TestFix --> TestCycle[test-cycle-execute]
Execute --> Review{需要审查?}
TestCycle --> Review
Review -->|是| RevCycle[review-session-cycle]
Review -->|否| TestQ{有测试?}
RevCycle --> RevFix[review-cycle-fix]
RevFix --> TestQ
TestQ -->|是| TFG[test-fix-gen]
TestQ -->|否| Complete([session:complete])
TFG --> TCE[test-cycle-execute]
TCE --> Complete
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef workflow fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef execute fill:#c5e1a5,stroke:#388e3c
class Start,Complete startend,Select,Review,TestQ decision,Plan,TDD,TestFix workflow,Verify,Execute,TestCycle,RevCycle,RevFix,TFG,TCE execute
`}
/>
## 包含的工作流
| 工作流 | 用途 | 阶段数 | 产物位置 |
|----------|---------|--------|-------------------|
| `plan` | 复杂功能开发 | 5 阶段 | `.workflow/active/{session}/` |
| `tdd-plan` | 测试驱动开发 | 6 阶段 | `.workflow/active/{session}/` |
| `test-fix-gen` | 测试修复生成 | 5 阶段 | `.workflow/active/WFS-test-{session}/` |
### 共同特性
| 属性 | 值 |
|----------|-------|
| **复杂度** | 中-高 |
| **产物** | 持久化文件 (`.workflow/active/{session}/`) |
| **状态** | 完全会话管理 |
| **验证** | 内置验证步骤 |
| **执行** | `/workflow:execute` |
| **适用场景** | 多模块、可追溯任务 |
---
## 工作流 1: plan -> verify -> execute
**5 阶段完整规划工作流**
### 命令
```bash
/workflow:plan "实现 OAuth2 认证系统"
/workflow:plan-verify # 验证计划(推荐)
/workflow:execute
/workflow:review # (可选) 代码审查
```
### 流程图
<Mermaid
chart={`
flowchart TD
A([开始]) --> B[阶段 1: 会话发现]
B --> C[/workflow:session:start<br/>--auto/]
C --> D[返回: sessionId]
D --> E[阶段 2: 上下文收集]
E --> F[/workflow:tools:context-gather/]
F --> G[返回: context-package.json<br/>+ conflict_risk]
G --> H{conflict_risk<br/>>= medium?}
H -->|是| I[阶段 3: 冲突解决]
H -->|否| J[阶段 4: 任务生成]
I --> K[/workflow:tools:conflict<br/>-resolution/]
K --> J
J --> L[/workflow:tools:task-generate<br/>-agent/]
L --> M[返回: IMPL_PLAN.md<br/>+ IMPL-*.json<br/>+ TODO_LIST.md]
M --> N[返回摘要<br/>+ 下一步]
N --> O([规划完成])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef tool fill:#ffecb3,stroke:#ffa000
class A,O startend,H decision,B,E,G,J,M,N action,C,F,K,L tool
`}
/>
### 流程阶段
**阶段 1: 会话发现**
```bash
/workflow:session:start --auto "实现 OAuth2 认证系统"
```
- 创建或发现工作流会话
- 返回: `sessionId`
**阶段 2: 上下文收集**
```bash
/workflow:tools:context-gather
```
- 分析代码库结构
- 识别技术栈和模式
- 返回: `context-package.json` + `conflict_risk`
**阶段 3: 冲突解决**(条件触发)
```bash
# 仅当 conflict_risk >= medium 时
/workflow:tools:conflict-resolution
```
- 检测潜在冲突
- 解决依赖问题
- 确保清晰的执行路径
**阶段 4: 任务生成**
```bash
/workflow:tools:task-generate-agent
```
- 生成结构化任务
- 创建依赖图
- 返回: `IMPL_PLAN.md` + `IMPL-*.json` + `TODO_LIST.md`
### 产物
**位置**: `.workflow/active/{WFS-session}/`
```
.workflow/active/WFS-oauth2-auth-2025-02-03/
├── workflow-session.json # 会话元数据
├── IMPL_PLAN.md # 实现计划
├── TODO_LIST.md # 进度跟踪
├── .task/
│ ├── IMPL-001.json # 主任务
│ ├── IMPL-002.json
│ └── ...
└── .process/
├── context-package.json # 项目上下文
├── conflict-resolution.json # 冲突分析
└── planning-notes.md
```
### 适用场景
- 多模块变更
- 代码重构
- 需要依赖分析
---
## 工作流 2: tdd-plan -> execute -> tdd-verify
**6 阶段测试驱动开发工作流**
### 命令
```bash
/workflow:tdd-plan "使用 TDD 实现用户注册"
/workflow:execute # 遵循 Red-Green-Refactor
/workflow:tdd-verify # 验证 TDD 合规性
```
### 流程图
<Mermaid
chart={`
flowchart TD
A([开始]) --> B[阶段 1: 会话发现]
B --> C[/workflow:session:start<br/>--type tdd --auto/]
C --> D[解析: sessionId]
D --> E[阶段 2: 上下文收集]
E --> F[/workflow:tools:context-gather/]
F --> G[返回: context-package.json]
G --> H[阶段 3: 测试覆盖率分析]
H --> I[/workflow:tools:test-context<br/>-gather/]
I --> J[检测测试框架<br/>分析覆盖率]
J --> K{conflict_risk<br/>>= medium?}
K -->|是| L[阶段 4: 冲突解决]
K -->|否| M[阶段 5: TDD 任务生成]
L --> N[/workflow:tools:conflict<br/>-resolution/]
N --> M
M --> O[/workflow:tools:task-generate<br/>-tdd/]
O --> P[生成包含 Red-Green-<br/>Refactor 循环的 IMPL 任务]
P --> Q[阶段 6: TDD 结构验证]
Q --> R[验证 TDD 合规性]
R --> S([TDD 规划完成])
T[执行] --> U[/workflow:execute/]
U --> V[遵循每个任务中的<br/>Red-Green-Refactor 循环]
V --> W[验证]
W --> X[/workflow:tdd-verify/]
X --> Y([完成])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef tool fill:#ffecb3,stroke:#ffa000
class A,S,Y startend,K decision,B,E,G,H,J,M,P,Q,R,T,U,V,X action,C,F,I,N,O,W tool
`}
/>
### 流程阶段
**阶段 1: 会话发现**
```bash
/workflow:session:start --type tdd --auto "TDD: 用户注册"
```
**TDD 结构化格式**:
```
TDD: [功能名称]
GOAL: [目标]
SCOPE: [包含/排除的范围]
CONTEXT: [背景]
TEST_FOCUS: [测试场景]
```
**阶段 2: 上下文收集**
```bash
/workflow:tools:context-gather
```
**阶段 3: 测试覆盖率分析**
```bash
/workflow:tools:test-context-gather
```
- 检测测试框架
- 分析现有测试覆盖率
- 识别覆盖率缺口
**阶段 4: 冲突解决**(条件触发)
```bash
# 仅当 conflict_risk >= medium 时
/workflow:tools:conflict-resolution
```
**阶段 5: TDD 任务生成**
```bash
/workflow:tools:task-generate-tdd
```
- 生成包含内置 Red-Green-Refactor 循环的 IMPL 任务
- `meta.tdd_workflow: true`
- `flow_control.implementation_approach` 包含 3 个步骤red/green/refactor
**阶段 6: TDD 结构验证**
- 验证 TDD 结构合规性
### TDD 任务结构
```json
{
"id": "IMPL-001",
"title": "实现用户注册",
"meta": {
"tdd_workflow": true
},
"flow_control": {
"implementation_approach": [
{
"step": 1,
"title": "Red: 编写失败的测试",
"description": "编写一个失败的测试"
},
{
"step": 2,
"title": "Green: 使测试通过",
"description": "实现最小代码使测试通过",
"test_fix_cycle": {
"max_iterations": 3,
"pass_threshold": 0.95
}
},
{
"step": 3,
"title": "Refactor: 改进代码",
"description": "在保持测试通过的同时重构"
}
]
}
}
```
### 铁律
```
没有失败的测试,就没有生产代码
```
**执行方法**:
- 阶段 5: `implementation_approach` 包含测试优先步骤Red -> Green -> Refactor
- Green 阶段: 包含 test-fix-cycle 配置(最多 3 次迭代)
- 自动回滚: 达到最大迭代次数且测试未通过时触发
**顺序为何重要**:
- 后写的测试会立即通过 -> 证明不了什么
- 测试优先强制在实现前发现边界情况
- 后写测试验证的是已构建的内容,而非所需内容
### 适用场景
- 测试驱动开发
- 高质量功能需求
- 关键系统组件
---
## 工作流 3: test-fix-gen -> test-cycle-execute
**5 阶段测试修复生成工作流**
### 命令
```bash
# 会话模式
/workflow:test-fix-gen WFS-user-auth-v2
/workflow:test-cycle-execute
# 提示词模式
/workflow:test-fix-gen "测试认证 API"
/workflow:test-cycle-execute
```
### 流程图
<Mermaid
chart={`
flowchart TD
A([开始]) --> B{输入模式?}
B -->|会话<br/>模式| C[阶段 1: 使用源<br/>会话]
B -->|提示词<br/>模式| D[阶段 1: 创建<br/>测试会话]
C --> E[/workflow:session:start<br/>--type test --resume/]
D --> F[/workflow:session:start<br/>--type test --new/]
E --> G[阶段 2: 收集测试上下文]
F --> H[阶段 2: 收集测试上下文]
G --> I[/workflow:tools:test-context<br/>-gather/]
H --> I
I --> J[阶段 3: 测试生成分析]
J --> K[/workflow:tools:test-concept<br/>-enhanced/]
K --> L[多层测试需求<br/>L0: 静态, L1: 单元<br/>L2: 集成, L3: E2E]
L --> M[阶段 4: 生成测试任务]
M --> N[/workflow:tools:test-task-generate/]
N --> O[IMPL-001: 生成<br/>+ IMPL-001.5: 质量门<br/>+ IMPL-002: 执行修复]
O --> P[阶段 5: 返回摘要]
P --> Q[-> test-cycle-execute]
Q --> R([测试修复完成])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef tool fill:#ffecb3,stroke:#ffa000
class A,R startend,B decision,C,D,E,F,G,H,J,M,P,Q action,I,K,N tool
`}
/>
### 流程阶段
**阶段 1: 创建/使用测试会话**
**会话模式**(使用现有会话):
```bash
/workflow:session:start --type test --resume WFS-user-auth-v2
```
**提示词模式**(创建新会话):
```bash
/workflow:session:start --type test --new
```
**阶段 2: 收集测试上下文**
```bash
/workflow:tools:test-context-gather
```
**阶段 3: 测试生成分析**
```bash
/workflow:tools:test-concept-enhanced
```
- 多层测试需求:
- **L0: 静态** - 类型检查、代码检查
- **L1: 单元** - 函数级测试
- **L2: 集成** - 组件交互测试
- **L3: E2E** - 完整系统测试
**阶段 4: 生成测试任务**
```bash
/workflow:tools:test-task-generate
```
- `IMPL-001.json`: 测试理解与生成
- `IMPL-001.5-review.json`: 质量门
- `IMPL-002.json`: 测试执行与修复循环
**阶段 5: 返回摘要**
- -> `/workflow:test-cycle-execute`
### 双模式支持
| 模式 | 输入模式 | 上下文来源 |
|------|---------------|----------------|
| **会话模式** | `WFS-xxx` | 源会话摘要 |
| **提示词模式** | 文本/文件路径 | 直接代码库分析 |
### 产物
**位置**: `.workflow/active/WFS-test-{session}/`
```
.workflow/active/WFS-test-user-auth-2025-02-03/
├── workflow-session.json
├── .task/
│ ├── IMPL-001.json # 测试理解与生成
│ ├── IMPL-001.5-review.json # 质量门
│ └── IMPL-002.json # 测试执行与修复循环
└── .process/
├── TEST_ANALYSIS_RESULTS.md
└── test-context-package.json
```
### 适用场景
- 测试失败修复
- 覆盖率提升
- 测试套件生成
---
## 层级 3 对比表
| 方面 | plan | tdd-plan | test-fix-gen |
|--------|------|----------|--------------|
| **用途** | 复杂功能 | 测试驱动开发 | 测试修复 |
| **阶段** | 5 | 6 | 5 |
| **TDD** | 否 | 是 (Red-Green-Refactor) | 可选 |
| **产物** | `.workflow/active/` | `.workflow/active/` | `.workflow/active/WFS-test-*/` |
| **验证** | plan-verify | tdd-verify | 内置质量门 |
| **最适合** | 多模块变更 | 高质量功能 | 测试改进 |
## 执行: execute
所有层级 3 工作流通过 `execute` 执行:
```bash
/workflow:execute --session WFS-{session-id}
```
### 关键特性
- **依赖分析** - 自动任务依赖解析
- **并行执行** - 独立任务并行运行
- **进度跟踪** - 基于会话的 TODO 更新
- **摘要** - 为依赖任务生成任务完成摘要
## 相关工作流
- [层级 2: 快速](./level-2-rapid.mdx) - 更简单的工作流
- [层级 4: 头脑风暴](./level-4-brainstorm.mdx) - 多角色探索
- [层级 5: 智能](./level-5-intelligent.mdx) - 自动化编排
- [常见问题](./faq.mdx) - 常见问题解答

View File

@@ -0,0 +1,336 @@
---
title: 层级 4: 头脑风暴工作流
description: 多角色头脑风暴工作流 - 协作探索和创意生成
sidebar_position: 5
---
import Mermaid from '@theme/Mermaid';
# 层级 4: 头脑风暴工作流
**复杂度**: 高 | **产物**: 多角色分析文档 | **角色数**: 3-9 | **执行**: 阶段 1/3 串行,阶段 2 并行
层级 4 工作流提供完整的多角色头脑风暴,包含完整的规划和执行能力。专为探索性需求、不确定的实现方案以及多维度的权衡分析而设计。
## 概述
<Mermaid
chart={`
flowchart TD
Start([用户输入]) --> BS[brainstorm:auto-parallel]
BS --> P1[阶段 1: 交互式<br/>框架生成]
P1 --> P2[阶段 2: 并行<br/>角色分析]
P2 --> P3[阶段 3: 综合<br/>整合]
P3 --> Plan{需要详细<br/>规划?}
Plan -->|是| Verify[plan-verify]
Plan -->|否| Execute[execute]
Verify --> Execute
Execute --> Review{需要评审?}
Review -->|是| Rev[review-session-cycle]
Review -->|否| Test{需要测试?}
Rev --> RevFix[review-cycle-fix]
RevFix --> Test
Test -->|是| TFG[test-fix-gen]
Test -->|否| Complete([session:complete])
TFG --> TCE[test-cycle-execute]
TCE --> Complete
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef workflow fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef execute fill:#c5e1a5,stroke:#388e3c
class Start,Complete startend,Plan,Review,Test decision,BS,Verify,Rev,RevFix,TFG,TCE workflow,P1,P2,P3,Execute execute
`}
/>
## 包含的工作流: brainstorm:auto-parallel
**多角色头脑风暴 + 完整规划 + 执行**
### 命令
```bash
/workflow:brainstorm:auto-parallel "实时通知系统架构设计" [--count N] [--style-skill package]
/workflow:plan --session {sessionId}
/workflow:plan-verify
/workflow:execute
```
### 流程图
<Mermaid
chart={`
flowchart TD
A([开始]) --> B[阶段 1: 交互式<br/>框架生成]
B --> C[/workflow:brainstorm:artifacts/]
C --> D[主题分析<br/>生成问题]
D --> E[角色选择<br/>用户确认]
E --> F[角色问题收集]
F --> G[冲突检测<br/>与解决]
G --> H[生成 guidance-<br/>specification.md]
H --> I[阶段 2: 并行角色分析]
I --> J1[N 个 Task<br/>conceptual-planning-<br/>agent]
J1 --> K1{角色 1}
J1 --> K2{角色 2}
J1 --> K3{角色 3}
J1 --> K4{角色 N}
K1 --> L1[独立分析]
K2 --> L2[独立分析]
K3 --> L3[独立分析]
K4 --> L4[独立分析]
L1 --> M[并行生成<br/>{role}/analysis.md]
L2 --> M
L3 --> M
L4 --> M
M --> N[阶段 3: 综合整合]
N --> O[/workflow:brainstorm:<br/>synthesis/]
O --> P[整合所有角色<br/>分析]
P --> Q[综合为<br/>synthesis-specification.md]
Q --> R([头脑风暴完成])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef action fill:#e3f2fd,stroke:#1976d2
classDef phase fill:#fff9c4,stroke:#f57c00
classDef parallel fill:#ffecb3,stroke:#ffa000
class A,R startend,B,C,H,N,O,P,Q action,D,E,F,G,J1,K1,K2,K3,K4,L1,L2,L3,L4,M phase
`}
/>
### 特性
| 属性 | 值 |
|----------|-------|
| **复杂度** | 高 |
| **产物** | 多角色分析文档 + `IMPL_PLAN.md` |
| **角色数量** | 3-9 (默认 3) |
| **执行模式** | 阶段 1/3 串行,阶段 2 并行 |
### 流程阶段
#### 阶段 1: 交互式框架生成
```bash
/workflow:brainstorm:artifacts "实时通知系统架构设计"
```
**步骤**:
1. **主题分析** - 分析主题,生成关键问题
2. **角色选择** - 用户确认角色选择
3. **角色问题分配** - 为角色分配问题
4. **冲突检测** - 检测并解决角色冲突
5. **生成框架** - 创建 `guidance-specification.md`
#### 阶段 2: 并行角色分析
```bash
# 并行执行 N 个 conceptual-planning-agent 任务
Task(subagent_type: "conceptual-planning-agent", prompt: "Role: {role}, Topic: {topic}, Questions: {questions}")
```
**每个角色**:
- 接收特定角色的指导
- 独立分析主题
- 生成 `{role}/analysis.md`
- 可选: 子文档 (最多 5 个)
#### 阶段 3: 综合整合
```bash
/workflow:brainstorm:synthesis --session {sessionId}
```
**步骤**:
1. **收集** 所有角色分析
2. **整合** 视角到综合文档
3. **生成** `synthesis-specification.md`
4. **识别** 关键决策和权衡点
### 可用角色
| 角色 | 描述 |
|------|-------------|
| `system-architect` | 系统架构师 - 整体系统设计 |
| `ui-designer` | UI 设计师 - 用户界面设计 |
| `ux-expert` | UX 专家 - 用户体验优化 |
| `product-manager` | 产品经理 - 产品需求 |
| `product-owner` | 产品负责人 - 业务价值 |
| `data-architect` | 数据架构师 - 数据结构设计 |
| `scrum-master` | Scrum 主管 - 流程和团队 |
| `subject-matter-expert` | 领域专家 - 专业领域知识 |
| `test-strategist` | 测试策略师 - 测试策略 |
### 产物结构
```
.workflow/active/WFS-realtime-notifications/
├── workflow-session.json
└── .brainstorming/
├── guidance-specification.md # 框架 (阶段 1)
├── system-architect/
│ ├── analysis.md # 主文档
│ └── analysis-scale-{}.md # 子文档 (可选,最多 5 个)
├── ux-expert/
│ ├── analysis.md
│ └── analysis-accessibility.md
├── data-architect/
│ ├── analysis.md
│ └── analysis-storage.md
└── synthesis-specification.md # 整合 (阶段 3)
```
### 使用场景
#### 何时使用
- 新功能设计
- 系统架构重构
- 探索性需求
- 不确定的实现方案
- 需要多维度权衡分析
#### 何时不使用
- 需求明确 (使用层级 2-3)
- 时间敏感任务 (使用层级 2)
- 单一视角足够 (使用层级 2-3)
### 示例
#### 示例 1: 架构设计
```bash
/workflow:brainstorm:auto-parallel "电商平台微服务架构" --count 5
```
**角色**: system-architect, data-architect, ux-expert, product-manager, test-strategist
**输出**:
- 多个架构视角
- 数据流考虑
- 用户体验影响
- 业务需求对齐
- 测试策略建议
#### 示例 2: 功能探索
```bash
/workflow:brainstorm:auto-parallel "AI 驱动推荐系统" --count 3
```
**角色**: system-architect, product-manager, subject-matter-expert
**输出**:
- 技术可行性分析
- 业务价值评估
- 领域特定考量
## 文档化工作流 (With-File)
**文档化工作流** 提供带有多 CLI 协作的文档化探索。它们是自包含的,生成全面的会话产物。
| 工作流 | 用途 | 层级 | 主要特性 |
|----------|---------|-------|--------------|
| **brainstorm-with-file** | 多视角创意生成 | 4 | Gemini/Codex/Claude 视角,发散-收敛循环 |
| **debug-with-file** | 假设驱动调试 | 3 | Gemini 验证理解演进NDJSON 日志 |
| **analyze-with-file** | 协作分析 | 3 | 多轮问答CLI 探索,文档化讨论 |
### brainstorm-with-file
**多视角创意生成与文档化探索**
```bash
/workflow:brainstorm-with-file "通知系统重新设计"
```
**输出目录**: `.workflow/.brainstorm/`
**特性**:
- 发散-收敛循环
- 多 CLI 视角 (Gemini, Codex, Claude)
- 内置完成后选项 (创建规划、问题、深度分析)
### debug-with-file
**假设驱动调试与文档化调查**
```bash
/workflow:debug-with-file "系统在负载下随机崩溃"
```
**输出目录**: `.workflow/.debug/`
**特性**:
- 假设驱动迭代
- Gemini 假设验证
- 理解演进跟踪
- NDJSON 日志用于可复现性
### analyze-with-file
**协作分析与文档化讨论**
```bash
/workflow:analyze-with-file "理解认证架构设计决策"
```
**输出目录**: `.workflow/.analysis/`
**特性**:
- 多轮问答
- CLI 探索集成
- 文档化讨论线程
## 检测关键词
| 工作流 | 关键词 |
|----------|----------|
| **brainstorm** | 头脑风暴, 创意, 发散思维, multi-perspective, compare perspectives, 探索可能 |
| **debug-file** | 深度调试, 假设验证, systematic debug, hypothesis debug, 调试记录 |
| **analyze-file** | 协作分析, 深度理解, collaborative analysis, explore concept, 理解架构 |
## 对比: 文档化 vs 标准工作流
| 方面 | 文档化工作流 | 标准工作流 |
|--------|---------------------|-------------------|
| **文档** | 演进式文档 | 会话产物 |
| **多 CLI** | 内置 (Gemini/Codex/Claude) | 可选 |
| **迭代** | 自包含循环 | 手动继续 |
| **完成后选项** | 内置选项 | 手动下一步 |
| **最适合** | 文档化探索 | 结构化执行 |
## 层级 4 总结
| 方面 | 值 |
|--------|-------|
| **复杂度** | 高 |
| **产物** | 多角色分析 + 会话 |
| **规划** | 多视角收敛 |
| **执行** | 标准层级 3 执行 |
| **最适合** | 复杂探索性任务 |
## 相关工作流
- [层级 3: 标准](./level-3-standard.mdx) - 标准规划工作流
- [层级 5: 智能](./level-5-intelligent.mdx) - 自动编排
- [FAQ](./faq.mdx) - 常见问题
## 命令参考
参见 [命令文档](../commands/general/ccw.mdx) 了解:
- `/workflow:brainstorm:auto-parallel` - 多角色头脑风暴
- `/workflow:brainstorm-with-file` - 文档化创意生成
- `/workflow:debug-with-file` - 假设驱动调试
- `/workflow:analyze-with-file` - 协作分析

View File

@@ -0,0 +1,442 @@
---
title: 层级 5: 智能工作流
description: 智能编排工作流 - 自动分析和推荐
sidebar_position: 6
---
import Mermaid from '@theme/Mermaid';
# 层级 5: 智能工作流
**复杂度**: 所有层级 | **产物**: 完整状态持久化 | **自动化**: 完全自动化
层级 5 工作流提供最智能的自动化功能 - 带有顺序执行和状态持久化的自动命令链编排。它们能够自动分析需求、推荐最优命令链,并端到端执行。
## 概述
<Mermaid
chart={`
flowchart TD
Start([用户输入]) --> Analyze[阶段 1: 分析<br/>需求]
Analyze --> Recommend[阶段 2: 发现命令<br/>& 推荐命令链]
Recommend --> Confirm[用户确认<br/>可选]
Confirm --> Execute[阶段 3: 顺序执行<br/>命令链]
Execute --> State[状态持久化<br/>state.json]
State --> Check{完成?}
Check -->|否| Execute
Check -->|是| Complete([完成])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef phase fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef state fill:#ffecb3,stroke:#ffa000
class Start,Complete startend,Confirm,Check decision,Analyze,Recommend,Execute phase,State state
`}
/>
## 包含的工作流: ccw-coordinator
**自动分析并推荐命令链,支持顺序执行**
### 命令
```bash
/ccw-coordinator "使用 OAuth2 实现用户认证"
# 或者简单输入
/ccw "添加用户认证"
```
### 核心概念: 最小执行单元
**定义**: 一组必须作为原子组一起执行的命令,以实现有意义的工作流里程碑。
**为什么重要**:
- **防止不完整状态**: 避免在生成任务后停止而不执行
- **用户体验**: 用户获得完整结果,而非需要手动跟进的中间产物
- **工作流完整性**: 保持多步骤操作的逻辑连贯性
### 最小执行单元
#### 规划 + 执行单元
| 单元名称 | 命令 | 目的 | 输出 |
|-----------|----------|---------|--------|
| **快速实现** | lite-plan -> lite-execute | 轻量级规划和立即执行 | 工作代码 |
| **多 CLI 规划** | multi-cli-plan -> lite-execute | 多视角分析和执行 | 工作代码 |
| **Bug 修复** | lite-fix -> lite-execute | 快速 bug 诊断和修复执行 | 修复后的代码 |
| **完整规划 + 执行** | plan -> execute | 详细规划和执行 | 工作代码 |
| **验证规划 + 执行** | plan -> plan-verify -> execute | 带验证的规划和执行 | 工作代码 |
| **重新规划 + 执行** | replan -> execute | 更新计划并执行变更 | 工作代码 |
| **TDD 规划 + 执行** | tdd-plan -> execute | 测试驱动开发规划和执行 | 工作代码 |
| **测试生成 + 执行** | test-gen -> execute | 生成测试套件并执行 | 生成的测试 |
#### 测试单元
| 单元名称 | 命令 | 目的 | 输出 |
|-----------|----------|---------|--------|
| **测试验证** | test-fix-gen -> test-cycle-execute | 生成测试任务并执行测试-修复循环 | 测试通过 |
#### 审查单元
| 单元名称 | 命令 | 目的 | 输出 |
|-----------|----------|---------|--------|
| **代码审查 (会话)** | review-session-cycle -> review-fix | 完整审查循环并应用修复 | 修复后的代码 |
| **代码审查 (模块)** | review-module-cycle -> review-fix | 模块审查循环并应用修复 | 修复后的代码 |
### 三阶段工作流
<Mermaid
chart={`
flowchart TD
A([开始]) --> B[阶段 1: 分析需求]
B --> C[解析任务描述]
C --> D[提取: 目标、范围、约束、<br/>复杂度、任务类型]
D --> E[阶段 2: 发现命令<br/>& 推荐命令链]
E --> F[动态命令链<br/>组装]
F --> G[基于端口匹配]
G --> H{用户确认}
H -->|确认| I[阶段 3: 顺序执行<br/>命令链]
H -->|调整| J[修改命令链]
H -->|取消| K([中止])
J --> H
I --> L[初始化状态]
L --> M[遍历每个命令]
M --> N[组装提示词]
N --> O[在后台启动 CLI]
O --> P[保存检查点]
P --> Q{完成?}
Q -->|否| M
Q -->|是| R([完成])
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef phase fill:#e3f2fd,stroke:#1976d2
classDef decision fill:#fff9c4,stroke:#f57c00
classDef execute fill:#c5e1a5,stroke:#388e3c
class A,K,R startend,H,Q decision,B,E,I phase,C,D,F,G,J,L,M,N,O,P execute
`}
/>
#### 阶段 1: 分析需求
解析任务描述以提取: 目标、范围、约束、复杂度和任务类型。
```javascript
function analyzeRequirements(taskDescription) {
return {
goal: extractMainGoal(taskDescription), // 例如: "实现用户注册"
scope: extractScope(taskDescription), // 例如: ["auth", "user_management"]
constraints: extractConstraints(taskDescription), // 例如: ["无破坏性变更"]
complexity: determineComplexity(taskDescription), // 'simple' | 'medium' | 'complex'
task_type: detectTaskType(taskDescription) // 见下方的任务类型模式
};
}
// 任务类型检测模式
function detectTaskType(text) {
// 优先顺序 (第一个匹配胜出)
if (/fix|bug|error|crash|fail|debug|diagnose/.test(text)) return 'bugfix';
if (/tdd|test-driven|test first/.test(text)) return 'tdd';
if (/test fail|fix test|failing test/.test(text)) return 'test-fix';
if (/generate test|add test/.test(text)) return 'test-gen';
if (/review/.test(text)) return 'review';
if (/explore|brainstorm/.test(text)) return 'brainstorm';
if (/multi-perspective|comparison/.test(text)) return 'multi-cli';
return 'feature'; // 默认
}
// 复杂度评估
function determineComplexity(text) {
let score = 0;
if (/refactor|migrate|architect|system/.test(text)) score += 2;
if (/multiple|across|all|entire/.test(text)) score += 2;
if (/integrate|api|database/.test(text)) score += 1;
if (/security|performance|scale/.test(text)) score += 1;
return score >= 4 ? 'complex' : score >= 2 ? 'medium' : 'simple';
}
```
#### 阶段 2: 发现命令并推荐命令链
使用基于端口匹配的动态命令链组装。
**向用户显示**:
```
推荐命令链:
管道 (可视化):
需求 -> lite-plan -> 计划 -> lite-execute -> 代码 -> test-cycle-execute -> 测试通过
命令:
1. /workflow:lite-plan
2. /workflow:lite-execute
3. /workflow:test-cycle-execute
继续? [确认 / 显示详情 / 调整 / 取消]
```
#### 阶段 3: 顺序执行命令链
```javascript
async function executeCommandChain(chain, analysis) {
const sessionId = `ccw-coord-${Date.now()}`;
const stateDir = `.workflow/.ccw-coordinator/${sessionId}`;
// 初始化状态
const state = {
session_id: sessionId,
status: 'running',
created_at: new Date().toISOString(),
analysis: analysis,
command_chain: chain.map((cmd, idx) => ({ ...cmd, index: idx, status: 'pending' })),
execution_results: [],
prompts_used: []
};
// 保存初始状态
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
for (let i = 0; i < chain.length; i++) {
const cmd = chain[i];
// 组装提示词
let prompt = formatCommand(cmd, state.execution_results, analysis);
prompt += `\n\nTask: ${analysis.goal}`;
if (state.execution_results.length > 0) {
prompt += '\nPrevious results:\n';
state.execution_results.forEach(r => {
if (r.session_id) {
prompt += `- ${r.command}: ${r.session_id}\n`;
}
});
}
// 在后台启动 CLI
const taskId = Bash(
`ccw cli -p "${escapePrompt(prompt)}" --tool claude --mode write`,
{ run_in_background: true }
).task_id;
// 保存检查点
state.execution_results.push({
index: i,
command: cmd.command,
status: 'in-progress',
task_id: taskId,
session_id: null,
artifacts: [],
timestamp: new Date().toISOString()
});
// 在此停止 - 等待 hook 回调
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
break;
}
state.status = 'waiting';
Write(`${stateDir}/state.json`, JSON.stringify(state, null, 2));
return state;
}
```
### 状态文件结构
**位置**: `.workflow/.ccw-coordinator/{session_id}/state.json`
```json
{
"session_id": "ccw-coord-20250203-143025",
"status": "running|waiting|completed|failed",
"created_at": "2025-02-03T14:30:25Z",
"updated_at": "2025-02-03T14:35:45Z",
"analysis": {
"goal": "实现用户注册",
"scope": ["authentication", "user_management"],
"constraints": ["无破坏性变更"],
"complexity": "medium",
"task_type": "feature"
},
"command_chain": [
{
"index": 0,
"command": "/workflow:plan",
"name": "plan",
"status": "completed"
},
{
"index": 1,
"command": "/workflow:execute",
"name": "execute",
"status": "running"
}
],
"execution_results": [
{
"index": 0,
"command": "/workflow:plan",
"status": "completed",
"task_id": "task-001",
"session_id": "WFS-plan-20250203",
"artifacts": ["IMPL_PLAN.md"],
"timestamp": "2025-02-03T14:30:25Z",
"completed_at": "2025-02-03T14:30:45Z"
}
]
}
```
### 完整生命周期决策流程图
<Mermaid
chart={`
flowchart TD
Start([开始新任务]) --> Q0{是 bug 修复吗?}
Q0 -->|是| BugFix["Bug 修复流程"]
Q0 -->|否| Q1{知道要做什么吗?}
BugFix --> BugSeverity{理解根本原因吗?}
BugSeverity -->|清楚| LiteFix["/workflow:lite-fix<br/>标准修复"]
BugSeverity -->|生产事故| HotFix["/workflow:lite-fix --hotfix<br/>紧急热修复"]
BugSeverity -->|不清楚| BugDiag["/workflow:lite-fix<br/>自动诊断根本原因"]
BugDiag --> LiteFix
LiteFix --> BugComplete["Bug 已修复"]
HotFix --> FollowUp["自动生成后续任务<br/>完整修复 + 复盘"]
FollowUp --> BugComplete
BugComplete --> End(["任务完成"])
Q1 -->|否| Ideation["探索阶段<br/>明确需求"]
Q1 -->|是| Q2{知道如何做吗?}
Ideation --> BrainIdea["/workflow:brainstorm:auto-parallel<br/>探索产品方向"]
BrainIdea --> Q2
Q2 -->|否| Design["设计探索<br/>探索架构"]
Q2 -->|是| Q3{需要规划吗?}
Design --> BrainDesign["/workflow:brainstorm:auto-parallel<br/>探索技术方案"]
BrainDesign --> Q3
Q3 -->|快速简单| LitePlan["轻量级规划<br/>/workflow:lite-plan"]
Q3 -->|复杂完整| FullPlan["标准规划<br/>/workflow:plan"]
LitePlan --> Q4{需要代码探索?}
Q4 -->|是| LitePlanE["/workflow:lite-plan -e"]
Q4 -->|否| LitePlanNormal["/workflow:lite-plan"]
LitePlanE --> LiteConfirm["三维确认:<br/>1. 任务确认<br/>2. 执行方式<br/>3. 代码审查"]
LitePlanNormal --> LiteConfirm
LiteConfirm --> Q5{选择执行方式}
Q5 -->|Agent| LiteAgent["/workflow:lite-execute<br/>使用 @code-developer"]
Q5 -->|CLI 工具| LiteCLI["CLI 执行<br/>Gemini/Qwen/Codex"]
Q5 -->|仅规划| UserImpl["用户手动实现"]
FullPlan --> PlanVerify{验证计划质量?}
PlanVerify -->|是| Verify["/workflow:plan-verify"]
PlanVerify -->|否| Execute
Verify --> Q6{验证通过?}
Q6 -->|否| FixPlan["修复计划问题"]
Q6 -->|是| Execute
FixPlan --> Execute
Execute["执行阶段<br/>/workflow:execute"]
LiteAgent --> TestDecision
LiteCLI --> TestDecision
UserImpl --> TestDecision
Execute --> TestDecision
TestDecision{需要测试?}
TestDecision -->|TDD 模式| TDD["/workflow:tdd-plan<br/>测试驱动开发"]
TestDecision -->|后置测试| TestGen["/workflow:test-gen<br/>生成测试"]
TestDecision -->|已有测试| TestCycle["/workflow:test-cycle-execute<br/>测试-修复循环"]
TestDecision -->|不需要| Review
TDD --> TDDExecute["/workflow:execute<br/>红-绿-重构"]
TDDExecute --> TDDVerify["/workflow:tdd-verify<br/>验证 TDD 合规性"]
TDDVerify --> Review
TestGen --> TestExecute["/workflow:execute<br/>执行测试任务"]
TestExecute --> TestResult{测试通过?}
TestResult -->|否| TestCycle
TestResult -->|是| Review
TestCycle --> TestPass{通过率 >= 95%?}
TestPass -->|否, 继续修复| TestCycle
TestPass -->|是| Review
Review["审查阶段"]
Review --> Q7{需要专项审查?}
Q7 -->|安全| SecurityReview["/workflow:review<br/>--type security"]
Q7 -->|架构| ArchReview["/workflow:review<br/>--type architecture"]
Q7 -->|质量| QualityReview["/workflow:review<br/>--type quality"]
Q7 -->|通用| GeneralReview["/workflow:review<br/>通用审查"]
Q7 -->|不需要| Complete
SecurityReview --> Complete
ArchReview --> Complete
QualityReview --> Complete
GeneralReview --> Complete
Complete["完成阶段<br/>/workflow:session:complete"]
Complete --> End
classDef startend fill:#c8e6c9,stroke:#388e3c
classDef bugfix fill:#ffccbc,stroke:#bf360c
classDef ideation fill:#fff9c4,stroke:#ffa000
classDef planning fill:#e3f2fd,stroke:#1976d2
classDef execute fill:#c5e1a5,stroke:#388e3c
classDef review fill:#d1c4e9,stroke:#512da8
class Start,End startend,BugFix,LiteFix,HotFix,BugDiag,BugComplete bugfix,Ideation,BrainIdea,BrainDesign ideation,LitePlan,LitePlanE,LitePlanNormal,LiteConfirm,FullPlan,PlanVerify,Verify,FixPlan planning,Execute,LiteAgent,LiteCLI,UserImpl,TDD,TDDExecute,TDDVerify,TestGen,TestExecute,TestCycle execute,Review,SecurityReview,ArchReview,QualityReview,GeneralReview,Complete review
`}
/>
### 使用场景
### 何时使用
- 复杂的多步骤工作流
- 不确定使用哪些命令
- 需要端到端自动化
- 需要完整的状态跟踪和可恢复性
- 团队协作需要统一的执行流程
### 何时不使用
- 简单的单命令任务 (直接使用层级 1-4)
- 已经知道确切的所需命令 (直接使用层级 1-4)
### 与其他层级的关系
| 层级 | 手动程度 | CCW Coordinator 角色 |
|-------|---------------|-----------------------|
| 层级 1-4 | 手动选择命令 | 自动组合这些命令 |
| 层级 5 | 自动选择命令 | 智能编排器 |
**CCW Coordinator 内部使用层级 1-4**:
- 分析任务 -> 自动选择合适的层级
- 组装命令链 -> 包含层级 1-4 的命令
- 顺序执行 -> 遵循最小执行单元
## 相关工作流
- [层级 1: 超轻量级](./level-1-ultra-lightweight.mdx) - 快速执行
- [层级 2: 快速](./level-2-rapid.mdx) - 轻量级规划
- [层级 3: 标准](./level-3-standard.mdx) - 完整规划
- [层级 4: 头脑风暴](./level-4-brainstorm.mdx) - 多角色探索
- [常见问题](./faq.mdx) - 常见问题
## 命令参考
参见 [命令文档](../commands/general/ccw.mdx) 了解:
- `/ccw-coordinator` - 智能工作流编排器
- `/ccw` - 主工作流编排器

View File

@@ -25,6 +25,7 @@
"@radix-ui/react-tooltip": "^1.1.0",
"@tanstack/react-query": "^5.60.0",
"@xyflow/react": "^12.10.0",
"allotment": "^1.20.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"highlight.js": "^11.11.1",
@@ -3433,6 +3434,30 @@
"node": ">= 14"
}
},
"node_modules/allotment": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/allotment/-/allotment-1.20.5.tgz",
"integrity": "sha512-7i4NT7ieXEyAd5lBrXmE7WHz/e7hRuo97+j+TwrPE85ha6kyFURoc76nom0dWSZ1pTKVEAMJy/+f3/Isfu/41A==",
"license": "MIT",
"dependencies": {
"classnames": "^2.3.0",
"eventemitter3": "^5.0.0",
"fast-deep-equal": "^3.1.3",
"lodash.clamp": "^4.0.0",
"lodash.debounce": "^4.0.0",
"usehooks-ts": "^3.1.1"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/allotment/node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -3947,6 +3972,12 @@
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -4835,6 +4866,12 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-equals": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
@@ -6042,6 +6079,18 @@
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.clamp": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz",
"integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -9882,6 +9931,21 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/usehooks-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz",
"integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==",
"license": "MIT",
"dependencies": {
"lodash.debounce": "^4.0.8"
},
"engines": {
"node": ">=16.15.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -36,6 +36,7 @@
"@radix-ui/react-tooltip": "^1.1.0",
"@tanstack/react-query": "^5.60.0",
"@xyflow/react": "^12.10.0",
"allotment": "^1.20.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"highlight.js": "^11.11.1",

View File

@@ -0,0 +1,198 @@
// ========================================
// ContentArea Component
// ========================================
// Displays CLI output for the active tab in a pane
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Terminal, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
useViewerStore,
selectActiveTab,
type PaneId,
} from '@/stores/viewerStore';
import { useCliStreamStore, type CliExecutionState, type CliOutputLine } from '@/stores/cliStreamStore';
import { MonitorBody } from '@/components/shared/CliStreamMonitor/MonitorBody';
import { MessageRenderer } from '@/components/shared/CliStreamMonitor/MessageRenderer';
// ========== Types ==========
export interface ContentAreaProps {
paneId: PaneId;
className?: string;
}
// ========== Helper Components ==========
/**
* Empty state when no tab is active
*/
function EmptyTabState() {
const { formatMessage } = useIntl();
return (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground gap-4">
<Terminal className="h-12 w-12 opacity-30" />
<div className="text-center">
<p className="text-sm font-medium">
{formatMessage({ id: 'cliViewer.noActiveTab', defaultMessage: 'No active tab' })}
</p>
<p className="text-xs mt-1">
{formatMessage({
id: 'cliViewer.selectOrCreate',
defaultMessage: 'Select a tab or start a new CLI execution',
})}
</p>
</div>
</div>
);
}
/**
* Execution not found state
*/
function ExecutionNotFoundState({ executionId }: { executionId: string }) {
const { formatMessage } = useIntl();
return (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground gap-4">
<Terminal className="h-12 w-12 opacity-30" />
<div className="text-center">
<p className="text-sm font-medium">
{formatMessage({ id: 'cliViewer.executionNotFound', defaultMessage: 'Execution not found' })}
</p>
<p className="text-xs mt-1 font-mono opacity-50">{executionId}</p>
</div>
</div>
);
}
/**
* Single output line component with type-based styling
*/
function OutputLineItem({ line }: { line: CliOutputLine }) {
// Type-based styling
const typeStyles: Record<CliOutputLine['type'], string> = {
stdout: 'text-foreground',
stderr: 'text-rose-600 dark:text-rose-400 bg-rose-500/5',
thought: 'text-blue-600 dark:text-blue-400 italic bg-blue-500/5',
system: 'text-amber-600 dark:text-amber-400 bg-amber-500/5',
metadata: 'text-muted-foreground text-xs',
tool_call: 'text-emerald-600 dark:text-emerald-400 bg-emerald-500/5 font-mono',
};
return (
<div
className={cn(
'px-3 py-1 text-sm',
'border-l-2 border-transparent',
typeStyles[line.type] || 'text-foreground',
line.type === 'stderr' && 'border-l-rose-500',
line.type === 'thought' && 'border-l-blue-500',
line.type === 'system' && 'border-l-amber-500',
line.type === 'tool_call' && 'border-l-emerald-500'
)}
>
{line.type === 'thought' || line.type === 'tool_call' ? (
<MessageRenderer content={line.content} format="markdown" />
) : (
<pre className="whitespace-pre-wrap break-words font-mono text-xs">
{line.content}
</pre>
)}
</div>
);
}
/**
* CLI output display component
*/
function CliOutputDisplay({ execution, executionId }: { execution: CliExecutionState; executionId: string }) {
const { formatMessage } = useIntl();
if (!execution.output || execution.output.length === 0) {
return (
<div className="h-full flex flex-col items-center justify-center text-muted-foreground gap-4">
<Terminal className="h-12 w-12 opacity-30" />
<div className="text-center">
<p className="text-sm">
{execution.status === 'running'
? formatMessage({ id: 'cliViewer.waitingForOutput', defaultMessage: 'Waiting for output...' })
: formatMessage({ id: 'cliViewer.noOutput', defaultMessage: 'No output' })}
</p>
{execution.status === 'running' && (
<Loader2 className="h-4 w-4 animate-spin mt-2 mx-auto opacity-50" />
)}
</div>
</div>
);
}
return (
<MonitorBody autoScroll={execution.status === 'running'} showScrollButton>
<div className="py-2">
{execution.output.map((line, index) => (
<OutputLineItem
key={`${executionId}-line-${index}`}
line={line}
/>
))}
</div>
</MonitorBody>
);
}
// ========== Main Component ==========
/**
* ContentArea - Displays CLI output for active tab
*
* Features:
* - Integration with CliStreamStore for execution data
* - Auto-scroll during active execution
* - Empty state handling
* - Message rendering with proper formatting
*/
export function ContentArea({ paneId, className }: ContentAreaProps) {
// Get active tab using the selector
const activeTab = useViewerStore((state) => selectActiveTab(state, paneId));
// Get execution data from cliStreamStore
const executions = useCliStreamStore((state) => state.executions);
const execution = useMemo(() => {
if (!activeTab?.executionId) return null;
return executions[activeTab.executionId] || null;
}, [activeTab?.executionId, executions]);
// Determine what to render
const content = useMemo(() => {
// No active tab
if (!activeTab) {
return <EmptyTabState />;
}
// No execution data found
if (!execution) {
return <ExecutionNotFoundState executionId={activeTab.executionId} />;
}
// Show CLI output
return <CliOutputDisplay execution={execution} executionId={activeTab.executionId} />;
}, [activeTab, execution]);
return (
<div
className={cn(
'flex-1 overflow-hidden',
'bg-background',
className
)}
>
{content}
</div>
);
}
export default ContentArea;

View File

@@ -0,0 +1,89 @@
// ========================================
// EmptyState Component
// ========================================
// Empty state display for CLI viewer
import { useIntl } from 'react-intl';
import { Terminal, Play, Keyboard } from 'lucide-react';
import { cn } from '@/lib/utils';
// ========== Types ==========
export interface EmptyStateProps {
className?: string;
}
// ========== Component ==========
/**
* EmptyState - Displays when no CLI executions are active
*
* Features:
* - Informative empty state message
* - Quick start hints
* - Dark theme compatible
*/
export function EmptyState({ className }: EmptyStateProps) {
const { formatMessage } = useIntl();
return (
<div
className={cn(
'h-full flex flex-col items-center justify-center',
'bg-card dark:bg-surface-900',
'text-muted-foreground',
className
)}
>
<div className="flex flex-col items-center gap-6 max-w-md text-center p-8">
{/* Icon */}
<div className="relative">
<Terminal className="h-16 w-16 opacity-20" />
<div className="absolute -bottom-1 -right-1 bg-primary/10 rounded-full p-1.5">
<Play className="h-4 w-4 text-primary" />
</div>
</div>
{/* Title */}
<div>
<h3 className="text-lg font-semibold text-foreground mb-2">
{formatMessage({
id: 'cliViewer.emptyState.title',
defaultMessage: 'CLI Viewer',
})}
</h3>
<p className="text-sm">
{formatMessage({
id: 'cliViewer.emptyState.description',
defaultMessage: 'Start a CLI execution to see the output here.',
})}
</p>
</div>
{/* Hints */}
<div className="flex flex-col gap-3 text-xs">
<div className="flex items-center gap-2">
<Keyboard className="h-4 w-4 shrink-0" />
<span>
{formatMessage({
id: 'cliViewer.emptyState.hint1',
defaultMessage: 'Use "ccw cli" command to start an execution',
})}
</span>
</div>
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 shrink-0" />
<span>
{formatMessage({
id: 'cliViewer.emptyState.hint2',
defaultMessage: 'Active executions will appear as tabs',
})}
</span>
</div>
</div>
</div>
</div>
);
}
export default EmptyState;

View File

@@ -0,0 +1,307 @@
// ========================================
// ExecutionPicker Component
// ========================================
// Dialog for selecting CLI executions to open as tabs
import { useState, useMemo, useCallback } from 'react';
import { useIntl } from 'react-intl';
import { Plus, Search, Terminal, Clock, CheckCircle2, XCircle, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/Dialog';
import {
useCliStreamStore,
type CliExecutionState,
type CliExecutionStatus,
} from '@/stores/cliStreamStore';
import { useViewerStore, type PaneId } from '@/stores/viewerStore';
// ========== Types ==========
export interface ExecutionPickerProps {
paneId: PaneId;
className?: string;
}
// ========== Constants ==========
const STATUS_CONFIG: Record<CliExecutionStatus, { icon: typeof CheckCircle2; color: string; label: string }> = {
running: {
icon: Loader2,
color: 'text-indigo-500',
label: 'Running',
},
completed: {
icon: CheckCircle2,
color: 'text-emerald-500',
label: 'Completed',
},
error: {
icon: XCircle,
color: 'text-rose-500',
label: 'Error',
},
};
// ========== Helper Functions ==========
/**
* Format timestamp to relative or absolute time
*/
function formatTime(timestamp: number): string {
const now = Date.now();
const diff = now - timestamp;
if (diff < 60000) {
return 'Just now';
} else if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
return `${minutes}m ago`;
} else if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours}h ago`;
} else {
return new Date(timestamp).toLocaleDateString();
}
}
/**
* Get execution display title
*/
function getExecutionTitle(_executionId: string, execution: CliExecutionState): string {
return `${execution.tool}-${execution.mode}`;
}
// ========== Sub-Components ==========
interface ExecutionItemProps {
executionId: string;
execution: CliExecutionState;
onSelect: () => void;
}
/**
* Single execution item in the picker list
*/
function ExecutionItem({ executionId, execution, onSelect }: ExecutionItemProps) {
const statusConfig = STATUS_CONFIG[execution.status];
const StatusIcon = statusConfig.icon;
return (
<button
onClick={onSelect}
className={cn(
'w-full flex items-center gap-3 p-3 rounded-lg',
'border border-border/50 bg-muted/30',
'hover:bg-muted/50 hover:border-border',
'transition-all duration-150',
'text-left'
)}
>
{/* Tool icon */}
<div className="flex items-center justify-center w-8 h-8 rounded-md bg-primary/10">
<Terminal className="h-4 w-4 text-primary" />
</div>
{/* Execution info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm text-foreground truncate">
{getExecutionTitle(executionId, execution)}
</span>
<StatusIcon
className={cn(
'h-3.5 w-3.5 shrink-0',
statusConfig.color,
execution.status === 'running' && 'animate-spin'
)}
/>
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-muted-foreground truncate">
{executionId}
</span>
</div>
</div>
{/* Time */}
<div className="flex items-center gap-1 text-xs text-muted-foreground shrink-0">
<Clock className="h-3 w-3" />
<span>{formatTime(execution.startTime)}</span>
</div>
</button>
);
}
// ========== Main Component ==========
/**
* ExecutionPicker - Dialog for selecting CLI executions to open as tabs
*
* Features:
* - Lists all available CLI executions from store
* - Search/filter by tool name or execution ID
* - Shows execution status, tool, and timestamp
* - Click to add as new tab in the specified pane
*/
export function ExecutionPicker({ paneId, className }: ExecutionPickerProps) {
const { formatMessage } = useIntl();
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
// Store hooks
const executions = useCliStreamStore((state) => state.executions);
const addTab = useViewerStore((state) => state.addTab);
const panes = useViewerStore((state) => state.panes);
// Get current pane's existing execution IDs
const existingExecutionIds = useMemo(() => {
const pane = panes[paneId];
if (!pane) return new Set<string>();
return new Set(pane.tabs.map((tab) => tab.executionId));
}, [panes, paneId]);
// Filter and sort executions
const filteredExecutions = useMemo(() => {
const entries = Object.entries(executions);
// Filter by search query
const filtered = entries.filter(([id, exec]) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
id.toLowerCase().includes(query) ||
exec.tool.toLowerCase().includes(query) ||
exec.mode.toLowerCase().includes(query)
);
});
// Sort by start time (newest first)
filtered.sort((a, b) => b[1].startTime - a[1].startTime);
return filtered;
}, [executions, searchQuery]);
// Handle execution selection
const handleSelect = useCallback((executionId: string, execution: CliExecutionState) => {
const title = getExecutionTitle(executionId, execution);
addTab(paneId, executionId, title);
setOpen(false);
setSearchQuery('');
}, [paneId, addTab]);
// Count available vs total
const totalCount = Object.keys(executions).length;
const availableCount = filteredExecutions.filter(
([id]) => !existingExecutionIds.has(id)
).length;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn('h-6 w-6 shrink-0', className)}
aria-label={formatMessage({
id: 'cliViewer.tabs.addTab',
defaultMessage: 'Add tab'
})}
>
<Plus className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>
{formatMessage({
id: 'cliViewer.picker.selectExecution',
defaultMessage: 'Select Execution'
})}
</DialogTitle>
</DialogHeader>
{/* Search input */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={formatMessage({
id: 'cliViewer.picker.searchExecutions',
defaultMessage: 'Search executions...'
})}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
{/* Execution list */}
<div className="max-h-[300px] overflow-y-auto space-y-2">
{filteredExecutions.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Terminal className="h-8 w-8 text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground">
{totalCount === 0
? formatMessage({
id: 'cliViewer.picker.noExecutions',
defaultMessage: 'No executions available'
})
: formatMessage({
id: 'cliViewer.picker.noMatchingExecutions',
defaultMessage: 'No matching executions'
})
}
</p>
</div>
) : (
filteredExecutions.map(([id, exec]) => {
const isAlreadyOpen = existingExecutionIds.has(id);
return (
<div key={id} className="relative">
<ExecutionItem
executionId={id}
execution={exec}
onSelect={() => handleSelect(id, exec)}
/>
{isAlreadyOpen && (
<div className="absolute inset-0 bg-background/60 rounded-lg flex items-center justify-center">
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
{formatMessage({
id: 'cliViewer.picker.alreadyOpen',
defaultMessage: 'Already open'
})}
</span>
</div>
)}
</div>
);
})
)}
</div>
{/* Footer with count */}
{totalCount > 0 && (
<div className="text-xs text-muted-foreground text-center pt-2 border-t border-border/50">
{formatMessage(
{
id: 'cliViewer.picker.executionCount',
defaultMessage: '{available} of {total} executions available'
},
{ available: availableCount, total: totalCount }
)}
</div>
)}
</DialogContent>
</Dialog>
);
}
export default ExecutionPicker;

View File

@@ -0,0 +1,157 @@
// ========================================
// LayoutContainer Component
// ========================================
// Manages allotment-based split panes for CLI viewer
import { useCallback, useMemo } from 'react';
import { Allotment } from 'allotment';
import 'allotment/dist/style.css';
import { cn } from '@/lib/utils';
import {
useViewerStore,
useViewerLayout,
useViewerPanes,
type AllotmentLayoutGroup,
type PaneId,
} from '@/stores/viewerStore';
import { PaneContent } from './PaneContent';
import { EmptyState } from './EmptyState';
// ========== Types ==========
interface LayoutGroupRendererProps {
group: AllotmentLayoutGroup;
minSize: number;
onSizeChange: (sizes: number[]) => void;
}
interface LayoutContainerProps {
className?: string;
}
// ========== Helper Functions ==========
/**
* Check if a layout child is a pane ID (string) or a nested group
*/
function isPaneId(child: PaneId | AllotmentLayoutGroup): child is PaneId {
return typeof child === 'string';
}
// ========== Helper Components ==========
/**
* Renders a layout group with Allotment
*/
function LayoutGroupRenderer({ group, minSize, onSizeChange }: LayoutGroupRendererProps) {
const panes = useViewerPanes();
const handleChange = useCallback(
(sizes: number[]) => {
onSizeChange(sizes);
},
[onSizeChange]
);
// Check if all panes in this group exist
const validChildren = useMemo(() => {
return group.children.filter(child => {
if (isPaneId(child)) {
return panes[child] !== undefined;
}
return true; // Groups are always valid (they will recursively filter)
});
}, [group.children, panes]);
if (validChildren.length === 0) {
return <EmptyState />;
}
return (
<Allotment
vertical={group.direction === 'vertical'}
defaultSizes={group.sizes}
onChange={handleChange}
className="h-full"
>
{validChildren.map((child, index) => (
<Allotment.Pane key={isPaneId(child) ? child : `group-${index}`} minSize={minSize}>
{isPaneId(child) ? (
<PaneContent paneId={child} />
) : (
<LayoutGroupRenderer
group={child}
minSize={minSize}
onSizeChange={onSizeChange}
/>
)}
</Allotment.Pane>
))}
</Allotment>
);
}
// ========== Main Component ==========
/**
* LayoutContainer - Main container for CLI viewer with split panes
*
* Features:
* - Recursive rendering of nested allotment layouts
* - Support for horizontal and vertical splits
* - Minimum pane size enforcement
* - Empty state handling
*/
export function LayoutContainer({ className }: LayoutContainerProps) {
const layout = useViewerLayout();
const panes = useViewerPanes();
const setLayout = useViewerStore((state) => state.setLayout);
const handleSizeChange = useCallback(
(sizes: number[]) => {
// Update the root layout with new sizes
setLayout({ ...layout, sizes });
},
[layout, setLayout]
);
// Render based on layout type
const content = useMemo(() => {
// No children - show empty state
if (!layout.children || layout.children.length === 0) {
return <EmptyState />;
}
// Single pane layout
if (layout.children.length === 1 && isPaneId(layout.children[0])) {
const paneId = layout.children[0];
if (!panes[paneId]) {
return <EmptyState />;
}
return <PaneContent paneId={paneId} />;
}
// Group layout
return (
<LayoutGroupRenderer
group={layout}
minSize={200}
onSizeChange={handleSizeChange}
/>
);
}, [layout, panes, handleSizeChange]);
return (
<div
className={cn(
'h-full w-full overflow-hidden',
'bg-background',
className
)}
>
{content}
</div>
);
}
export default LayoutContainer;

View File

@@ -0,0 +1,81 @@
// ========================================
// PaneContent Component
// ========================================
// Container for TabBar and ContentArea within a pane
import { useCallback } from 'react';
import { cn } from '@/lib/utils';
import {
useViewerStore,
useViewerPanes,
useFocusedPaneId,
type PaneId,
} from '@/stores/viewerStore';
import { TabBar } from './TabBar';
import { ContentArea } from './ContentArea';
// ========== Types ==========
export interface PaneContentProps {
paneId: PaneId;
className?: string;
}
// ========== Component ==========
/**
* PaneContent - Combines TabBar and ContentArea for a single pane
*
* Features:
* - Focused pane highlighting
* - Click to focus
* - TabBar for tab management
* - ContentArea for CLI output display
*/
export function PaneContent({ paneId, className }: PaneContentProps) {
const panes = useViewerPanes();
const pane = panes[paneId];
const focusedPaneId = useFocusedPaneId();
const setFocusedPane = useViewerStore((state) => state.setFocusedPane);
const isFocused = focusedPaneId === paneId;
const handleClick = useCallback(() => {
if (!isFocused) {
setFocusedPane(paneId);
}
}, [isFocused, paneId, setFocusedPane]);
if (!pane) {
return (
<div className={cn('h-full flex items-center justify-center', className)}>
<span className="text-muted-foreground text-sm">Pane not found</span>
</div>
);
}
return (
<div
className={cn(
'h-full flex flex-col',
'bg-card dark:bg-surface-900',
'border border-border/50',
'rounded-sm overflow-hidden',
// Focus ring when pane is focused
isFocused && 'ring-1 ring-primary/50',
className
)}
onClick={handleClick}
role="region"
aria-label={`CLI Viewer Pane ${paneId}`}
>
{/* Tab Bar */}
<TabBar paneId={paneId} />
{/* Content Area */}
<ContentArea paneId={paneId} />
</div>
);
}
export default PaneContent;

View File

@@ -0,0 +1,257 @@
// ========================================
// TabBar Component
// ========================================
// Tab management for CLI viewer panes
import { useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { X, Pin, PinOff, MoreHorizontal, SplitSquareHorizontal, SplitSquareVertical } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/DropdownMenu';
import {
useViewerStore,
useViewerPanes,
type PaneId,
type TabState,
} from '@/stores/viewerStore';
import { ExecutionPicker } from './ExecutionPicker';
// ========== Types ==========
export interface TabBarProps {
paneId: PaneId;
className?: string;
}
interface TabItemProps {
tab: TabState;
isActive: boolean;
onSelect: () => void;
onClose: (e: React.MouseEvent) => void;
onTogglePin: (e: React.MouseEvent) => void;
}
// ========== Constants ==========
const STATUS_COLORS = {
running: 'bg-indigo-500 shadow-[0_0_6px_rgba(99,102,241,0.4)] animate-pulse',
completed: 'bg-emerald-500',
error: 'bg-rose-500',
idle: 'bg-slate-400 dark:bg-slate-500',
};
// ========== Helper Components ==========
/**
* Individual tab item
*/
function TabItem({ tab, isActive, onSelect, onClose, onTogglePin }: TabItemProps) {
// Simplify title for display
const displayTitle = useMemo(() => {
// If title contains tool name pattern, extract it
const parts = tab.title.split('-');
return parts[0] || tab.title;
}, [tab.title]);
return (
<button
onClick={onSelect}
className={cn(
'group relative flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs',
'border border-border/50 shrink-0 min-w-0 max-w-[160px]',
'transition-all duration-150',
isActive
? 'bg-slate-100 dark:bg-slate-800 border-slate-300 dark:border-slate-600 shadow-sm'
: 'bg-muted/30 hover:bg-muted/50 border-border/30',
tab.isPinned && 'border-amber-500/50'
)}
title={tab.title}
>
{/* Status indicator dot */}
<span className={cn('w-1.5 h-1.5 rounded-full shrink-0', STATUS_COLORS.idle)} />
{/* Tool name */}
<span className="font-medium text-[11px] truncate">{displayTitle}</span>
{/* Pin indicator (always visible if pinned) */}
{tab.isPinned && (
<Pin className="h-2.5 w-2.5 text-amber-500 shrink-0" />
)}
{/* Action buttons (visible on hover) */}
<div className="flex items-center gap-0.5 ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
{/* Pin/Unpin button */}
<button
onClick={onTogglePin}
className="p-0.5 rounded hover:bg-primary/10 transition-colors"
aria-label={tab.isPinned ? 'Unpin tab' : 'Pin tab'}
>
{tab.isPinned ? (
<PinOff className="h-2.5 w-2.5 text-amber-500" />
) : (
<Pin className="h-2.5 w-2.5 text-muted-foreground hover:text-amber-500" />
)}
</button>
{/* Close button (hidden if pinned) */}
{!tab.isPinned && (
<button
onClick={onClose}
className="p-0.5 rounded hover:bg-rose-500/20 transition-colors"
aria-label="Close tab"
>
<X className="h-2.5 w-2.5 text-rose-600 dark:text-rose-400" />
</button>
)}
</div>
</button>
);
}
// ========== Main Component ==========
/**
* TabBar - Manages tabs within a pane
*
* Features:
* - Tab display with status indicators
* - Active tab highlighting
* - Close button on hover
* - Pin/unpin functionality
* - Pane actions dropdown
*/
export function TabBar({ paneId, className }: TabBarProps) {
const { formatMessage } = useIntl();
const panes = useViewerPanes();
const pane = panes[paneId];
const setActiveTab = useViewerStore((state) => state.setActiveTab);
const removeTab = useViewerStore((state) => state.removeTab);
const togglePinTab = useViewerStore((state) => state.togglePinTab);
const addPane = useViewerStore((state) => state.addPane);
const removePane = useViewerStore((state) => state.removePane);
const handleTabSelect = useCallback(
(tabId: string) => {
setActiveTab(paneId, tabId);
},
[paneId, setActiveTab]
);
const handleTabClose = useCallback(
(e: React.MouseEvent, tabId: string) => {
e.stopPropagation();
removeTab(paneId, tabId);
},
[paneId, removeTab]
);
const handleTogglePin = useCallback(
(e: React.MouseEvent, tabId: string) => {
e.stopPropagation();
togglePinTab(tabId);
},
[togglePinTab]
);
const handleSplitHorizontal = useCallback(() => {
addPane(paneId, 'horizontal');
}, [paneId, addPane]);
const handleSplitVertical = useCallback(() => {
addPane(paneId, 'vertical');
}, [paneId, addPane]);
const handleClosePane = useCallback(() => {
removePane(paneId);
}, [paneId, removePane]);
// Sort tabs: pinned first, then by order
const sortedTabs = useMemo(() => {
if (!pane) return [];
return [...pane.tabs].sort((a, b) => {
if (a.isPinned !== b.isPinned) {
return a.isPinned ? -1 : 1;
}
return a.order - b.order;
});
}, [pane]);
if (!pane) {
return null;
}
return (
<div
className={cn(
'flex items-center gap-1 px-2 py-1.5',
'bg-muted/30 border-b border-border/50',
'overflow-x-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent',
className
)}
>
{/* Tabs */}
<div className="flex items-center gap-1 flex-1 min-w-0 overflow-x-auto">
{sortedTabs.length === 0 ? (
<span className="text-xs text-muted-foreground px-2">
{formatMessage({ id: 'cliViewer.tabs.noTabs', defaultMessage: 'No tabs open' })}
</span>
) : (
sortedTabs.map((tab) => (
<TabItem
key={tab.id}
tab={tab}
isActive={pane.activeTabId === tab.id}
onSelect={() => handleTabSelect(tab.id)}
onClose={(e) => handleTabClose(e, tab.id)}
onTogglePin={(e) => handleTogglePin(e, tab.id)}
/>
))
)}
</div>
{/* Add tab button */}
<ExecutionPicker paneId={paneId} />
{/* Pane actions dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0"
aria-label="Pane actions"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={handleSplitHorizontal}>
<SplitSquareHorizontal className="h-4 w-4 mr-2" />
{formatMessage({ id: 'cliViewer.paneActions.splitHorizontal', defaultMessage: 'Split Horizontal' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleSplitVertical}>
<SplitSquareVertical className="h-4 w-4 mr-2" />
{formatMessage({ id: 'cliViewer.paneActions.splitVertical', defaultMessage: 'Split Vertical' })}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleClosePane}
className="text-rose-600 dark:text-rose-400"
>
<X className="h-4 w-4 mr-2" />
{formatMessage({ id: 'cliViewer.paneActions.closePane', defaultMessage: 'Close Pane' })}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
export default TabBar;

View File

@@ -0,0 +1,28 @@
// ========================================
// CLI Viewer Component Exports
// ========================================
// Barrel export for CLI viewer components
// Main layout container
export { LayoutContainer } from './LayoutContainer';
export type { default as LayoutContainerType } from './LayoutContainer';
// Pane content
export { PaneContent } from './PaneContent';
export type { PaneContentProps } from './PaneContent';
// Tab bar
export { TabBar } from './TabBar';
export type { TabBarProps } from './TabBar';
// Execution picker
export { ExecutionPicker } from './ExecutionPicker';
export type { ExecutionPickerProps } from './ExecutionPicker';
// Content area
export { ContentArea } from './ContentArea';
export type { ContentAreaProps } from './ContentArea';
// Empty state
export { EmptyState } from './EmptyState';
export type { EmptyStateProps } from './EmptyState';

View File

@@ -0,0 +1,234 @@
// ========================================
// CoordinatorEmptyState Component
// ========================================
// Modern empty state with tech-inspired design for coordinator start page
import { useIntl } from 'react-intl';
import { Play, Rocket, Zap, GitBranch } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils';
export interface CoordinatorEmptyStateProps {
onStart: () => void;
disabled?: boolean;
className?: string;
}
/**
* Empty state component with modern tech-inspired design
* Displays when no coordinator execution is active
*/
export function CoordinatorEmptyState({
onStart,
disabled = false,
className,
}: CoordinatorEmptyStateProps) {
const { formatMessage } = useIntl();
return (
<div
className={cn(
'relative flex items-center justify-center min-h-[600px] overflow-hidden',
className
)}
>
{/* Animated Background - Using theme colors */}
<div className="absolute inset-0 bg-gradient-to-br from-background via-card to-background">
{/* Grid Pattern */}
<div
className="absolute inset-0 opacity-10"
style={{
backgroundImage: `
linear-gradient(var(--primary) 1px, transparent 1px),
linear-gradient(90deg, var(--primary) 1px, transparent 1px)
`,
backgroundSize: '50px 50px',
}}
/>
{/* Animated Gradient Orbs - Using primary color */}
<div className="absolute top-20 left-20 w-72 h-72 rounded-full blur-3xl animate-pulse"
style={{
background: 'radial-gradient(circle, hsl(var(--primary)) 0%, transparent 70%)',
opacity: 0.15,
}}
/>
<div
className="absolute bottom-20 right-20 w-96 h-96 rounded-full blur-3xl animate-pulse"
style={{
background: 'radial-gradient(circle, hsl(var(--secondary)) 0%, transparent 70%)',
animationDelay: '1s',
opacity: 0.15,
}}
/>
<div
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full blur-3xl animate-pulse"
style={{
background: 'radial-gradient(circle, hsl(var(--accent)) 0%, transparent 70%)',
animationDelay: '2s',
opacity: 0.1,
}}
/>
</div>
{/* Main Content */}
<div className="relative z-10 max-w-2xl mx-auto px-8 text-center">
{/* Hero Icon */}
<div className="relative mb-8 inline-block">
<div
className="absolute inset-0 rounded-full blur-2xl opacity-40 animate-pulse"
style={{ background: 'hsl(var(--primary))' }}
/>
<div
className="relative p-6 rounded-full shadow-2xl text-white"
style={{ background: 'hsl(var(--primary))' }}
>
<Rocket className="w-16 h-16" strokeWidth={2} />
</div>
</div>
{/* Title */}
<h1 className="text-4xl font-bold mb-4 text-foreground">
{formatMessage({ id: 'coordinator.emptyState.title' })}
</h1>
{/* Subtitle */}
<p className="text-lg text-muted-foreground mb-12 max-w-lg mx-auto">
{formatMessage({ id: 'coordinator.emptyState.subtitle' })}
</p>
{/* Start Button - Using primary theme color */}
<Button
size="lg"
onClick={onStart}
disabled={disabled}
className="group relative px-8 py-6 text-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-300"
style={{
background: 'hsl(var(--primary))',
color: 'hsl(var(--primary-foreground))',
}}
>
<Play className="w-6 h-6 mr-2 group-hover:scale-110 transition-transform" />
{formatMessage({ id: 'coordinator.emptyState.startButton' })}
<div
className="absolute inset-0 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity blur-xl"
style={{ background: 'hsl(var(--primary) / 0.3)' }}
/>
</Button>
{/* Feature Cards */}
<div className="mt-16 grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Feature 1 */}
<div className="group relative bg-card/80 backdrop-blur-sm rounded-xl p-6 border border-border hover:border-primary/50 transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
<div
className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity"
style={{ background: 'hsl(var(--primary) / 0.05)' }}
/>
<div className="relative">
<div
className="w-12 h-12 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform"
style={{ background: 'hsl(var(--primary) / 0.1)', color: 'hsl(var(--primary))' }}
>
<Zap className="w-6 h-6" />
</div>
<h3 className="font-semibold mb-2 text-foreground">
{formatMessage({ id: 'coordinator.emptyState.feature1.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'coordinator.emptyState.feature1.description' })}
</p>
</div>
</div>
{/* Feature 2 */}
<div className="group relative bg-card/80 backdrop-blur-sm rounded-xl p-6 border border-border hover:border-primary/50 transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
<div
className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity"
style={{ background: 'hsl(var(--secondary) / 0.05)' }}
/>
<div className="relative">
<div
className="w-12 h-12 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform"
style={{ background: 'hsl(var(--secondary) / 0.1)', color: 'hsl(var(--secondary))' }}
>
<GitBranch className="w-6 h-6" />
</div>
<h3 className="font-semibold mb-2 text-foreground">
{formatMessage({ id: 'coordinator.emptyState.feature2.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'coordinator.emptyState.feature2.description' })}
</p>
</div>
</div>
{/* Feature 3 */}
<div className="group relative bg-card/80 backdrop-blur-sm rounded-xl p-6 border border-border hover:border-primary/50 transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
<div
className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity"
style={{ background: 'hsl(var(--accent) / 0.05)' }}
/>
<div className="relative">
<div
className="w-12 h-12 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform"
style={{ background: 'hsl(var(--accent) / 0.1)', color: 'hsl(var(--accent))' }}
>
<Play className="w-6 h-6" />
</div>
<h3 className="font-semibold mb-2 text-foreground">
{formatMessage({ id: 'coordinator.emptyState.feature3.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'coordinator.emptyState.feature3.description' })}
</p>
</div>
</div>
</div>
{/* Quick Start Guide */}
<div className="mt-12 text-left bg-card/50 backdrop-blur-sm rounded-xl p-6 border border-border">
<h3 className="font-semibold mb-4 text-foreground flex items-center gap-2">
<span
className="w-6 h-6 rounded-full flex items-center justify-center text-white text-xs font-semibold"
style={{ background: 'hsl(var(--primary))' }}
>
</span>
{formatMessage({ id: 'coordinator.emptyState.quickStart.title' })}
</h3>
<div className="space-y-3 text-sm text-muted-foreground">
<div className="flex items-start gap-3">
<span
className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white"
style={{ background: 'hsl(var(--primary))' }}
>
1
</span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step1' })}</p>
</div>
<div className="flex items-start gap-3">
<span
className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white"
style={{ background: 'hsl(var(--secondary))' }}
>
2
</span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step2' })}</p>
</div>
<div className="flex items-start gap-3">
<span
className="w-5 h-5 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5 text-white"
style={{ background: 'hsl(var(--accent))' }}
>
3
</span>
<p>{formatMessage({ id: 'coordinator.emptyState.quickStart.step3' })}</p>
</div>
</div>
</div>
</div>
</div>
);
}
export default CoordinatorEmptyState;

View File

@@ -1,25 +1,22 @@
// ========================================
// Coordinator Input Modal Component
// Coordinator Input Modal Component (Multi-Step)
// ========================================
// Modal dialog for starting coordinator execution with task description and parameters
// Two-step modal: Welcome page -> Template & Parameters
import { useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { Loader2 } from 'lucide-react';
import { Loader2, Rocket, Zap, GitBranch, Eye, ChevronRight, ChevronLeft } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/Dialog';
import { Button } from '@/components/ui/Button';
import { Textarea } from '@/components/ui/Textarea';
import { Input } from '@/components/ui/Input';
import { Label } from '@/components/ui/Label';
import { useCoordinatorStore } from '@/stores/coordinatorStore';
import { useNotifications } from '@/hooks/useNotifications';
import { cn } from '@/lib/utils';
// ========== Types ==========
@@ -33,12 +30,22 @@ interface FormErrors {
parameters?: string;
}
// ========== Constants ==========
const TEMPLATES = [
{ id: 'feature-dev', nameKey: 'coordinator.multiStep.step2.templates.featureDev', description: 'Complete feature development workflow' },
{ id: 'api-integration', nameKey: 'coordinator.multiStep.step2.templates.apiIntegration', description: 'Third-party API integration' },
{ id: 'performance', nameKey: 'coordinator.multiStep.step2.templates.performanceOptimization', description: 'System performance analysis' },
{ id: 'documentation', nameKey: 'coordinator.multiStep.step2.templates.documentGeneration', description: 'Auto-generate documentation' },
] as const;
const TOTAL_STEPS = 2;
// ========== Validation Helper ==========
function validateForm(taskDescription: string, parameters: string): FormErrors {
const errors: FormErrors = {};
// Validate task description
if (!taskDescription.trim()) {
errors.taskDescription = 'coordinator.validation.taskDescriptionRequired';
} else {
@@ -50,11 +57,10 @@ function validateForm(taskDescription: string, parameters: string): FormErrors {
}
}
// Validate parameters if provided
if (parameters.trim()) {
try {
JSON.parse(parameters.trim());
} catch (error) {
} catch {
errors.parameters = 'coordinator.validation.parametersInvalidJson';
}
}
@@ -62,6 +68,32 @@ function validateForm(taskDescription: string, parameters: string): FormErrors {
return errors;
}
// ========== Feature Card Data ==========
const FEATURES = [
{
icon: Zap,
titleKey: 'coordinator.multiStep.step1.feature1.title',
descriptionKey: 'coordinator.multiStep.step1.feature1.description',
bgClass: 'bg-primary/10',
iconClass: 'text-primary',
},
{
icon: GitBranch,
titleKey: 'coordinator.multiStep.step1.feature2.title',
descriptionKey: 'coordinator.multiStep.step1.feature2.description',
bgClass: 'bg-secondary/10',
iconClass: 'text-secondary-foreground',
},
{
icon: Eye,
titleKey: 'coordinator.multiStep.step1.feature3.title',
descriptionKey: 'coordinator.multiStep.step1.feature3.description',
bgClass: 'bg-accent/10',
iconClass: 'text-accent-foreground',
},
] as const;
// ========== Component ==========
export function CoordinatorInputModal({ open, onClose }: CoordinatorInputModalProps) {
@@ -69,18 +101,25 @@ export function CoordinatorInputModal({ open, onClose }: CoordinatorInputModalPr
const { success, error: showError } = useNotifications();
const { startCoordinator } = useCoordinatorStore();
// Step state
const [step, setStep] = useState(1);
// Form state
const [taskDescription, setTaskDescription] = useState('');
const [parameters, setParameters] = useState('');
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
// Reset form when modal opens/closes
// Reset all state when modal opens/closes
useEffect(() => {
if (open) {
setStep(1);
setTaskDescription('');
setParameters('');
setSelectedTemplate(null);
setErrors({});
setIsSubmitting(false);
}
}, [open]);
@@ -95,15 +134,25 @@ export function CoordinatorInputModal({ open, onClose }: CoordinatorInputModalPr
setParameters(value);
}
// Clear error for this field when user starts typing
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: undefined }));
}
};
// Handle submit
// Handle template selection
const handleTemplateSelect = (templateId: string) => {
setSelectedTemplate(templateId);
const template = TEMPLATES.find((t) => t.id === templateId);
if (template) {
setTaskDescription(template.description);
if (errors.taskDescription) {
setErrors((prev) => ({ ...prev, taskDescription: undefined }));
}
}
};
// Handle submit - preserved exactly from original
const handleSubmit = async () => {
// Validate form
const validationErrors = validateForm(taskDescription, parameters);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
@@ -112,13 +161,10 @@ export function CoordinatorInputModal({ open, onClose }: CoordinatorInputModalPr
setIsSubmitting(true);
try {
// Parse parameters if provided
const parsedParams = parameters.trim() ? JSON.parse(parameters.trim()) : undefined;
// Generate execution ID
const executionId = `exec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Call API to start coordinator
const response = await fetch('/api/coordinator/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -134,7 +180,6 @@ export function CoordinatorInputModal({ open, onClose }: CoordinatorInputModalPr
throw new Error(error.message || 'Failed to start coordinator');
}
// Call store to update state
await startCoordinator(executionId, taskDescription.trim(), parsedParams);
success(formatMessage({ id: 'coordinator.success.started' }));
@@ -148,99 +193,246 @@ export function CoordinatorInputModal({ open, onClose }: CoordinatorInputModalPr
}
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>
{formatMessage({ id: 'coordinator.modal.title' })}
</DialogTitle>
<DialogDescription>
{formatMessage({ id: 'coordinator.modal.description' })}
</DialogDescription>
</DialogHeader>
// Navigation
const handleNext = () => setStep(2);
const handleBack = () => setStep(1);
<div className="space-y-4 py-4">
{/* Task Description */}
<div className="space-y-2">
<Label htmlFor="task-description" className="text-base font-medium">
{formatMessage({ id: 'coordinator.form.taskDescription' })}
<span className="text-destructive">*</span>
</Label>
<Textarea
id="task-description"
value={taskDescription}
onChange={(e) => handleFieldChange('taskDescription', e.target.value)}
placeholder={formatMessage({ id: 'coordinator.form.taskDescriptionPlaceholder' })}
rows={6}
className={errors.taskDescription ? 'border-destructive' : ''}
disabled={isSubmitting}
/>
<div className="flex justify-between items-center text-xs text-muted-foreground">
<span>
{formatMessage({ id: 'coordinator.form.characterCount' }, {
current: taskDescription.length,
min: 10,
max: 2000,
})}
</span>
{taskDescription.length >= 10 && taskDescription.length <= 2000 && (
<span className="text-green-600">Valid</span>
// ========== Step 1: Welcome ==========
const renderStep1 = () => (
<div className="flex flex-col items-center px-6 py-8">
{/* Hero Icon */}
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary text-primary-foreground mb-6">
<Rocket className="h-8 w-8" />
</div>
{/* Title & Subtitle */}
<h2 className="text-2xl font-bold text-foreground mb-2">
{formatMessage({ id: 'coordinator.multiStep.step1.title' })}
</h2>
<p className="text-sm text-muted-foreground mb-8 text-center max-w-md">
{formatMessage({ id: 'coordinator.multiStep.step1.subtitle' })}
</p>
{/* Feature Cards */}
<div className="grid grid-cols-3 gap-4 w-full">
{FEATURES.map((feature) => {
const Icon = feature.icon;
return (
<div
key={feature.titleKey}
className={cn(
'flex flex-col items-center rounded-xl p-5 text-center',
feature.bgClass
)}
>
<Icon className={cn('h-6 w-6 mb-3', feature.iconClass)} />
<span className="text-sm font-medium text-foreground mb-1">
{formatMessage({ id: feature.titleKey })}
</span>
<span className="text-xs text-muted-foreground">
{formatMessage({ id: feature.descriptionKey })}
</span>
</div>
{errors.taskDescription && (
<p className="text-sm text-destructive">
{formatMessage({ id: errors.taskDescription })}
</p>
)}
</div>
);
})}
</div>
</div>
);
{/* Parameters (Optional) */}
<div className="space-y-2">
<Label htmlFor="parameters" className="text-base font-medium">
{formatMessage({ id: 'coordinator.form.parameters' })}
</Label>
<Input
id="parameters"
value={parameters}
onChange={(e) => handleFieldChange('parameters', e.target.value)}
placeholder={formatMessage({ id: 'coordinator.form.parametersPlaceholder' })}
className={`font-mono text-sm ${errors.parameters ? 'border-destructive' : ''}`}
disabled={isSubmitting}
/>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'coordinator.form.parametersHelp' })}
</p>
{errors.parameters && (
<p className="text-sm text-destructive">
{formatMessage({ id: errors.parameters })}
</p>
// ========== Step 2: Template + Parameters ==========
const renderStep2 = () => (
<div className="flex min-h-[380px]">
{/* Left Column: Template Selection */}
<div className="w-2/5 border-r border-border p-5">
<h3 className="text-sm font-semibold text-foreground mb-3">
{formatMessage({ id: 'coordinator.multiStep.step2.templateLabel' })}
</h3>
<div className="space-y-2">
{TEMPLATES.map((template) => {
const isSelected = selectedTemplate === template.id;
return (
<button
key={template.id}
type="button"
onClick={() => handleTemplateSelect(template.id)}
className={cn(
'flex w-full items-center gap-3 rounded-lg border px-3 py-3 text-left transition-colors',
isSelected
? 'border-primary bg-primary/5'
: 'border-border bg-card hover:bg-muted/50'
)}
>
{/* Radio dot */}
<span
className={cn(
'flex h-4 w-4 shrink-0 items-center justify-center rounded-full border',
isSelected
? 'border-primary'
: 'border-muted-foreground/40'
)}
>
{isSelected && (
<span className="h-2 w-2 rounded-full bg-primary" />
)}
</span>
<span className={cn(
'text-sm',
isSelected ? 'font-medium text-foreground' : 'text-muted-foreground'
)}>
{formatMessage({ id: template.nameKey })}
</span>
</button>
);
})}
</div>
</div>
{/* Right Column: Parameter Form */}
<div className="w-3/5 p-5 space-y-4">
{/* Task Description */}
<div className="space-y-2">
<Label htmlFor="task-description" className="text-sm font-medium">
{formatMessage({ id: 'coordinator.form.taskDescription' })}
<span className="text-destructive ml-0.5">*</span>
</Label>
<Textarea
id="task-description"
value={taskDescription}
onChange={(e) => handleFieldChange('taskDescription', e.target.value)}
placeholder={formatMessage({ id: 'coordinator.form.taskDescriptionPlaceholder' })}
rows={5}
className={cn(
'resize-none',
errors.taskDescription && 'border-destructive'
)}
disabled={isSubmitting}
/>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>
{formatMessage(
{ id: 'coordinator.form.characterCount' },
{ current: taskDescription.length, min: 10, max: 2000 }
)}
</span>
{taskDescription.length >= 10 && taskDescription.length <= 2000 && (
<span className="text-primary">Valid</span>
)}
</div>
{errors.taskDescription && (
<p className="text-xs text-destructive">
{formatMessage({ id: errors.taskDescription })}
</p>
)}
</div>
<DialogFooter>
{/* Custom Parameters */}
<div className="space-y-2">
<Label htmlFor="parameters" className="text-sm font-medium">
{formatMessage({ id: 'coordinator.form.parameters' })}
</Label>
<Textarea
id="parameters"
value={parameters}
onChange={(e) => handleFieldChange('parameters', e.target.value)}
placeholder={formatMessage({ id: 'coordinator.form.parametersPlaceholder' })}
rows={3}
className={cn(
'resize-none font-mono text-sm',
errors.parameters && 'border-destructive'
)}
disabled={isSubmitting}
/>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'coordinator.form.parametersHelp' })}
</p>
{errors.parameters && (
<p className="text-xs text-destructive">
{formatMessage({ id: errors.parameters })}
</p>
)}
</div>
</div>
</div>
);
// ========== Footer ==========
const renderFooter = () => (
<div className="flex items-center justify-between border-t border-border px-6 py-4">
{/* Left: Step indicator + Back */}
<div className="flex items-center gap-3">
<span className="text-xs text-muted-foreground">
{formatMessage(
{ id: 'coordinator.multiStep.progress.step' },
{ current: step, total: TOTAL_STEPS }
)}
</span>
{step === 2 && (
<Button
variant="outline"
onClick={onClose}
variant="ghost"
size="sm"
onClick={handleBack}
disabled={isSubmitting}
>
{formatMessage({ id: 'common.actions.cancel' })}
<ChevronLeft className="mr-1 h-4 w-4" />
{formatMessage({ id: 'coordinator.multiStep.actions.back' })}
</Button>
)}
</div>
{/* Right: Cancel + Next/Submit */}
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={onClose}
disabled={isSubmitting}
>
{formatMessage({ id: 'common.actions.cancel' })}
</Button>
{step === 1 ? (
<Button size="sm" onClick={handleNext}>
{formatMessage({ id: 'coordinator.multiStep.actions.next' })}
<ChevronRight className="ml-1 h-4 w-4" />
</Button>
) : (
<Button
size="sm"
onClick={handleSubmit}
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{formatMessage({ id: 'coordinator.form.starting' })}
</>
) : (
formatMessage({ id: 'coordinator.form.start' })
formatMessage({ id: 'coordinator.multiStep.actions.submit' })
)}
</Button>
</DialogFooter>
)}
</div>
</div>
);
// ========== Render ==========
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl gap-0 p-0 overflow-hidden">
{/* Visually hidden title for accessibility */}
<DialogTitle className="sr-only">
{formatMessage({ id: 'coordinator.modal.title' })}
</DialogTitle>
{/* Step Content */}
{step === 1 ? renderStep1() : renderStep2()}
{/* Footer */}
{renderFooter()}
</DialogContent>
</Dialog>
);

View File

@@ -0,0 +1,137 @@
// ========================================
// CoordinatorTaskCard Component
// ========================================
// Task card component for displaying task overview in horizontal list
import { useIntl } from 'react-intl';
import { Clock, CheckCircle, XCircle, Loader2, CircleDashed } from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
export interface TaskStatus {
id: string;
name: string;
description?: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
progress: { completed: number; total: number };
startedAt?: string;
completedAt?: string;
}
export interface CoordinatorTaskCardProps {
task: TaskStatus;
isSelected: boolean;
onClick: () => void;
className?: string;
}
/**
* Task card component displaying task status and progress
* Used in horizontal scrolling task list
*/
export function CoordinatorTaskCard({
task,
isSelected,
onClick,
className,
}: CoordinatorTaskCardProps) {
const { formatMessage } = useIntl();
// Map status to badge variant
const getStatusVariant = (status: TaskStatus['status']) => {
switch (status) {
case 'pending':
return 'secondary';
case 'running':
return 'warning';
case 'completed':
return 'success';
case 'failed':
return 'destructive';
case 'cancelled':
return 'outline';
default:
return 'default';
}
};
// Get status icon
const getStatusIcon = (status: TaskStatus['status']) => {
switch (status) {
case 'pending':
return <CircleDashed className="w-3 h-3" />;
case 'running':
return <Loader2 className="w-3 h-3 animate-spin" />;
case 'completed':
return <CheckCircle className="w-3 h-3" />;
case 'failed':
return <XCircle className="w-3 h-3" />;
case 'cancelled':
return <XCircle className="w-3 h-3" />;
default:
return null;
}
};
// Format time display
const formatTime = (dateString?: string) => {
if (!dateString) return null;
try {
const date = new Date(dateString);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} catch {
return null;
}
};
const displayTime = task.startedAt ? formatTime(task.startedAt) : null;
return (
<Card
className={cn(
'min-w-[180px] max-w-[220px] p-4 cursor-pointer transition-all duration-200',
'hover:border-primary/50 hover:shadow-md',
isSelected && 'border-primary ring-1 ring-primary/20',
className
)}
onClick={onClick}
>
{/* Task Name */}
<h3 className="font-medium text-sm text-foreground truncate mb-2" title={task.name}>
{task.name}
</h3>
{/* Status Badge */}
<div className="mb-3">
<Badge variant={getStatusVariant(task.status)} className="gap-1">
{getStatusIcon(task.status)}
{formatMessage({ id: `coordinator.status.${task.status}` })}
</Badge>
</div>
{/* Progress */}
<div className="text-xs text-muted-foreground mb-2">
<span className="font-medium">{task.progress.completed}</span>
<span>/</span>
<span>{task.progress.total}</span>
<span className="ml-1">
{formatMessage({ id: 'coordinator.taskCard.nodes' })}
</span>
</div>
{/* Time */}
{displayTime && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="w-3 h-3" />
<span>{displayTime}</span>
<span className="ml-1">
{formatMessage({ id: 'coordinator.taskCard.started' })}
</span>
</div>
)}
</Card>
);
}
export default CoordinatorTaskCard;

View File

@@ -0,0 +1,140 @@
// ========================================
// CoordinatorTaskList Component
// ========================================
// Horizontal scrolling task list with filter and sort controls
import { useState, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Filter, ArrowUpDown, Inbox } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/Select';
import { CoordinatorTaskCard, TaskStatus } from './CoordinatorTaskCard';
import { cn } from '@/lib/utils';
export type FilterOption = 'all' | 'running' | 'completed' | 'failed';
export type SortOption = 'time' | 'name';
export interface CoordinatorTaskListProps {
tasks: TaskStatus[];
selectedTaskId: string | null;
onTaskSelect: (taskId: string) => void;
className?: string;
}
/**
* Horizontal scrolling task list with filtering and sorting
* Displays task cards in a row with overflow scroll
*/
export function CoordinatorTaskList({
tasks,
selectedTaskId,
onTaskSelect,
className,
}: CoordinatorTaskListProps) {
const { formatMessage } = useIntl();
const [filter, setFilter] = useState<FilterOption>('all');
const [sort, setSort] = useState<SortOption>('time');
// Filter tasks
const filteredTasks = useMemo(() => {
let result = [...tasks];
// Apply filter
if (filter !== 'all') {
result = result.filter((task) => task.status === filter);
}
// Apply sort
result.sort((a, b) => {
if (sort === 'time') {
// Sort by start time (newest first), pending tasks last
const timeA = a.startedAt ? new Date(a.startedAt).getTime() : 0;
const timeB = b.startedAt ? new Date(b.startedAt).getTime() : 0;
return timeB - timeA;
} else {
// Sort by name alphabetically
return a.name.localeCompare(b.name);
}
});
return result;
}, [tasks, filter, sort]);
return (
<div className={cn('space-y-4', className)}>
{/* Controls Row */}
<div className="flex items-center gap-3">
{/* Filter Select */}
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-muted-foreground" />
<Select value={filter} onValueChange={(v) => setFilter(v as FilterOption)}>
<SelectTrigger className="w-[140px] h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">
{formatMessage({ id: 'coordinator.taskList.filter.all' })}
</SelectItem>
<SelectItem value="running">
{formatMessage({ id: 'coordinator.taskList.filter.running' })}
</SelectItem>
<SelectItem value="completed">
{formatMessage({ id: 'coordinator.taskList.filter.completed' })}
</SelectItem>
<SelectItem value="failed">
{formatMessage({ id: 'coordinator.taskList.filter.failed' })}
</SelectItem>
</SelectContent>
</Select>
</div>
{/* Sort Select */}
<div className="flex items-center gap-2">
<ArrowUpDown className="w-4 h-4 text-muted-foreground" />
<Select value={sort} onValueChange={(v) => setSort(v as SortOption)}>
<SelectTrigger className="w-[120px] h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="time">
{formatMessage({ id: 'coordinator.taskList.sort.time' })}
</SelectItem>
<SelectItem value="name">
{formatMessage({ id: 'coordinator.taskList.sort.name' })}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Task Cards - Horizontal Scroll */}
{filteredTasks.length > 0 ? (
<div className="flex gap-4 overflow-x-auto pb-2 scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
{filteredTasks.map((task) => (
<CoordinatorTaskCard
key={task.id}
task={task}
isSelected={task.id === selectedTaskId}
onClick={() => onTaskSelect(task.id)}
/>
))}
</div>
) : (
/* Empty State */
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Inbox className="w-12 h-12 mb-4 opacity-50" />
<p className="text-sm">
{formatMessage({ id: 'coordinator.taskList.empty' })}
</p>
</div>
)}
</div>
);
}
export default CoordinatorTaskList;

View File

@@ -21,3 +21,12 @@ export type { CoordinatorLogStreamProps } from './CoordinatorLogStream';
export { CoordinatorQuestionModal } from './CoordinatorQuestionModal';
export type { CoordinatorQuestionModalProps } from './CoordinatorQuestionModal';
export { CoordinatorEmptyState } from './CoordinatorEmptyState';
export type { CoordinatorEmptyStateProps } from './CoordinatorEmptyState';
export { CoordinatorTaskCard } from './CoordinatorTaskCard';
export type { CoordinatorTaskCardProps, TaskStatus } from './CoordinatorTaskCard';
export { CoordinatorTaskList } from './CoordinatorTaskList';
export type { CoordinatorTaskListProps, FilterOption, SortOption } from './CoordinatorTaskList';

View File

@@ -0,0 +1,165 @@
// ========================================
// DashboardWidgetConfig Component
// ========================================
// Configuration panel for managing dashboard widgets visibility and layout
import * as React from 'react';
import { useIntl } from 'react-intl';
import { ChevronDown, Eye, EyeOff, RotateCcw } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Checkbox } from '@/components/ui/Checkbox';
import { cn } from '@/lib/utils';
import type { WidgetConfig } from '@/types/store';
export interface DashboardWidgetConfigProps {
/** List of widget configurations */
widgets: WidgetConfig[];
/** Callback when widget visibility changes */
onWidgetToggle: (widgetId: string) => void;
/** Callback when reset layout is requested */
onResetLayout: () => void;
/** Whether the panel is currently open */
isOpen?: boolean;
/** Callback when open state changes */
onOpenChange?: (open: boolean) => void;
}
/**
* DashboardWidgetConfig - Widget configuration panel
*
* Allows users to:
* - Toggle widget visibility
* - Reset layout to defaults
* - Quickly manage what widgets appear on dashboard
*/
export function DashboardWidgetConfig({
widgets,
onWidgetToggle,
onResetLayout,
isOpen = false,
onOpenChange,
}: DashboardWidgetConfigProps) {
const { formatMessage } = useIntl();
const [open, setOpen] = React.useState(isOpen);
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
onOpenChange?.(newOpen);
};
const visibleCount = widgets.filter((w) => w.visible).length;
const allVisible = visibleCount === widgets.length;
const handleToggleAll = () => {
// If all visible, hide all. If any hidden, show all.
widgets.forEach((widget) => {
if (allVisible) {
onWidgetToggle(widget.i);
} else if (!widget.visible) {
onWidgetToggle(widget.i);
}
});
};
const handleResetLayout = () => {
onResetLayout();
setOpen(false);
};
return (
<div className="relative">
{/* Toggle button */}
<Button
variant="outline"
size="sm"
onClick={() => handleOpenChange(!open)}
className="gap-2"
aria-label="Toggle widget configuration"
aria-expanded={open}
>
<Eye className="h-4 w-4" />
{formatMessage({ id: 'common.dashboard.config.title' })}
<ChevronDown
className={cn('h-4 w-4 transition-transform', open && 'rotate-180')}
/>
</Button>
{/* Dropdown panel */}
{open && (
<div className="absolute right-0 top-full mt-2 w-64 rounded-lg border border-border bg-card shadow-lg z-50">
<div className="p-4 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">
{formatMessage({ id: 'common.dashboard.config.widgets' })}
</h3>
<span className="text-xs text-muted-foreground">
{visibleCount}/{widgets.length}
</span>
</div>
{/* Toggle all button */}
<div>
<Button
variant="ghost"
size="sm"
onClick={handleToggleAll}
className="w-full justify-start gap-2 text-xs h-8"
>
{allVisible ? (
<>
<EyeOff className="h-3.5 w-3.5" />
{formatMessage({ id: 'common.dashboard.config.hideAll' })}
</>
) : (
<>
<Eye className="h-3.5 w-3.5" />
{formatMessage({ id: 'common.dashboard.config.showAll' })}
</>
)}
</Button>
</div>
{/* Widget list */}
<div className="space-y-2 border-t border-border pt-4">
{widgets.map((widget) => (
<div
key={widget.i}
className="flex items-center gap-2 p-2 rounded hover:bg-muted/50 transition-colors"
>
<Checkbox
id={`widget-${widget.i}`}
checked={widget.visible}
onCheckedChange={() => onWidgetToggle(widget.i)}
className="h-4 w-4"
/>
<label
htmlFor={`widget-${widget.i}`}
className="flex-1 text-sm text-foreground cursor-pointer"
>
{widget.name}
</label>
</div>
))}
</div>
{/* Reset button */}
<div className="border-t border-border pt-4">
<Button
variant="outline"
size="sm"
onClick={handleResetLayout}
className="w-full justify-start gap-2"
>
<RotateCcw className="h-3.5 w-3.5" />
{formatMessage({ id: 'common.dashboard.config.resetLayout' })}
</Button>
</div>
</div>
</div>
)}
</div>
);
}
export default DashboardWidgetConfig;

View File

@@ -0,0 +1,99 @@
// ========================================
// WidgetWrapper Component
// ========================================
// Wrapper component for dashboard widgets with drag handle and common styling
import * as React from 'react';
import { GripVertical, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
export interface WidgetWrapperProps {
/** Widget ID for identification */
id: string;
/** Widget title displayed in header */
title: string;
/** Children to render inside the widget */
children: React.ReactNode;
/** Whether the widget can be dragged */
isDraggable?: boolean;
/** Whether the widget can be removed */
canRemove?: boolean;
/** Callback when remove button is clicked */
onRemove?: (id: string) => void;
/** Additional CSS classes */
className?: string;
/** Whether to show the header with drag handle */
showHeader?: boolean;
/** Style prop passed by react-grid-layout */
style?: React.CSSProperties;
}
/**
* WidgetWrapper - Standardized wrapper for dashboard widgets
*
* Uses forwardRef to support react-grid-layout which requires
* refs on child elements for positioning and measurement.
*/
export const WidgetWrapper = React.forwardRef<HTMLDivElement, WidgetWrapperProps>(
function WidgetWrapper(
{
id,
title,
children,
isDraggable = true,
canRemove = false,
onRemove,
className,
showHeader = true,
style,
...rest
},
ref
) {
const handleRemove = React.useCallback(() => {
onRemove?.(id);
}, [id, onRemove]);
return (
<div ref={ref} className={cn('h-full flex flex-col', className)} style={style} {...rest}>
{/* Header with drag handle */}
{showHeader && (
<div className="flex items-center justify-between px-2 py-1 border-b border-border/50 bg-muted/30 rounded-t-lg">
<div className="flex items-center gap-2">
{/* Drag handle - must have .drag-handle class for react-grid-layout */}
{isDraggable && (
<div className="drag-handle cursor-grab active:cursor-grabbing p-1 rounded hover:bg-muted transition-colors">
<GripVertical className="h-4 w-4 text-muted-foreground" />
</div>
)}
<span className="text-sm font-medium text-foreground">{title}</span>
</div>
{/* Widget actions */}
<div className="flex items-center gap-1">
{canRemove && onRemove && (
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={handleRemove}
aria-label={`Remove ${title} widget`}
>
<X className="h-3.5 w-3.5" />
</Button>
)}
</div>
</div>
)}
{/* Widget content */}
<div className="flex-1 min-h-0">
{children}
</div>
</div>
);
}
);
export default WidgetWrapper;

View File

@@ -7,44 +7,33 @@ import type { WidgetConfig, DashboardLayouts, DashboardLayoutState } from '@/typ
/** Widget IDs used across the dashboard */
export const WIDGET_IDS = {
STATS: 'detailed-stats',
WORKFLOW_TASK: 'workflow-task',
RECENT_SESSIONS: 'recent-sessions',
WORKFLOW_STATUS: 'workflow-status-pie',
ACTIVITY: 'activity-line',
TASK_TYPES: 'task-type-bar',
} as const;
/** Default widget configurations */
export const DEFAULT_WIDGETS: WidgetConfig[] = [
{ i: WIDGET_IDS.STATS, name: 'Statistics', visible: true, minW: 4, minH: 2 },
{ i: WIDGET_IDS.RECENT_SESSIONS, name: 'Recent Sessions', visible: true, minW: 4, minH: 3 },
{ i: WIDGET_IDS.WORKFLOW_STATUS, name: 'Workflow Status', visible: true, minW: 3, minH: 3 },
{ i: WIDGET_IDS.ACTIVITY, name: 'Activity', visible: true, minW: 4, minH: 3 },
{ i: WIDGET_IDS.TASK_TYPES, name: 'Task Types', visible: true, minW: 3, minH: 3 },
{ i: WIDGET_IDS.WORKFLOW_TASK, name: 'Workflow & Tasks', visible: true, minW: 6, minH: 4 },
{ i: WIDGET_IDS.RECENT_SESSIONS, name: 'Recent Sessions', visible: true, minW: 6, minH: 3 },
];
/** Default responsive layouts */
export const DEFAULT_LAYOUTS: DashboardLayouts = {
lg: [
{ i: WIDGET_IDS.STATS, x: 0, y: 0, w: 12, h: 2, minW: 4, minH: 2 },
{ i: WIDGET_IDS.RECENT_SESSIONS, x: 0, y: 2, w: 6, h: 4, minW: 4, minH: 3 },
{ i: WIDGET_IDS.WORKFLOW_STATUS, x: 6, y: 2, w: 6, h: 4, minW: 3, minH: 3 },
{ i: WIDGET_IDS.ACTIVITY, x: 0, y: 6, w: 7, h: 4, minW: 4, minH: 3 },
{ i: WIDGET_IDS.TASK_TYPES, x: 7, y: 6, w: 5, h: 4, minW: 3, minH: 3 },
// Row 1: Combined WorkflowTask (full width - includes Stats, Workflow, Tasks, Heatmap)
{ i: WIDGET_IDS.WORKFLOW_TASK, x: 0, y: 0, w: 12, h: 5, minW: 6, minH: 4 },
// Row 2: Recent Sessions (full width)
{ i: WIDGET_IDS.RECENT_SESSIONS, x: 0, y: 5, w: 12, h: 4, minW: 6, minH: 3 },
],
md: [
{ i: WIDGET_IDS.STATS, x: 0, y: 0, w: 6, h: 2, minW: 3, minH: 2 },
{ i: WIDGET_IDS.RECENT_SESSIONS, x: 0, y: 2, w: 6, h: 4, minW: 3, minH: 3 },
{ i: WIDGET_IDS.WORKFLOW_STATUS, x: 0, y: 6, w: 6, h: 4, minW: 3, minH: 3 },
{ i: WIDGET_IDS.ACTIVITY, x: 0, y: 10, w: 6, h: 4, minW: 3, minH: 3 },
{ i: WIDGET_IDS.TASK_TYPES, x: 0, y: 14, w: 6, h: 4, minW: 3, minH: 3 },
// Medium: Stack vertically, full width each
{ i: WIDGET_IDS.WORKFLOW_TASK, x: 0, y: 0, w: 6, h: 5, minW: 4, minH: 4 },
{ i: WIDGET_IDS.RECENT_SESSIONS, x: 0, y: 5, w: 6, h: 4, minW: 4, minH: 3 },
],
sm: [
{ i: WIDGET_IDS.STATS, x: 0, y: 0, w: 2, h: 3, minW: 2, minH: 2 },
{ i: WIDGET_IDS.RECENT_SESSIONS, x: 0, y: 3, w: 2, h: 4, minW: 2, minH: 3 },
{ i: WIDGET_IDS.WORKFLOW_STATUS, x: 0, y: 7, w: 2, h: 4, minW: 2, minH: 3 },
{ i: WIDGET_IDS.ACTIVITY, x: 0, y: 11, w: 2, h: 4, minW: 2, minH: 3 },
{ i: WIDGET_IDS.TASK_TYPES, x: 0, y: 15, w: 2, h: 4, minW: 2, minH: 3 },
// Small: Stack vertically
{ i: WIDGET_IDS.WORKFLOW_TASK, x: 0, y: 0, w: 2, h: 8, minW: 2, minH: 6 },
{ i: WIDGET_IDS.RECENT_SESSIONS, x: 0, y: 8, w: 2, h: 5, minW: 2, minH: 4 },
],
};

View File

@@ -0,0 +1,138 @@
// ========================================
// ActivityHeatmapWidget Component
// ========================================
// Widget showing activity distribution as a vertical heatmap (narrow layout)
import { memo } from 'react';
import { useIntl } from 'react-intl';
import { Card } from '@/components/ui/Card';
import { useActivityTimeline } from '@/hooks/useActivityTimeline';
import { cn } from '@/lib/utils';
export interface ActivityHeatmapWidgetProps {
className?: string;
}
const WEEKS = 12; // 12 weeks = ~3 months
const DAYS_PER_WEEK = 7;
const TOTAL_CELLS = WEEKS * DAYS_PER_WEEK;
// Generate heatmap data for WEEKS x 7 grid
function generateHeatmapData(activityData: number[] = []): { value: number; intensity: number }[] {
const heatmap: { value: number; intensity: number }[] = [];
for (let i = 0; i < TOTAL_CELLS; i++) {
const value = activityData[i] ?? Math.floor(Math.random() * 10);
const intensity = Math.min(100, (value / 10) * 100);
heatmap.push({ value, intensity });
}
return heatmap;
}
function getIntensityColor(intensity: number): string {
if (intensity === 0) return 'bg-muted/50';
if (intensity < 25) return 'bg-primary/20';
if (intensity < 50) return 'bg-primary/40';
if (intensity < 75) return 'bg-primary/60';
return 'bg-primary';
}
// Short day labels for narrow layout
const DAY_LABELS = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
function ActivityHeatmapWidgetComponent({ className }: ActivityHeatmapWidgetProps) {
const { formatMessage } = useIntl();
const { data, isLoading } = useActivityTimeline();
const activityValues = data?.map((item) => item.sessions + item.tasks) || [];
const heatmapData = generateHeatmapData(activityValues);
// Get month labels for week rows
const getWeekLabel = (weekIdx: number): string => {
const date = new Date();
date.setDate(date.getDate() - (WEEKS - 1 - weekIdx) * 7);
// Only show month for first week of each month
if (weekIdx === 0 || date.getDate() <= 7) {
return date.toLocaleString('default', { month: 'short' });
}
return '';
};
return (
<Card className={cn('h-full p-3 flex flex-col', className)}>
<h3 className="text-sm font-semibold text-foreground mb-2">
{formatMessage({ id: 'home.widgets.activity' })}
</h3>
{isLoading ? (
<div className="flex-1 flex items-center justify-center">
<div className="h-full w-full bg-muted rounded animate-pulse" />
</div>
) : (
<div className="flex-1 flex flex-col overflow-hidden min-h-0">
{/* Day header row */}
<div className="flex gap-[2px] mb-1">
<div className="w-8 shrink-0" /> {/* Spacer for month labels */}
{DAY_LABELS.map((label, i) => (
<div
key={i}
className="flex-1 text-center text-[9px] text-muted-foreground font-medium"
>
{label}
</div>
))}
</div>
{/* Vertical grid: rows = weeks (flowing down), columns = days */}
<div className="flex-1 flex flex-col gap-[2px] min-h-0 overflow-auto">
{Array.from({ length: WEEKS }).map((_, weekIdx) => {
const monthLabel = getWeekLabel(weekIdx);
return (
<div key={weekIdx} className="flex gap-[2px] items-center">
{/* Month label */}
<div className="w-8 shrink-0 text-[9px] text-muted-foreground truncate">
{monthLabel}
</div>
{/* Day cells for this week */}
{Array.from({ length: DAYS_PER_WEEK }).map((_, dayIdx) => {
const cellIndex = weekIdx * DAYS_PER_WEEK + dayIdx;
const cell = heatmapData[cellIndex];
return (
<div
key={dayIdx}
className={cn(
'flex-1 aspect-square rounded-sm border border-border/30 transition-opacity hover:opacity-80 cursor-help relative group min-w-0',
getIntensityColor(cell.intensity)
)}
title={`${cell.value} activities`}
>
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-1.5 py-0.5 bg-foreground text-background rounded text-[10px] font-medium whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none z-10 transition-opacity">
{cell.value}
</div>
</div>
);
})}
</div>
);
})}
</div>
{/* Legend */}
<div className="flex items-center justify-center gap-1 mt-2 text-[9px] text-muted-foreground">
<span>Less</span>
{[0, 25, 50, 75, 100].map((intensity, i) => (
<div
key={i}
className={cn('w-[8px] h-[8px] rounded-sm border border-border/30', getIntensityColor(intensity))}
/>
))}
<span>More</span>
</div>
</div>
)}
</Card>
);
}
export const ActivityHeatmapWidget = memo(ActivityHeatmapWidgetComponent);
export default ActivityHeatmapWidget;

View File

@@ -28,9 +28,9 @@ export interface ActivityLineChartWidgetProps {
*/
function ActivityLineChartWidgetComponent({ className, ...props }: ActivityLineChartWidgetProps) {
const { formatMessage } = useIntl();
const { data, isLoading, error } = useActivityTimeline();
const { data, isLoading } = useActivityTimeline();
// Use mock data if API is not ready
// Use mock data if API call fails or returns no data
const chartData = data || generateMockActivityTimeline();
return (
@@ -41,10 +41,6 @@ function ActivityLineChartWidgetComponent({ className, ...props }: ActivityLineC
</h3>
{isLoading ? (
<ChartSkeleton type="line" height={280} />
) : error ? (
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-destructive">Failed to load chart data</p>
</div>
) : (
<ActivityLineChart data={chartData} height={280} />
)}

View File

@@ -119,8 +119,8 @@ function DetailedStatsWidgetComponent({ className, ...props }: DetailedStatsWidg
return (
<div {...props} className={className}>
<Card className="h-full p-4">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<Card className="h-full p-4 flex flex-col">
<div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 md:gap-4">
{isLoading
? Array.from({ length: 6 }).map((_, i) => <StatCardSkeleton key={i} />)
: statCards.map((card) => (

View File

@@ -1,103 +1,401 @@
// ========================================
// RecentSessionsWidget Component
// ========================================
// Widget wrapper for recent sessions list in dashboard grid layout
// Widget showing recent sessions across different task types (workflow, lite, orchestrator)
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { FolderKanban } from 'lucide-react';
import {
FolderKanban,
Workflow,
Zap,
Play,
Clock,
CheckCircle2,
XCircle,
PauseCircle,
FileEdit,
Wrench,
GitBranch,
Tag,
Loader2,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { SessionCard, SessionCardSkeleton } from '@/components/shared/SessionCard';
import { useSessions } from '@/hooks/useSessions';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { Progress } from '@/components/ui/Progress';
import { useSessions } from '@/hooks/useSessions';
import { useLiteTasks } from '@/hooks/useLiteTasks';
import { useCoordinatorStore } from '@/stores/coordinatorStore';
import { cn } from '@/lib/utils';
export interface RecentSessionsWidgetProps {
/** Data grid attributes for react-grid-layout */
'data-grid'?: {
i: string;
x: number;
y: number;
w: number;
h: number;
};
/** Additional CSS classes */
className?: string;
/** Maximum number of sessions to display */
maxSessions?: number;
maxItems?: number;
}
// Task type definitions
type TaskType = 'all' | 'workflow' | 'lite' | 'orchestrator';
// Unified task item for display
interface UnifiedTaskItem {
id: string;
name: string;
type: TaskType;
subType?: string;
status: string;
statusKey: string; // i18n key for status
createdAt: string;
description?: string;
tags?: string[];
progress?: number;
}
// Tab configuration for different task types
const TABS: { key: TaskType; label: string; icon: React.ElementType }[] = [
{ key: 'all', label: 'home.tabs.allTasks', icon: FolderKanban },
{ key: 'workflow', label: 'home.tabs.workflow', icon: Workflow },
{ key: 'lite', label: 'home.tabs.liteTasks', icon: Zap },
{ key: 'orchestrator', label: 'home.tabs.orchestrator', icon: Play },
];
// Status icon mapping
const statusIcons: Record<string, React.ElementType> = {
in_progress: Loader2,
running: Loader2,
planning: FileEdit,
completed: CheckCircle2,
failed: XCircle,
paused: PauseCircle,
pending: Clock,
cancelled: XCircle,
idle: Clock,
initializing: Loader2,
};
// Status color mapping
const statusColors: Record<string, string> = {
in_progress: 'bg-warning/20 text-warning border-warning/30',
running: 'bg-warning/20 text-warning border-warning/30',
planning: 'bg-violet-500/20 text-violet-600 border-violet-500/30',
completed: 'bg-success/20 text-success border-success/30',
failed: 'bg-destructive/20 text-destructive border-destructive/30',
paused: 'bg-slate-400/20 text-slate-500 border-slate-400/30',
pending: 'bg-muted text-muted-foreground border-border',
cancelled: 'bg-destructive/20 text-destructive border-destructive/30',
idle: 'bg-muted text-muted-foreground border-border',
initializing: 'bg-info/20 text-info border-info/30',
};
// Status to i18n key mapping
const statusI18nKeys: Record<string, string> = {
in_progress: 'inProgress',
running: 'running',
planning: 'planning',
completed: 'completed',
failed: 'failed',
paused: 'paused',
pending: 'pending',
cancelled: 'cancelled',
idle: 'idle',
initializing: 'initializing',
};
// Lite task sub-type icons
const liteTypeIcons: Record<string, React.ElementType> = {
'lite-plan': FileEdit,
'lite-fix': Wrench,
'multi-cli-plan': GitBranch,
};
// Task type colors
const typeColors: Record<TaskType, string> = {
all: 'bg-muted text-muted-foreground',
workflow: 'bg-primary/20 text-primary',
lite: 'bg-amber-500/20 text-amber-600',
orchestrator: 'bg-violet-500/20 text-violet-600',
};
function TaskItemCard({ item, onClick }: { item: UnifiedTaskItem; onClick: () => void }) {
const { formatMessage } = useIntl();
const StatusIcon = statusIcons[item.status] || Clock;
const TypeIcon = item.subType ? (liteTypeIcons[item.subType] || Zap) :
item.type === 'workflow' ? Workflow :
item.type === 'orchestrator' ? Play : Zap;
const isAnimated = item.status === 'in_progress' || item.status === 'running' || item.status === 'initializing';
return (
<button
onClick={onClick}
className="w-full text-left p-3 rounded-lg border border-border bg-card hover:bg-accent/50 hover:border-primary/30 transition-all group"
>
<div className="flex items-start gap-2.5">
<div className={cn('p-1.5 rounded-md shrink-0', typeColors[item.type])}>
<TypeIcon className="h-4 w-4" />
</div>
<div className="flex-1 min-w-0">
{/* Header: name + status */}
<div className="flex items-start gap-2 mb-1">
<h4 className="text-sm font-medium text-foreground truncate flex-1 group-hover:text-primary transition-colors">
{item.name}
</h4>
<Badge className={cn('text-[10px] px-1.5 py-0 shrink-0 border', statusColors[item.status])}>
<StatusIcon className={cn('h-2.5 w-2.5 mr-0.5', isAnimated && 'animate-spin')} />
{formatMessage({ id: `common.status.${item.statusKey}` })}
</Badge>
</div>
{/* Description */}
{item.description && (
<p className="text-xs text-muted-foreground line-clamp-2 mb-1.5">
{item.description}
</p>
)}
{/* Progress bar (if available) */}
{typeof item.progress === 'number' && item.progress > 0 && (
<div className="flex items-center gap-2 mb-1.5">
<Progress value={item.progress} className="h-1 flex-1 bg-muted" />
<span className="text-[10px] text-muted-foreground w-8 text-right">{item.progress}%</span>
</div>
)}
{/* Footer: time + tags */}
<div className="flex items-center gap-2 flex-wrap">
<span className="flex items-center gap-0.5 text-[10px] text-muted-foreground">
<Clock className="h-2.5 w-2.5" />
{item.createdAt}
</span>
{item.subType && (
<Badge variant="outline" className="text-[9px] px-1 py-0 bg-background">
{item.subType}
</Badge>
)}
{item.tags && item.tags.slice(0, 2).map((tag) => (
<Badge key={tag} variant="outline" className="text-[9px] px-1 py-0 gap-0.5 bg-background">
<Tag className="h-2 w-2" />
{tag}
</Badge>
))}
{item.tags && item.tags.length > 2 && (
<span className="text-[9px] text-muted-foreground">+{item.tags.length - 2}</span>
)}
</div>
</div>
</div>
</button>
);
}
function TaskItemSkeleton() {
return (
<div className="p-3 rounded-lg border border-border bg-card animate-pulse">
<div className="flex items-start gap-2.5">
<div className="w-8 h-8 rounded-md bg-muted" />
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<div className="h-4 bg-muted rounded flex-1" />
<div className="h-4 w-16 bg-muted rounded" />
</div>
<div className="h-3 bg-muted rounded w-3/4 mb-2" />
<div className="flex gap-2">
<div className="h-3 w-16 bg-muted rounded" />
<div className="h-3 w-12 bg-muted rounded" />
</div>
</div>
</div>
</div>
);
}
/**
* RecentSessionsWidget - Dashboard widget showing recent workflow sessions
*
* Displays recent active sessions (max 6 by default) with navigation to session detail.
* Wrapped with React.memo to prevent unnecessary re-renders when parent updates.
*/
function RecentSessionsWidgetComponent({
className,
maxSessions = 6,
...props
maxItems = 6,
}: RecentSessionsWidgetProps) {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const [activeTab, setActiveTab] = React.useState<TaskType>('all');
// Fetch recent sessions (active only)
const { activeSessions, isLoading } = useSessions({
// Fetch workflow sessions
const { activeSessions, isLoading: sessionsLoading } = useSessions({
filter: { location: 'active' },
});
// Get recent sessions (sorted by creation date)
const recentSessions = React.useMemo(
() =>
[...activeSessions]
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, maxSessions),
[activeSessions, maxSessions]
);
// Fetch lite tasks
const { allSessions: liteSessions, isLoading: liteLoading } = useLiteTasks();
const handleSessionClick = (sessionId: string) => {
navigate(`/sessions/${sessionId}`);
// Get coordinator state
const coordinatorState = useCoordinatorStore();
// Format relative time with fallback
const formatRelativeTime = React.useCallback((dateStr: string | undefined): string => {
if (!dateStr) return formatMessage({ id: 'common.time.justNow' });
const date = new Date(dateStr);
if (isNaN(date.getTime())) return formatMessage({ id: 'common.time.justNow' });
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return formatMessage({ id: 'common.time.justNow' });
if (diffMins < 60) return formatMessage({ id: 'common.time.minutesAgo' }, { count: diffMins });
if (diffHours < 24) return formatMessage({ id: 'common.time.hoursAgo' }, { count: diffHours });
return formatMessage({ id: 'common.time.daysAgo' }, { count: diffDays });
}, [formatMessage]);
// Convert to unified items
const unifiedItems = React.useMemo((): UnifiedTaskItem[] => {
const items: UnifiedTaskItem[] = [];
// Add workflow sessions
activeSessions.forEach((session) => {
const status = session.status || 'pending';
items.push({
id: session.session_id,
name: session.title || session.description || session.session_id,
type: 'workflow',
status,
statusKey: statusI18nKeys[status] || status,
createdAt: formatRelativeTime(session.created_at),
description: session.description || `Session: ${session.session_id}`,
tags: [],
progress: undefined,
});
});
// Add lite tasks
liteSessions.forEach((session) => {
const status = session.status || 'pending';
const sessionId = session.session_id || session.id;
items.push({
id: sessionId,
name: session.title || sessionId,
type: 'lite',
subType: session._type,
status,
statusKey: statusI18nKeys[status] || status,
createdAt: formatRelativeTime(session.createdAt),
description: session.description || `${session._type} task`,
tags: [],
progress: undefined,
});
});
// Add current coordinator execution if exists
if (coordinatorState.currentExecutionId && coordinatorState.status !== 'idle') {
const status = coordinatorState.status;
const completedSteps = coordinatorState.commandChain.filter(n => n.status === 'completed').length;
const totalSteps = coordinatorState.commandChain.length;
const progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
items.push({
id: coordinatorState.currentExecutionId,
name: coordinatorState.pipelineDetails?.nodes[0]?.name || 'Orchestrator Task',
type: 'orchestrator',
status,
statusKey: statusI18nKeys[status] || status,
createdAt: formatRelativeTime(coordinatorState.startedAt),
description: `${completedSteps}/${totalSteps} steps completed`,
progress,
});
}
// Sort by most recent (use original date for sorting, not formatted string)
return items;
}, [activeSessions, liteSessions, coordinatorState, formatRelativeTime]);
// Filter items by tab
const filteredItems = React.useMemo(() => {
if (activeTab === 'all') return unifiedItems.slice(0, maxItems);
return unifiedItems.filter((item) => item.type === activeTab).slice(0, maxItems);
}, [unifiedItems, activeTab, maxItems]);
// Handle item click
const handleItemClick = (item: UnifiedTaskItem) => {
switch (item.type) {
case 'workflow':
navigate(`/sessions/${item.id}`);
break;
case 'lite':
navigate(`/lite-tasks/${item.subType}/${item.id}`);
break;
case 'orchestrator':
navigate(`/orchestrator`);
break;
}
};
const handleViewAll = () => {
navigate('/sessions');
};
const isLoading = sessionsLoading || liteLoading;
return (
<div {...props} className={className}>
<div className={className}>
<Card className="h-full p-4 flex flex-col">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-foreground">
{formatMessage({ id: 'home.sections.recentSessions' })}
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-foreground">
{formatMessage({ id: 'home.sections.recentTasks' })}
</h3>
<Button variant="link" size="sm" onClick={handleViewAll}>
<Button variant="link" size="sm" className="text-xs h-auto p-0" onClick={handleViewAll}>
{formatMessage({ id: 'common.actions.viewAll' })}
</Button>
</div>
{/* Tabs */}
<div className="flex gap-1 mb-3 overflow-x-auto pb-1">
{TABS.map((tab) => {
const TabIcon = tab.icon;
const count = tab.key === 'all' ? unifiedItems.length :
unifiedItems.filter((i) => i.type === tab.key).length;
return (
<Button
key={tab.key}
variant={activeTab === tab.key ? 'default' : 'ghost'}
size="sm"
onClick={() => setActiveTab(tab.key)}
className={cn(
'whitespace-nowrap text-xs gap-1 h-7 px-2',
activeTab === tab.key && 'bg-primary text-primary-foreground'
)}
>
<TabIcon className="h-3 w-3" />
{formatMessage({ id: tab.label })}
<span className="text-[10px] opacity-70">({count})</span>
</Button>
);
})}
</div>
{/* Task items */}
<div className="flex-1 overflow-auto">
{isLoading ? (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<SessionCardSkeleton key={i} />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{Array.from({ length: 6 }).map((_, i) => (
<TaskItemSkeleton key={i} />
))}
</div>
) : recentSessions.length === 0 ? (
) : filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8">
<FolderKanban className="h-12 w-12 text-muted-foreground mb-2" />
<FolderKanban className="h-10 w-10 text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'home.emptyState.noSessions.message' })}
{formatMessage({ id: 'home.emptyState.noTasks.message' })}
</p>
</div>
) : (
<div className="space-y-3">
{recentSessions.map((session) => (
<SessionCard
key={session.session_id}
session={session}
onClick={handleSessionClick}
onView={handleSessionClick}
showActions={false}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{filteredItems.map((item) => (
<TaskItemCard
key={`${item.type}-${item.id}`}
item={item}
onClick={() => handleItemClick(item)}
/>
))}
</div>
@@ -108,10 +406,6 @@ function RecentSessionsWidgetComponent({
);
}
/**
* Memoized RecentSessionsWidget - Prevents re-renders when parent updates
* Props are compared shallowly; use useCallback for function props
*/
export const RecentSessionsWidget = React.memo(RecentSessionsWidgetComponent);
export default RecentSessionsWidget;

View File

@@ -0,0 +1,140 @@
// ========================================
// TaskMarqueeWidget Component
// ========================================
// Widget showing scrolling task details in a marquee/ticker format
import { memo, useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { ListChecks } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface TaskMarqueeWidgetProps {
className?: string;
}
interface TaskItem {
id: string;
name: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
priority: 'low' | 'medium' | 'high' | 'critical';
progress: number;
}
// Mock task data
const MOCK_TASKS: TaskItem[] = [
{ id: '1', name: 'Implement user authentication system', status: 'in_progress', priority: 'high', progress: 75 },
{ id: '2', name: 'Design database schema', status: 'completed', priority: 'high', progress: 100 },
{ id: '3', name: 'Setup CI/CD pipeline', status: 'in_progress', priority: 'critical', progress: 45 },
{ id: '4', name: 'Write API documentation', status: 'pending', priority: 'medium', progress: 0 },
{ id: '5', name: 'Performance optimization', status: 'completed', priority: 'medium', progress: 100 },
{ id: '6', name: 'Security audit and fixes', status: 'failed', priority: 'critical', progress: 30 },
{ id: '7', name: 'Integration testing', status: 'in_progress', priority: 'high', progress: 60 },
{ id: '8', name: 'Deploy to staging', status: 'pending', priority: 'medium', progress: 0 },
];
// Status color mapping
const statusColors: Record<string, string> = {
pending: 'bg-muted',
in_progress: 'bg-warning/20 text-warning',
completed: 'bg-success/20 text-success',
failed: 'bg-destructive/20 text-destructive',
};
const priorityColors: Record<string, string> = {
low: 'bg-muted text-muted-foreground',
medium: 'bg-info/20 text-info',
high: 'bg-warning/20 text-warning',
critical: 'bg-destructive/20 text-destructive',
};
// Map status values to i18n keys
const statusLabelKeys: Record<string, string> = {
pending: 'common.status.pending',
in_progress: 'common.status.inProgress',
completed: 'common.status.completed',
failed: 'common.status.failed',
};
function TaskMarqueeWidgetComponent({ className }: TaskMarqueeWidgetProps) {
const { formatMessage } = useIntl();
const [currentIndex, setCurrentIndex] = useState(0);
// Auto-advance task display every 4 seconds
useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % MOCK_TASKS.length);
}, 4000);
return () => clearInterval(interval);
}, []);
const currentTask = MOCK_TASKS[currentIndex];
return (
<Card className={cn('h-full p-4 flex flex-col', className)}>
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<ListChecks className="h-5 w-5" />
{formatMessage({ id: 'home.sections.taskDetails' })}
</h3>
<div className="flex-1 flex flex-col justify-center gap-4">
{/* Task name with marquee effect */}
<div className="overflow-hidden">
<div className="animate-marquee">
<h4 className="text-base font-semibold text-foreground whitespace-nowrap">
{currentTask.name}
</h4>
</div>
</div>
{/* Status and Priority badges */}
<div className="flex items-center gap-2 flex-wrap">
<Badge className={cn(statusColors[currentTask.status], 'capitalize')}>
{formatMessage({ id: statusLabelKeys[currentTask.status] })}
</Badge>
<Badge className={cn(priorityColors[currentTask.priority], 'capitalize')}>
{formatMessage({ id: `common.priority.${currentTask.priority}` })}
</Badge>
</div>
{/* Progress bar */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">{formatMessage({ id: 'common.labels.progress' })}</span>
<span className="font-semibold text-foreground">{currentTask.progress}%</span>
</div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${currentTask.progress}%` }}
/>
</div>
</div>
{/* Task counter */}
<div className="flex items-center justify-between text-xs text-muted-foreground pt-2 border-t border-border">
<span>
{currentIndex + 1} / {MOCK_TASKS.length}
</span>
<div className="flex gap-1">
{MOCK_TASKS.map((_, idx) => (
<div
key={idx}
className={cn(
'h-1.5 w-1.5 rounded-full transition-colors',
idx === currentIndex ? 'bg-primary' : 'bg-muted'
)}
/>
))}
</div>
</div>
</div>
</Card>
);
}
export const TaskMarqueeWidget = memo(TaskMarqueeWidgetComponent);
export default TaskMarqueeWidget;

View File

@@ -28,9 +28,9 @@ export interface TaskTypeBarChartWidgetProps {
*/
function TaskTypeBarChartWidgetComponent({ className, ...props }: TaskTypeBarChartWidgetProps) {
const { formatMessage } = useIntl();
const { data, isLoading, error } = useTaskTypeCounts();
const { data, isLoading } = useTaskTypeCounts();
// Use mock data if API is not ready
// Use mock data if API call fails or returns no data
const chartData = data || generateMockTaskTypeCounts();
return (
@@ -41,10 +41,6 @@ function TaskTypeBarChartWidgetComponent({ className, ...props }: TaskTypeBarCha
</h3>
{isLoading ? (
<ChartSkeleton type="bar" height={280} />
) : error ? (
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-destructive">Failed to load chart data</p>
</div>
) : (
<TaskTypeBarChart data={chartData} height={280} />
)}

View File

@@ -28,9 +28,9 @@ export interface WorkflowStatusPieChartWidgetProps {
*/
function WorkflowStatusPieChartWidgetComponent({ className, ...props }: WorkflowStatusPieChartWidgetProps) {
const { formatMessage } = useIntl();
const { data, isLoading, error } = useWorkflowStatusCounts();
const { data, isLoading } = useWorkflowStatusCounts();
// Use mock data if API is not ready
// Use mock data if API call fails or returns no data
const chartData = data || generateMockWorkflowStatusCounts();
return (
@@ -41,10 +41,6 @@ function WorkflowStatusPieChartWidgetComponent({ className, ...props }: Workflow
</h3>
{isLoading ? (
<ChartSkeleton type="pie" height={280} />
) : error ? (
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-destructive">Failed to load chart data</p>
</div>
) : (
<WorkflowStatusPieChart data={chartData} height={280} />
)}

View File

@@ -0,0 +1,109 @@
// ========================================
// WorkflowStatusProgressWidget Component
// ========================================
// Widget showing workflow status distribution using progress bars
import { memo } from 'react';
import { useIntl } from 'react-intl';
import { Card } from '@/components/ui/Card';
import { Progress } from '@/components/ui/Progress';
import { Badge } from '@/components/ui/Badge';
import { useWorkflowStatusCounts, generateMockWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts';
import { cn } from '@/lib/utils';
export interface WorkflowStatusProgressWidgetProps {
className?: string;
}
// Status color mapping
const statusColors: Record<string, { bg: string; text: string }> = {
completed: { bg: 'bg-success', text: 'text-success' },
in_progress: { bg: 'bg-warning', text: 'text-warning' },
planning: { bg: 'bg-info', text: 'text-info' },
paused: { bg: 'bg-muted', text: 'text-muted-foreground' },
archived: { bg: 'bg-secondary', text: 'text-secondary-foreground' },
};
// Status label keys for i18n
const statusLabelKeys: Record<string, string> = {
completed: 'sessions.status.completed',
in_progress: 'sessions.status.inProgress',
planning: 'sessions.status.planning',
paused: 'sessions.status.paused',
archived: 'sessions.status.archived',
};
function WorkflowStatusProgressWidgetComponent({ className }: WorkflowStatusProgressWidgetProps) {
const { formatMessage } = useIntl();
const { data, isLoading } = useWorkflowStatusCounts();
// Use mock data if API call fails or returns no data
const chartData = data || generateMockWorkflowStatusCounts();
// Calculate total for percentage
const total = chartData.reduce((sum, item) => sum + item.count, 0);
return (
<Card className={cn('h-full p-4 flex flex-col', className)}>
<h3 className="text-lg font-semibold text-foreground mb-4">
{formatMessage({ id: 'home.widgets.workflowStatus' })}
</h3>
{isLoading ? (
<div className="space-y-4 flex-1">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="space-y-2">
<div className="h-4 bg-muted rounded animate-pulse" />
<div className="h-2 bg-muted rounded animate-pulse" />
</div>
))}
</div>
) : (
<div className="space-y-4 flex-1">
{chartData.map((item) => {
const percentage = total > 0 ? Math.round((item.count / total) * 100) : 0;
const colors = statusColors[item.status] || statusColors.completed;
return (
<div key={item.status} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">
{formatMessage({ id: statusLabelKeys[item.status] })}
</span>
<Badge variant="secondary" className="text-xs">
{item.count}
</Badge>
</div>
<span className={cn('text-sm font-semibold', colors.text)}>
{percentage}%
</span>
</div>
<Progress
value={percentage}
className="h-2"
indicatorClassName={colors.bg}
/>
</div>
);
})}
</div>
)}
{!isLoading && total > 0 && (
<div className="mt-4 pt-4 border-t border-border">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{formatMessage({ id: 'common.stats.total' })}
</span>
<span className="font-semibold text-foreground">{total}</span>
</div>
</div>
)}
</Card>
);
}
export const WorkflowStatusProgressWidget = memo(WorkflowStatusProgressWidgetComponent);
export default WorkflowStatusProgressWidget;

View File

@@ -0,0 +1,723 @@
// ========================================
// WorkflowTaskWidget Component
// ========================================
// Combined dashboard widget: project info + stats + workflow status + orchestrator + task carousel
import { memo, useMemo, useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { Card } from '@/components/ui/Card';
import { Progress } from '@/components/ui/Progress';
import { Button } from '@/components/ui/Button';
import { Sparkline } from '@/components/charts/Sparkline';
import { useWorkflowStatusCounts, generateMockWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts';
import { useDashboardStats } from '@/hooks/useDashboardStats';
import { useCoordinatorStore } from '@/stores/coordinatorStore';
import { useProjectOverview } from '@/hooks/useProjectOverview';
import { cn } from '@/lib/utils';
import {
ListChecks,
Clock,
FolderKanban,
CheckCircle2,
XCircle,
Activity,
Play,
Pause,
Square,
Loader2,
AlertCircle,
ChevronLeft,
ChevronRight,
ChevronDown,
ChevronUp,
Tag,
Calendar,
Code2,
Server,
Layers,
GitBranch,
Wrench,
FileCode,
Bug,
Sparkles,
BookOpen,
} from 'lucide-react';
export interface WorkflowTaskWidgetProps {
className?: string;
}
// ---- Workflow Status section ----
const statusColors: Record<string, { bg: string; text: string; dot: string }> = {
completed: { bg: 'bg-success', text: 'text-success', dot: 'bg-emerald-500' },
in_progress: { bg: 'bg-warning', text: 'text-warning', dot: 'bg-amber-500' },
planning: { bg: 'bg-violet-500', text: 'text-violet-600', dot: 'bg-violet-500' },
paused: { bg: 'bg-slate-400', text: 'text-slate-500', dot: 'bg-slate-400' },
archived: { bg: 'bg-slate-300', text: 'text-slate-400', dot: 'bg-slate-300' },
};
const statusLabelKeys: Record<string, string> = {
completed: 'sessions.status.completed',
in_progress: 'sessions.status.inProgress',
planning: 'sessions.status.planning',
paused: 'sessions.status.paused',
archived: 'sessions.status.archived',
};
// ---- Task List section ----
interface TaskItem {
id: string;
name: string;
status: 'pending' | 'completed';
}
// Session with its tasks
interface SessionWithTasks {
id: string;
name: string;
description?: string;
status: 'planning' | 'in_progress' | 'completed' | 'paused';
tags: string[];
createdAt: string;
updatedAt: string;
tasks: TaskItem[];
}
// Mock sessions with their tasks
const MOCK_SESSIONS: SessionWithTasks[] = [
{
id: 'WFS-auth-001',
name: 'User Authentication System',
description: 'Implement OAuth2 and JWT based authentication with role-based access control',
status: 'in_progress',
tags: ['auth', 'security', 'backend'],
createdAt: '2024-01-15',
updatedAt: '2024-01-20',
tasks: [
{ id: '1', name: 'Implement user authentication', status: 'pending' },
{ id: '2', name: 'Design database schema', status: 'completed' },
{ id: '3', name: 'Setup CI/CD pipeline', status: 'pending' },
],
},
{
id: 'WFS-api-002',
name: 'API Documentation',
description: 'Create comprehensive API documentation with OpenAPI 3.0 specification',
status: 'planning',
tags: ['docs', 'api'],
createdAt: '2024-01-18',
updatedAt: '2024-01-19',
tasks: [
{ id: '4', name: 'Write API documentation', status: 'pending' },
{ id: '5', name: 'Create OpenAPI spec', status: 'pending' },
],
},
{
id: 'WFS-perf-003',
name: 'Performance Optimization',
description: 'Optimize database queries and implement caching strategies',
status: 'completed',
tags: ['performance', 'optimization', 'database'],
createdAt: '2024-01-10',
updatedAt: '2024-01-17',
tasks: [
{ id: '6', name: 'Performance optimization', status: 'completed' },
{ id: '7', name: 'Security audit', status: 'completed' },
],
},
{
id: 'WFS-test-004',
name: 'Integration Testing',
description: 'Setup E2E testing framework and write integration tests',
status: 'in_progress',
tags: ['testing', 'e2e', 'ci'],
createdAt: '2024-01-19',
updatedAt: '2024-01-20',
tasks: [
{ id: '8', name: 'Integration testing', status: 'completed' },
{ id: '9', name: 'Deploy to staging', status: 'pending' },
{ id: '10', name: 'E2E test setup', status: 'pending' },
],
},
];
const taskStatusColors: Record<string, { bg: string; text: string; icon: typeof CheckCircle2 }> = {
pending: { bg: 'bg-muted', text: 'text-muted-foreground', icon: Clock },
completed: { bg: 'bg-success/20', text: 'text-success', icon: CheckCircle2 },
};
const sessionStatusColors: Record<string, { bg: string; text: string }> = {
planning: { bg: 'bg-violet-500/20', text: 'text-violet-600' },
in_progress: { bg: 'bg-warning/20', text: 'text-warning' },
completed: { bg: 'bg-success/20', text: 'text-success' },
paused: { bg: 'bg-slate-400/20', text: 'text-slate-500' },
};
// ---- Mini Stat Card with Sparkline ----
interface MiniStatCardProps {
icon: React.ElementType;
title: string;
value: number;
variant: 'primary' | 'info' | 'success' | 'warning' | 'danger' | 'default';
sparklineData?: number[];
}
const variantStyles: Record<string, { card: string; icon: string }> = {
primary: { card: 'border-primary/30 bg-primary/5', icon: 'bg-primary/10 text-primary' },
info: { card: 'border-info/30 bg-info/5', icon: 'bg-info/10 text-info' },
success: { card: 'border-success/30 bg-success/5', icon: 'bg-success/10 text-success' },
warning: { card: 'border-warning/30 bg-warning/5', icon: 'bg-warning/10 text-warning' },
danger: { card: 'border-destructive/30 bg-destructive/5', icon: 'bg-destructive/10 text-destructive' },
default: { card: 'border-border', icon: 'bg-muted text-muted-foreground' },
};
function MiniStatCard({ icon: Icon, title, value, variant, sparklineData }: MiniStatCardProps) {
const styles = variantStyles[variant] || variantStyles.default;
return (
<div className={cn('rounded-lg border p-2 transition-all hover:shadow-sm', styles.card)}>
<div className="flex items-start justify-between gap-1">
<div className="flex-1 min-w-0">
<p className="text-[10px] font-medium text-muted-foreground truncate">{title}</p>
<p className="text-lg font-semibold text-card-foreground mt-0.5">{value.toLocaleString()}</p>
</div>
<div className={cn('flex h-7 w-7 items-center justify-center rounded-md shrink-0', styles.icon)}>
<Icon className="h-3.5 w-3.5" />
</div>
</div>
{sparklineData && sparklineData.length > 0 && (
<div className="mt-1 -mx-1">
<Sparkline data={sparklineData} height={24} strokeWidth={1.5} />
</div>
)}
</div>
);
}
// Generate sparkline data
function generateSparklineData(currentValue: number, variance = 0.3): number[] {
const days = 7;
const data: number[] = [];
let value = Math.max(0, currentValue * (1 - variance));
for (let i = 0; i < days - 1; i++) {
data.push(Math.round(value));
const change = (Math.random() - 0.5) * 2 * variance * currentValue;
value = Math.max(0, value + change);
}
data.push(currentValue);
return data;
}
// Orchestrator status icons and colors
const orchestratorStatusConfig: Record<string, { icon: typeof Play; color: string; bg: string }> = {
idle: { icon: Square, color: 'text-muted-foreground', bg: 'bg-muted' },
initializing: { icon: Loader2, color: 'text-info', bg: 'bg-info/20' },
running: { icon: Play, color: 'text-success', bg: 'bg-success/20' },
paused: { icon: Pause, color: 'text-warning', bg: 'bg-warning/20' },
completed: { icon: CheckCircle2, color: 'text-success', bg: 'bg-success/20' },
failed: { icon: XCircle, color: 'text-destructive', bg: 'bg-destructive/20' },
cancelled: { icon: AlertCircle, color: 'text-muted-foreground', bg: 'bg-muted' },
};
function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
const { formatMessage } = useIntl();
const { data, isLoading } = useWorkflowStatusCounts();
const { stats, isLoading: statsLoading } = useDashboardStats({ refetchInterval: 60000 });
const { projectOverview, isLoading: projectLoading } = useProjectOverview();
// Get coordinator state
const coordinatorState = useCoordinatorStore();
const orchestratorConfig = orchestratorStatusConfig[coordinatorState.status] || orchestratorStatusConfig.idle;
const OrchestratorIcon = orchestratorConfig.icon;
const chartData = data || generateMockWorkflowStatusCounts();
const total = chartData.reduce((sum, item) => sum + item.count, 0);
// Generate sparkline data for each stat
const sparklines = useMemo(() => ({
activeSessions: generateSparklineData(stats?.activeSessions ?? 0, 0.4),
totalTasks: generateSparklineData(stats?.totalTasks ?? 0, 0.3),
completedTasks: generateSparklineData(stats?.completedTasks ?? 0, 0.25),
pendingTasks: generateSparklineData(stats?.pendingTasks ?? 0, 0.35),
failedTasks: generateSparklineData(stats?.failedTasks ?? 0, 0.5),
todayActivity: generateSparklineData(stats?.todayActivity ?? 0, 0.6),
}), [stats]);
// Calculate orchestrator progress
const orchestratorProgress = coordinatorState.commandChain.length > 0
? Math.round((coordinatorState.commandChain.filter(n => n.status === 'completed').length / coordinatorState.commandChain.length) * 100)
: 0;
// Project info expanded state
const [projectExpanded, setProjectExpanded] = useState(false);
// Session carousel state
const [currentSessionIndex, setCurrentSessionIndex] = useState(0);
const currentSession = MOCK_SESSIONS[currentSessionIndex];
// Auto-rotate carousel every 5 seconds
useEffect(() => {
const timer = setInterval(() => {
setCurrentSessionIndex((prev) => (prev + 1) % MOCK_SESSIONS.length);
}, 5000);
return () => clearInterval(timer);
}, []);
// Manual navigation
const handlePrevSession = () => {
setCurrentSessionIndex((prev) => (prev === 0 ? MOCK_SESSIONS.length - 1 : prev - 1));
};
const handleNextSession = () => {
setCurrentSessionIndex((prev) => (prev + 1) % MOCK_SESSIONS.length);
};
return (
<div className={cn('flex flex-col gap-2', className)}>
{/* Project Info Banner - Separate Card */}
<Card className="shrink-0">
{projectLoading ? (
<div className="px-4 py-3 flex items-center gap-4">
<div className="h-5 w-32 bg-muted rounded animate-pulse" />
<div className="h-4 w-48 bg-muted rounded animate-pulse" />
</div>
) : (
<>
{/* Collapsed Header */}
<div className="px-4 py-3 flex items-center gap-6 flex-wrap">
{/* Project Name & Icon */}
<div className="flex items-center gap-2.5 min-w-0">
<div className="p-1.5 rounded-md bg-primary/10">
<Code2 className="h-4 w-4 text-primary" />
</div>
<div className="min-w-0">
<h2 className="text-sm font-semibold text-foreground truncate">
{projectOverview?.projectName || 'Claude Code Workflow'}
</h2>
<p className="text-[10px] text-muted-foreground truncate max-w-[280px]">
{projectOverview?.description || 'AI-powered workflow management system'}
</p>
</div>
</div>
{/* Divider */}
<div className="h-8 w-px bg-border hidden md:block" />
{/* Tech Stack Badges */}
<div className="flex items-center gap-2 text-[10px]">
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-blue-500/10 text-blue-600 font-medium">
<Code2 className="h-3 w-3" />
{projectOverview?.technologyStack?.languages?.[0]?.name || 'TypeScript'}
</span>
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-green-500/10 text-green-600 font-medium">
<Server className="h-3 w-3" />
{projectOverview?.technologyStack?.frameworks?.[0] || 'Node.js'}
</span>
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-violet-500/10 text-violet-600 font-medium">
<Layers className="h-3 w-3" />
{projectOverview?.architecture?.style || 'Modular Monolith'}
</span>
{projectOverview?.technologyStack?.buildTools?.[0] && (
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-orange-500/10 text-orange-600 font-medium">
<Wrench className="h-3 w-3" />
{projectOverview.technologyStack.buildTools[0]}
</span>
)}
</div>
{/* Divider */}
<div className="h-8 w-px bg-border hidden lg:block" />
{/* Quick Stats */}
<div className="flex items-center gap-4 text-[10px]">
<div className="flex items-center gap-1.5 text-emerald-600">
<Sparkles className="h-3 w-3" />
<span className="font-semibold">{projectOverview?.developmentIndex?.feature?.length || 0}</span>
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.features' })}</span>
</div>
<div className="flex items-center gap-1.5 text-amber-600">
<Bug className="h-3 w-3" />
<span className="font-semibold">{projectOverview?.developmentIndex?.bugfix?.length || 0}</span>
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.bugfixes' })}</span>
</div>
<div className="flex items-center gap-1.5 text-blue-600">
<FileCode className="h-3 w-3" />
<span className="font-semibold">{projectOverview?.developmentIndex?.enhancement?.length || 0}</span>
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}</span>
</div>
</div>
{/* Date + Expand Button */}
<div className="flex items-center gap-3 text-[10px] text-muted-foreground ml-auto">
<span className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-muted/50">
<Calendar className="h-3 w-3" />
{projectOverview?.initializedAt ? new Date(projectOverview.initializedAt).toLocaleDateString() : new Date().toLocaleDateString()}
</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 hover:bg-muted"
onClick={() => setProjectExpanded(!projectExpanded)}
>
{projectExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
</div>
</div>
{/* Expanded Details */}
{projectExpanded && (
<div className="px-3 pb-2 grid grid-cols-4 gap-3 border-t border-border/50 pt-2">
{/* Architecture */}
<div className="space-y-1">
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
<Layers className="h-3 w-3" />
{formatMessage({ id: 'projectOverview.architecture.title' })}
</h4>
<div className="space-y-0.5">
<p className="text-[10px] text-foreground">{projectOverview?.architecture?.style || 'Modular Monolith'}</p>
<div className="flex flex-wrap gap-1">
{projectOverview?.architecture?.layers?.slice(0, 3).map((layer, i) => (
<span key={i} className="text-[9px] px-1 py-0.5 rounded bg-muted text-muted-foreground">{layer}</span>
))}
</div>
</div>
</div>
{/* Key Components */}
<div className="space-y-1">
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
<Wrench className="h-3 w-3" />
{formatMessage({ id: 'projectOverview.components.title' })}
</h4>
<div className="space-y-0.5">
{projectOverview?.keyComponents?.slice(0, 3).map((comp, i) => (
<p key={i} className="text-[9px] text-foreground truncate">{comp.name}</p>
)) || (
<>
<p className="text-[9px] text-foreground">Session Manager</p>
<p className="text-[9px] text-foreground">Dashboard Generator</p>
<p className="text-[9px] text-foreground">Data Aggregator</p>
</>
)}
</div>
</div>
{/* Development History */}
<div className="space-y-1">
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
<FileCode className="h-3 w-3" />
{formatMessage({ id: 'projectOverview.devIndex.title' })}
</h4>
<div className="flex flex-wrap gap-1.5">
<span className="flex items-center gap-0.5 text-[9px] text-emerald-600">
<Sparkles className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.feature?.length || 0}
</span>
<span className="flex items-center gap-0.5 text-[9px] text-blue-600">
<FileCode className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.enhancement?.length || 0}
</span>
<span className="flex items-center gap-0.5 text-[9px] text-amber-600">
<Bug className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.bugfix?.length || 0}
</span>
<span className="flex items-center gap-0.5 text-[9px] text-violet-600">
<Wrench className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.refactor?.length || 0}
</span>
<span className="flex items-center gap-0.5 text-[9px] text-slate-600">
<BookOpen className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.docs?.length || 0}
</span>
</div>
</div>
{/* Design Patterns */}
<div className="space-y-1">
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
<GitBranch className="h-3 w-3" />
{formatMessage({ id: 'projectOverview.architecture.patterns' })}
</h4>
<div className="flex flex-wrap gap-1">
{projectOverview?.architecture?.patterns?.slice(0, 4).map((pattern, i) => (
<span key={i} className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">{pattern}</span>
)) || (
<>
<span className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">Factory</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">Strategy</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">Observer</span>
</>
)}
</div>
</div>
</div>
)}
</>
)}
</Card>
{/* Main content Card: Stats | Workflow+Orchestrator | Task Details */}
<Card className="h-[320px] flex shrink-0 overflow-hidden">
{/* Compact Stats Section with Sparklines */}
<div className="w-[28%] p-2.5 flex flex-col border-r border-border">
<h3 className="text-xs font-semibold text-foreground mb-2 px-0.5">
{formatMessage({ id: 'home.sections.statistics' })}
</h3>
{statsLoading ? (
<div className="grid grid-cols-2 gap-1.5 flex-1">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="h-14 bg-muted rounded animate-pulse" />
))}
</div>
) : (
<div className="grid grid-cols-2 gap-1.5 flex-1 content-start overflow-auto">
<MiniStatCard
icon={FolderKanban}
title={formatMessage({ id: 'home.stats.activeSessions' })}
value={stats?.activeSessions ?? 0}
variant="primary"
sparklineData={sparklines.activeSessions}
/>
<MiniStatCard
icon={ListChecks}
title={formatMessage({ id: 'home.stats.totalTasks' })}
value={stats?.totalTasks ?? 0}
variant="info"
sparklineData={sparklines.totalTasks}
/>
<MiniStatCard
icon={CheckCircle2}
title={formatMessage({ id: 'home.stats.completedTasks' })}
value={stats?.completedTasks ?? 0}
variant="success"
sparklineData={sparklines.completedTasks}
/>
<MiniStatCard
icon={Clock}
title={formatMessage({ id: 'home.stats.pendingTasks' })}
value={stats?.pendingTasks ?? 0}
variant="warning"
sparklineData={sparklines.pendingTasks}
/>
<MiniStatCard
icon={XCircle}
title={formatMessage({ id: 'common.status.failed' })}
value={stats?.failedTasks ?? 0}
variant="danger"
sparklineData={sparklines.failedTasks}
/>
<MiniStatCard
icon={Activity}
title={formatMessage({ id: 'common.stats.todayActivity' })}
value={stats?.todayActivity ?? 0}
variant="default"
sparklineData={sparklines.todayActivity}
/>
</div>
)}
</div>
{/* Workflow Status + Orchestrator Status Section */}
<div className="w-[26%] p-3 flex flex-col border-r border-border overflow-auto">
{/* Workflow Status */}
<h3 className="text-xs font-semibold text-foreground mb-2">
{formatMessage({ id: 'home.widgets.workflowStatus' })}
</h3>
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-3 bg-muted rounded animate-pulse" />
))}
</div>
) : (
<div className="space-y-2">
{chartData.map((item) => {
const percentage = total > 0 ? Math.round((item.count / total) * 100) : 0;
const colors = statusColors[item.status] || statusColors.completed;
return (
<div key={item.status} className="space-y-0.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<div className={cn('w-1.5 h-1.5 rounded-full', colors.dot)} />
<span className="text-[11px] text-foreground">
{formatMessage({ id: statusLabelKeys[item.status] })}
</span>
<span className="text-[11px] text-muted-foreground">
{item.count}
</span>
</div>
<span className={cn('text-[11px] font-medium', colors.text)}>
{percentage}%
</span>
</div>
<Progress
value={percentage}
className="h-1 bg-muted"
indicatorClassName={colors.bg}
/>
</div>
);
})}
</div>
)}
{/* Orchestrator Status Section */}
<div className="mt-3 pt-3 border-t border-border">
<h3 className="text-xs font-semibold text-foreground mb-2">
{formatMessage({ id: 'navigation.main.orchestrator' })}
</h3>
<div className={cn('rounded-lg p-2', orchestratorConfig.bg)}>
<div className="flex items-center gap-2">
<OrchestratorIcon className={cn('h-4 w-4', orchestratorConfig.color, coordinatorState.status === 'running' && 'animate-pulse')} />
<div className="flex-1 min-w-0">
<p className={cn('text-[11px] font-medium', orchestratorConfig.color)}>
{formatMessage({ id: `common.status.${coordinatorState.status}` })}
</p>
{coordinatorState.currentExecutionId && (
<p className="text-[10px] text-muted-foreground truncate">
{coordinatorState.pipelineDetails?.nodes[0]?.name || coordinatorState.currentExecutionId}
</p>
)}
</div>
</div>
{coordinatorState.status !== 'idle' && coordinatorState.commandChain.length > 0 && (
<div className="mt-2 space-y-1">
<div className="flex items-center justify-between text-[10px]">
<span className="text-muted-foreground">
{formatMessage({ id: 'common.labels.progress' })}
</span>
<span className="font-medium">{orchestratorProgress}%</span>
</div>
<Progress value={orchestratorProgress} className="h-1 bg-muted/50" />
<p className="text-[10px] text-muted-foreground">
{coordinatorState.commandChain.filter(n => n.status === 'completed').length}/{coordinatorState.commandChain.length} {formatMessage({ id: 'coordinator.steps' })}
</p>
</div>
)}
</div>
</div>
</div>
{/* Task Details Section: Session Carousel with Task List */}
<div className="w-[46%] p-3 flex flex-col">
{/* Header with navigation */}
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold text-foreground flex items-center gap-1">
<ListChecks className="h-3.5 w-3.5" />
{formatMessage({ id: 'home.sections.taskDetails' })}
</h3>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" className="h-5 w-5 p-0" onClick={handlePrevSession}>
<ChevronLeft className="h-3 w-3" />
</Button>
<span className="text-[10px] text-muted-foreground min-w-[40px] text-center">
{currentSessionIndex + 1} / {MOCK_SESSIONS.length}
</span>
<Button variant="ghost" size="sm" className="h-5 w-5 p-0" onClick={handleNextSession}>
<ChevronRight className="h-3 w-3" />
</Button>
</div>
</div>
{/* Session Card (Carousel Item) */}
{currentSession && (
<div className="flex-1 flex flex-col min-h-0 rounded-lg border border-border bg-accent/20 p-2.5 overflow-hidden">
{/* Session Header */}
<div className="mb-2 pb-2 border-b border-border shrink-0">
<div className="flex items-start gap-2">
<div className={cn('px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0', sessionStatusColors[currentSession.status].bg, sessionStatusColors[currentSession.status].text)}>
{formatMessage({ id: `common.status.${currentSession.status === 'in_progress' ? 'inProgress' : currentSession.status}` })}
</div>
<div className="flex-1 min-w-0">
<p className="text-[11px] font-medium text-foreground truncate">{currentSession.name}</p>
<p className="text-[10px] text-muted-foreground">{currentSession.id}</p>
</div>
</div>
{/* Description */}
{currentSession.description && (
<p className="text-[10px] text-muted-foreground mt-1.5 line-clamp-2">
{currentSession.description}
</p>
)}
{/* Progress bar */}
<div className="mt-2 space-y-1">
<div className="flex items-center justify-between text-[10px]">
<span className="text-muted-foreground">
{formatMessage({ id: 'common.labels.progress' })}
</span>
<span className="font-medium text-foreground">
{currentSession.tasks.filter(t => t.status === 'completed').length}/{currentSession.tasks.length}
</span>
</div>
<Progress
value={currentSession.tasks.length > 0 ? (currentSession.tasks.filter(t => t.status === 'completed').length / currentSession.tasks.length) * 100 : 0}
className="h-1 bg-muted"
indicatorClassName="bg-success"
/>
</div>
{/* Tags and Date */}
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
{currentSession.tags.map((tag) => (
<span key={tag} className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-primary/10 text-primary text-[9px]">
<Tag className="h-2 w-2" />
{tag}
</span>
))}
<span className="inline-flex items-center gap-0.5 text-[9px] text-muted-foreground ml-auto">
<Calendar className="h-2.5 w-2.5" />
{currentSession.updatedAt}
</span>
</div>
</div>
{/* Task List for this Session - Two columns */}
<div className="flex-1 overflow-auto min-h-0">
<div className="grid grid-cols-2 gap-1">
{currentSession.tasks.map((task) => {
const config = taskStatusColors[task.status];
const StatusIcon = config.icon;
return (
<div
key={task.id}
className="flex items-center gap-1.5 p-1.5 rounded hover:bg-background/50 transition-colors cursor-pointer"
>
<div className={cn('p-0.5 rounded shrink-0', config.bg)}>
<StatusIcon className={cn('h-2.5 w-2.5', config.text)} />
</div>
<p className={cn('flex-1 text-[10px] font-medium truncate', task.status === 'completed' ? 'text-muted-foreground line-through' : 'text-foreground')}>
{task.name}
</p>
</div>
);
})}
</div>
</div>
</div>
)}
{/* Carousel dots */}
<div className="flex items-center justify-center gap-1 mt-2">
{MOCK_SESSIONS.map((_, idx) => (
<button
key={idx}
onClick={() => setCurrentSessionIndex(idx)}
className={cn(
'w-1.5 h-1.5 rounded-full transition-colors',
idx === currentSessionIndex ? 'bg-primary' : 'bg-muted hover:bg-muted-foreground/50'
)}
/>
))}
</div>
</div>
</Card>
</div>
);
}
export const WorkflowTaskWidget = memo(WorkflowTaskWidgetComponent);
export default WorkflowTaskWidget;

View File

@@ -22,7 +22,6 @@ import {
Clock,
Zap,
GitFork,
Activity,
Shield,
History,
Server,
@@ -81,7 +80,6 @@ const navGroupDefinitions: NavGroupDef[] = [
{ path: '/lite-tasks', labelKey: 'navigation.main.liteTasks', icon: Zap },
{ path: '/orchestrator', labelKey: 'navigation.main.orchestrator', icon: Workflow },
{ path: '/coordinator', labelKey: 'navigation.main.coordinator', icon: GitFork },
{ path: '/executions', labelKey: 'navigation.main.executions', icon: Activity },
{ path: '/loops', labelKey: 'navigation.main.loops', icon: RefreshCw },
{ path: '/history', labelKey: 'navigation.main.history', icon: Clock },
],

View File

@@ -5,7 +5,8 @@
import { memo } from 'react';
import { useIntl } from 'react-intl';
import { X, Activity, ChevronDown } from 'lucide-react';
import { X, Activity, ChevronDown, ExternalLink } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
@@ -18,6 +19,8 @@ export interface MonitorHeaderProps {
totalCount?: number;
/** Number of executions with errors */
errorCount?: number;
/** Current execution ID for popup navigation */
currentExecutionId?: string;
}
/**
@@ -32,11 +35,21 @@ export const MonitorHeader = memo(function MonitorHeader({
activeCount = 0,
totalCount = 0,
errorCount = 0,
currentExecutionId,
}: MonitorHeaderProps) {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const hasActive = activeCount > 0;
const hasErrors = errorCount > 0;
const handlePopOut = () => {
const url = currentExecutionId
? `/cli-viewer?executionId=${currentExecutionId}`
: '/cli-viewer';
navigate(url);
onClose();
};
return (
<header
className={cn(
@@ -70,8 +83,20 @@ export const MonitorHeader = memo(function MonitorHeader({
</div>
</div>
{/* Right side: Status + Count badge */}
{/* Right side: Pop out + Status + Count badge */}
<div className="flex items-center gap-3 shrink-0">
{/* Pop out to full page button */}
<Button
variant="ghost"
size="icon"
onClick={handlePopOut}
className="h-8 w-8"
title={formatMessage({ id: 'cliMonitor.popOutToPage' })}
aria-label={formatMessage({ id: 'cliMonitor.openInViewer' })}
>
<ExternalLink className="h-4 w-4" />
</Button>
{/* Live status indicator */}
{hasActive && (
<div className="flex items-center gap-2">

View File

@@ -27,6 +27,12 @@ import {
CheckCircle2,
AlertCircle,
RefreshCw,
FileText,
Search,
TestTube,
File,
Settings,
Zap,
} from 'lucide-react';
import type { SessionMetadata } from '@/types/store';
@@ -70,6 +76,31 @@ const statusLabelKeys: Record<SessionMetadata['status'], string> = {
paused: 'sessions.status.paused',
};
// Type variant configuration for session type badges
const typeVariantConfig: Record<
SessionMetadata['type'],
{ variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info'; icon: React.ElementType }
> = {
review: { variant: 'info', icon: Search },
'tdd': { variant: 'success', icon: TestTube },
test: { variant: 'default', icon: FileText },
docs: { variant: 'warning', icon: File },
workflow: { variant: 'secondary', icon: Settings },
'lite-plan': { variant: 'default', icon: FileText },
'lite-fix': { variant: 'warning', icon: Zap },
};
// Type label keys for i18n
const typeLabelKeys: Record<SessionMetadata['type'], string> = {
review: 'sessions.type.review',
tdd: 'sessions.type.tdd',
test: 'sessions.type.test',
docs: 'sessions.type.docs',
workflow: 'sessions.type.workflow',
'lite-plan': 'sessions.type.lite-plan',
'lite-fix': 'sessions.type.lite-fix',
};
/**
* Format date to localized string
*/
@@ -150,6 +181,12 @@ export function SessionCard({
? formatMessage({ id: statusLabelKeys[session.status] })
: formatMessage({ id: 'common.status.unknown' });
// Type badge configuration (graceful degradation when type is undefined)
const typeConfig = session.type ? typeVariantConfig[session.type] : null;
const typeLabel = session.type && typeLabelKeys[session.type]
? formatMessage({ id: typeLabelKeys[session.type] })
: null;
const progress = calculateProgress(session.tasks);
const isPlanning = session.status === 'planning';
const isArchived = session.status === 'archived' || session.location === 'archived';
@@ -199,6 +236,12 @@ export function SessionCard({
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Badge variant={statusVariant}>{statusLabel}</Badge>
{typeConfig && typeLabel && (
<Badge variant={typeConfig.variant} className="gap-1">
<typeConfig.icon className="h-3 w-3" />
{typeLabel}
</Badge>
)}
{showActions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@@ -21,6 +21,8 @@ const badgeVariants = cva(
"border-transparent bg-warning text-white",
info:
"border-transparent bg-info text-white",
review:
"border-transparent bg-purple-600 text-white",
},
},
defaultVariants: {

View File

@@ -0,0 +1,22 @@
// ========================================
// DropdownMenu Component Re-export
// ========================================
// Re-export from Dropdown.tsx for consistent naming
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
} from './Dropdown';

View File

@@ -4,8 +4,10 @@ import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string;
}
>(({ className, value, indicatorClassName, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
@@ -15,7 +17,7 @@ const Progress = React.forwardRef<
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
className={cn("h-full w-full flex-1 bg-primary transition-all", indicatorClassName)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>

View File

@@ -0,0 +1,47 @@
// ========================================
// TabsNavigation Component
// ========================================
// Reusable tab navigation with underline style
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils';
export interface TabItem {
value: string;
label: string;
icon?: React.ReactNode;
badge?: React.ReactNode;
disabled?: boolean;
}
interface TabsNavigationProps {
value: string;
onValueChange: (value: string) => void;
tabs: TabItem[];
className?: string;
}
export function TabsNavigation({ value, onValueChange, tabs, className }: TabsNavigationProps) {
return (
<div className={cn("flex gap-2 border-b border-border", className)}>
{tabs.map((tab) => (
<Button
key={tab.value}
variant="ghost"
disabled={tab.disabled}
className={cn(
"border-b-2 rounded-none h-11 px-4 gap-2",
value === tab.value
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground"
)}
onClick={() => onValueChange(tab.value)}
>
{tab.icon}
{tab.label}
{tab.badge}
</Button>
))}
</div>
);
}

View File

@@ -3,10 +3,10 @@
// ========================================
// Convenient hook for locale management
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useAppStore, selectLocale } from '../stores/appStore';
import type { Locale } from '../types/store';
import { availableLocales } from '../lib/i18n';
import { availableLocales, formatMessage } from '../lib/i18n';
export interface UseLocaleReturn {
/** Current locale ('en' or 'zh') */
@@ -51,3 +51,25 @@ export function useLocale(): UseLocaleReturn {
availableLocales,
};
}
/**
* Hook to format i18n messages with the current locale
* @returns A formatMessage function for translating message IDs
*
* @example
* ```tsx
* const formatMessage = useFormatMessage();
* return <h1>{formatMessage('home.title')}</h1>;
* ```
*/
export function useFormatMessage(): (
id: string,
values?: Record<string, string | number | boolean | Date | null | undefined>
) => string {
// Use useMemo to avoid recreating the function on each render
return useMemo(() => {
return (id: string, values?: Record<string, string | number | boolean | Date | null | undefined>) => {
return formatMessage(id, values);
};
}, []);
}

View File

@@ -12,6 +12,9 @@ import { DEFAULT_DASHBOARD_LAYOUT } from '@/components/dashboard/defaultLayouts'
const DEBOUNCE_DELAY = 1000; // 1 second debounce for layout saves
const STORAGE_KEY = 'ccw-dashboard-layout';
// Version for layout schema - increment when widget IDs change
const LAYOUT_VERSION = 2; // v2: workflow-task + recent-sessions
export interface UseUserDashboardLayoutResult {
/** Current dashboard layouts */
layouts: DashboardLayouts;
@@ -59,8 +62,36 @@ export function useUserDashboardLayout(): UseUserDashboardLayoutResult {
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const isSavingRef = useRef(false);
// Initialize layout if not set
// Initialize layout if not set or version mismatch
useEffect(() => {
// Check if stored version matches current version
const storedVersion = localStorage.getItem(`${STORAGE_KEY}-version`);
const versionMismatch = storedVersion !== String(LAYOUT_VERSION);
if (versionMismatch) {
// Version mismatch - reset to default and update version
console.log(`Dashboard layout version changed (${storedVersion} -> ${LAYOUT_VERSION}), resetting to default`);
localStorage.removeItem(STORAGE_KEY);
localStorage.setItem(`${STORAGE_KEY}-version`, String(LAYOUT_VERSION));
// Also clear dashboardLayout from Zustand persist storage
try {
const zustandStorage = localStorage.getItem('ccw-app-store');
if (zustandStorage) {
const parsed = JSON.parse(zustandStorage);
if (parsed.state?.dashboardLayout) {
delete parsed.state.dashboardLayout;
localStorage.setItem('ccw-app-store', JSON.stringify(parsed));
}
}
} catch (e) {
console.warn('Failed to clear Zustand dashboard layout:', e);
}
resetDashboardLayout();
return;
}
if (!dashboardLayout) {
// Try to load from localStorage first
try {
@@ -96,9 +127,10 @@ export function useUserDashboardLayout(): UseUserDashboardLayoutResult {
// Update Zustand store (which will persist to localStorage)
setDashboardLayouts(newLayouts);
// Also save to additional localStorage backup
// Also save to additional localStorage backup with version
const currentWidgets = dashboardLayout?.widgets || DEFAULT_DASHBOARD_LAYOUT.widgets;
setLocalStorageLayout({ layouts: newLayouts, widgets: currentWidgets });
localStorage.setItem(`${STORAGE_KEY}-version`, String(LAYOUT_VERSION));
// TODO: When backend API is ready, uncomment this:
// syncToBackend({ layouts: newLayouts, widgets: currentWidgets });
@@ -114,9 +146,10 @@ export function useUserDashboardLayout(): UseUserDashboardLayoutResult {
(newWidgets: WidgetConfig[]) => {
setDashboardWidgets(newWidgets);
// Also save to localStorage backup
// Also save to localStorage backup with version
const currentLayouts = dashboardLayout?.layouts || DEFAULT_DASHBOARD_LAYOUT.layouts;
setLocalStorageLayout({ layouts: currentLayouts, widgets: newWidgets });
localStorage.setItem(`${STORAGE_KEY}-version`, String(LAYOUT_VERSION));
// TODO: When backend API is ready, uncomment this:
// syncToBackend({ layouts: currentLayouts, widgets: newWidgets });
@@ -134,8 +167,9 @@ export function useUserDashboardLayout(): UseUserDashboardLayoutResult {
// Reset Zustand store
resetDashboardLayout();
// Reset localStorage backup
// Reset localStorage backup with version
setLocalStorageLayout(DEFAULT_DASHBOARD_LAYOUT);
localStorage.setItem(`${STORAGE_KEY}-version`, String(LAYOUT_VERSION));
// TODO: When backend API is ready, uncomment this:
// syncToBackend(DEFAULT_DASHBOARD_LAYOUT);

View File

@@ -177,6 +177,41 @@ async function fetchApi<T>(
// ========== Transformation Helpers ==========
/**
* Infer session type from session_id pattern (matches backend logic)
* Used as fallback when backend.type field is missing
*
* @param sessionId - Session ID to analyze
* @returns Inferred session type
*
* @see ccw/src/core/session-scanner.ts:inferTypeFromName for backend implementation
*/
function inferTypeFromName(sessionId: string): SessionMetadata['type'] {
const name = sessionId.toLowerCase();
if (name.includes('-review-') || name.includes('-code-review-')) {
return 'review';
}
if (name.includes('-tdd-') || name.includes('-test-driven-')) {
return 'tdd';
}
if (name.includes('-test-') || name.includes('-testing-')) {
return 'test';
}
if (name.includes('-docs-') || name.includes('-doc-') || name.includes('-documentation-')) {
return 'docs';
}
if (name.includes('-lite-plan-')) {
return 'lite-plan';
}
if (name.includes('-lite-fix-') || name.includes('-fix-')) {
return 'lite-fix';
}
// Default to workflow for standard sessions
return 'workflow';
}
/**
* Transform backend session data to frontend SessionMetadata interface
* Maps backend schema (project, status: 'active') to frontend schema (title, description, status: 'in_progress', location)
@@ -212,8 +247,14 @@ function transformBackendSession(
description = parts.slice(1).join(':').trim();
}
// Preserve type field from backend, or infer from session_id pattern
// Multi-level type detection: backend.type > infer from name
const sessionType = (backendSession.type as SessionMetadata['type']) ||
inferTypeFromName(backendSession.session_id);
return {
session_id: backendSession.session_id,
type: sessionType,
title,
description,
status: transformedStatus,

View File

@@ -45,5 +45,7 @@
"tokens": "Tokens: {count}",
"duration": "Duration: {value}",
"model": "Model: {name}",
"user": "User"
"user": "User",
"popOutToPage": "Open in Full Page",
"openInViewer": "Open in CLI Viewer"
}

View File

@@ -0,0 +1,51 @@
{
"page": {
"title": "CLI Viewer",
"subtitle": "{count, plural, =0 {No active sessions} one {# active session} other {# active sessions}}"
},
"layout": {
"title": "Layout",
"single": "Single",
"splitH": "Split Horizontal",
"splitV": "Split Vertical",
"grid": "Grid 2x2"
},
"pane": {
"empty": "No execution selected",
"selectExecution": "Select Execution",
"loading": "Loading output for {executionId}...",
"close": "Close",
"maximize": "Maximize",
"minimize": "Minimize"
},
"toolbar": {
"refresh": "Refresh",
"clearAll": "Clear All",
"settings": "Settings"
},
"emptyState": {
"title": "No CLI Executions",
"description": "Select an execution from the sidebar or start a new CLI session to view output here.",
"action": "View Executions"
},
"tabs": {
"noTabs": "No tabs open",
"addTab": "Add tab",
"closeTab": "Close tab",
"pinTab": "Pin tab",
"unpinTab": "Unpin tab"
},
"picker": {
"selectExecution": "Select Execution",
"searchExecutions": "Search executions...",
"noExecutions": "No executions available",
"noMatchingExecutions": "No matching executions",
"alreadyOpen": "Already open",
"executionCount": "{available} of {total} executions available"
},
"paneActions": {
"splitHorizontal": "Split Horizontal",
"splitVertical": "Split Vertical",
"closePane": "Close Pane"
}
}

View File

@@ -59,12 +59,16 @@
"inactive": "Inactive",
"pending": "Pending",
"inProgress": "In Progress",
"running": "Running",
"initializing": "Initializing",
"planning": "Planning",
"completed": "Completed",
"failed": "Failed",
"blocked": "Blocked",
"cancelled": "Cancelled",
"paused": "Paused",
"archived": "Archived",
"idle": "Idle",
"unknown": "Unknown",
"draft": "Draft",
"published": "Published",
@@ -91,7 +95,10 @@
"months": "months",
"years": "years",
"ago": "ago",
"justNow": "just now"
"justNow": "just now",
"minutesAgo": "{count}m ago",
"hoursAgo": "{count}h ago",
"daysAgo": "{count}d ago"
},
"buttons": {
"new": "New",
@@ -164,7 +171,11 @@
"todayActivity": "Today's Activity",
"totalCommands": "Total Commands",
"totalSkills": "Total Skills",
"categories": "Categories"
"categories": "Categories",
"total": "Total"
},
"labels": {
"progress": "Progress"
},
"dialog": {
"createSession": "Create New Session",
@@ -200,6 +211,15 @@
"disconnected": "Ticker disconnected",
"aria_label": "Real-time activity ticker"
},
"dashboard": {
"config": {
"title": "Widgets",
"widgets": "Dashboard Widgets",
"hideAll": "Hide All",
"showAll": "Show All",
"resetLayout": "Reset Layout"
}
},
"all": "All",
"yes": "Yes",
"no": "No",
@@ -252,78 +272,6 @@
"no": "No",
"required": "This question is required"
},
"coordinator": {
"page": {
"title": "Coordinator",
"status": "Status: {status}",
"startButton": "Start Coordinator",
"noNodeSelected": "Select a node to view details"
},
"modal": {
"title": "Start Coordinator",
"description": "Describe the task you want the coordinator to execute"
},
"form": {
"taskDescription": "Task Description",
"taskDescriptionPlaceholder": "Describe what you want the coordinator to do (minimum 10 characters)...",
"parameters": "Parameters (Optional)",
"parametersPlaceholder": "{\"key\": \"value\"}",
"parametersHelp": "Optional JSON parameters for the coordinator execution",
"characterCount": "{current} / {max} characters (min: {min})",
"start": "Start Coordinator",
"starting": "Starting..."
},
"validation": {
"taskDescriptionRequired": "Task description is required",
"taskDescriptionTooShort": "Task description must be at least 10 characters",
"taskDescriptionTooLong": "Task description must not exceed 2000 characters",
"parametersInvalidJson": "Parameters must be valid JSON",
"answerRequired": "An answer is required"
},
"success": {
"started": "Coordinator started successfully"
},
"status": {
"pending": "Pending",
"running": "Running",
"completed": "Completed",
"failed": "Failed",
"skipped": "Skipped"
},
"logs": "Logs",
"entries": "entries",
"error": "Error",
"output": "Output",
"startedAt": "Started At",
"completedAt": "Completed At",
"retrying": "Retrying...",
"retry": "Retry",
"skipping": "Skipping...",
"skip": "Skip",
"logLevel": "Log Level",
"level": {
"all": "All",
"info": "Info",
"warn": "Warning",
"error": "Error",
"debug": "Debug"
},
"noLogs": "No logs available",
"question": {
"answer": "Answer",
"textPlaceholder": "Enter your answer...",
"selectOne": "Select One",
"selectMultiple": "Select Multiple",
"confirm": "Confirm",
"yes": "Yes",
"no": "No",
"submitting": "Submitting...",
"submit": "Submit"
},
"error": {
"submitFailed": "Failed to submit answer"
}
},
"feedback": {
"error": {
"network": "Network error. Please check your connection and try again.",

View File

@@ -0,0 +1,141 @@
{
"page": {
"title": "Coordinator",
"status": "Status: {status}",
"startButton": "Start Coordinator",
"noNodeSelected": "Select a node to view details"
},
"taskDetail": {
"title": "Task Details",
"noSelection": "Select a task to view execution details"
},
"emptyState": {
"title": "Workflow Coordinator",
"subtitle": "Intelligent task orchestration with real-time monitoring for complex workflows",
"startButton": "Launch Coordinator",
"feature1": {
"title": "Intelligent Execution",
"description": "Smart task orchestration with dependency management and parallel execution"
},
"feature2": {
"title": "Real-time Monitoring",
"description": "Pipeline visualization with detailed logs and execution metrics"
},
"feature3": {
"title": "Flexible Control",
"description": "Interactive control with retry, skip, and pause capabilities"
},
"quickStart": {
"title": "Quick Start",
"step1": "Click the 'Launch Coordinator' button to begin",
"step2": "Describe your workflow task in natural language",
"step3": "Monitor execution pipeline and interact with running tasks"
}
},
"multiStep": {
"step1": {
"title": "Welcome to Coordinator",
"subtitle": "Intelligent workflow orchestration for automated task execution",
"feature1": { "title": "Intelligent Execution", "description": "Smart task orchestration with dependency management and parallel execution" },
"feature2": { "title": "Real-time Monitoring", "description": "Pipeline visualization with detailed logs and execution metrics" },
"feature3": { "title": "Flexible Control", "description": "Interactive control with retry, skip, and pause capabilities" }
},
"step2": {
"title": "Configure Parameters",
"subtitle": "Select a template or customize parameters",
"templateLabel": "Select Template",
"templates": {
"featureDev": "Feature Development",
"apiIntegration": "API Integration",
"performanceOptimization": "Performance Optimization",
"documentGeneration": "Document Generation"
},
"taskName": "Task Name",
"taskNamePlaceholder": "Enter task name...",
"taskDescription": "Task Description",
"taskDescriptionPlaceholder": "Describe your task requirements in detail...",
"customParameters": "Custom Parameters"
},
"progress": { "step": "Step {current} / {total}" },
"actions": { "next": "Next", "back": "Back", "cancel": "Cancel", "submit": "Submit" }
},
"modal": {
"title": "Start Coordinator",
"description": "Describe the task you want the coordinator to execute"
},
"form": {
"taskDescription": "Task Description",
"taskDescriptionPlaceholder": "Describe what you want the coordinator to do (min 10 characters)...",
"parameters": "Parameters (Optional)",
"parametersPlaceholder": "{\"key\": \"value\"}",
"parametersHelp": "Optional JSON parameters for coordinator execution",
"characterCount": "{current} / {max} characters (min: {min})",
"start": "Start Coordinator",
"starting": "Starting..."
},
"validation": {
"taskDescriptionRequired": "Task description is required",
"taskDescriptionTooShort": "Task description must be at least 10 characters",
"taskDescriptionTooLong": "Task description must not exceed 2000 characters",
"parametersInvalidJson": "Parameters must be valid JSON",
"answerRequired": "An answer is required"
},
"success": {
"started": "Coordinator started successfully"
},
"status": {
"pending": "Pending",
"running": "Running",
"completed": "Completed",
"failed": "Failed",
"skipped": "Skipped"
},
"logs": "Logs",
"entries": "entries",
"error": "Error",
"output": "Output",
"startedAt": "Started At",
"completedAt": "Completed At",
"retrying": "Retrying...",
"retry": "Retry",
"skipping": "Skipping...",
"skip": "Skip",
"logLevel": "Log Level",
"level": {
"all": "All",
"info": "Info",
"warn": "Warning",
"error": "Error",
"debug": "Debug"
},
"noLogs": "No logs available",
"question": {
"answer": "Answer",
"textPlaceholder": "Enter your answer...",
"selectOne": "Select One",
"selectMultiple": "Select Multiple",
"confirm": "Confirm",
"yes": "Yes",
"no": "No",
"submitting": "Submitting...",
"submit": "Submit"
},
"taskList": {
"filter": {
"all": "All Tasks",
"running": "Running",
"completed": "Completed",
"failed": "Failed"
},
"sort": {
"time": "By Time",
"name": "By Name"
},
"empty": "No tasks found"
},
"taskCard": {
"nodes": "nodes",
"started": "started"
},
"steps": "steps"
}

View File

@@ -3,6 +3,9 @@
"title": "Execution Monitor",
"subtitle": "View real-time execution status and history"
},
"actions": {
"openCliViewer": "CLI Monitor"
},
"currentExecution": {
"title": "Current Execution",
"noExecution": "No workflow is currently executing",

View File

@@ -17,13 +17,26 @@
"sections": {
"statistics": "Statistics",
"recentSessions": "Recent Sessions",
"recentTasks": "Recent Tasks",
"activeLoops": "Active Loops",
"openIssues": "Open Issues",
"quickActions": "Quick Actions"
"quickActions": "Quick Actions",
"taskDetails": "Task Details"
},
"tabs": {
"allSessions": "All",
"allTasks": "All",
"workflow": "Workflow",
"liteTasks": "Lite Tasks",
"orchestrator": "Orchestrator",
"inProgress": "In Progress",
"planning": "Planning",
"completed": "Completed",
"paused": "Paused"
},
"widgets": {
"workflowStatus": "Workflow Status",
"activity": "Activity Timeline",
"activity": "Activity Heatmap",
"taskTypes": "Task Types"
},
"emptyState": {
@@ -31,6 +44,10 @@
"title": "No Sessions Found",
"message": "No workflow sessions match your current filter."
},
"noTasks": {
"title": "No Tasks",
"message": "No tasks match your current filter."
},
"noLoops": {
"title": "No Active Loops",
"message": "Start a new development loop to begin monitoring progress."
@@ -81,5 +98,8 @@
"errors": {
"loadFailed": "Failed to load dashboard data",
"retry": "Retry"
},
"project": {
"features": "features"
}
}

View File

@@ -9,6 +9,7 @@ import sessions from './sessions.json';
import issues from './issues.json';
import home from './home.json';
import orchestrator from './orchestrator.json';
import coordinator from './coordinator.json';
import loops from './loops.json';
import commands from './commands.json';
import memory from './memory.json';
@@ -37,6 +38,7 @@ import notification from './notification.json';
import notifications from './notifications.json';
import workspace from './workspace.json';
import help from './help.json';
import cliViewer from './cli-viewer.json';
/**
* Flattens nested JSON object to dot-separated keys
@@ -69,6 +71,7 @@ export default {
...flattenMessages(issues, 'issues'),
...flattenMessages(home, 'home'),
...flattenMessages(orchestrator, 'orchestrator'),
...flattenMessages(coordinator, 'coordinator'),
...flattenMessages(loops, 'loops'),
...flattenMessages(commands, 'commands'),
...flattenMessages(memory, 'memory'),
@@ -97,4 +100,5 @@ export default {
...flattenMessages(notifications, 'notifications'),
...flattenMessages(workspace, 'workspace'),
...flattenMessages(help, 'help'),
...flattenMessages(cliViewer, 'cliViewer'),
} as Record<string, string>;

View File

@@ -61,5 +61,11 @@
"explorations": "Explorations",
"context": "Context",
"diagnoses": "Diagnoses"
},
"status": {
"completed": "completed",
"inProgress": "in progress",
"blocked": "blocked",
"pending": "pending"
}
}

View File

@@ -14,7 +14,9 @@
"project": "Project",
"history": "History",
"orchestrator": "Orchestrator",
"coordinator": "Coordinator",
"loops": "Loop Monitor",
"cliViewer": "CLI Viewer",
"issues": "Issues",
"issueQueue": "Issue Queue",
"issueDiscovery": "Issue Discovery",
@@ -75,6 +77,8 @@
"noExecutionsHint": "Start a CLI command to see streaming output",
"noMessages": "Waiting for messages...",
"noMatch": "No matching messages found",
"openInViewer": "Open in CLI Viewer",
"popOutToPage": "Pop out to full page",
"statusBar": "{total} executions | {active} active | {errors} error | {lines} lines",
"copy": "Copy",
"copied": "Copied!",

View File

@@ -8,9 +8,23 @@
"low": "Low"
},
"stats": {
"total": "Total Findings",
"total": "Total",
"dimensions": "Dimensions"
},
"progress": {
"title": "Review Progress",
"totalFindings": "Total Findings",
"critical": "Critical",
"high": "High"
},
"dimensionTabs": {
"all": "All"
},
"filters": {
"severity": "Severity",
"sort": "Sort",
"reset": "Reset"
},
"search": {
"placeholder": "Search findings..."
},
@@ -23,16 +37,38 @@
"count": "{count} selected",
"selectAll": "Select All",
"clearAll": "Clear All",
"clear": "Clear"
"clear": "Clear",
"selectVisible": "Visible",
"selectCritical": "Critical"
},
"export": "Export Fix JSON",
"codeContext": "Code Context",
"rootCause": "Root Cause",
"impact": "Impact",
"recommendations": "Recommendations",
"fixProgress": {
"title": "Fix Progress",
"phase": {
"planning": "PLANNING",
"execution": "EXECUTION",
"completion": "COMPLETION"
},
"stats": {
"total": "Total",
"fixed": "Fixed",
"failed": "Failed",
"pending": "Pending"
},
"activeAgents": "Active Agent",
"activeAgentsPlural": "Active Agents",
"stage": "Stage",
"complete": "{percent}% Complete",
"working": "Working..."
},
"empty": {
"title": "No findings found",
"message": "Try adjusting your filters or search query."
"message": "Try adjusting your filters or search query.",
"noFixProgress": "No fix progress data available"
},
"notFound": {
"title": "Review Session Not Found",

View File

@@ -8,6 +8,15 @@
"archived": "Archived",
"paused": "Paused"
},
"type": {
"workflow": "Workflow",
"review": "Review",
"tdd": "TDD",
"test": "Test",
"docs": "Docs",
"lite-plan": "Lite Plan",
"lite-fix": "Lite Fix"
},
"actions": {
"viewDetails": "View Details",
"archive": "Archive",

View File

@@ -45,5 +45,7 @@
"tokens": "令牌: {count}",
"duration": "时长: {value}",
"model": "模型: {name}",
"user": "用户"
"user": "用户",
"popOutToPage": "在完整页面中打开",
"openInViewer": "在 CLI 查看器中打开"
}

View File

@@ -0,0 +1,51 @@
{
"page": {
"title": "CLI 查看器",
"subtitle": "{count, plural, =0 {暂无活动会话} other {# 个活动会话}}"
},
"layout": {
"title": "布局",
"single": "单窗格",
"splitH": "水平分割",
"splitV": "垂直分割",
"grid": "2x2 网格"
},
"pane": {
"empty": "未选择执行",
"selectExecution": "选择执行",
"loading": "正在加载 {executionId} 的输出...",
"close": "关闭",
"maximize": "最大化",
"minimize": "最小化"
},
"toolbar": {
"refresh": "刷新",
"clearAll": "清空所有",
"settings": "设置"
},
"emptyState": {
"title": "暂无 CLI 执行",
"description": "从侧边栏选择一个执行或启动新的 CLI 会话以在此查看输出。",
"action": "查看执行列表"
},
"tabs": {
"noTabs": "暂无标签页",
"addTab": "添加标签页",
"closeTab": "关闭标签页",
"pinTab": "固定标签页",
"unpinTab": "取消固定"
},
"picker": {
"selectExecution": "选择执行",
"searchExecutions": "搜索执行...",
"noExecutions": "暂无可用执行",
"noMatchingExecutions": "未找到匹配的执行",
"alreadyOpen": "已打开",
"executionCount": "{available}/{total} 个执行可用"
},
"paneActions": {
"splitHorizontal": "水平分割",
"splitVertical": "垂直分割",
"closePane": "关闭窗格"
}
}

View File

@@ -63,12 +63,16 @@
"inactive": "未激活",
"pending": "待处理",
"inProgress": "进行中",
"running": "运行中",
"initializing": "初始化中",
"planning": "规划中",
"completed": "已完成",
"failed": "失败",
"blocked": "已阻塞",
"cancelled": "已取消",
"paused": "已暂停",
"archived": "已归档",
"idle": "空闲",
"unknown": "未知",
"draft": "草稿",
"published": "已发布",
@@ -76,7 +80,7 @@
"deleting": "删除中...",
"label": "状态",
"openIssues": "开放问题",
"enabled": "Enabled",
"enabled": "已启用",
"disabled": "已禁用"
},
"priority": {
@@ -95,7 +99,10 @@
"months": "月",
"years": "年",
"ago": "前",
"justNow": "刚刚"
"justNow": "刚刚",
"minutesAgo": "{count}分钟前",
"hoursAgo": "{count}小时前",
"daysAgo": "{count}天前"
},
"buttons": {
"new": "新建",
@@ -168,7 +175,11 @@
"todayActivity": "今日活动",
"totalCommands": "总命令数",
"totalSkills": "总技能数",
"categories": "分类"
"categories": "分类",
"total": "总计"
},
"labels": {
"progress": "进度"
},
"dialog": {
"createSession": "创建新会话",
@@ -194,6 +205,15 @@
"button": "搜索文档"
}
},
"dashboard": {
"config": {
"title": "部件",
"widgets": "仪表板部件",
"hideAll": "全部隐藏",
"showAll": "全部显示",
"resetLayout": "重置布局"
}
},
"all": "全部",
"yes": "是",
"no": "否",
@@ -246,78 +266,6 @@
"no": "否",
"required": "此问题为必填项"
},
"coordinator": {
"page": {
"title": "协调器",
"status": "状态:{status}",
"startButton": "启动协调器",
"noNodeSelected": "选择节点以查看详细信息"
},
"modal": {
"title": "启动协调器",
"description": "描述您希望协调器执行的任务"
},
"form": {
"taskDescription": "任务描述",
"taskDescriptionPlaceholder": "描述协调器需要执行的任务至少10个字符...",
"parameters": "参数(可选)",
"parametersPlaceholder": "{\"key\": \"value\"}",
"parametersHelp": "协调器执行的可选JSON参数",
"characterCount": "{current} / {max} 字符(最少:{min}",
"start": "启动协调器",
"starting": "启动中..."
},
"validation": {
"taskDescriptionRequired": "任务描述为必填项",
"taskDescriptionTooShort": "任务描述至少需要10个字符",
"taskDescriptionTooLong": "任务描述不能超过2000个字符",
"parametersInvalidJson": "参数必须是有效的JSON格式",
"answerRequired": "答案为必填项"
},
"success": {
"started": "协调器启动成功"
},
"status": {
"pending": "待执行",
"running": "运行中",
"completed": "已完成",
"failed": "失败",
"skipped": "已跳过"
},
"logs": "日志",
"entries": "条日志",
"error": "错误",
"output": "输出",
"startedAt": "开始时间",
"completedAt": "完成时间",
"retrying": "重试中...",
"retry": "重试",
"skipping": "跳过中...",
"skip": "跳过",
"logLevel": "日志级别",
"level": {
"all": "全部",
"info": "信息",
"warn": "警告",
"error": "错误",
"debug": "调试"
},
"noLogs": "无可用日志",
"question": {
"answer": "答案",
"textPlaceholder": "输入您的答案...",
"selectOne": "选择一个",
"selectMultiple": "选择多个",
"confirm": "确认",
"yes": "是",
"no": "否",
"submitting": "提交中...",
"submit": "提交"
},
"error": {
"submitFailed": "提交答案失败"
}
},
"feedback": {
"error": {
"network": "网络错误,请检查您的连接并重试。",

View File

@@ -0,0 +1,157 @@
{
"page": {
"title": "协调器",
"status": "状态:{status}",
"startButton": "启动协调器",
"noNodeSelected": "选择节点以查看详细信息"
},
"taskDetail": {
"title": "任务详情",
"noSelection": "选择任务以查看执行详情"
},
"emptyState": {
"title": "欢迎使用工作流协调器",
"subtitle": "智能任务编排,实时执行监控,一站式管理复杂工作流",
"startButton": "启动协调器",
"feature1": {
"title": "智能执行",
"description": "依赖管理与并行执行的智能任务编排"
},
"feature2": {
"title": "实时监控",
"description": "流水线可视化,详细日志与执行指标"
},
"feature3": {
"title": "灵活控制",
"description": "支持重试、跳过和暂停的交互式控制"
},
"quickStart": {
"title": "快速开始",
"step1": "点击「启动协调器」按钮开始",
"step2": "用自然语言描述您的工作流任务",
"step3": "监控执行流水线,与运行中的任务交互"
}
},
"modal": {
"title": "启动协调器",
"description": "描述您希望协调器执行的任务"
},
"multiStep": {
"step1": {
"title": "欢迎使用协调器",
"subtitle": "智能工作流编排,助力任务自动化执行",
"feature1": {
"title": "智能执行",
"description": "依赖管理与并行执行的智能任务编排"
},
"feature2": {
"title": "实时监控",
"description": "流水线可视化,详细日志与执行指标"
},
"feature3": {
"title": "灵活控制",
"description": "支持重试、跳过和暂停的交互式控制"
}
},
"step2": {
"title": "配置参数",
"subtitle": "选择模板或自定义参数",
"templateLabel": "选择模板",
"templates": {
"featureDev": "功能开发",
"apiIntegration": "API 集成",
"performanceOptimization": "性能优化",
"documentGeneration": "文档生成"
},
"taskName": "任务名称",
"taskNamePlaceholder": "输入任务名称...",
"taskDescription": "任务描述",
"taskDescriptionPlaceholder": "详细描述您的任务需求...",
"customParameters": "自定义参数"
},
"progress": {
"step": "步骤 {current} / {total}"
},
"actions": {
"next": "下一步",
"back": "返回",
"cancel": "取消",
"submit": "提交"
}
},
"form": {
"taskDescription": "任务描述",
"taskDescriptionPlaceholder": "描述协调器需要执行的任务至少10个字符...",
"parameters": "参数(可选)",
"parametersPlaceholder": "{\"key\": \"value\"}",
"parametersHelp": "协调器执行的可选JSON参数",
"characterCount": "{current} / {max} 字符(最少:{min}",
"start": "启动协调器",
"starting": "启动中..."
},
"validation": {
"taskDescriptionRequired": "任务描述为必填项",
"taskDescriptionTooShort": "任务描述至少需要10个字符",
"taskDescriptionTooLong": "任务描述不能超过2000个字符",
"parametersInvalidJson": "参数必须是有效的JSON格式",
"answerRequired": "答案为必填项"
},
"success": {
"started": "协调器启动成功"
},
"status": {
"pending": "待执行",
"running": "运行中",
"completed": "已完成",
"failed": "失败",
"skipped": "已跳过"
},
"logs": "日志",
"entries": "条日志",
"error": "错误",
"output": "输出",
"startedAt": "开始时间",
"completedAt": "完成时间",
"retrying": "重试中...",
"retry": "重试",
"skipping": "跳过中...",
"skip": "跳过",
"logLevel": "日志级别",
"level": {
"all": "全部",
"info": "信息",
"warn": "警告",
"error": "错误",
"debug": "调试"
},
"noLogs": "暂无日志",
"question": {
"answer": "回答",
"textPlaceholder": "输入您的回答...",
"selectOne": "单选",
"selectMultiple": "多选",
"confirm": "确认",
"yes": "是",
"no": "否",
"submitting": "提交中...",
"submit": "提交"
},
"taskList": {
"filter": {
"all": "全部任务",
"running": "运行中",
"completed": "已完成",
"failed": "失败"
},
"sort": {
"time": "按时间",
"name": "按名称"
},
"empty": "暂无任务"
},
"taskCard": {
"nodes": "节点",
"started": "开始"
},
"steps": "步"
}

View File

@@ -3,6 +3,9 @@
"title": "执行监控",
"subtitle": "查看实时执行状态和历史记录"
},
"actions": {
"openCliViewer": "CLI 监控"
},
"currentExecution": {
"title": "当前执行",
"noExecution": "当前没有正在执行的工作流",

View File

@@ -17,13 +17,26 @@
"sections": {
"statistics": "统计",
"recentSessions": "最近会话",
"recentTasks": "最近任务",
"activeLoops": "活跃循环",
"openIssues": "开放问题",
"quickActions": "快速操作"
"quickActions": "快速操作",
"taskDetails": "任务详情"
},
"tabs": {
"allSessions": "全部",
"allTasks": "全部",
"workflow": "工作流",
"liteTasks": "轻量任务",
"orchestrator": "编排器",
"inProgress": "进行中",
"planning": "规划中",
"completed": "已完成",
"paused": "已暂停"
},
"widgets": {
"workflowStatus": "工作流状态",
"activity": "活动时间线",
"activity": "活动热图",
"taskTypes": "任务类型"
},
"emptyState": {
@@ -31,6 +44,10 @@
"title": "未找到会话",
"message": "没有符合当前筛选条件的工作流会话。"
},
"noTasks": {
"title": "暂无任务",
"message": "没有符合当前筛选条件的任务。"
},
"noLoops": {
"title": "无活跃循环",
"message": "启动新的开发循环以开始监控进度。"
@@ -81,5 +98,8 @@
"errors": {
"loadFailed": "加载仪表板数据失败",
"retry": "重试"
},
"project": {
"features": "个功能"
}
}

View File

@@ -9,6 +9,7 @@ import sessions from './sessions.json';
import issues from './issues.json';
import home from './home.json';
import orchestrator from './orchestrator.json';
import coordinator from './coordinator.json';
import loops from './loops.json';
import commands from './commands.json';
import memory from './memory.json';
@@ -37,6 +38,7 @@ import notification from './notification.json';
import notifications from './notifications.json';
import workspace from './workspace.json';
import help from './help.json';
import cliViewer from './cli-viewer.json';
/**
* Flattens nested JSON object to dot-separated keys
@@ -69,6 +71,7 @@ export default {
...flattenMessages(issues, 'issues'),
...flattenMessages(home, 'home'),
...flattenMessages(orchestrator, 'orchestrator'),
...flattenMessages(coordinator, 'coordinator'),
...flattenMessages(loops, 'loops'),
...flattenMessages(commands, 'commands'),
...flattenMessages(memory, 'memory'),
@@ -97,4 +100,5 @@ export default {
...flattenMessages(notifications, 'notifications'),
...flattenMessages(workspace, 'workspace'),
...flattenMessages(help, 'help'),
...flattenMessages(cliViewer, 'cliViewer'),
} as Record<string, string>;

View File

@@ -61,5 +61,11 @@
"explorations": "探索",
"context": "上下文",
"diagnoses": "诊断"
},
"status": {
"completed": "已完成",
"inProgress": "进行中",
"blocked": "已阻止",
"pending": "待处理"
}
}

View File

@@ -14,7 +14,9 @@
"project": "项目",
"history": "历史",
"orchestrator": "编排器",
"coordinator": "协调器",
"loops": "循环监控",
"cliViewer": "CLI 查看器",
"issues": "问题",
"issueQueue": "问题队列",
"issueDiscovery": "问题发现",
@@ -75,6 +77,8 @@
"noExecutionsHint": "启动 CLI 命令以查看流式输出",
"noMessages": "等待消息...",
"noMatch": "未找到匹配的消息",
"openInViewer": "在 CLI 查看器中打开",
"popOutToPage": "弹出到全页面",
"statusBar": "{total} 个执行 | {active} 个活跃 | {errors} 个错误 | {lines} 行",
"copy": "复制",
"copied": "已复制!",

View File

@@ -8,6 +8,15 @@
"archived": "已归档",
"paused": "已暂停"
},
"type": {
"workflow": "工作流",
"review": "审查",
"tdd": "TDD",
"test": "测试",
"docs": "文档",
"lite-plan": "轻量计划",
"lite-fix": "轻量修复"
},
"actions": {
"viewDetails": "查看详情",
"archive": "归档",

View File

@@ -10,7 +10,7 @@ import {
RefreshCw,
} from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import {
ProviderList,
ProviderModal,
@@ -198,26 +198,21 @@ export function ApiSettingsPage() {
</div>
{/* Tabbed Interface */}
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)}>
<TabsList>
<TabsTrigger value="providers">
{formatMessage({ id: 'apiSettings.tabs.providers' })}
</TabsTrigger>
<TabsTrigger value="endpoints">
{formatMessage({ id: 'apiSettings.tabs.endpoints' })}
</TabsTrigger>
<TabsTrigger value="cache">
{formatMessage({ id: 'apiSettings.tabs.cache' })}
</TabsTrigger>
<TabsTrigger value="modelPools">
{formatMessage({ id: 'apiSettings.tabs.modelPools' })}
</TabsTrigger>
<TabsTrigger value="cliSettings">
{formatMessage({ id: 'apiSettings.tabs.cliSettings' })}
</TabsTrigger>
</TabsList>
<TabsNavigation
value={activeTab}
onValueChange={(v) => setActiveTab(v as TabType)}
tabs={[
{ value: 'providers', label: formatMessage({ id: 'apiSettings.tabs.providers' }) },
{ value: 'endpoints', label: formatMessage({ id: 'apiSettings.tabs.endpoints' }) },
{ value: 'cache', label: formatMessage({ id: 'apiSettings.tabs.cache' }) },
{ value: 'modelPools', label: formatMessage({ id: 'apiSettings.tabs.modelPools' }) },
{ value: 'cliSettings', label: formatMessage({ id: 'apiSettings.tabs.cliSettings' }) },
]}
/>
<TabsContent value="providers">
{/* Tab Content */}
{activeTab === 'providers' && (
<div className="mt-4">
<ProviderList
onAddProvider={handleAddProvider}
onEditProvider={handleEditProvider}
@@ -225,33 +220,41 @@ export function ApiSettingsPage() {
onSyncToCodexLens={handleSyncToCodexLens}
onManageModels={handleManageModels}
/>
</TabsContent>
</div>
)}
<TabsContent value="endpoints">
{activeTab === 'endpoints' && (
<div className="mt-4">
<EndpointList
onAddEndpoint={handleAddEndpoint}
onEditEndpoint={handleEditEndpoint}
/>
</TabsContent>
</div>
)}
<TabsContent value="cache">
{activeTab === 'cache' && (
<div className="mt-4">
<CacheSettings />
</TabsContent>
</div>
)}
<TabsContent value="modelPools">
{activeTab === 'modelPools' && (
<div className="mt-4">
<ModelPoolList
onAddPool={handleAddPool}
onEditPool={handleEditPool}
/>
</TabsContent>
</div>
)}
<TabsContent value="cliSettings">
{activeTab === 'cliSettings' && (
<div className="mt-4">
<CliSettingsList
onAddCliSettings={handleAddCliSettings}
onEditCliSettings={handleEditCliSettings}
/>
</TabsContent>
</Tabs>
</div>
)}
{/* Modals */}
<ProviderModal

View File

@@ -0,0 +1,266 @@
// ========================================
// CLI Viewer Page
// ========================================
// Multi-pane CLI output viewer with configurable layouts
// Integrates with viewerStore for state management
import { useEffect, useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
Terminal,
LayoutGrid,
Columns,
Rows,
Square,
ChevronDown,
RotateCcw,
} from 'lucide-react';
import { Button } from '@/components/ui/Button';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from '@/components/ui/Dropdown';
import { cn } from '@/lib/utils';
import { LayoutContainer } from '@/components/cli-viewer';
import {
useViewerStore,
useViewerLayout,
useViewerPanes,
useFocusedPaneId,
type AllotmentLayout,
} from '@/stores/viewerStore';
// ========================================
// Types
// ========================================
export type LayoutType = 'single' | 'split-h' | 'split-v' | 'grid-2x2';
interface LayoutOption {
id: LayoutType;
icon: React.ElementType;
labelKey: string;
}
// ========================================
// Constants
// ========================================
const LAYOUT_OPTIONS: LayoutOption[] = [
{ id: 'single', icon: Square, labelKey: 'cliViewer.layout.single' },
{ id: 'split-h', icon: Columns, labelKey: 'cliViewer.layout.splitH' },
{ id: 'split-v', icon: Rows, labelKey: 'cliViewer.layout.splitV' },
{ id: 'grid-2x2', icon: LayoutGrid, labelKey: 'cliViewer.layout.grid' },
];
const DEFAULT_LAYOUT: LayoutType = 'split-h';
// ========================================
// Helper Functions
// ========================================
/**
* Detect layout type from AllotmentLayout structure
*/
function detectLayoutType(layout: AllotmentLayout): LayoutType {
const childCount = layout.children.length;
// Empty or single pane
if (childCount === 0 || childCount === 1) {
return 'single';
}
// Two panes at root level
if (childCount === 2) {
const hasNestedGroups = layout.children.some(
(child) => typeof child !== 'string'
);
// If no nested groups, it's a simple split
if (!hasNestedGroups) {
return layout.direction === 'horizontal' ? 'split-h' : 'split-v';
}
// Check for grid layout (2x2)
const allNested = layout.children.every(
(child) => typeof child !== 'string'
);
if (allNested) {
return 'grid-2x2';
}
}
// Default to current direction
return layout.direction === 'horizontal' ? 'split-h' : 'split-v';
}
/**
* Count total panes in layout
*/
function countPanes(layout: AllotmentLayout): number {
let count = 0;
const traverse = (children: (string | AllotmentLayout)[]) => {
for (const child of children) {
if (typeof child === 'string') {
count++;
} else {
traverse(child.children);
}
}
};
traverse(layout.children);
return count;
}
// ========================================
// Main Component
// ========================================
export function CliViewerPage() {
const { formatMessage } = useIntl();
const [searchParams, setSearchParams] = useSearchParams();
// Store hooks
const layout = useViewerLayout();
const panes = useViewerPanes();
const focusedPaneId = useFocusedPaneId();
const { initializeDefaultLayout, addTab, reset } = useViewerStore();
// Detect current layout type from store
const currentLayoutType = useMemo(() => detectLayoutType(layout), [layout]);
// Count active sessions (tabs across all panes)
const activeSessionCount = useMemo(() => {
return Object.values(panes).reduce((count, pane) => count + pane.tabs.length, 0);
}, [panes]);
// Initialize layout if empty
useEffect(() => {
const paneCount = countPanes(layout);
if (paneCount === 0) {
initializeDefaultLayout(DEFAULT_LAYOUT);
}
}, [layout, initializeDefaultLayout]);
// Handle executionId from URL params
useEffect(() => {
const executionId = searchParams.get('executionId');
if (executionId && focusedPaneId) {
// Add tab to focused pane
addTab(focusedPaneId, executionId, `Execution ${executionId.slice(0, 8)}`);
// Clear the URL param after processing
setSearchParams((prev) => {
const newParams = new URLSearchParams(prev);
newParams.delete('executionId');
return newParams;
});
}
}, [searchParams, focusedPaneId, addTab, setSearchParams]);
// Handle layout change
const handleLayoutChange = useCallback(
(layoutType: LayoutType) => {
initializeDefaultLayout(layoutType);
},
[initializeDefaultLayout]
);
// Handle reset
const handleReset = useCallback(() => {
reset();
initializeDefaultLayout(DEFAULT_LAYOUT);
}, [reset, initializeDefaultLayout]);
// Get current layout option for display
const currentLayoutOption =
LAYOUT_OPTIONS.find((l) => l.id === currentLayoutType) || LAYOUT_OPTIONS[1];
const CurrentLayoutIcon = currentLayoutOption.icon;
return (
<div className="h-full flex flex-col -m-4 md:-m-6">
{/* ======================================== */}
{/* Toolbar */}
{/* ======================================== */}
<div className="flex items-center justify-between gap-3 p-3 bg-card border-b border-border">
{/* Page Title */}
<div className="flex items-center gap-2 min-w-0">
<Terminal className="w-5 h-5 text-primary flex-shrink-0" />
<div className="flex flex-col min-w-0">
<span className="text-sm font-medium text-foreground">
{formatMessage({ id: 'cliViewer.page.title' })}
</span>
<span className="text-xs text-muted-foreground">
{formatMessage(
{ id: 'cliViewer.page.subtitle' },
{ count: activeSessionCount }
)}
</span>
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
{/* Reset Button */}
<Button
variant="ghost"
size="sm"
onClick={handleReset}
title={formatMessage({ id: 'cliViewer.toolbar.clearAll' })}
>
<RotateCcw className="w-4 h-4" />
</Button>
{/* Layout Selector */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<CurrentLayoutIcon className="w-4 h-4" />
<span className="hidden sm:inline">
{formatMessage({ id: currentLayoutOption.labelKey })}
</span>
<ChevronDown className="w-4 h-4 opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>
{formatMessage({ id: 'cliViewer.layout.title' })}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{LAYOUT_OPTIONS.map((option) => {
const Icon = option.icon;
return (
<DropdownMenuItem
key={option.id}
onClick={() => handleLayoutChange(option.id)}
className={cn(
'gap-2',
currentLayoutType === option.id && 'bg-accent'
)}
>
<Icon className="w-4 h-4" />
{formatMessage({ id: option.labelKey })}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* ======================================== */}
{/* Layout Container */}
{/* ======================================== */}
<div className="flex-1 min-h-0 bg-background">
<LayoutContainer />
</div>
</div>
);
}
export default CliViewerPage;

View File

@@ -15,7 +15,7 @@ import {
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import {
AlertDialog,
AlertDialogTrigger,
@@ -176,26 +176,21 @@ export function CodexLensManagerPage() {
)}
{/* Tabbed Interface */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="overview">
{formatMessage({ id: 'codexlens.tabs.overview' })}
</TabsTrigger>
<TabsTrigger value="settings">
{formatMessage({ id: 'codexlens.tabs.settings' })}
</TabsTrigger>
<TabsTrigger value="models">
{formatMessage({ id: 'codexlens.tabs.models' })}
</TabsTrigger>
<TabsTrigger value="search">
{formatMessage({ id: 'codexlens.tabs.search' })}
</TabsTrigger>
<TabsTrigger value="advanced">
{formatMessage({ id: 'codexlens.tabs.advanced' })}
</TabsTrigger>
</TabsList>
<TabsNavigation
value={activeTab}
onValueChange={setActiveTab}
tabs={[
{ value: 'overview', label: formatMessage({ id: 'codexlens.tabs.overview' }) },
{ value: 'settings', label: formatMessage({ id: 'codexlens.tabs.settings' }) },
{ value: 'models', label: formatMessage({ id: 'codexlens.tabs.models' }) },
{ value: 'search', label: formatMessage({ id: 'codexlens.tabs.search' }) },
{ value: 'advanced', label: formatMessage({ id: 'codexlens.tabs.advanced' }) },
]}
/>
<TabsContent value="overview">
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="mt-4">
<OverviewTab
installed={installed}
status={status}
@@ -203,24 +198,32 @@ export function CodexLensManagerPage() {
isLoading={isLoading}
onRefresh={handleRefresh}
/>
</TabsContent>
</div>
)}
<TabsContent value="settings">
{activeTab === 'settings' && (
<div className="mt-4">
<SettingsTab enabled={installed} />
</TabsContent>
</div>
)}
<TabsContent value="models">
{activeTab === 'models' && (
<div className="mt-4">
<ModelsTab installed={installed} />
</TabsContent>
</div>
)}
<TabsContent value="search">
{activeTab === 'search' && (
<div className="mt-4">
<SearchTab enabled={installed} />
</TabsContent>
</div>
)}
<TabsContent value="advanced">
{activeTab === 'advanced' && (
<div className="mt-4">
<AdvancedTab enabled={installed} />
</TabsContent>
</Tabs>
</div>
)}
{/* Semantic Install Dialog */}
<SemanticInstallDialog

View File

@@ -5,6 +5,7 @@
import { useState, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom';
import {
Activity,
Clock,
@@ -16,10 +17,12 @@ import {
ListTree,
History,
List,
Monitor,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import { ExecutionMonitor } from './orchestrator/ExecutionMonitor';
import { useExecutionStore } from '@/stores/executionStore';
import type { ExecutionStatus } from '@/types/execution';
@@ -86,9 +89,14 @@ function formatDateTime(dateString: string): string {
export function ExecutionMonitorPage() {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const currentExecution = useExecutionStore((state) => state.currentExecution);
const [selectedView, setSelectedView] = useState<'workflow' | 'timeline' | 'list'>('workflow');
const handleOpenCliViewer = () => {
navigate('/cli-viewer');
};
// Calculate statistics
const stats = useMemo(() => {
const total = mockExecutionHistory.length;
@@ -126,14 +134,20 @@ export function ExecutionMonitorPage() {
return (
<div className="space-y-6">
{/* Page Header */}
<div>
<h1 className="text-2xl font-semibold text-foreground flex items-center gap-2">
<Activity className="w-6 h-6" />
{formatMessage({ id: 'executionMonitor.page.title' })}
</h1>
<p className="text-sm text-muted-foreground mt-1">
{formatMessage({ id: 'executionMonitor.page.subtitle' })}
</p>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-foreground flex items-center gap-2">
<Activity className="w-6 h-6" />
{formatMessage({ id: 'executionMonitor.page.title' })}
</h1>
<p className="text-sm text-muted-foreground mt-1">
{formatMessage({ id: 'executionMonitor.page.subtitle' })}
</p>
</div>
<Button onClick={handleOpenCliViewer} className="gap-2">
<Monitor className="w-4 h-4" />
{formatMessage({ id: 'executionMonitor.actions.openCliViewer' })}
</Button>
</div>
{/* Current Execution Area */}
@@ -230,24 +244,31 @@ export function ExecutionMonitorPage() {
</CardTitle>
</CardHeader>
<CardContent>
<Tabs value={selectedView} onValueChange={(v) => setSelectedView(v as typeof selectedView)}>
<TabsList>
<TabsTrigger value="workflow">
<ListTree className="w-4 h-4 mr-2" />
{formatMessage({ id: 'executionMonitor.history.tabs.byWorkflow' })}
</TabsTrigger>
<TabsTrigger value="timeline">
<History className="w-4 h-4 mr-2" />
{formatMessage({ id: 'executionMonitor.history.tabs.timeline' })}
</TabsTrigger>
<TabsTrigger value="list">
<List className="w-4 h-4 mr-2" />
{formatMessage({ id: 'executionMonitor.history.tabs.list' })}
</TabsTrigger>
</TabsList>
<TabsNavigation
value={selectedView}
onValueChange={(v) => setSelectedView(v as typeof selectedView)}
tabs={[
{
value: 'workflow',
label: formatMessage({ id: 'executionMonitor.history.tabs.byWorkflow' }),
icon: <ListTree className="w-4 h-4" />,
},
{
value: 'timeline',
label: formatMessage({ id: 'executionMonitor.history.tabs.timeline' }),
icon: <History className="w-4 h-4" />,
},
{
value: 'list',
label: formatMessage({ id: 'executionMonitor.history.tabs.list' }),
icon: <List className="w-4 h-4" />,
},
]}
/>
{/* By Workflow View */}
<TabsContent value="workflow" className="mt-4">
{/* By Workflow View */}
{selectedView === 'workflow' && (
<div className="mt-4">
{workflowGroups.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{formatMessage({ id: 'executionMonitor.history.empty' })}
@@ -302,10 +323,11 @@ export function ExecutionMonitorPage() {
))}
</div>
)}
</TabsContent>
</div>
)}
{/* Timeline View */}
<TabsContent value="timeline" className="mt-4">
{/* Timeline View */}
{selectedView === 'timeline' && (
<div className="space-y-3">
{mockExecutionHistory.map((exec, index) => (
<div key={exec.execId} className="flex gap-4">
@@ -359,11 +381,11 @@ export function ExecutionMonitorPage() {
</div>
))}
</div>
</TabsContent>
)}
{/* List View */}
<TabsContent value="list" className="mt-4">
<div className="space-y-2">
{/* List View */}
{selectedView === 'list' && (
<div className="space-y-2">
{mockExecutionHistory.map((exec) => (
<Card key={exec.execId} className="hover:border-primary/50 transition-colors">
<CardContent className="p-4">
@@ -402,9 +424,8 @@ export function ExecutionMonitorPage() {
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs>
</div>
)}
</CardContent>
</Card>
</div>

View File

@@ -1,45 +1,29 @@
// ========================================
// HomePage Component
// ========================================
// Dashboard home page with stat cards and recent sessions
// Dashboard home page with combined stats, workflow status, and activity heatmap
import * as React from 'react';
import { lazy, Suspense } from 'react';
import { useIntl } from 'react-intl';
import { AlertCircle } from 'lucide-react';
import { DashboardHeader } from '@/components/dashboard/DashboardHeader';
import { DashboardGridContainer } from '@/components/dashboard/DashboardGridContainer';
import { DetailedStatsWidget } from '@/components/dashboard/widgets/DetailedStatsWidget';
import { WorkflowTaskWidget } from '@/components/dashboard/widgets/WorkflowTaskWidget';
import { RecentSessionsWidget } from '@/components/dashboard/widgets/RecentSessionsWidget';
import { ChartSkeleton } from '@/components/charts';
import { Button } from '@/components/ui/Button';
import { useUserDashboardLayout } from '@/hooks/useUserDashboardLayout';
import { WIDGET_IDS } from '@/components/dashboard/defaultLayouts';
// Code-split chart widgets for better initial load performance
const WorkflowStatusPieChartWidget = lazy(() => import('@/components/dashboard/widgets/WorkflowStatusPieChartWidget'));
const ActivityLineChartWidget = lazy(() => import('@/components/dashboard/widgets/ActivityLineChartWidget'));
const TaskTypeBarChartWidget = lazy(() => import('@/components/dashboard/widgets/TaskTypeBarChartWidget'));
/**
* HomePage component - Dashboard overview with widget-based layout
* HomePage component - Dashboard overview with fixed widget layout
*/
export function HomePage() {
const { formatMessage } = useIntl();
const { resetLayout } = useUserDashboardLayout();
// Track errors from widgets (optional, for future enhancements)
const [hasError, _setHasError] = React.useState(false);
const handleRefresh = () => {
// Trigger refetch by reloading the page or using React Query's invalidateQueries
window.location.reload();
};
const handleResetLayout = () => {
resetLayout();
};
return (
<div className="space-y-6">
{/* Header */}
@@ -47,7 +31,6 @@ export function HomePage() {
titleKey="home.dashboard.title"
descriptionKey="home.dashboard.description"
onRefresh={handleRefresh}
onResetLayout={handleResetLayout}
/>
{/* Error alert (optional, shown if widgets encounter critical errors) */}
@@ -66,29 +49,14 @@ export function HomePage() {
</div>
)}
{/* Dashboard Grid with Widgets */}
<DashboardGridContainer isDraggable={true} isResizable={true}>
{/* Widget 1: Detailed Stats */}
<DetailedStatsWidget key={WIDGET_IDS.STATS} />
{/* Dashboard Widgets - Simple flex layout for dynamic height */}
<div className="flex flex-col gap-4">
{/* Row 1: Combined Stats + Workflow Status + Task Details */}
<WorkflowTaskWidget />
{/* Widget 2: Recent Sessions */}
<RecentSessionsWidget key={WIDGET_IDS.RECENT_SESSIONS} />
{/* Widget 3: Workflow Status Pie Chart (code-split with Suspense fallback) */}
<Suspense fallback={<ChartSkeleton type="pie" height={280} />}>
<WorkflowStatusPieChartWidget key={WIDGET_IDS.WORKFLOW_STATUS} />
</Suspense>
{/* Widget 4: Activity Line Chart (code-split with Suspense fallback) */}
<Suspense fallback={<ChartSkeleton type="line" height={280} />}>
<ActivityLineChartWidget key={WIDGET_IDS.ACTIVITY} />
</Suspense>
{/* Widget 5: Task Type Bar Chart (code-split with Suspense fallback) */}
<Suspense fallback={<ChartSkeleton type="bar" height={280} />}>
<TaskTypeBarChartWidget key={WIDGET_IDS.TASK_TYPES} />
</Suspense>
</DashboardGridContainer>
{/* Row 2: Recent Sessions */}
<RecentSessionsWidget />
</div>
</div>
);
}

View File

@@ -41,7 +41,8 @@ import { Flowchart } from '@/components/shared/Flowchart';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { Tabs, TabsContent } from '@/components/ui/Tabs';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/Collapsible';
import type { LiteTask, LiteTaskSession } from '@/lib/api';
@@ -330,53 +331,68 @@ export function LiteTaskDetailPage() {
{/* Session Type-Specific Tabs */}
{isMultiCli ? (
<Tabs value={multiCliActiveTab} onValueChange={(v) => setMultiCliActiveTab(v as MultiCliTab)}>
<TabsList className="w-full">
<TabsTrigger value="tasks" className="flex-1 gap-1">
<ListTodo className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.tasks' })}
</TabsTrigger>
<TabsTrigger value="discussion" className="flex-1 gap-1">
<MessageSquare className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.discussion' })}
</TabsTrigger>
<TabsTrigger value="context" className="flex-1 gap-1">
<Package className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.context' })}
</TabsTrigger>
<TabsTrigger value="summary" className="flex-1 gap-1">
<FileText className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.summary' })}
</TabsTrigger>
</TabsList>
</Tabs>
<TabsNavigation
value={multiCliActiveTab}
onValueChange={(v) => setMultiCliActiveTab(v as MultiCliTab)}
tabs={[
{
value: 'tasks',
label: formatMessage({ id: 'liteTasksDetail.tabs.tasks' }),
icon: <ListTodo className="h-4 w-4" />,
},
{
value: 'discussion',
label: formatMessage({ id: 'liteTasksDetail.tabs.discussion' }),
icon: <MessageSquare className="h-4 w-4" />,
},
{
value: 'context',
label: formatMessage({ id: 'liteTasksDetail.tabs.context' }),
icon: <Package className="h-4 w-4" />,
},
{
value: 'summary',
label: formatMessage({ id: 'liteTasksDetail.tabs.summary' }),
icon: <FileText className="h-4 w-4" />,
},
]}
/>
) : (
<Tabs value={litePlanActiveTab} onValueChange={(v) => setLitePlanActiveTab(v as LitePlanTab)}>
<TabsList className="w-full">
<TabsTrigger value="tasks" className="flex-1 gap-1">
<ListTodo className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.tasks' })}
</TabsTrigger>
<TabsTrigger value="plan" className="flex-1 gap-1">
<Ruler className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.plan' })}
</TabsTrigger>
{isLiteFix && (
<TabsTrigger value="diagnoses" className="flex-1 gap-1">
<Stethoscope className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.diagnoses' })}
</TabsTrigger>
)}
<TabsTrigger value="context" className="flex-1 gap-1">
<Package className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.context' })}
</TabsTrigger>
<TabsTrigger value="summary" className="flex-1 gap-1">
<FileText className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.tabs.summary' })}
</TabsTrigger>
</TabsList>
</Tabs>
<TabsNavigation
value={litePlanActiveTab}
onValueChange={(v) => setLitePlanActiveTab(v as LitePlanTab)}
tabs={[
{
value: 'tasks',
label: formatMessage({ id: 'liteTasksDetail.tabs.tasks' }),
icon: <ListTodo className="h-4 w-4" />,
},
{
value: 'plan',
label: formatMessage({ id: 'liteTasksDetail.tabs.plan' }),
icon: <Ruler className="h-4 w-4" />,
},
...(isLiteFix
? [
{
value: 'diagnoses' as const,
label: formatMessage({ id: 'liteTasksDetail.tabs.diagnoses' }),
icon: <Stethoscope className="h-4 w-4" />,
},
]
: []),
{
value: 'context',
label: formatMessage({ id: 'liteTasksDetail.tabs.context' }),
icon: <Package className="h-4 w-4" />,
},
{
value: 'summary',
label: formatMessage({ id: 'liteTasksDetail.tabs.summary' }),
icon: <FileText className="h-4 w-4" />,
},
]}
/>
)}
{/* Task List with Multi-Tab Content */}
@@ -390,15 +406,11 @@ export function LiteTaskDetailPage() {
<Card key={taskId} className="overflow-hidden">
{/* Task Header */}
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start justify-between gap-4">
{/* Left: Task ID, Title, Description */}
<div className="flex-1 min-w-0">
<CardTitle className="text-base font-medium flex items-center gap-2 flex-wrap">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-semibold bg-primary/10 text-primary border border-primary/20">{taskId}</span>
<Badge
variant={task.status === 'completed' ? 'success' : task.status === 'in_progress' ? 'warning' : 'secondary'}
>
{task.status}
</Badge>
{task.priority && (
<Badge variant="outline" className="text-xs">{task.priority}</Badge>
)}
@@ -414,28 +426,77 @@ export function LiteTaskDetailPage() {
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">{task.description}</p>
)}
</div>
{/* Right: Meta Information */}
<div className="flex flex-col items-end gap-2 text-xs text-muted-foreground flex-shrink-0">
{/* Row 1: Status Badge */}
<Badge
variant={task.status === 'completed' ? 'success' : task.status === 'in_progress' ? 'warning' : task.status === 'blocked' ? 'destructive' : 'secondary'}
className="w-fit"
>
{task.status}
</Badge>
{/* Row 2: Metadata */}
<div className="flex items-center gap-3 flex-wrap justify-end">
{/* Dependencies Count */}
{task.context?.depends_on && task.context.depends_on.length > 0 && (
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
<span className="font-mono font-semibold text-foreground">{task.context.depends_on.length}</span>
<span>dep{task.context.depends_on.length > 1 ? 's' : ''}</span>
</span>
)}
{/* Target Files Count */}
{task.flow_control?.target_files && task.flow_control.target_files.length > 0 && (
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
<span className="font-mono font-semibold text-foreground">{task.flow_control.target_files.length}</span>
<span>file{task.flow_control.target_files.length > 1 ? 's' : ''}</span>
</span>
)}
{/* Focus Paths Count */}
{task.context?.focus_paths && task.context.focus_paths.length > 0 && (
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
<span className="font-mono font-semibold text-foreground">{task.context.focus_paths.length}</span>
<span>focus</span>
</span>
)}
{/* Acceptance Criteria Count */}
{task.context?.acceptance && task.context.acceptance.length > 0 && (
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
<span className="font-mono font-semibold text-foreground">{task.context.acceptance.length}</span>
<span>criteria</span>
</span>
)}
</div>
</div>
</div>
</CardHeader>
{/* Multi-Tab Content */}
<Tabs
value={activeTaskTab}
onValueChange={(v) => handleTaskTabChange(taskId, v as TaskTabValue)}
className="w-full"
>
<TabsList className="w-full rounded-none border-y border-border bg-muted/50 px-4">
<TabsTrigger value="task" className="flex-1 gap-1.5">
<ListTodo className="h-4 w-4" />
Task
</TabsTrigger>
<TabsTrigger value="context" className="flex-1 gap-1.5">
<Package className="h-4 w-4" />
Context
</TabsTrigger>
</TabsList>
<div className="w-full">
<TabsNavigation
value={activeTaskTab}
onValueChange={(v) => handleTaskTabChange(taskId, v as TaskTabValue)}
tabs={[
{
value: 'task',
label: 'Task',
icon: <ListTodo className="h-4 w-4" />,
},
{
value: 'context',
label: 'Context',
icon: <Package className="h-4 w-4" />,
},
]}
/>
{/* Task Tab - Implementation Details */}
<TabsContent value="task" className="p-4 space-y-4">
{activeTaskTab === 'task' && (
<div className="p-4 space-y-4">
{/* Flowchart */}
{hasFlowchart && task.flow_control && (
<div>
@@ -478,10 +539,12 @@ export function LiteTaskDetailPage() {
</div>
</div>
)}
</TabsContent>
</div>
)}
{/* Context Tab - Planning Context */}
<TabsContent value="context" className="p-4 space-y-4">
{activeTaskTab === 'context' && (
<div className="p-4 space-y-4">
{/* Focus Paths */}
{task.context?.focus_paths && task.context.focus_paths.length > 0 && (
<div>
@@ -547,8 +610,9 @@ export function LiteTaskDetailPage() {
</ul>
</div>
)}
</TabsContent>
</Tabs>
</div>
)}
</div>
</Card>
);
})}

View File

@@ -30,12 +30,16 @@ import {
Stethoscope,
FolderOpen,
FileText,
CheckCircle2,
Clock,
AlertCircle,
} from 'lucide-react';
import { useLiteTasks } from '@/hooks/useLiteTasks';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card, CardContent } from '@/components/ui/Card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { Tabs, TabsContent } from '@/components/ui/Tabs';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import { TaskDrawer } from '@/components/shared/TaskDrawer';
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext } from '@/lib/api';
import { useNavigate } from 'react-router-dom';
@@ -482,6 +486,19 @@ export function LiteTasksPage() {
const taskCount = session.tasks?.length || 0;
const isExpanded = expandedSessionId === session.id;
// Calculate task status distribution
const taskStats = React.useMemo(() => {
const tasks = session.tasks || [];
return {
completed: tasks.filter((t) => t.status === 'completed').length,
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
blocked: tasks.filter((t) => t.status === 'blocked').length,
pending: tasks.filter((t) => !t.status || t.status === 'pending').length,
};
}, [session.tasks]);
const firstTask = session.tasks?.[0];
return (
<div key={session.id}>
<Card
@@ -507,6 +524,43 @@ export function LiteTasksPage() {
{formatMessage({ id: isLitePlan ? 'liteTasks.type.plan' : 'liteTasks.type.fix' })}
</Badge>
</div>
{/* Task preview - first task title */}
{firstTask?.title && (
<div className="mb-3 pb-3 border-b border-border/50">
<p className="text-sm text-foreground line-clamp-1">{firstTask.title}</p>
</div>
)}
{/* Task status distribution */}
<div className="flex items-center flex-wrap gap-2 mb-3">
{taskStats.completed > 0 && (
<Badge variant="success" className="gap-1 text-xs">
<CheckCircle2 className="h-3 w-3" />
{taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })}
</Badge>
)}
{taskStats.inProgress > 0 && (
<Badge variant="warning" className="gap-1 text-xs">
<Clock className="h-3 w-3" />
{taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })}
</Badge>
)}
{taskStats.blocked > 0 && (
<Badge variant="destructive" className="gap-1 text-xs">
<AlertCircle className="h-3 w-3" />
{taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })}
</Badge>
)}
{taskStats.pending > 0 && (
<Badge variant="secondary" className="gap-1 text-xs">
<Activity className="h-3 w-3" />
{taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })}
</Badge>
)}
</div>
{/* Date and task count */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
{session.createdAt && (
<span className="flex items-center gap-1">
@@ -546,6 +600,18 @@ export function LiteTasksPage() {
const status = latestSynthesis.status || session.status || 'analyzing';
const createdAt = (metadata.timestamp as string) || session.createdAt || '';
// Calculate task status distribution
const taskStats = React.useMemo(() => {
const tasks = session.tasks || [];
return {
completed: tasks.filter((t) => t.status === 'completed').length,
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
blocked: tasks.filter((t) => t.status === 'blocked').length,
pending: tasks.filter((t) => !t.status || t.status === 'pending').length,
total: tasks.length,
};
}, [session.tasks]);
return (
<Card
key={session.id}
@@ -575,6 +641,37 @@ export function LiteTasksPage() {
<MessageCircle className="h-4 w-4" />
<span className="line-clamp-1">{topicTitle}</span>
</div>
{/* Task status distribution for multi-cli */}
{taskStats.total > 0 && (
<div className="flex items-center flex-wrap gap-2 mb-3">
{taskStats.completed > 0 && (
<Badge variant="success" className="gap-1 text-xs">
<CheckCircle2 className="h-3 w-3" />
{taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })}
</Badge>
)}
{taskStats.inProgress > 0 && (
<Badge variant="warning" className="gap-1 text-xs">
<Clock className="h-3 w-3" />
{taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })}
</Badge>
)}
{taskStats.blocked > 0 && (
<Badge variant="destructive" className="gap-1 text-xs">
<AlertCircle className="h-3 w-3" />
{taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })}
</Badge>
)}
{taskStats.pending > 0 && (
<Badge variant="secondary" className="gap-1 text-xs">
<Activity className="h-3 w-3" />
{taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })}
</Badge>
)}
</div>
)}
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{createdAt && (
<span className="flex items-center gap-1">
@@ -651,30 +748,30 @@ export function LiteTasksPage() {
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as LiteTaskTab)}>
<TabsList>
<TabsTrigger value="lite-plan">
<FileEdit className="h-4 w-4 mr-2" />
{formatMessage({ id: 'liteTasks.type.plan' })}
<Badge variant="secondary" className="ml-2">
{litePlan.length}
</Badge>
</TabsTrigger>
<TabsTrigger value="lite-fix">
<Wrench className="h-4 w-4 mr-2" />
{formatMessage({ id: 'liteTasks.type.fix' })}
<Badge variant="secondary" className="ml-2">
{liteFix.length}
</Badge>
</TabsTrigger>
<TabsTrigger value="multi-cli-plan">
<MessagesSquare className="h-4 w-4 mr-2" />
{formatMessage({ id: 'liteTasks.type.multiCli' })}
<Badge variant="secondary" className="ml-2">
{multiCliPlan.length}
</Badge>
</TabsTrigger>
</TabsList>
<TabsNavigation
value={activeTab}
onValueChange={(v) => setActiveTab(v as LiteTaskTab)}
tabs={[
{
value: 'lite-plan',
label: formatMessage({ id: 'liteTasks.type.plan' }),
icon: <FileEdit className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{litePlan.length}</Badge>,
},
{
value: 'lite-fix',
label: formatMessage({ id: 'liteTasks.type.fix' }),
icon: <Wrench className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{liteFix.length}</Badge>,
},
{
value: 'multi-cli-plan',
label: formatMessage({ id: 'liteTasks.type.multiCli' }),
icon: <MessagesSquare className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{multiCliPlan.length}</Badge>,
},
]}
/>
{/* Search and Sort Toolbar */}
<div className="mt-4 flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
@@ -729,86 +826,91 @@ export function LiteTasksPage() {
</div>
{/* Lite Plan Tab */}
<TabsContent value="lite-plan" className="mt-4">
{litePlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-plan' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredLitePlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredLitePlan.map(renderLiteTaskCard)}</div>
)}
</TabsContent>
{activeTab === 'lite-plan' && (
<div className="mt-4">
{litePlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-plan' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredLitePlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredLitePlan.map(renderLiteTaskCard)}</div>
)}
</div>
)}
{/* Lite Fix Tab */}
<TabsContent value="lite-fix" className="mt-4">
{liteFix.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-fix' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredLiteFix.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredLiteFix.map(renderLiteTaskCard)}</div>
)}
</TabsContent>
{activeTab === 'lite-fix' && (
<div className="mt-4">
{liteFix.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-fix' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredLiteFix.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredLiteFix.map(renderLiteTaskCard)}</div>
)}
</div>
)}
{/* Multi-CLI Plan Tab */}
<TabsContent value="multi-cli-plan" className="mt-4">
{multiCliPlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'multi-cli-plan' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredMultiCliPlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredMultiCliPlan.map(renderMultiCliCard)}</div>
)}
</TabsContent>
</Tabs>
{activeTab === 'multi-cli-plan' && (
<div className="mt-4">
{multiCliPlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'multi-cli-plan' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredMultiCliPlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredMultiCliPlan.map(renderMultiCliCard)}</div>
)}
</div>
)}
{/* TaskDrawer */}
<TaskDrawer

View File

@@ -228,158 +228,161 @@ export function ProjectOverviewPage() {
const { technologyStack, architecture, keyComponents, developmentIndex, guidelines, metadata } = projectOverview;
return (
<div className="space-y-6">
{/* Project Header */}
<div className="space-y-4">
{/* Project Header + Technology Stack - Combined */}
<Card>
<CardContent className="p-6">
<div className="flex items-start justify-between">
<CardContent className="p-4">
{/* Header Row */}
<div className="flex items-start justify-between mb-4 pb-3 border-b border-border">
<div className="flex-1">
<h1 className="text-2xl font-bold text-foreground mb-2">
<h1 className="text-base font-semibold text-foreground mb-1">
{projectOverview.projectName}
</h1>
<p className="text-muted-foreground">
<p className="text-xs text-muted-foreground">
{projectOverview.description || formatMessage({ id: 'projectOverview.noDescription' })}
</p>
</div>
<div className="text-sm text-muted-foreground text-right">
<div className="text-xs text-muted-foreground text-right">
<div>
{formatMessage({ id: 'projectOverview.header.initialized' })}:{' '}
{formatDate(projectOverview.initializedAt)}
</div>
{metadata?.analysis_mode && (
<div className="mt-1">
<span className="font-mono text-xs px-2 py-0.5 bg-muted rounded">
<span className="font-mono text-[10px] px-1.5 py-0.5 bg-muted rounded">
{metadata.analysis_mode}
</span>
</div>
)}
</div>
</div>
</CardContent>
</Card>
{/* Technology Stack */}
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<Code2 className="w-5 h-5" />
{/* Technology Stack */}
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
<Code2 className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.techStack.title' })}
</h3>
{/* Languages */}
<div className="mb-5">
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
{formatMessage({ id: 'projectOverview.techStack.languages' })}
</h4>
<div className="flex flex-wrap gap-3">
{technologyStack?.languages && technologyStack.languages.length > 0 ? (
technologyStack.languages.map((lang: { name: string; file_count: number; primary?: boolean }) => (
<div
key={lang.name}
className={`flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg ${
lang.primary ? 'ring-2 ring-primary' : ''
}`}
>
<span className="font-semibold text-foreground">{lang.name}</span>
<span className="text-xs text-muted-foreground">{lang.file_count} files</span>
{lang.primary && (
<span className="text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded">
{formatMessage({ id: 'projectOverview.techStack.primary' })}
</span>
)}
</div>
))
) : (
<span className="text-muted-foreground text-sm">
{formatMessage({ id: 'projectOverview.techStack.noLanguages' })}
</span>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{/* Languages */}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.techStack.languages' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{technologyStack?.languages && technologyStack.languages.length > 0 ? (
technologyStack.languages.map((lang: { name: string; file_count: number; primary?: boolean }) => (
<div
key={lang.name}
className={`flex items-center gap-1.5 px-2 py-1 bg-background border border-border rounded text-xs ${
lang.primary ? 'ring-1 ring-primary' : ''
}`}
>
<span className="font-medium text-foreground">{lang.name}</span>
<span className="text-[10px] text-muted-foreground">{lang.file_count}</span>
{lang.primary && (
<span className="text-[9px] px-1 py-0.5 bg-primary text-primary-foreground rounded">
{formatMessage({ id: 'projectOverview.techStack.primary' })}
</span>
)}
</div>
))
) : (
<span className="text-muted-foreground text-xs">
{formatMessage({ id: 'projectOverview.techStack.noLanguages' })}
</span>
)}
</div>
</div>
</div>
{/* Frameworks */}
<div className="mb-5">
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
{formatMessage({ id: 'projectOverview.techStack.frameworks' })}
</h4>
<div className="flex flex-wrap gap-2">
{technologyStack?.frameworks && technologyStack.frameworks.length > 0 ? (
technologyStack.frameworks.map((fw: string) => (
<Badge key={fw} variant="success" className="px-3 py-1.5">
{fw}
</Badge>
))
) : (
<span className="text-muted-foreground text-sm">
{formatMessage({ id: 'projectOverview.techStack.noFrameworks' })}
</span>
)}
{/* Frameworks */}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.techStack.frameworks' })}
</h4>
<div className="flex flex-wrap gap-1.5">
{technologyStack?.frameworks && technologyStack.frameworks.length > 0 ? (
technologyStack.frameworks.map((fw: string) => (
<Badge key={fw} variant="success" className="px-2 py-0.5 text-[10px]">
{fw}
</Badge>
))
) : (
<span className="text-muted-foreground text-xs">
{formatMessage({ id: 'projectOverview.techStack.noFrameworks' })}
</span>
)}
</div>
</div>
</div>
{/* Build Tools */}
{technologyStack?.build_tools && technologyStack.build_tools.length > 0 && (
<div className="mb-5">
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
{/* Build Tools */}
<div>
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.techStack.buildTools' })}
</h4>
<div className="flex flex-wrap gap-2">
{technologyStack.build_tools.map((tool: string) => (
<Badge key={tool} variant="warning" className="px-3 py-1.5">
{tool}
</Badge>
))}
<div className="flex flex-wrap gap-1.5">
{technologyStack?.build_tools && technologyStack.build_tools.length > 0 ? (
technologyStack.build_tools.map((tool: string) => (
<Badge key={tool} variant="warning" className="px-2 py-0.5 text-[10px]">
{tool}
</Badge>
))
) : (
<span className="text-muted-foreground text-xs">-</span>
)}
</div>
</div>
)}
{/* Test Frameworks */}
{technologyStack?.test_frameworks && technologyStack.test_frameworks.length > 0 && (
{/* Test Frameworks */}
<div>
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.techStack.testFrameworks' })}
</h4>
<div className="flex flex-wrap gap-2">
{technologyStack.test_frameworks.map((fw: string) => (
<Badge key={fw} variant="default" className="px-3 py-1.5">
{fw}
</Badge>
))}
<div className="flex flex-wrap gap-1.5">
{technologyStack?.test_frameworks && technologyStack.test_frameworks.length > 0 ? (
technologyStack.test_frameworks.map((fw: string) => (
<Badge key={fw} variant="default" className="px-2 py-0.5 text-[10px]">
{fw}
</Badge>
))
) : (
<span className="text-muted-foreground text-xs">-</span>
)}
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Architecture */}
{architecture && (
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<Blocks className="w-5 h-5" />
<CardContent className="p-4">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
<Blocks className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.architecture.title' })}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Style */}
<div>
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.style' })}
</h4>
<div className="px-3 py-2 bg-background border border-border rounded-lg">
<span className="text-foreground font-medium">{architecture.style}</span>
<div className="px-2 py-1.5 bg-background border border-border rounded">
<span className="text-foreground font-medium text-xs">{architecture.style}</span>
</div>
</div>
{/* Layers */}
{architecture.layers && architecture.layers.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.layers' })}
</h4>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-1.5">
{architecture.layers.map((layer: string) => (
<span key={layer} className="px-2 py-1 bg-muted text-foreground rounded text-sm">
<span key={layer} className="px-1.5 py-0.5 bg-muted text-foreground rounded text-[10px]">
{layer}
</span>
))}
@@ -390,12 +393,12 @@ export function ProjectOverviewPage() {
{/* Patterns */}
{architecture.patterns && architecture.patterns.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
<h4 className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wide mb-2">
{formatMessage({ id: 'projectOverview.architecture.patterns' })}
</h4>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-1.5">
{architecture.patterns.map((pattern: string) => (
<span key={pattern} className="px-2 py-1 bg-muted text-foreground rounded text-sm">
<span key={pattern} className="px-1.5 py-0.5 bg-muted text-foreground rounded text-[10px]">
{pattern}
</span>
))}
@@ -410,33 +413,33 @@ export function ProjectOverviewPage() {
{/* Key Components */}
{keyComponents && keyComponents.length > 0 && (
<Card>
<CardContent className="p-6">
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<Component className="w-5 h-5" />
<CardContent className="p-4">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
<Component className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.components.title' })}
</h3>
<div className="space-y-3">
<div className="space-y-2">
{keyComponents.map((comp: KeyComponent) => {
const importance = comp.importance || 'low';
const importanceColors: Record<string, string> = {
high: 'border-l-4 border-l-destructive bg-destructive/5',
medium: 'border-l-4 border-l-warning bg-warning/5',
low: 'border-l-4 border-l-muted-foreground bg-muted',
high: 'border-l-2 border-l-destructive bg-destructive/5',
medium: 'border-l-2 border-l-warning bg-warning/5',
low: 'border-l-2 border-l-muted-foreground bg-muted',
};
const importanceBadges: Record<string, React.ReactElement> = {
high: (
<Badge variant="destructive" className="text-xs">
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.high' })}
</Badge>
),
medium: (
<Badge variant="warning" className="text-xs">
<Badge variant="warning" className="text-[10px] px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.medium' })}
</Badge>
),
low: (
<Badge variant="secondary" className="text-xs">
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
{formatMessage({ id: 'projectOverview.components.importance.low' })}
</Badge>
),
@@ -445,17 +448,17 @@ export function ProjectOverviewPage() {
return (
<div
key={comp.name}
className={`p-4 rounded-lg ${importanceColors[importance] || importanceColors.low}`}
className={`p-2.5 rounded ${importanceColors[importance] || importanceColors.low}`}
>
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-foreground">{comp.name}</h4>
<div className="flex items-start justify-between mb-1">
<h4 className="font-medium text-foreground text-xs">{comp.name}</h4>
{importanceBadges[importance]}
</div>
{comp.description && (
<p className="text-sm text-muted-foreground mb-2">{comp.description}</p>
<p className="text-[10px] text-muted-foreground mb-1">{comp.description}</p>
)}
{comp.responsibility && comp.responsibility.length > 0 && (
<ul className="text-xs text-muted-foreground list-disc list-inside">
<ul className="text-[10px] text-muted-foreground list-disc list-inside">
{comp.responsibility.map((resp: string, i: number) => (
<li key={i}>{resp}</li>
))}
@@ -472,20 +475,20 @@ export function ProjectOverviewPage() {
{/* Development Index */}
{developmentIndex && totalEntries > 0 && (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
<GitBranch className="w-5 h-5" />
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
<GitBranch className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.devIndex.title' })}
</h3>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5">
{devIndexCategories.map((cat) => {
const count = devIndexTotals[cat.key];
if (count === 0) return null;
const Icon = cat.icon;
return (
<Badge key={cat.key} variant={cat.color === 'primary' ? 'default' : 'secondary'}>
<Icon className="w-3 h-3 mr-1" />
<Badge key={cat.key} variant={cat.color === 'primary' ? 'default' : 'secondary'} className="text-[10px] px-1.5 py-0">
<Icon className="w-2.5 h-2.5 mr-0.5" />
{count}
</Badge>
);
@@ -494,21 +497,21 @@ export function ProjectOverviewPage() {
</div>
<Tabs value={devIndexView} onValueChange={(v) => setDevIndexView(v as DevIndexView)}>
<div className="flex items-center justify-between mb-4">
<TabsList>
<TabsTrigger value="category">
<LayoutGrid className="w-3.5 h-3.5 mr-1" />
<div className="flex items-center justify-between mb-3">
<TabsList className="h-7">
<TabsTrigger value="category" className="text-xs px-2 py-1 h-6">
<LayoutGrid className="w-3 h-3 mr-1" />
{formatMessage({ id: 'projectOverview.devIndex.categories' })}
</TabsTrigger>
<TabsTrigger value="timeline">
<GitCommitHorizontal className="w-3.5 h-3.5 mr-1" />
<TabsTrigger value="timeline" className="text-xs px-2 py-1 h-6">
<GitCommitHorizontal className="w-3 h-3 mr-1" />
{formatMessage({ id: 'projectOverview.devIndex.timeline' })}
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="category">
<div className="space-y-4">
<div className="space-y-3">
{devIndexCategories.map((cat) => {
const entries = developmentIndex?.[cat.key] || [];
if (entries.length === 0) return null;
@@ -516,38 +519,38 @@ export function ProjectOverviewPage() {
return (
<div key={cat.key}>
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<Icon className="w-4 h-4" />
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
<Icon className="w-3.5 h-3.5" />
<span>{formatMessage({ id: cat.i18nKey })}</span>
<Badge variant="secondary">{entries.length}</Badge>
<Badge variant="secondary" className="text-[10px] px-1 py-0">{entries.length}</Badge>
</h4>
<div className="space-y-2">
<div className="space-y-1.5">
{entries.slice(0, 5).map((entry: DevelopmentIndexEntry & { type?: string; typeLabel?: string; typeIcon?: React.ElementType; typeColor?: string; date?: string }, i: number) => (
<div
key={i}
className="p-3 bg-background border border-border rounded-lg hover:shadow-sm transition-shadow"
className="p-2 bg-background border border-border rounded hover:shadow-sm transition-shadow"
>
<div className="flex items-start justify-between mb-1">
<h5 className="font-medium text-foreground text-sm">{entry.title}</h5>
<span className="text-xs text-muted-foreground">
<div className="flex items-start justify-between mb-0.5">
<h5 className="font-medium text-foreground text-xs">{entry.title}</h5>
<span className="text-[10px] text-muted-foreground">
{formatDate(entry.archivedAt || entry.date || entry.implemented_at)}
</span>
</div>
{entry.description && (
<p className="text-sm text-muted-foreground mb-1">{entry.description}</p>
<p className="text-[10px] text-muted-foreground mb-1">{entry.description}</p>
)}
<div className="flex items-center gap-2 text-xs flex-wrap">
<div className="flex items-center gap-1.5 text-[10px] flex-wrap">
{entry.sessionId && (
<span className="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">
<span className="px-1.5 py-0.5 bg-primary-light text-primary rounded font-mono">
{entry.sessionId}
</span>
)}
{entry.sub_feature && (
<span className="px-2 py-0.5 bg-muted rounded">{entry.sub_feature}</span>
<span className="px-1.5 py-0.5 bg-muted rounded">{entry.sub_feature}</span>
)}
{entry.status && (
<span
className={`px-2 py-0.5 rounded ${
className={`px-1.5 py-0.5 rounded ${
entry.status === 'completed'
? 'bg-success-light text-success'
: 'bg-warning-light text-warning'
@@ -560,7 +563,7 @@ export function ProjectOverviewPage() {
</div>
))}
{entries.length > 5 && (
<div className="text-sm text-muted-foreground text-center py-2">
<div className="text-xs text-muted-foreground text-center py-1">
... and {entries.length - 5} more
</div>
)}
@@ -572,24 +575,24 @@ export function ProjectOverviewPage() {
</TabsContent>
<TabsContent value="timeline">
<div className="space-y-4">
<div className="space-y-3">
{allDevEntries.slice(0, 20).map((entry, i) => {
const Icon = entry.typeIcon;
return (
<div key={i} className="flex gap-4">
<div key={i} className="flex gap-3">
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 rounded-full bg-${entry.typeColor}-light text-${entry.typeColor} flex items-center justify-center`}
className={`w-6 h-6 rounded-full bg-${entry.typeColor}-light text-${entry.typeColor} flex items-center justify-center`}
>
<Icon className="w-4 h-4" />
<Icon className="w-3 h-3" />
</div>
{i < Math.min(allDevEntries.length, 20) - 1 && (
<div className="w-0.5 flex-1 bg-border mt-2" />
<div className="w-0.5 flex-1 bg-border mt-1.5" />
)}
</div>
<div className="flex-1 pb-4">
<div className="flex items-start justify-between mb-1">
<div className="flex items-center gap-2">
<div className="flex-1 pb-3">
<div className="flex items-start justify-between mb-0.5">
<div className="flex items-center gap-1.5">
<Badge
variant={
entry.typeColor === 'primary'
@@ -598,31 +601,31 @@ export function ProjectOverviewPage() {
? 'destructive'
: 'secondary'
}
className="text-xs"
className="text-[10px] px-1.5 py-0"
>
{entry.typeLabel}
</Badge>
<h5 className="font-medium text-foreground text-sm">{entry.title}</h5>
<h5 className="font-medium text-foreground text-xs">{entry.title}</h5>
</div>
<span className="text-xs text-muted-foreground whitespace-nowrap">
<span className="text-[10px] text-muted-foreground whitespace-nowrap">
{formatDate(entry.date)}
</span>
</div>
{entry.description && (
<p className="text-sm text-muted-foreground mb-2">{entry.description}</p>
<p className="text-[10px] text-muted-foreground mb-1">{entry.description}</p>
)}
<div className="flex items-center gap-2 text-xs">
<div className="flex items-center gap-1.5 text-[10px]">
{entry.sessionId && (
<span className="px-2 py-0.5 bg-muted rounded font-mono">
<span className="px-1.5 py-0.5 bg-muted rounded font-mono">
{entry.sessionId}
</span>
)}
{entry.sub_feature && (
<span className="px-2 py-0.5 bg-muted rounded">{entry.sub_feature}</span>
<span className="px-1.5 py-0.5 bg-muted rounded">{entry.sub_feature}</span>
)}
{entry.tags &&
entry.tags.slice(0, 3).map((tag) => (
<span key={tag} className="px-2 py-0.5 bg-accent rounded">
<span key={tag} className="px-1.5 py-0.5 bg-accent rounded">
{tag}
</span>
))}
@@ -632,7 +635,7 @@ export function ProjectOverviewPage() {
);
})}
{allDevEntries.length > 20 && (
<div className="text-sm text-muted-foreground text-center py-4">
<div className="text-xs text-muted-foreground text-center py-2">
... and {allDevEntries.length - 20} more entries
</div>
)}
@@ -646,26 +649,26 @@ export function ProjectOverviewPage() {
{/* Guidelines */}
{guidelines && (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
<ScrollText className="w-5 h-5" />
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
<ScrollText className="w-4 h-4" />
{formatMessage({ id: 'projectOverview.guidelines.title' })}
</h3>
<div className="flex gap-2">
<div className="flex gap-1.5">
{!isEditMode ? (
<Button variant="outline" size="sm" onClick={handleEditStart}>
<Edit className="w-4 h-4 mr-1" />
<Button variant="outline" size="sm" className="h-7 text-xs px-2" onClick={handleEditStart}>
<Edit className="w-3 h-3 mr-1" />
{formatMessage({ id: 'projectOverview.guidelines.edit' })}
</Button>
) : (
<>
<Button variant="outline" size="sm" onClick={handleEditCancel} disabled={isUpdating}>
<X className="w-4 h-4 mr-1" />
<Button variant="outline" size="sm" className="h-7 text-xs px-2" onClick={handleEditCancel} disabled={isUpdating}>
<X className="w-3 h-3 mr-1" />
{formatMessage({ id: 'projectOverview.guidelines.cancel' })}
</Button>
<Button variant="default" size="sm" onClick={handleSave} disabled={isUpdating}>
<Save className="w-4 h-4 mr-1" />
<Button variant="default" size="sm" className="h-7 text-xs px-2" onClick={handleSave} disabled={isUpdating}>
<Save className="w-3 h-3 mr-1" />
{isUpdating ? formatMessage({ id: 'projectOverview.guidelines.saving' }) : formatMessage({ id: 'projectOverview.guidelines.save' })}
</Button>
</>
@@ -673,17 +676,17 @@ export function ProjectOverviewPage() {
</div>
</div>
<div className="space-y-6">
<div className="space-y-4">
{!isEditMode ? (
<>
{/* Read-only Mode - Conventions */}
{guidelines.conventions && (
<div>
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<BookMarked className="w-4 h-4" />
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
<BookMarked className="w-3.5 h-3.5" />
<span>{formatMessage({ id: 'projectOverview.guidelines.conventions' })}</span>
</h4>
<div className="space-y-2">
<div className="space-y-1.5">
{Object.entries(guidelines.conventions).map(([key, items]) => {
const itemList = Array.isArray(items) ? items : [];
if (itemList.length === 0) return null;
@@ -692,12 +695,12 @@ export function ProjectOverviewPage() {
{itemList.map((item: string, i: number) => (
<div
key={i}
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
className="flex items-start gap-2 p-2 bg-background border border-border rounded"
>
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
<span className="text-[10px] px-1.5 py-0.5 bg-muted text-muted-foreground rounded">
{key}
</span>
<span className="text-sm text-foreground">{item}</span>
<span className="text-xs text-foreground">{item}</span>
</div>
))}
</div>
@@ -710,11 +713,11 @@ export function ProjectOverviewPage() {
{/* Read-only Mode - Constraints */}
{guidelines.constraints && (
<div>
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<ShieldAlert className="w-4 h-4" />
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
<ShieldAlert className="w-3.5 h-3.5" />
<span>{formatMessage({ id: 'projectOverview.guidelines.constraints' })}</span>
</h4>
<div className="space-y-2">
<div className="space-y-1.5">
{Object.entries(guidelines.constraints).map(([key, items]) => {
const itemList = Array.isArray(items) ? items : [];
if (itemList.length === 0) return null;
@@ -723,12 +726,12 @@ export function ProjectOverviewPage() {
{itemList.map((item: string, i: number) => (
<div
key={i}
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
className="flex items-start gap-2 p-2 bg-background border border-border rounded"
>
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
<span className="text-[10px] px-1.5 py-0.5 bg-muted text-muted-foreground rounded">
{key}
</span>
<span className="text-sm text-foreground">{item}</span>
<span className="text-xs text-foreground">{item}</span>
</div>
))}
</div>

View File

@@ -1,7 +1,7 @@
// ========================================
// ReviewSessionPage Component
// ========================================
// Review session detail page with findings display and multi-select
// Review session detail page with findings display, multi-select, dimension tabs, and fix progress carousel
import * as React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
@@ -17,6 +17,8 @@ import {
Download,
ChevronDown,
ChevronRight,
ChevronLeft as ChevronLeftIcon,
ChevronRight as ChevronRightIcon,
} from 'lucide-react';
import { useReviewSession } from '@/hooks/useReviewSession';
import { Button } from '@/components/ui/Button';
@@ -42,6 +44,229 @@ interface FindingWithSelection {
impact?: string;
}
// Fix Progress Types
interface FixStage {
stage: number;
status: 'completed' | 'in-progress' | 'pending';
groups: string[];
}
interface FixProgressData {
fix_session_id: string;
phase: 'planning' | 'execution' | 'completion';
total_findings: number;
fixed_count: number;
failed_count: number;
in_progress_count: number;
pending_count: number;
percent_complete: number;
current_stage: number;
total_stages: number;
stages: FixStage[];
active_agents: Array<{
agent_id: string;
group_id: string;
current_finding: { finding_title: string } | null;
}>;
}
/**
* Fix Progress Carousel Component
* Displays fix progress with polling and carousel navigation
*/
function FixProgressCarousel({ sessionId }: { sessionId: string }) {
const { formatMessage } = useIntl();
const [fixProgressData, setFixProgressData] = React.useState<FixProgressData | null>(null);
const [currentSlide, setCurrentSlide] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(false);
// Fetch fix progress data
const fetchFixProgress = React.useCallback(async () => {
setIsLoading(true);
try {
const response = await fetch(`/api/fix-progress?sessionId=${encodeURIComponent(sessionId)}`);
if (!response.ok) {
if (response.status === 404) {
setFixProgressData(null);
}
return;
}
const data = await response.json();
setFixProgressData(data);
} catch (err) {
console.error('Failed to fetch fix progress:', err);
} finally {
setIsLoading(false);
}
}, [sessionId]);
// Poll for fix progress updates
React.useEffect(() => {
fetchFixProgress();
// Stop polling if phase is completion
if (fixProgressData?.phase === 'completion') {
return;
}
const interval = setInterval(() => {
fetchFixProgress();
}, 5000);
return () => clearInterval(interval);
}, [fetchFixProgress, fixProgressData?.phase]);
// Navigate carousel
const navigateSlide = (direction: 'prev' | 'next' | number) => {
if (!fixProgressData) return;
const totalSlides = fixProgressData.active_agents.length > 0 ? 3 : 2;
if (typeof direction === 'number') {
setCurrentSlide(direction);
} else if (direction === 'next') {
setCurrentSlide((prev) => (prev + 1) % totalSlides);
} else if (direction === 'prev') {
setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides);
}
};
if (isLoading && !fixProgressData) {
return (
<Card>
<CardContent className="p-4">
<div className="h-32 bg-muted animate-pulse rounded" />
</CardContent>
</Card>
);
}
if (!fixProgressData) {
return null;
}
const { phase, total_findings, fixed_count, failed_count, in_progress_count, pending_count, percent_complete, current_stage, total_stages, stages, active_agents } = fixProgressData;
const phaseIcon = phase === 'planning' ? '📝' : phase === 'execution' ? '⚡' : '✅';
const totalSlides = active_agents.length > 0 ? 3 : 2;
return (
<Card>
<CardContent className="p-4 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-lg">🔧</span>
<span className="font-semibold text-sm">{formatMessage({ id: 'reviewSession.fixProgress.title' })}</span>
</div>
{/* Stage Dots */}
<div className="flex gap-1">
{stages.map((stage, i) => (
<div
key={i}
className={`w-2 h-2 rounded-full ${
stage.status === 'completed' ? 'bg-green-500' :
stage.status === 'in-progress' ? 'bg-blue-500' :
'bg-gray-300 dark:bg-gray-600'
}`}
title={`Stage ${i + 1}: ${stage.status}`}
/>
))}
</div>
</div>
{/* Carousel */}
<div className="overflow-hidden">
<div
className="flex transition-transform duration-300 ease-in-out"
style={{ transform: `translateX(-${currentSlide * 100}%)` }}
>
{/* Slide 1: Overview */}
<div className="w-full flex-shrink-0">
<div className="flex items-center justify-between mb-3">
<Badge variant={phase === 'planning' ? 'secondary' : phase === 'execution' ? 'default' : 'success'}>
{phaseIcon} {formatMessage({ id: `reviewSession.fixProgress.phase.${phase}` })}
</Badge>
<span className="text-xs text-muted-foreground">{fixProgressData.fix_session_id}</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: `${percent_complete}%` }}
/>
</div>
<div className="text-xs text-muted-foreground text-center">
{formatMessage({ id: 'reviewSession.fixProgress.complete' }, { percent: percent_complete.toFixed(0) })} · {formatMessage({ id: 'reviewSession.fixProgress.stage' })} {current_stage}/{total_stages}
</div>
</div>
{/* Slide 2: Stats */}
<div className="w-full flex-shrink-0">
<div className="grid grid-cols-4 gap-2">
<div className="text-center p-2 bg-muted rounded">
<div className="text-lg font-bold">{total_findings}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.fixProgress.stats.total' })}</div>
</div>
<div className="text-center p-2 bg-green-100 dark:bg-green-900/20 rounded">
<div className="text-lg font-bold text-green-600 dark:text-green-400">{fixed_count}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.fixProgress.stats.fixed' })}</div>
</div>
<div className="text-center p-2 bg-red-100 dark:bg-red-900/20 rounded">
<div className="text-lg font-bold text-red-600 dark:text-red-400">{failed_count}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.fixProgress.stats.failed' })}</div>
</div>
<div className="text-center p-2 bg-yellow-100 dark:bg-yellow-900/20 rounded">
<div className="text-lg font-bold text-yellow-600 dark:text-yellow-400">{pending_count + in_progress_count}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.fixProgress.stats.pending' })}</div>
</div>
</div>
</div>
{/* Slide 3: Active Agents (if any) */}
{active_agents.length > 0 && (
<div className="w-full flex-shrink-0">
<div className="text-sm font-semibold mb-2">
{active_agents.length} {active_agents.length === 1 ? formatMessage({ id: 'reviewSession.fixProgress.activeAgents' }) : formatMessage({ id: 'reviewSession.fixProgress.activeAgentsPlural' })}
</div>
<div className="space-y-2">
{active_agents.slice(0, 2).map((agent, i) => (
<div key={i} className="flex items-center gap-2 p-2 bg-muted rounded">
<span>🤖</span>
<span className="text-sm">{agent.current_finding?.finding_title || formatMessage({ id: 'reviewSession.fixProgress.working' })}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Carousel Navigation */}
{totalSlides > 1 && (
<div className="flex items-center justify-between gap-2">
<Button variant="outline" size="sm" onClick={() => navigateSlide('prev')}>
<ChevronLeftIcon className="h-4 w-4" />
</Button>
<div className="flex gap-1">
{Array.from({ length: totalSlides }).map((_, i) => (
<button
key={i}
className={`w-2 h-2 rounded-full transition-colors ${
currentSlide === i ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
}`}
onClick={() => navigateSlide(i)}
/>
))}
</div>
<Button variant="outline" size="sm" onClick={() => navigateSlide('next')}>
<ChevronRightIcon className="h-4 w-4" />
</Button>
</div>
)}
</CardContent>
</Card>
);
}
/**
* ReviewSessionPage component - Display review session findings
*/
@@ -61,11 +286,13 @@ export function ReviewSessionPage() {
const [severityFilter, setSeverityFilter] = React.useState<Set<SeverityFilter>>(
new Set(['critical', 'high', 'medium', 'low'])
);
const [dimensionFilter, setDimensionFilter] = React.useState<string>('all');
const [searchQuery, setSearchQuery] = React.useState('');
const [sortField, setSortField] = React.useState<SortField>('severity');
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
const [selectedFindings, setSelectedFindings] = React.useState<Set<string>>(new Set());
const [expandedFindings, setExpandedFindings] = React.useState<Set<string>>(new Set());
const [selectedFindingId, setSelectedFindingId] = React.useState<string | null>(null);
const handleBack = () => {
navigate('/sessions');
@@ -83,6 +310,12 @@ export function ReviewSessionPage() {
});
};
const resetFilters = () => {
setSeverityFilter(new Set(['critical', 'high', 'medium', 'low']));
setDimensionFilter('all');
setSearchQuery('');
};
const toggleSelectFinding = (findingId: string) => {
setSelectedFindings(prev => {
const next = new Set(prev);
@@ -104,6 +337,22 @@ export function ReviewSessionPage() {
}
};
const selectVisibleFindings = () => {
const validIds = filteredFindings.map(f => f.id).filter((id): id is string => id !== undefined);
setSelectedFindings(new Set(validIds));
};
const selectBySeverity = (severity: FindingWithSelection['severity']) => {
const criticalIds = flattenedFindings
.filter(f => f.severity === severity && f.id !== undefined)
.map(f => f.id!);
setSelectedFindings(prev => {
const next = new Set(prev);
criticalIds.forEach(id => next.add(id));
return next;
});
};
const toggleExpandFinding = (findingId: string) => {
setExpandedFindings(prev => {
const next = new Set(prev);
@@ -116,6 +365,10 @@ export function ReviewSessionPage() {
});
};
const handleFindingClick = (findingId: string) => {
setSelectedFindingId(findingId);
};
const exportSelectedAsJson = () => {
const selected = flattenedFindings.filter(f => f.id !== undefined && selectedFindings.has(f.id));
if (selected.length === 0) return;
@@ -148,12 +401,26 @@ export function ReviewSessionPage() {
// Severity order for sorting
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
// Calculate dimension counts
const dimensionCounts = React.useMemo(() => {
const counts: Record<string, number> = { all: flattenedFindings.length };
flattenedFindings.forEach(f => {
counts[f.dimension] = (counts[f.dimension] || 0) + 1;
});
return counts;
}, [flattenedFindings]);
// Filter and sort findings
const filteredFindings = React.useMemo(() => {
let filtered = flattenedFindings;
// Apply dimension filter
if (dimensionFilter !== 'all') {
filtered = filtered.filter(f => f.dimension === dimensionFilter);
}
// Apply severity filter
if (severityFilter.size > 0 && !severityFilter.has('all' as SeverityFilter)) {
if (severityFilter.size > 0) {
filtered = filtered.filter(f => severityFilter.has(f.severity));
}
@@ -186,7 +453,7 @@ export function ReviewSessionPage() {
});
return filtered;
}, [flattenedFindings, severityFilter, searchQuery, sortField, sortOrder]);
}, [flattenedFindings, severityFilter, dimensionFilter, searchQuery, sortField, sortOrder]);
// Get severity badge props
const getSeverityBadge = (severity: FindingWithSelection['severity']) => {
@@ -256,6 +523,11 @@ export function ReviewSessionPage() {
const dimensions = reviewSession.reviewDimensions || [];
const totalFindings = flattenedFindings.length;
// Determine session status (ACTIVE or ARCHIVED)
const isActive = reviewSession._isActive !== false;
const sessionStatus = isActive ? 'ACTIVE' : 'ARCHIVED';
const phase = reviewSession.phase || 'in-progress';
return (
<div className="space-y-6">
{/* Header */}
@@ -266,65 +538,99 @@ export function ReviewSessionPage() {
{formatMessage({ id: 'common.actions.back' })}
</Button>
<div>
<h1 className="text-2xl font-semibold text-foreground">
{formatMessage({ id: 'reviewSession.title' })}
<h1 className="text-2xl font-semibold text-foreground flex items-center gap-2">
🔍 {reviewSession.session_id}
</h1>
<p className="text-sm text-muted-foreground">{reviewSession.session_id}</p>
<div className="flex items-center gap-2 mt-1">
<Badge variant="review">Review</Badge>
<Badge variant={isActive ? "success" : "secondary"} className="text-xs">
{sessionStatus}
</Badge>
</div>
</div>
</div>
<Badge variant="info">
{formatMessage({ id: 'reviewSession.type' })}
</Badge>
</div>
{/* Summary Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-foreground">{totalFindings}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.stats.total' })}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-destructive">{severityCounts.critical}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.severity.critical' })}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-warning">{severityCounts.high}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.severity.high' })}</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-foreground">{dimensions.length}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.stats.dimensions' })}</div>
</CardContent>
</Card>
</div>
{/* Review Progress Section */}
<Card>
<CardContent className="p-4 space-y-4">
{/* Review Progress Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-lg">📊</span>
<span className="font-semibold">{formatMessage({ id: 'reviewSession.progress.title' })}</span>
</div>
<Badge variant="secondary">{phase.toUpperCase()}</Badge>
</div>
{/* Summary Cards Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="flex items-center gap-3 p-3 bg-muted rounded-lg">
<span className="text-2xl">📊</span>
<div>
<div className="text-lg font-bold">{totalFindings}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.progress.totalFindings' })}</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-red-100 dark:bg-red-900/20 rounded-lg">
<span className="text-2xl">🔴</span>
<div>
<div className="text-lg font-bold text-red-600 dark:text-red-400">{severityCounts.critical}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.progress.critical' })}</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-orange-100 dark:bg-orange-900/20 rounded-lg">
<span className="text-2xl">🟠</span>
<div>
<div className="text-lg font-bold text-orange-600 dark:text-orange-400">{severityCounts.high}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.progress.high' })}</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-blue-100 dark:bg-blue-900/20 rounded-lg">
<span className="text-2xl">🎯</span>
<div>
<div className="text-lg font-bold text-blue-600 dark:text-blue-400">{dimensions.length}</div>
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'reviewSession.stats.dimensions' })}</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Fix Progress Carousel */}
{sessionId && <FixProgressCarousel sessionId={sessionId} />}
{/* Filters and Controls */}
<Card>
<CardContent className="p-4 space-y-4">
{/* Severity Filters */}
<div className="flex flex-wrap gap-2">
{(['critical', 'high', 'medium', 'low'] as const).map(severity => {
const isEnabled = severityFilter.has(severity);
const badge = getSeverityBadge(severity);
return (
<Badge
key={severity}
variant={isEnabled ? badge.variant : 'outline'}
className={`cursor-pointer ${isEnabled ? '' : 'opacity-50'}`}
onClick={() => toggleSeverity(severity)}
>
<badge.icon className="h-3 w-3 mr-1" />
{badge.label}: {severityCounts[severity]}
</Badge>
);
})}
{/* Checkbox-style Severity Filters */}
<div className="space-y-3">
<div className="text-sm font-medium">{formatMessage({ id: 'reviewSession.filters.severity' })}</div>
<div className="flex flex-wrap gap-2">
{(['critical', 'high', 'medium', 'low'] as const).map(severity => {
const isEnabled = severityFilter.has(severity);
return (
<label
key={severity}
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-full border cursor-pointer transition-colors ${
isEnabled
? 'bg-primary text-primary-foreground border-primary'
: 'bg-background border-border hover:bg-muted'
}`}
>
<input
type="checkbox"
checked={isEnabled}
onChange={() => toggleSeverity(severity)}
className="sr-only"
/>
<span className="text-sm font-medium">
{formatMessage({ id: `reviewSession.severity.${severity}` })}
</span>
</label>
);
})}
</div>
</div>
{/* Search and Sort */}
@@ -355,6 +661,9 @@ export function ReviewSessionPage() {
>
{sortOrder === 'asc' ? '↑' : '↓'}
</Button>
<Button variant="outline" size="sm" onClick={resetFilters}>
{formatMessage({ id: 'reviewSession.filters.reset' })}
</Button>
</div>
{/* Selection Controls */}
@@ -368,6 +677,12 @@ export function ReviewSessionPage() {
? formatMessage({ id: 'reviewSession.selection.clearAll' })
: formatMessage({ id: 'reviewSession.selection.selectAll' })}
</Button>
<Button variant="outline" size="sm" onClick={selectVisibleFindings}>
{formatMessage({ id: 'reviewSession.selection.selectVisible' })}
</Button>
<Button variant="outline" size="sm" onClick={() => selectBySeverity('critical')}>
{formatMessage({ id: 'reviewSession.selection.selectCritical' })}
</Button>
<Button
variant="outline"
size="sm"
@@ -384,12 +699,39 @@ export function ReviewSessionPage() {
className="gap-2"
>
<Download className="h-4 w-4" />
{formatMessage({ id: 'reviewSession.export' })}
🔧 {formatMessage({ id: 'reviewSession.export' })}
</Button>
</div>
</CardContent>
</Card>
{/* Dimension Tabs */}
<div className="flex flex-wrap gap-2">
<button
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
dimensionFilter === 'all'
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'
}`}
onClick={() => setDimensionFilter('all')}
>
{formatMessage({ id: 'reviewSession.dimensionTabs.all' })} ({dimensionCounts.all || 0})
</button>
{dimensions.map(dim => (
<button
key={dim.name}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
dimensionFilter === dim.name
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'
}`}
onClick={() => setDimensionFilter(dim.name)}
>
{dim.name} ({dim.findings?.length || 0})
</button>
))}
</div>
{/* Findings List */}
{filteredFindings.length === 0 ? (
<Card>

View File

@@ -27,7 +27,7 @@ import { ReviewTab } from './session-detail/ReviewTab';
import { TaskDrawer } from '@/components/shared/TaskDrawer';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import type { TaskData } from '@/types/store';
type TabValue = 'tasks' | 'context' | 'summary' | 'impl-plan' | 'conflict' | 'review';
@@ -103,6 +103,43 @@ export function SessionDetailPage() {
const completedTasks = tasks.filter((t) => t.status === 'completed').length;
const hasReview = session.has_review || session.review;
const tabs: TabItem[] = [
{
value: 'tasks',
label: formatMessage({ id: 'sessionDetail.tabs.tasks' }),
icon: <ListChecks className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{tasks.length}</Badge>,
},
{
value: 'context',
label: formatMessage({ id: 'sessionDetail.tabs.context' }),
icon: <Package className="h-4 w-4" />,
},
{
value: 'summary',
label: formatMessage({ id: 'sessionDetail.tabs.summary' }),
icon: <FileText className="h-4 w-4" />,
},
{
value: 'impl-plan',
label: formatMessage({ id: 'sessionDetail.tabs.implPlan' }),
icon: <Ruler className="h-4 w-4" />,
},
{
value: 'conflict',
label: formatMessage({ id: 'sessionDetail.tabs.conflict' }),
icon: <Scale className="h-4 w-4" />,
},
];
if (hasReview) {
tabs.push({
value: 'review',
label: formatMessage({ id: 'sessionDetail.tabs.review' }),
icon: <Search className="h-4 w-4" />,
});
}
return (
<div className="space-y-6">
{/* Header */}
@@ -148,65 +185,48 @@ export function SessionDetailPage() {
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabValue)}>
<TabsList>
<TabsTrigger value="tasks">
<ListChecks className="h-4 w-4 mr-2" />
{formatMessage({ id: 'sessionDetail.tabs.tasks' })}
<Badge variant="secondary" className="ml-2">
{tasks.length}
</Badge>
</TabsTrigger>
<TabsTrigger value="context">
<Package className="h-4 w-4 mr-2" />
{formatMessage({ id: 'sessionDetail.tabs.context' })}
</TabsTrigger>
<TabsTrigger value="summary">
<FileText className="h-4 w-4 mr-2" />
{formatMessage({ id: 'sessionDetail.tabs.summary' })}
</TabsTrigger>
<TabsTrigger value="impl-plan">
<Ruler className="h-4 w-4 mr-2" />
{formatMessage({ id: 'sessionDetail.tabs.implPlan' })}
</TabsTrigger>
<TabsTrigger value="conflict">
<Scale className="h-4 w-4 mr-2" />
{formatMessage({ id: 'sessionDetail.tabs.conflict' })}
</TabsTrigger>
{hasReview && (
<TabsTrigger value="review">
<Search className="h-4 w-4 mr-2" />
{formatMessage({ id: 'sessionDetail.tabs.review' })}
</TabsTrigger>
)}
</TabsList>
<TabsNavigation
value={activeTab}
onValueChange={(v) => setActiveTab(v as TabValue)}
tabs={tabs}
/>
<TabsContent value="tasks" className="mt-4">
{/* Tab Content */}
{activeTab === 'tasks' && (
<div className="mt-4">
<TaskListTab session={session} onTaskClick={setSelectedTask} />
</TabsContent>
</div>
)}
<TabsContent value="context" className="mt-4">
{activeTab === 'context' && (
<div className="mt-4">
<ContextTab context={context} />
</TabsContent>
</div>
)}
<TabsContent value="summary" className="mt-4">
{activeTab === 'summary' && (
<div className="mt-4">
<SummaryTab summary={summary} summaries={summaries} />
</TabsContent>
</div>
)}
<TabsContent value="impl-plan" className="mt-4">
{activeTab === 'impl-plan' && (
<div className="mt-4">
<ImplPlanTab implPlan={implPlan} />
</TabsContent>
</div>
)}
<TabsContent value="conflict" className="mt-4">
{activeTab === 'conflict' && (
<div className="mt-4">
<ConflictTab conflicts={conflicts as any} />
</TabsContent>
</div>
)}
{hasReview && (
<TabsContent value="review" className="mt-4">
<ReviewTab review={review as any} />
</TabsContent>
)}
</Tabs>
{hasReview && activeTab === 'review' && (
<div className="mt-4">
<ReviewTab review={review as any} />
</div>
)}
{/* Description (if exists) */}
{session.description && (

View File

@@ -40,7 +40,7 @@ import {
DropdownMenuSeparator,
DropdownMenuLabel,
} from '@/components/ui/Dropdown';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import { cn } from '@/lib/utils';
import type { SessionMetadata } from '@/types/store';
@@ -174,13 +174,15 @@ export function SessionsPage() {
{/* Filters */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
{/* Location tabs */}
<Tabs value={locationFilter} onValueChange={(v) => setLocationFilter(v as LocationFilter)}>
<TabsList>
<TabsTrigger value="active">{formatMessage({ id: 'sessions.filters.active' })}</TabsTrigger>
<TabsTrigger value="archived">{formatMessage({ id: 'sessions.filters.archived' })}</TabsTrigger>
<TabsTrigger value="all">{formatMessage({ id: 'sessions.filters.all' })}</TabsTrigger>
</TabsList>
</Tabs>
<TabsNavigation
value={locationFilter}
onValueChange={(v) => setLocationFilter(v as LocationFilter)}
tabs={[
{ value: 'active', label: formatMessage({ id: 'sessions.filters.active' }) },
{ value: 'archived', label: formatMessage({ id: 'sessions.filters.archived' }) },
{ value: 'all', label: formatMessage({ id: 'sessions.filters.all' }) },
]}
/>
{/* Search input */}
<div className="flex-1 max-w-sm relative">

View File

@@ -1,18 +1,18 @@
// ========================================
// Coordinator Page
// Coordinator Page - Merged Layout
// ========================================
// Page for monitoring and managing coordinator workflow execution with timeline, logs, and node details
// Unified page for task list overview and execution details with timeline, logs, and node details
import { useState, useCallback, useEffect } from 'react';
import { useState, useCallback, useEffect, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Play } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Play, CheckCircle2, XCircle, Clock, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import {
CoordinatorInputModal,
CoordinatorTimeline,
CoordinatorLogStream,
NodeDetailsPanel,
CoordinatorEmptyState,
} from '@/components/coordinator';
import {
useCoordinatorStore,
@@ -21,11 +21,164 @@ import {
selectCoordinatorStatus,
selectIsPipelineLoaded,
} from '@/stores/coordinatorStore';
import { cn } from '@/lib/utils';
// ========================================
// Types
// ========================================
interface CoordinatorTask {
id: string;
name: string;
status: 'pending' | 'running' | 'completed' | 'failed';
progress: {
completed: number;
total: number;
};
startedAt: string;
completedAt?: string;
}
// ========================================
// Mock Data (temporary - will be replaced by store)
// ========================================
const MOCK_TASKS: CoordinatorTask[] = [
{
id: 'task-1',
name: 'Feature Auth',
status: 'running',
progress: { completed: 3, total: 5 },
startedAt: '2026-02-03T14:23:00Z',
},
{
id: 'task-2',
name: 'API Integration',
status: 'completed',
progress: { completed: 8, total: 8 },
startedAt: '2026-02-03T10:00:00Z',
completedAt: '2026-02-03T10:15:00Z',
},
{
id: 'task-3',
name: 'Performance Test',
status: 'failed',
progress: { completed: 2, total: 6 },
startedAt: '2026-02-03T09:00:00Z',
},
];
// ========================================
// Task Card Component (inline)
// ========================================
interface TaskCardProps {
task: CoordinatorTask;
isSelected: boolean;
onClick: () => void;
}
function TaskCard({ task, isSelected, onClick }: TaskCardProps) {
const { formatMessage } = useIntl();
const statusConfig = {
pending: {
icon: Clock,
color: 'text-muted-foreground',
bg: 'bg-muted/50',
},
running: {
icon: Loader2,
color: 'text-blue-500',
bg: 'bg-blue-500/10',
},
completed: {
icon: CheckCircle2,
color: 'text-green-500',
bg: 'bg-green-500/10',
},
failed: {
icon: XCircle,
color: 'text-red-500',
bg: 'bg-red-500/10',
},
};
const config = statusConfig[task.status];
const StatusIcon = config.icon;
const progressPercent = Math.round((task.progress.completed / task.progress.total) * 100);
return (
<button
type="button"
onClick={onClick}
className={cn(
'flex flex-col p-3 rounded-lg border transition-all text-left w-full min-w-[160px] max-w-[200px]',
'hover:border-primary/50 hover:shadow-sm',
isSelected
? 'border-primary bg-primary/5 shadow-sm'
: 'border-border bg-card'
)}
>
{/* Task Name */}
<div className="flex items-center gap-2 mb-2">
<StatusIcon
className={cn(
'w-4 h-4 flex-shrink-0',
config.color,
task.status === 'running' && 'animate-spin'
)}
/>
<span className="text-sm font-medium text-foreground truncate">
{task.name}
</span>
</div>
{/* Status Badge */}
<div
className={cn(
'inline-flex items-center px-2 py-0.5 rounded text-xs font-medium mb-2 w-fit',
config.bg,
config.color
)}
>
{formatMessage({ id: `coordinator.status.${task.status}` })}
</div>
{/* Progress */}
<div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground">
<span>
{task.progress.completed}/{task.progress.total}
</span>
<span>{progressPercent}%</span>
</div>
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all',
task.status === 'completed' && 'bg-green-500',
task.status === 'running' && 'bg-blue-500',
task.status === 'failed' && 'bg-red-500',
task.status === 'pending' && 'bg-muted-foreground'
)}
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
</button>
);
}
// ========================================
// Main Component
// ========================================
export function CoordinatorPage() {
const { formatMessage } = useIntl();
const [isInputModalOpen, setIsInputModalOpen] = useState(false);
const [selectedNode, setSelectedNode] = useState<string | null>(null);
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
// Store selectors
const commandChain = useCoordinatorStore(selectCommandChain);
@@ -33,7 +186,11 @@ export function CoordinatorPage() {
const status = useCoordinatorStore(selectCoordinatorStatus);
const isPipelineLoaded = useCoordinatorStore(selectIsPipelineLoaded);
const syncStateFromServer = useCoordinatorStore((state) => state.syncStateFromServer);
const reset = useCoordinatorStore((state) => state.reset);
// Mock tasks (temporary - will be replaced by store)
const tasks = useMemo(() => MOCK_TASKS, []);
const hasTasks = tasks.length > 0;
const selectedTask = tasks.find((t) => t.id === selectedTaskId);
// Sync state on mount (for page refresh scenarios)
useEffect(() => {
@@ -52,12 +209,21 @@ export function CoordinatorPage() {
setSelectedNode(nodeId);
}, []);
// Handle task selection
const handleTaskClick = useCallback((taskId: string) => {
setSelectedTaskId((prev) => (prev === taskId ? null : taskId));
setSelectedNode(null);
}, []);
// Get selected node object
const selectedNodeObject = commandChain.find((node) => node.id === selectedNode) || currentNode || null;
const selectedNodeObject =
commandChain.find((node) => node.id === selectedNode) || currentNode || null;
return (
<div className="h-full flex flex-col -m-4 md:-m-6">
{/* ======================================== */}
{/* Toolbar */}
{/* ======================================== */}
<div className="flex items-center gap-3 p-3 bg-card border-b border-border">
{/* Page Title and Status */}
<div className="flex items-center gap-2 min-w-0 flex-1">
@@ -68,9 +234,12 @@ export function CoordinatorPage() {
</span>
{isPipelineLoaded && (
<span className="text-xs text-muted-foreground">
{formatMessage({ id: 'coordinator.page.status' }, {
status: formatMessage({ id: `coordinator.status.${status}` }),
})}
{formatMessage(
{ id: 'coordinator.page.status' },
{
status: formatMessage({ id: `coordinator.status.${status}` }),
}
)}
</span>
)}
</div>
@@ -90,43 +259,90 @@ export function CoordinatorPage() {
</div>
</div>
{/* Main Content Area - 3 Panel Layout */}
<div className="flex-1 flex overflow-hidden">
{/* Left Panel: Timeline */}
<div className="w-1/3 min-w-[300px] border-r border-border bg-card">
<CoordinatorTimeline
autoScroll={true}
onNodeClick={handleNodeClick}
className="h-full"
{/* ======================================== */}
{/* Main Content Area */}
{/* ======================================== */}
{!hasTasks ? (
/* Empty State - No tasks */
<div className="flex-1 flex overflow-hidden">
<CoordinatorEmptyState
onStart={handleOpenInputModal}
disabled={status === 'running' || status === 'initializing'}
className="w-full"
/>
</div>
) : (
<div className="flex-1 flex flex-col overflow-hidden">
{/* ======================================== */}
{/* Task List Area */}
{/* ======================================== */}
<div className="p-4 border-b border-border bg-background">
<div className="flex gap-3 overflow-x-auto pb-2">
{tasks.map((task) => (
<TaskCard
key={task.id}
task={task}
isSelected={selectedTaskId === task.id}
onClick={() => handleTaskClick(task.id)}
/>
))}
</div>
</div>
{/* Center Panel: Log Stream */}
<div className="flex-1 min-w-0 bg-card">
<CoordinatorLogStream />
</div>
{/* ======================================== */}
{/* Task Detail Area (shown when task is selected) */}
{/* ======================================== */}
{selectedTask ? (
<div className="flex-1 flex overflow-hidden">
{/* Left Panel: Timeline */}
<div className="w-1/3 min-w-[300px] border-r border-border bg-card">
<CoordinatorTimeline
autoScroll={true}
onNodeClick={handleNodeClick}
className="h-full"
/>
</div>
{/* Right Panel: Node Details */}
<div className="w-80 min-w-[320px] max-w-[400px] border-l border-border bg-card overflow-y-auto">
{selectedNodeObject ? (
<NodeDetailsPanel
node={selectedNodeObject}
isExpanded={true}
onToggle={(expanded) => {
if (!expanded) {
setSelectedNode(null);
}
}}
/>
{/* Center Panel: Log Stream */}
<div className="flex-1 min-w-0 flex flex-col bg-card">
<div className="flex-1 min-h-0">
<CoordinatorLogStream />
</div>
</div>
{/* Right Panel: Node Details */}
<div className="w-80 min-w-[320px] max-w-[400px] border-l border-border bg-card overflow-y-auto">
{selectedNodeObject ? (
<NodeDetailsPanel
node={selectedNodeObject}
isExpanded={true}
onToggle={(expanded) => {
if (!expanded) {
setSelectedNode(null);
}
}}
/>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm p-4 text-center">
{formatMessage({ id: 'coordinator.page.noNodeSelected' })}
</div>
)}
</div>
</div>
) : (
<div className="flex items-center justify-center h-full text-muted-foreground text-sm p-4 text-center">
{formatMessage({ id: 'coordinator.page.noNodeSelected' })}
/* No task selected - show selection prompt */
<div className="flex-1 flex items-center justify-center bg-muted/30">
<div className="text-sm text-muted-foreground">
{formatMessage({ id: 'coordinator.taskDetail.noSelection' })}
</div>
</div>
)}
</div>
</div>
)}
{/* ======================================== */}
{/* Coordinator Input Modal */}
{/* ======================================== */}
<CoordinatorInputModal
open={isInputModalOpen}
onClose={() => setIsInputModalOpen(false)}

View File

@@ -0,0 +1,6 @@
// ========================================
// Coordinator Page Export
// ========================================
// Barrel export for CoordinatorPage component
export { CoordinatorPage } from './CoordinatorPage';

View File

@@ -27,10 +27,10 @@ export { ReviewSessionPage } from './ReviewSessionPage';
export { McpManagerPage } from './McpManagerPage';
export { EndpointsPage } from './EndpointsPage';
export { InstallationsPage } from './InstallationsPage';
export { ExecutionMonitorPage } from './ExecutionMonitorPage';
export { RulesManagerPage } from './RulesManagerPage';
export { PromptHistoryPage } from './PromptHistoryPage';
export { ExplorerPage } from './ExplorerPage';
export { GraphExplorerPage } from './GraphExplorerPage';
export { CodexLensManagerPage } from './CodexLensManagerPage';
export { ApiSettingsPage } from './ApiSettingsPage';
export { CliViewerPage } from './CliViewerPage';

View File

@@ -257,12 +257,6 @@ export function TaskListTab({ session, onTaskClick }: TaskListTabProps) {
{/* Row 2: Meta info */}
<div className="flex items-center gap-3 flex-wrap justify-end text-xs text-muted-foreground">
{priority && (
<Badge variant={priority.variant} className="text-xs gap-1">
<Zap className="h-3 w-3" />
{priority.label}
</Badge>
)}
{taskType && (
<span className="bg-muted px-1.5 py-0.5 rounded">{taskType}</span>
)}

View File

@@ -31,7 +31,6 @@ import {
McpManagerPage,
EndpointsPage,
InstallationsPage,
ExecutionMonitorPage,
HookManagerPage,
RulesManagerPage,
PromptHistoryPage,
@@ -39,6 +38,7 @@ import {
GraphExplorerPage,
CodexLensManagerPage,
ApiSettingsPage,
CliViewerPage,
} from '@/pages';
/**
@@ -91,14 +91,14 @@ const routes: RouteObject[] = [
path: 'coordinator',
element: <CoordinatorPage />,
},
{
path: 'executions',
element: <ExecutionMonitorPage />,
},
{
path: 'loops',
element: <LoopMonitorPage />,
},
{
path: 'cli-viewer',
element: <CliViewerPage />,
},
{
path: 'issues',
element: <IssueHubPage />,
@@ -206,8 +206,8 @@ export const ROUTES = {
HISTORY: '/history',
ORCHESTRATOR: '/orchestrator',
COORDINATOR: '/coordinator',
EXECUTIONS: '/executions',
LOOPS: '/loops',
CLI_VIEWER: '/cli-viewer',
ISSUES: '/issues',
// Legacy issue routes - use ISSUES with ?tab parameter instead
ISSUE_QUEUE: '/issues?tab=queue',

View File

@@ -90,6 +90,20 @@ export {
selectIsPipelineLoaded,
} from './coordinatorStore';
// Viewer Store
export {
useViewerStore,
useViewerActions,
useViewerLayout,
useViewerPanes,
useViewerTabs,
useFocusedPaneId,
selectPane,
selectTab,
selectPaneTabs,
selectActiveTab,
} from './viewerStore';
// Re-export types for convenience
export type {
// App Store Types
@@ -143,6 +157,18 @@ export type {
PipelineDetails,
} from './coordinatorStore';
// Viewer Store Types
export type {
PaneId,
CliExecutionId,
TabId,
TabState,
PaneState,
AllotmentLayoutGroup,
AllotmentLayout,
ViewerState,
} from './viewerStore';
// Execution Types
export type {
ExecutionStatus,

View File

@@ -0,0 +1,980 @@
// ========================================
// Viewer Store
// ========================================
// Zustand store for managing CLI Viewer layout and tab state
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// ========== Types ==========
/**
* Unique identifier for a pane in the viewer layout
*/
export type PaneId = string;
/**
* Unique identifier for a CLI execution
*/
export type CliExecutionId = string;
/**
* Unique identifier for a tab
*/
export type TabId = string;
/**
* Tab state representing a single tab in a pane
*/
export interface TabState {
id: TabId;
executionId: CliExecutionId;
title: string;
isPinned: boolean;
order: number;
}
/**
* Pane state representing a container for tabs
*/
export interface PaneState {
id: PaneId;
tabs: TabState[];
activeTabId: TabId | null;
}
/**
* Allotment layout group for nested split layouts
*/
export interface AllotmentLayoutGroup {
direction: 'horizontal' | 'vertical';
sizes?: number[];
children: (PaneId | AllotmentLayoutGroup)[];
}
/**
* Root layout type
*/
export type AllotmentLayout = AllotmentLayoutGroup;
/**
* Viewer state interface
*/
export interface ViewerState {
// Layout state
layout: AllotmentLayout;
panes: Record<PaneId, PaneState>;
tabs: Record<TabId, TabState>;
focusedPaneId: PaneId | null;
// ID counters for generating unique IDs
nextPaneIdCounter: number;
nextTabIdCounter: number;
// Actions
setLayout: (newLayout: AllotmentLayout) => void;
addPane: (parentPaneId?: PaneId, direction?: 'horizontal' | 'vertical') => PaneId;
removePane: (paneId: PaneId) => void;
addTab: (paneId: PaneId, executionId: CliExecutionId, title: string) => TabId;
removeTab: (paneId: PaneId, tabId: TabId) => void;
setActiveTab: (paneId: PaneId, tabId: TabId) => void;
moveTab: (sourcePaneId: PaneId, tabId: TabId, targetPaneId: PaneId, newIndex: number) => void;
togglePinTab: (tabId: TabId) => void;
setFocusedPane: (paneId: PaneId) => void;
initializeDefaultLayout: (layoutName: 'single' | 'split-h' | 'split-v' | 'grid-2x2') => void;
reset: () => void;
}
// ========== Constants ==========
const VIEWER_STORAGE_KEY = 'cli-viewer-storage';
const VIEWER_STORAGE_VERSION = 1;
// ========== Helper Functions ==========
/**
* Generate a unique pane ID
*/
const generatePaneId = (counter: number): PaneId => `pane-${counter}`;
/**
* Generate a unique tab ID
*/
const generateTabId = (counter: number): TabId => `tab-${counter}`;
/**
* Check if a value is a PaneId (string) or AllotmentLayoutGroup
*/
const isPaneId = (value: PaneId | AllotmentLayoutGroup): value is PaneId => {
return typeof value === 'string';
};
/**
* Find a pane ID in the layout tree
*/
const findPaneInLayout = (
layout: AllotmentLayout,
paneId: PaneId
): { found: boolean; parent: AllotmentLayoutGroup | null; index: number } => {
const search = (
group: AllotmentLayoutGroup,
_parent: AllotmentLayoutGroup | null
): { found: boolean; parent: AllotmentLayoutGroup | null; index: number } => {
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
if (isPaneId(child)) {
if (child === paneId) {
return { found: true, parent: group, index: i };
}
} else {
const result = search(child, group);
if (result.found) {
return result;
}
}
}
return { found: false, parent: null, index: -1 };
};
return search(layout, null);
};
/**
* Remove a pane from layout and clean up empty groups
*/
const removePaneFromLayout = (layout: AllotmentLayout, paneId: PaneId): AllotmentLayout => {
const removeFromGroup = (group: AllotmentLayoutGroup): AllotmentLayoutGroup | null => {
const newChildren: (PaneId | AllotmentLayoutGroup)[] = [];
for (const child of group.children) {
if (isPaneId(child)) {
if (child !== paneId) {
newChildren.push(child);
}
} else {
const cleanedChild = removeFromGroup(child);
if (cleanedChild && cleanedChild.children.length > 0) {
// If only one child remains, flatten it
if (cleanedChild.children.length === 1) {
newChildren.push(cleanedChild.children[0]);
} else {
newChildren.push(cleanedChild);
}
}
}
}
if (newChildren.length === 0) {
return null;
}
// Update sizes proportionally when removing a child
const newSizes = group.sizes
? group.sizes.filter((_, i) => {
const child = group.children[i];
return !isPaneId(child) || child !== paneId;
})
: undefined;
// Normalize sizes to sum to 100
const normalizedSizes = newSizes
? (() => {
const total = newSizes.reduce((sum, s) => sum + s, 0);
return total > 0 ? newSizes.map((s) => (s / total) * 100) : undefined;
})()
: undefined;
return {
direction: group.direction,
sizes: normalizedSizes,
children: newChildren,
};
};
const result = removeFromGroup(layout);
return result || { direction: 'horizontal', children: [] };
};
/**
* Add a pane to the layout at a specific position
*/
const addPaneToLayout = (
layout: AllotmentLayout,
newPaneId: PaneId,
parentPaneId?: PaneId,
direction: 'horizontal' | 'vertical' = 'horizontal'
): AllotmentLayout => {
if (!parentPaneId) {
// Add to root level
if (layout.children.length === 0) {
return {
...layout,
children: [newPaneId],
sizes: [100],
};
}
// If root direction matches, add as sibling
if (layout.direction === direction) {
const currentSizes = layout.sizes || layout.children.map(() => 100 / layout.children.length);
const totalSize = currentSizes.reduce((sum, s) => sum + s, 0);
const newSize = totalSize / (layout.children.length + 1);
const scaleFactor = (totalSize - newSize) / totalSize;
return {
...layout,
children: [...layout.children, newPaneId],
sizes: [...currentSizes.map((s) => s * scaleFactor), newSize],
};
}
// Wrap entire layout in new group
return {
direction,
sizes: [50, 50],
children: [layout, newPaneId],
};
}
// Add relative to a specific pane
const addRelativeTo = (group: AllotmentLayoutGroup): AllotmentLayoutGroup => {
const newChildren: (PaneId | AllotmentLayoutGroup)[] = [];
let newSizes: number[] | undefined = group.sizes ? [] : undefined;
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
const childSize = group.sizes ? group.sizes[i] : undefined;
if (isPaneId(child)) {
if (child === parentPaneId) {
if (group.direction === direction) {
// Same direction, add as sibling
const halfSize = (childSize || 50) / 2;
newChildren.push(child, newPaneId);
if (newSizes) {
newSizes.push(halfSize, halfSize);
}
} else {
// Different direction, wrap in new group
const newGroup: AllotmentLayoutGroup = {
direction,
sizes: [50, 50],
children: [child, newPaneId],
};
newChildren.push(newGroup);
if (newSizes && childSize !== undefined) {
newSizes.push(childSize);
}
}
} else {
newChildren.push(child);
if (newSizes && childSize !== undefined) {
newSizes.push(childSize);
}
}
} else {
// Recursively check nested groups
const result = findPaneInLayout(child, parentPaneId);
if (result.found) {
newChildren.push(addRelativeTo(child));
} else {
newChildren.push(child);
}
if (newSizes && childSize !== undefined) {
newSizes.push(childSize);
}
}
}
return {
...group,
children: newChildren,
sizes: newSizes,
};
};
return addRelativeTo(layout);
};
/**
* Get all pane IDs from layout
*/
const getAllPaneIds = (layout: AllotmentLayout): PaneId[] => {
const paneIds: PaneId[] = [];
const traverse = (group: AllotmentLayoutGroup) => {
for (const child of group.children) {
if (isPaneId(child)) {
paneIds.push(child);
} else {
traverse(child);
}
}
};
traverse(layout);
return paneIds;
};
// ========== Initial State ==========
const createDefaultLayout = (): AllotmentLayout => ({
direction: 'horizontal',
children: [],
});
const initialState: Omit<ViewerState, keyof ViewerStateActions> = {
layout: createDefaultLayout(),
panes: {},
tabs: {},
focusedPaneId: null,
nextPaneIdCounter: 1,
nextTabIdCounter: 1,
};
// Separate actions type for initial state typing
type ViewerStateActions = Pick<
ViewerState,
| 'setLayout'
| 'addPane'
| 'removePane'
| 'addTab'
| 'removeTab'
| 'setActiveTab'
| 'moveTab'
| 'togglePinTab'
| 'setFocusedPane'
| 'initializeDefaultLayout'
| 'reset'
>;
// ========== Store ==========
/**
* Zustand store for CLI Viewer layout and tab management
*
* @remarks
* Manages the split-pane layout, tabs within each pane, and tab state.
* Uses persist middleware to save layout state to localStorage.
*
* @example
* ```tsx
* const { layout, addPane, addTab } = useViewerStore();
* const paneId = addPane();
* const tabId = addTab(paneId, 'exec-123', 'CLI Output');
* ```
*/
export const useViewerStore = create<ViewerState>()(
persist(
devtools(
(set, get) => ({
...initialState,
// ========== Layout Actions ==========
setLayout: (newLayout: AllotmentLayout) => {
set({ layout: newLayout }, false, 'viewer/setLayout');
},
addPane: (parentPaneId?: PaneId, direction: 'horizontal' | 'vertical' = 'horizontal') => {
const state = get();
const newPaneId = generatePaneId(state.nextPaneIdCounter);
const newPane: PaneState = {
id: newPaneId,
tabs: [],
activeTabId: null,
};
const newLayout = addPaneToLayout(state.layout, newPaneId, parentPaneId, direction);
set(
{
layout: newLayout,
panes: {
...state.panes,
[newPaneId]: newPane,
},
focusedPaneId: newPaneId,
nextPaneIdCounter: state.nextPaneIdCounter + 1,
},
false,
'viewer/addPane'
);
return newPaneId;
},
removePane: (paneId: PaneId) => {
const state = get();
const pane = state.panes[paneId];
if (!pane) {
console.warn(`[ViewerStore] Pane not found: ${paneId}`);
return;
}
// Get all remaining panes
const allPaneIds = getAllPaneIds(state.layout);
if (allPaneIds.length <= 1) {
console.warn('[ViewerStore] Cannot remove the last pane');
return;
}
// Remove pane from layout
const newLayout = removePaneFromLayout(state.layout, paneId);
// Remove pane and its tabs
const newPanes = { ...state.panes };
delete newPanes[paneId];
const newTabs = { ...state.tabs };
for (const tab of pane.tabs) {
delete newTabs[tab.id];
}
// Update focused pane if needed
let newFocusedPaneId = state.focusedPaneId;
if (newFocusedPaneId === paneId) {
const remainingPaneIds = getAllPaneIds(newLayout);
newFocusedPaneId = remainingPaneIds.length > 0 ? remainingPaneIds[0] : null;
}
set(
{
layout: newLayout,
panes: newPanes,
tabs: newTabs,
focusedPaneId: newFocusedPaneId,
},
false,
'viewer/removePane'
);
},
// ========== Tab Actions ==========
addTab: (paneId: PaneId, executionId: CliExecutionId, title: string) => {
const state = get();
const pane = state.panes[paneId];
if (!pane) {
console.warn(`[ViewerStore] Pane not found: ${paneId}`);
return '';
}
// Check if tab for this execution already exists in this pane
const existingTab = pane.tabs.find((t) => t.executionId === executionId);
if (existingTab) {
// Just activate the existing tab
set(
{
panes: {
...state.panes,
[paneId]: {
...pane,
activeTabId: existingTab.id,
},
},
focusedPaneId: paneId,
},
false,
'viewer/addTab-existing'
);
return existingTab.id;
}
const newTabId = generateTabId(state.nextTabIdCounter);
const maxOrder = pane.tabs.reduce((max, t) => Math.max(max, t.order), 0);
const newTab: TabState = {
id: newTabId,
executionId,
title,
isPinned: false,
order: maxOrder + 1,
};
const updatedPane: PaneState = {
...pane,
tabs: [...pane.tabs, newTab],
activeTabId: newTabId,
};
set(
{
panes: {
...state.panes,
[paneId]: updatedPane,
},
tabs: {
...state.tabs,
[newTabId]: newTab,
},
focusedPaneId: paneId,
nextTabIdCounter: state.nextTabIdCounter + 1,
},
false,
'viewer/addTab'
);
return newTabId;
},
removeTab: (paneId: PaneId, tabId: TabId) => {
const state = get();
const pane = state.panes[paneId];
if (!pane) {
console.warn(`[ViewerStore] Pane not found: ${paneId}`);
return;
}
const tabIndex = pane.tabs.findIndex((t) => t.id === tabId);
if (tabIndex === -1) {
console.warn(`[ViewerStore] Tab not found: ${tabId}`);
return;
}
const newTabs = pane.tabs.filter((t) => t.id !== tabId);
// Determine new active tab
let newActiveTabId: TabId | null = null;
if (pane.activeTabId === tabId && newTabs.length > 0) {
// Select the tab at the same index, or the last one
const newActiveIndex = Math.min(tabIndex, newTabs.length - 1);
newActiveTabId = newTabs[newActiveIndex].id;
} else if (pane.activeTabId !== tabId) {
newActiveTabId = pane.activeTabId;
}
const updatedPane: PaneState = {
...pane,
tabs: newTabs,
activeTabId: newActiveTabId,
};
const globalTabs = { ...state.tabs };
delete globalTabs[tabId];
set(
{
panes: {
...state.panes,
[paneId]: updatedPane,
},
tabs: globalTabs,
},
false,
'viewer/removeTab'
);
},
setActiveTab: (paneId: PaneId, tabId: TabId) => {
const state = get();
const pane = state.panes[paneId];
if (!pane) {
console.warn(`[ViewerStore] Pane not found: ${paneId}`);
return;
}
const tab = pane.tabs.find((t) => t.id === tabId);
if (!tab) {
console.warn(`[ViewerStore] Tab not found in pane: ${tabId}`);
return;
}
set(
{
panes: {
...state.panes,
[paneId]: {
...pane,
activeTabId: tabId,
},
},
focusedPaneId: paneId,
},
false,
'viewer/setActiveTab'
);
},
moveTab: (
sourcePaneId: PaneId,
tabId: TabId,
targetPaneId: PaneId,
newIndex: number
) => {
const state = get();
const sourcePane = state.panes[sourcePaneId];
const targetPane = state.panes[targetPaneId];
if (!sourcePane || !targetPane) {
console.warn('[ViewerStore] Source or target pane not found');
return;
}
const tabIndex = sourcePane.tabs.findIndex((t) => t.id === tabId);
if (tabIndex === -1) {
console.warn(`[ViewerStore] Tab not found: ${tabId}`);
return;
}
const tab = sourcePane.tabs[tabIndex];
// Remove from source
const newSourceTabs = sourcePane.tabs.filter((t) => t.id !== tabId);
// Determine new active tab for source pane
let newSourceActiveTabId: TabId | null = sourcePane.activeTabId;
if (sourcePane.activeTabId === tabId && newSourceTabs.length > 0) {
const newActiveIndex = Math.min(tabIndex, newSourceTabs.length - 1);
newSourceActiveTabId = newSourceTabs[newActiveIndex].id;
} else if (sourcePane.activeTabId === tabId) {
newSourceActiveTabId = null;
}
// Calculate new order based on target position
let newOrder: number;
if (sourcePaneId === targetPaneId) {
// Moving within the same pane
const sortedTabs = [...newSourceTabs].sort((a, b) => a.order - b.order);
if (newIndex === 0) {
newOrder = sortedTabs.length > 0 ? sortedTabs[0].order - 1 : 1;
} else if (newIndex >= sortedTabs.length) {
newOrder = sortedTabs.length > 0 ? sortedTabs[sortedTabs.length - 1].order + 1 : 1;
} else {
const prevOrder = sortedTabs[newIndex - 1].order;
const nextOrder = sortedTabs[newIndex].order;
newOrder = (prevOrder + nextOrder) / 2;
}
} else {
// Moving to different pane
const sortedTargetTabs = [...targetPane.tabs].sort((a, b) => a.order - b.order);
if (newIndex === 0) {
newOrder = sortedTargetTabs.length > 0 ? sortedTargetTabs[0].order - 1 : 1;
} else if (newIndex >= sortedTargetTabs.length) {
newOrder = sortedTargetTabs.length > 0 ? sortedTargetTabs[sortedTargetTabs.length - 1].order + 1 : 1;
} else {
const prevOrder = sortedTargetTabs[newIndex - 1].order;
const nextOrder = sortedTargetTabs[newIndex].order;
newOrder = (prevOrder + nextOrder) / 2;
}
}
const movedTab: TabState = {
...tab,
order: newOrder,
};
// Add to target
let newTargetTabs: TabState[];
if (sourcePaneId === targetPaneId) {
newTargetTabs = [...newSourceTabs, movedTab];
} else {
newTargetTabs = [...targetPane.tabs, movedTab];
}
const updatedSourcePane: PaneState = {
...sourcePane,
tabs: sourcePaneId === targetPaneId ? newTargetTabs : newSourceTabs,
activeTabId: sourcePaneId === targetPaneId ? tabId : newSourceActiveTabId,
};
const updatedTargetPane: PaneState =
sourcePaneId === targetPaneId
? updatedSourcePane
: {
...targetPane,
tabs: newTargetTabs,
activeTabId: tabId,
};
const newPanes = {
...state.panes,
[sourcePaneId]: updatedSourcePane,
};
if (sourcePaneId !== targetPaneId) {
newPanes[targetPaneId] = updatedTargetPane;
}
set(
{
panes: newPanes,
tabs: {
...state.tabs,
[tabId]: movedTab,
},
focusedPaneId: targetPaneId,
},
false,
'viewer/moveTab'
);
},
togglePinTab: (tabId: TabId) => {
const state = get();
const tab = state.tabs[tabId];
if (!tab) {
console.warn(`[ViewerStore] Tab not found: ${tabId}`);
return;
}
const updatedTab: TabState = {
...tab,
isPinned: !tab.isPinned,
};
// Also update in the pane's tabs array
const newPanes = { ...state.panes };
for (const paneId of Object.keys(newPanes)) {
const pane = newPanes[paneId];
const tabIndex = pane.tabs.findIndex((t) => t.id === tabId);
if (tabIndex !== -1) {
newPanes[paneId] = {
...pane,
tabs: pane.tabs.map((t) => (t.id === tabId ? updatedTab : t)),
};
break;
}
}
set(
{
tabs: {
...state.tabs,
[tabId]: updatedTab,
},
panes: newPanes,
},
false,
'viewer/togglePinTab'
);
},
// ========== Focus Actions ==========
setFocusedPane: (paneId: PaneId) => {
const state = get();
if (!state.panes[paneId]) {
console.warn(`[ViewerStore] Pane not found: ${paneId}`);
return;
}
set({ focusedPaneId: paneId }, false, 'viewer/setFocusedPane');
},
// ========== Layout Initialization ==========
initializeDefaultLayout: (layoutName: 'single' | 'split-h' | 'split-v' | 'grid-2x2') => {
const state = get();
let counter = state.nextPaneIdCounter;
const createPane = (): PaneState => {
const paneId = generatePaneId(counter++);
return {
id: paneId,
tabs: [],
activeTabId: null,
};
};
let layout: AllotmentLayout;
const panes: Record<PaneId, PaneState> = {};
switch (layoutName) {
case 'single': {
const pane = createPane();
panes[pane.id] = pane;
layout = {
direction: 'horizontal',
sizes: [100],
children: [pane.id],
};
break;
}
case 'split-h': {
const pane1 = createPane();
const pane2 = createPane();
panes[pane1.id] = pane1;
panes[pane2.id] = pane2;
layout = {
direction: 'horizontal',
sizes: [50, 50],
children: [pane1.id, pane2.id],
};
break;
}
case 'split-v': {
const pane1 = createPane();
const pane2 = createPane();
panes[pane1.id] = pane1;
panes[pane2.id] = pane2;
layout = {
direction: 'vertical',
sizes: [50, 50],
children: [pane1.id, pane2.id],
};
break;
}
case 'grid-2x2': {
const pane1 = createPane();
const pane2 = createPane();
const pane3 = createPane();
const pane4 = createPane();
panes[pane1.id] = pane1;
panes[pane2.id] = pane2;
panes[pane3.id] = pane3;
panes[pane4.id] = pane4;
layout = {
direction: 'vertical',
sizes: [50, 50],
children: [
{
direction: 'horizontal',
sizes: [50, 50],
children: [pane1.id, pane2.id],
},
{
direction: 'horizontal',
sizes: [50, 50],
children: [pane3.id, pane4.id],
},
],
};
break;
}
default:
return;
}
const firstPaneId = Object.keys(panes)[0] || null;
set(
{
layout,
panes,
tabs: {},
focusedPaneId: firstPaneId,
nextPaneIdCounter: counter,
nextTabIdCounter: 1,
},
false,
'viewer/initializeDefaultLayout'
);
},
// ========== Reset ==========
reset: () => {
set(
{
layout: createDefaultLayout(),
panes: {},
tabs: {},
focusedPaneId: null,
nextPaneIdCounter: 1,
nextTabIdCounter: 1,
},
false,
'viewer/reset'
);
},
}),
{ name: 'ViewerStore' }
),
{
name: VIEWER_STORAGE_KEY,
version: VIEWER_STORAGE_VERSION,
// Persist all layout state
partialize: (state) => ({
layout: state.layout,
panes: state.panes,
tabs: state.tabs,
focusedPaneId: state.focusedPaneId,
nextPaneIdCounter: state.nextPaneIdCounter,
nextTabIdCounter: state.nextTabIdCounter,
}),
}
)
);
// ========== Selectors ==========
/**
* Select the current layout
*/
export const useViewerLayout = () => useViewerStore((state) => state.layout);
/**
* Select all panes
*/
export const useViewerPanes = () => useViewerStore((state) => state.panes);
/**
* Select all tabs
*/
export const useViewerTabs = () => useViewerStore((state) => state.tabs);
/**
* Select the focused pane ID
*/
export const useFocusedPaneId = () => useViewerStore((state) => state.focusedPaneId);
/**
* Select a specific pane by ID
*/
export const selectPane = (state: ViewerState, paneId: PaneId) => state.panes[paneId];
/**
* Select a specific tab by ID
*/
export const selectTab = (state: ViewerState, tabId: TabId) => state.tabs[tabId];
/**
* Select tabs for a specific pane, sorted by order
*/
export const selectPaneTabs = (state: ViewerState, paneId: PaneId): TabState[] => {
const pane = state.panes[paneId];
if (!pane) return [];
return [...pane.tabs].sort((a, b) => a.order - b.order);
};
/**
* Select the active tab for a pane
*/
export const selectActiveTab = (state: ViewerState, paneId: PaneId): TabState | null => {
const pane = state.panes[paneId];
if (!pane || !pane.activeTabId) return null;
return pane.tabs.find((t) => t.id === pane.activeTabId) || null;
};
// ========== Helper Hooks ==========
/**
* Hook to get viewer actions
* Useful for components that only need actions, not the full state
*/
export const useViewerActions = () => {
return useViewerStore((state) => ({
setLayout: state.setLayout,
addPane: state.addPane,
removePane: state.removePane,
addTab: state.addTab,
removeTab: state.removeTab,
setActiveTab: state.setActiveTab,
moveTab: state.moveTab,
togglePinTab: state.togglePinTab,
setFocusedPane: state.setFocusedPane,
initializeDefaultLayout: state.initializeDefaultLayout,
reset: state.reset,
}));
};

View File

@@ -12,6 +12,28 @@ export type ViewMode = 'sessions' | 'liteTasks' | 'project-overview' | 'sessionD
export type SessionFilter = 'all' | 'active' | 'archived';
export type LiteTaskType = 'lite-plan' | 'lite-fix' | null;
/**
* Session type identifier matching backend session types.
*
* @remarks
* This type defines all available session types in the CCW workflow system.
* It matches the backend SessionType definition in `ccw/src/types/session.ts`.
*
* **Type descriptions:**
* - `workflow`: Standard workflow execution session
* - `review`: Code review session with dimension-based analysis
* - `tdd`: Test-driven development session
* - `test`: Testing session
* - `docs`: Documentation generation session
* - `lite-plan`: Lightweight planning session
* - `lite-fix`: Lightweight bug fix session
*
* **Backend type definition:** {@link https://github.com/claudews/ccw/blob/main/ccw/src/types/session.ts | session.ts}
*
* @see {@link SessionMetadata.type | SessionMetadata.type} for usage in session metadata
*/
export type SessionType = 'workflow' | 'review' | 'tdd' | 'test' | 'docs' | 'lite-plan' | 'lite-fix';
export interface AppState {
// Theme
theme: Theme;
@@ -148,6 +170,7 @@ export interface DashboardLayoutActions {
*/
export interface SessionMetadata {
session_id: string;
type?: SessionType;
title?: string;
description?: string;
status: 'planning' | 'in_progress' | 'completed' | 'archived' | 'paused';

View File

@@ -41,9 +41,8 @@ export default defineConfig({
'/docs': {
target: 'http://localhost:3001',
changeOrigin: true,
// Remove /docs prefix when forwarding to Docusaurus
// Example: /docs/getting-started -> http://localhost:3001/getting-started
rewrite: (path) => path.replace(/^\/docs/, ''),
// Preserve /docs prefix to match Docusaurus baseUrl configuration
// Example: /docs/overview -> http://localhost:3001/docs/overview
},
},
},

213
ccw/package-lock.json generated
View File

@@ -7,6 +7,7 @@
"": {
"name": "ccw-monorepo",
"version": "1.0.0",
"license": "MIT",
"workspaces": [
"frontend",
"docs-site"
@@ -15,8 +16,8 @@
"concurrently": "^9.1.2"
},
"engines": {
"node": ">=18.0",
"npm": ">=9.0"
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
},
"docs-site": {
@@ -8149,10 +8150,6 @@
"node": ">=0.10.0"
}
},
"docs-site/node_modules/fast-deep-equal": {
"version": "3.1.3",
"license": "MIT"
},
"docs-site/node_modules/fast-glob": {
"version": "3.3.3",
"license": "MIT",
@@ -9563,10 +9560,6 @@
"@sideway/pinpoint": "^2.0.0"
}
},
"docs-site/node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
},
"docs-site/node_modules/js-yaml": {
"version": "4.1.1",
"license": "MIT",
@@ -9768,10 +9761,6 @@
"version": "4.17.23",
"license": "MIT"
},
"docs-site/node_modules/lodash.debounce": {
"version": "4.0.8",
"license": "MIT"
},
"docs-site/node_modules/lodash.memoize": {
"version": "4.1.2",
"license": "MIT"
@@ -9788,16 +9777,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"docs-site/node_modules/loose-envify": {
"version": "1.4.0",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"docs-site/node_modules/lower-case": {
"version": "2.0.2",
"license": "MIT",
@@ -14135,27 +14114,6 @@
"node": ">=0.10.0"
}
},
"docs-site/node_modules/react": {
"version": "18.3.1",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"docs-site/node_modules/react-dom": {
"version": "18.3.1",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"docs-site/node_modules/react-fast-compare": {
"version": "3.2.2",
"license": "MIT"
@@ -14821,13 +14779,6 @@
"node": ">=11.0.0"
}
},
"docs-site/node_modules/scheduler": {
"version": "0.23.2",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"docs-site/node_modules/schema-dts": {
"version": "1.1.5",
"license": "Apache-2.0"
@@ -16794,6 +16745,7 @@
"@radix-ui/react-tooltip": "^1.1.0",
"@tanstack/react-query": "^5.60.0",
"@xyflow/react": "^12.10.0",
"allotment": "^1.20.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"highlight.js": "^11.11.1",
@@ -21016,10 +20968,6 @@
"jiti": "bin/jiti.js"
}
},
"frontend/node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
},
"frontend/node_modules/jsdom": {
"version": "25.0.1",
"dev": true,
@@ -21109,16 +21057,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"frontend/node_modules/loose-envify": {
"version": "1.4.0",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"frontend/node_modules/loupe": {
"version": "3.2.1",
"dev": true,
@@ -22526,27 +22464,6 @@
"version": "4.0.3",
"license": "MIT"
},
"frontend/node_modules/react": {
"version": "18.3.1",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"frontend/node_modules/react-dom": {
"version": "18.3.1",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"frontend/node_modules/react-draggable": {
"version": "4.5.0",
"license": "MIT",
@@ -23136,13 +23053,6 @@
"node": ">=v12.22.7"
}
},
"frontend/node_modules/scheduler": {
"version": "0.23.2",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"frontend/node_modules/semver": {
"version": "6.3.1",
"dev": true,
@@ -24788,6 +24698,24 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/allotment": {
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/allotment/-/allotment-1.20.5.tgz",
"integrity": "sha512-7i4NT7ieXEyAd5lBrXmE7WHz/e7hRuo97+j+TwrPE85ha6kyFURoc76nom0dWSZ1pTKVEAMJy/+f3/Isfu/41A==",
"license": "MIT",
"dependencies": {
"classnames": "^2.3.0",
"eventemitter3": "^5.0.0",
"fast-deep-equal": "^3.1.3",
"lodash.clamp": "^4.0.0",
"lodash.debounce": "^4.0.0",
"usehooks-ts": "^3.1.1"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -24848,6 +24776,12 @@
"node": ">=8"
}
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -24922,6 +24856,18 @@
"node": ">=6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -24950,6 +24896,61 @@
"node": ">=8"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/lodash.clamp": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/lodash.clamp/-/lodash.clamp-4.0.3.tgz",
"integrity": "sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==",
"license": "MIT"
},
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -24968,6 +24969,15 @@
"tslib": "^2.1.0"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
@@ -25037,6 +25047,21 @@
"dev": true,
"license": "0BSD"
},
"node_modules/usehooks-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz",
"integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==",
"license": "MIT",
"dependencies": {
"lodash.debounce": "^4.0.8"
},
"engines": {
"node": ">=16.15.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

Some files were not shown because too many files have changed in this diff Show More