mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -21,7 +21,7 @@
|
||||
"sidebar": "docs",
|
||||
"previous": {
|
||||
"title": "Overview",
|
||||
"permalink": "/docs/docs/"
|
||||
"permalink": "/docs/docs/overview"
|
||||
},
|
||||
"next": {
|
||||
"title": "/ccw-plan",
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")],
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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 个工作流级别。
|
||||
@@ -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) - 命令参考
|
||||
@@ -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(扩展) |
|
||||
|
||||
- **高复杂度** (>=4): 自动选择层级 3-4
|
||||
- **中等复杂度** (2-3): 自动选择层级 2
|
||||
- **低复杂度** (<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) - 常见问题和解答
|
||||
@@ -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) - 常见问题
|
||||
@@ -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) - 常见问题解答
|
||||
@@ -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) - 常见问题解答
|
||||
@@ -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` - 协作分析
|
||||
@@ -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` - 主工作流编排器
|
||||
64
ccw/frontend/package-lock.json
generated
64
ccw/frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
198
ccw/frontend/src/components/cli-viewer/ContentArea.tsx
Normal file
198
ccw/frontend/src/components/cli-viewer/ContentArea.tsx
Normal 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;
|
||||
89
ccw/frontend/src/components/cli-viewer/EmptyState.tsx
Normal file
89
ccw/frontend/src/components/cli-viewer/EmptyState.tsx
Normal 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;
|
||||
307
ccw/frontend/src/components/cli-viewer/ExecutionPicker.tsx
Normal file
307
ccw/frontend/src/components/cli-viewer/ExecutionPicker.tsx
Normal 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;
|
||||
157
ccw/frontend/src/components/cli-viewer/LayoutContainer.tsx
Normal file
157
ccw/frontend/src/components/cli-viewer/LayoutContainer.tsx
Normal 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;
|
||||
81
ccw/frontend/src/components/cli-viewer/PaneContent.tsx
Normal file
81
ccw/frontend/src/components/cli-viewer/PaneContent.tsx
Normal 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;
|
||||
257
ccw/frontend/src/components/cli-viewer/TabBar.tsx
Normal file
257
ccw/frontend/src/components/cli-viewer/TabBar.tsx
Normal 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;
|
||||
28
ccw/frontend/src/components/cli-viewer/index.ts
Normal file
28
ccw/frontend/src/components/cli-viewer/index.ts
Normal 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';
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
137
ccw/frontend/src/components/coordinator/CoordinatorTaskCard.tsx
Normal file
137
ccw/frontend/src/components/coordinator/CoordinatorTaskCard.tsx
Normal 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;
|
||||
140
ccw/frontend/src/components/coordinator/CoordinatorTaskList.tsx
Normal file
140
ccw/frontend/src/components/coordinator/CoordinatorTaskList.tsx
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
165
ccw/frontend/src/components/dashboard/DashboardWidgetConfig.tsx
Normal file
165
ccw/frontend/src/components/dashboard/DashboardWidgetConfig.tsx
Normal 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;
|
||||
99
ccw/frontend/src/components/dashboard/WidgetWrapper.tsx
Normal file
99
ccw/frontend/src/components/dashboard/WidgetWrapper.tsx
Normal 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;
|
||||
@@ -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 },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 },
|
||||
],
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
22
ccw/frontend/src/components/ui/DropdownMenu.tsx
Normal file
22
ccw/frontend/src/components/ui/DropdownMenu.tsx
Normal 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';
|
||||
@@ -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>
|
||||
|
||||
47
ccw/frontend/src/components/ui/TabsNavigation.tsx
Normal file
47
ccw/frontend/src/components/ui/TabsNavigation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
51
ccw/frontend/src/locales/en/cli-viewer.json
Normal file
51
ccw/frontend/src/locales/en/cli-viewer.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
141
ccw/frontend/src/locales/en/coordinator.json
Normal file
141
ccw/frontend/src/locales/en/coordinator.json
Normal 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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -61,5 +61,11 @@
|
||||
"explorations": "Explorations",
|
||||
"context": "Context",
|
||||
"diagnoses": "Diagnoses"
|
||||
},
|
||||
"status": {
|
||||
"completed": "completed",
|
||||
"inProgress": "in progress",
|
||||
"blocked": "blocked",
|
||||
"pending": "pending"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -45,5 +45,7 @@
|
||||
"tokens": "令牌: {count}",
|
||||
"duration": "时长: {value}",
|
||||
"model": "模型: {name}",
|
||||
"user": "用户"
|
||||
"user": "用户",
|
||||
"popOutToPage": "在完整页面中打开",
|
||||
"openInViewer": "在 CLI 查看器中打开"
|
||||
}
|
||||
|
||||
51
ccw/frontend/src/locales/zh/cli-viewer.json
Normal file
51
ccw/frontend/src/locales/zh/cli-viewer.json
Normal 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": "关闭窗格"
|
||||
}
|
||||
}
|
||||
@@ -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": "网络错误,请检查您的连接并重试。",
|
||||
|
||||
157
ccw/frontend/src/locales/zh/coordinator.json
Normal file
157
ccw/frontend/src/locales/zh/coordinator.json
Normal 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": "步"
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
"title": "执行监控",
|
||||
"subtitle": "查看实时执行状态和历史记录"
|
||||
},
|
||||
"actions": {
|
||||
"openCliViewer": "CLI 监控"
|
||||
},
|
||||
"currentExecution": {
|
||||
"title": "当前执行",
|
||||
"noExecution": "当前没有正在执行的工作流",
|
||||
|
||||
@@ -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": "个功能"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -61,5 +61,11 @@
|
||||
"explorations": "探索",
|
||||
"context": "上下文",
|
||||
"diagnoses": "诊断"
|
||||
},
|
||||
"status": {
|
||||
"completed": "已完成",
|
||||
"inProgress": "进行中",
|
||||
"blocked": "已阻止",
|
||||
"pending": "待处理"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "已复制!",
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
"archived": "已归档",
|
||||
"paused": "已暂停"
|
||||
},
|
||||
"type": {
|
||||
"workflow": "工作流",
|
||||
"review": "审查",
|
||||
"tdd": "TDD",
|
||||
"test": "测试",
|
||||
"docs": "文档",
|
||||
"lite-plan": "轻量计划",
|
||||
"lite-fix": "轻量修复"
|
||||
},
|
||||
"actions": {
|
||||
"viewDetails": "查看详情",
|
||||
"archive": "归档",
|
||||
|
||||
@@ -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
|
||||
|
||||
266
ccw/frontend/src/pages/CliViewerPage.tsx
Normal file
266
ccw/frontend/src/pages/CliViewerPage.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
6
ccw/frontend/src/pages/coordinator/index.ts
Normal file
6
ccw/frontend/src/pages/coordinator/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// ========================================
|
||||
// Coordinator Page Export
|
||||
// ========================================
|
||||
// Barrel export for CoordinatorPage component
|
||||
|
||||
export { CoordinatorPage } from './CoordinatorPage';
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
980
ccw/frontend/src/stores/viewerStore.ts
Normal file
980
ccw/frontend/src/stores/viewerStore.ts
Normal 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,
|
||||
}));
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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
213
ccw/package-lock.json
generated
@@ -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
Reference in New Issue
Block a user