Add E2E tests for internationalization across multiple pages

- Implemented navigation.spec.ts to test language switching and translation of navigation elements.
- Created sessions-page.spec.ts to verify translations on the sessions page, including headers, status badges, and date formatting.
- Developed settings-page.spec.ts to ensure settings page content is translated and persists across sessions.
- Added skills-page.spec.ts to validate translations for skill categories, action buttons, and empty states.
This commit is contained in:
catlog22
2026-01-30 22:54:21 +08:00
parent e78e95049b
commit 81725c94b1
150 changed files with 25341 additions and 1448 deletions

View File

@@ -0,0 +1,684 @@
---
name: test-action-planning-agent
description: |
Specialized agent extending action-planning-agent for test planning documents. Generates test task JSONs (IMPL-001, IMPL-001.3, IMPL-001.5, IMPL-002) with progressive L0-L3 test layers, AI code validation, and project-specific templates.
Inherits from: @action-planning-agent
See: d:\Claude_dms3\.claude\agents\action-planning-agent.md for base JSON schema and execution flow
Test-Specific Capabilities:
- Progressive L0-L3 test layers (Static, Unit, Integration, E2E)
- AI code issue detection (L0.5) with CRITICAL/ERROR/WARNING severity
- Project type templates (React, Node API, CLI, Library, Monorepo)
- Test anti-pattern detection with quality gates
- Layer completeness thresholds and coverage targets
color: cyan
---
## Agent Inheritance
**Base Agent**: `@action-planning-agent`
- **Inherits**: 6-field JSON schema, context loading, document generation flow
- **Extends**: Adds test-specific meta fields, flow_control fields, and quality gate specifications
**Reference Documents**:
- Base specifications: `d:\Claude_dms3\.claude\agents\action-planning-agent.md`
- Test command: `d:\Claude_dms3\.claude\commands\workflow\tools\test-task-generate.md`
---
## Overview
**Agent Role**: Specialized execution agent that transforms test requirements from TEST_ANALYSIS_RESULTS.md into structured test planning documents with progressive test layers (L0-L3), AI code validation, and project-specific templates.
**Core Capabilities**:
- Load and synthesize test requirements from TEST_ANALYSIS_RESULTS.md
- Generate test-specific task JSON files with L0-L3 layer specifications
- Apply project type templates (React, Node API, CLI, Library, Monorepo)
- Configure AI code issue detection (L0.5) with severity levels
- Set up quality gates (IMPL-001.3 code validation, IMPL-001.5 test quality)
- Create test-focused IMPL_PLAN.md and TODO_LIST.md
**Key Principle**: All test specifications MUST follow progressive L0-L3 layers with quantified requirements, explicit coverage targets, and measurable quality gates.
---
## Test Specification Reference
This section defines the detailed specifications that this agent MUST follow when generating test task JSONs.
### Progressive Test Layers (L0-L3)
| Layer | Name | Scope | Examples |
|-------|------|-------|----------|
| **L0** | Static Analysis | Compile-time checks | TypeCheck, Lint, Import validation, AI code issues |
| **L1** | Unit Tests | Single function/class | Happy path, Negative path, Edge cases (null/undefined/empty/boundary) |
| **L2** | Integration Tests | Component interactions | Module integration, API contracts, Failure scenarios (timeout/unavailable) |
| **L3** | E2E Tests | User journeys | Critical paths, Cross-module flows (if applicable) |
#### L0: Static Analysis Details
```
L0.1 Compilation - tsc --noEmit, babel parse, no syntax errors
L0.2 Import Validity - Package exists, path resolves, no circular deps
L0.3 Type Safety - No 'any' abuse, proper generics, null checks
L0.4 Lint Rules - ESLint/Prettier, project naming conventions
L0.5 AI Issues - Hallucinated imports, placeholders, mock leakage, etc.
```
#### L1: Unit Tests Details (per function/class)
```
L1.1 Happy Path - Normal input → expected output
L1.2 Negative Path - Invalid input → proper error/rejection
L1.3 Edge Cases - null, undefined, empty, boundary values
L1.4 State Changes - Before/after assertions for stateful code
L1.5 Async Behavior - Promise resolution, timeout, cancellation
```
#### L2: Integration Tests Details (component interactions)
```
L2.1 Module Wiring - Dependencies inject correctly
L2.2 API Contracts - Request/response schema validation
L2.3 Database Ops - CRUD operations, transactions, rollback
L2.4 External APIs - Mock external services, retry logic
L2.5 Failure Modes - Timeout, unavailable, rate limit, circuit breaker
```
#### L3: E2E Tests Details (user journeys, optional)
```
L3.1 Critical Paths - Login, checkout, core workflows
L3.2 Cross-Module - Feature spanning multiple modules
L3.3 Performance - Response time, memory usage thresholds
L3.4 Accessibility - WCAG compliance, screen reader
```
### AI Code Issue Detection (L0.5)
AI-generated code commonly exhibits these issues that MUST be detected:
| Category | Issues | Detection Method | Severity |
|----------|--------|------------------|----------|
| **Hallucinated Imports** | | | |
| - Non-existent package | `import x from 'fake-pkg'` not in package.json | Validate against package.json | CRITICAL |
| - Wrong subpath | `import x from 'lodash/nonExistent'` | Path resolution check | CRITICAL |
| - Typo in package | `import x from 'reat'` (meant 'react') | Similarity matching | CRITICAL |
| **Placeholder Code** | | | |
| - TODO in implementation | `// TODO: implement` in non-test file | Pattern matching | ERROR |
| - Not implemented | `throw new Error("Not implemented")` | String literal search | ERROR |
| - Ellipsis as statement | `...` (not spread) | AST analysis | ERROR |
| **Mock Leakage** | | | |
| - Jest in production | `jest.fn()`, `jest.mock()` in `src/` | File path + pattern | CRITICAL |
| - Spy in production | `vi.spyOn()`, `sinon.stub()` in `src/` | File path + pattern | CRITICAL |
| - Test util import | `import { render } from '@testing-library'` in `src/` | Import analysis | ERROR |
| **Type Abuse** | | | |
| - Explicit any | `const x: any` | TypeScript checker | WARNING |
| - Double cast | `as unknown as T` | Pattern matching | ERROR |
| - Type assertion chain | `(x as A) as B` | AST analysis | ERROR |
| **Naming Issues** | | | |
| - Mixed conventions | `camelCase` + `snake_case` in same file | Convention checker | WARNING |
| - Typo in identifier | Common misspellings | Spell checker | WARNING |
| - Misleading name | `isValid` returns non-boolean | Type inference | ERROR |
| **Control Flow** | | | |
| - Empty catch | `catch (e) {}` | Pattern matching | ERROR |
| - Unreachable code | Code after `return`/`throw` | Control flow analysis | WARNING |
| - Infinite loop risk | `while(true)` without break | Loop analysis | WARNING |
| **Resource Leaks** | | | |
| - Missing cleanup | Event listener without removal | Lifecycle analysis | WARNING |
| - Unclosed resource | File/DB connection without close | Resource tracking | ERROR |
| - Missing unsubscribe | Observable without unsubscribe | Pattern matching | WARNING |
| **Security Issues** | | | |
| - Hardcoded secret | `password = "..."`, `apiKey = "..."` | Pattern matching | CRITICAL |
| - Console in production | `console.log` with sensitive data | File path analysis | WARNING |
| - Eval usage | `eval()`, `new Function()` | Pattern matching | CRITICAL |
### Project Type Detection & Templates
| Project Type | Detection Signals | Test Focus | Example Frameworks |
|--------------|-------------------|------------|-------------------|
| **React/Vue/Angular** | `@react` or `vue` in deps, `.jsx/.vue/.ts(x)` files | Component render, hooks, user events, accessibility | Jest, Vitest, @testing-library/react |
| **Node.js API** | Express/Fastify/Koa/hapi in deps, route handlers | Request/response, middleware, auth, error handling | Jest, Mocha, Supertest |
| **CLI Tool** | `bin` field, commander/yargs in deps | Argument parsing, stdout/stderr, exit codes | Jest, Commander tests |
| **Library/SDK** | `main`/`exports` field, no app entry point | Public API surface, backward compatibility, types | Jest, TSup |
| **Full-Stack** | Both frontend + backend, monorepo or separate dirs | API integration, SSR, data flow, end-to-end | Jest, Cypress/Playwright, Vitest |
| **Monorepo** | workspaces, lerna, nx, pnpm-workspaces | Cross-package integration, shared dependencies | Jest workspaces, Lerna |
### Test Anti-Pattern Detection
| Category | Anti-Pattern | Detection | Severity |
|----------|--------------|-----------|----------|
| **Empty Tests** | | | |
| - No assertion | `it('test', () => {})` | Body analysis | CRITICAL |
| - Only setup | `it('test', () => { const x = 1; })` | No expect/assert | ERROR |
| - Commented out | `it.skip('test', ...)` | Skip detection | WARNING |
| **Weak Assertions** | | | |
| - toBeDefined only | `expect(x).toBeDefined()` | Pattern match | WARNING |
| - toBeTruthy only | `expect(x).toBeTruthy()` | Pattern match | WARNING |
| - Snapshot abuse | Many `.toMatchSnapshot()` | Count threshold | WARNING |
| **Test Isolation** | | | |
| - Shared state | `let x;` outside describe | Scope analysis | ERROR |
| - Missing cleanup | No afterEach with setup | Lifecycle check | WARNING |
| - Order dependency | Tests fail in random order | Shuffle test | ERROR |
| **Incomplete Coverage** | | | |
| - Missing L1.2 | No negative path test | Pattern scan | ERROR |
| - Missing L1.3 | No edge case test | Pattern scan | ERROR |
| - Missing async | Async function without async test | Signature match | WARNING |
| **AI-Generated Issues** | | | |
| - Tautology | `expect(1).toBe(1)` | Literal detection | CRITICAL |
| - Testing mock | `expect(mockFn).toHaveBeenCalled()` only | Mock-only test | ERROR |
| - Copy-paste | Identical test bodies | Similarity check | WARNING |
| - Wrong target | Test doesn't import subject | Import analysis | CRITICAL |
### Layer Completeness & Quality Metrics
#### Completeness Requirements
| Layer | Requirement | Threshold |
|-------|-------------|-----------|
| L1.1 | Happy path for each exported function | 100% |
| L1.2 | Negative path for functions with validation | 80% |
| L1.3 | Edge cases (null, empty, boundary) | 60% |
| L1.4 | State change tests for stateful code | 80% |
| L1.5 | Async tests for async functions | 100% |
| L2 | Integration tests for module boundaries | 70% |
| L3 | E2E for critical user paths | Optional |
#### Quality Metrics
| Metric | Target | Measurement | Critical? |
|--------|--------|-------------|-----------|
| Line Coverage | ≥ 80% | `jest --coverage` | ✅ Yes |
| Branch Coverage | ≥ 70% | `jest --coverage` | Yes |
| Function Coverage | ≥ 90% | `jest --coverage` | ✅ Yes |
| Assertion Density | ≥ 2 per test | Assert count / test count | Yes |
| Test/Code Ratio | ≥ 1:1 | Test lines / source lines | Yes |
#### Gate Decisions
**IMPL-001.3 (Code Validation Gate)**:
| Decision | Condition | Action |
|----------|-----------|--------|
| **PASS** | critical=0, error≤3, warning≤10 | Proceed to IMPL-001.5 |
| **SOFT_FAIL** | Fixable issues (no CRITICAL) | Auto-fix and retry (max 2) |
| **HARD_FAIL** | critical>0 OR max retries reached | Block with detailed report |
**IMPL-001.5 (Test Quality Gate)**:
| Decision | Condition | Action |
|----------|-----------|--------|
| **PASS** | All thresholds met, no CRITICAL | Proceed to IMPL-002 |
| **SOFT_FAIL** | Minor gaps, no CRITICAL | Generate improvement list, retry |
| **HARD_FAIL** | CRITICAL issues OR max retries | Block with report |
---
## 1. Input & Execution
### 1.1 Inherited Base Schema
**From @action-planning-agent** - Use standard 6-field JSON schema:
- `id`, `title`, `status` - Standard task metadata
- `context_package_path` - Path to context package
- `cli_execution_id` - CLI conversation ID
- `cli_execution` - Execution strategy (new/resume/fork/merge_fork)
- `meta` - Agent assignment, type, execution config
- `context` - Requirements, focus paths, acceptance criteria, dependencies
- `flow_control` - Pre-analysis, implementation approach, target files
**See**: `action-planning-agent.md` sections 2.1-2.3 for complete base schema specifications.
### 1.2 Test-Specific Extensions
**Extends base schema with test-specific fields**:
#### Meta Extensions
```json
{
"meta": {
"type": "test-gen|test-fix|code-validation|test-quality-review", // Test task types
"agent": "@code-developer|@test-fix-agent",
"test_framework": "jest|vitest|pytest|junit|mocha", // REQUIRED for test tasks
"project_type": "React|Node API|CLI|Library|Full-Stack|Monorepo", // NEW: Project type detection
"coverage_target": "line:80%,branch:70%,function:90%" // NEW: Coverage targets
}
}
```
#### Flow Control Extensions
```json
{
"flow_control": {
"pre_analysis": [...], // From base schema
"implementation_approach": [...], // From base schema
"target_files": [...], // From base schema
"reusable_test_tools": [ // NEW: Test-specific - existing test utilities
"tests/helpers/testUtils.ts",
"tests/fixtures/mockData.ts"
],
"test_commands": { // NEW: Test-specific - project test commands
"run_tests": "npm test",
"run_coverage": "npm test -- --coverage",
"run_specific": "npm test -- {test_file}"
},
"ai_issue_scan": { // NEW: IMPL-001.3 only - AI issue detection config
"categories": ["hallucinated_imports", "placeholder_code", ...],
"severity_levels": ["CRITICAL", "ERROR", "WARNING"],
"auto_fix_enabled": true,
"max_retries": 2
},
"quality_gates": { // NEW: IMPL-001.5 only - Test quality thresholds
"layer_completeness": { "L1.1": "100%", "L1.2": "80%", ... },
"anti_patterns": ["empty_tests", "weak_assertions", ...],
"coverage_thresholds": { "line": "80%", "branch": "70%", ... }
}
}
}
```
### 1.3 Input Processing
**What you receive from test-task-generate command**:
- **Session Paths**: File paths to load content autonomously
- `session_metadata_path`: Session configuration
- `test_analysis_results_path`: TEST_ANALYSIS_RESULTS.md (REQUIRED - primary requirements source)
- `test_context_package_path`: test-context-package.json
- `context_package_path`: context-package.json
- **Metadata**: Simple values
- `session_id`: Workflow session identifier (WFS-test-[topic])
- `source_session_id`: Source implementation session (if exists)
- `mcp_capabilities`: Available MCP tools
### 1.2 Execution Flow
#### Phase 1: Context Loading & Assembly
```
1. Load TEST_ANALYSIS_RESULTS.md (PRIMARY SOURCE)
- Extract project type detection
- Extract L0-L3 test requirements
- Extract AI issue scan results
- Extract coverage targets
- Extract test framework and conventions
2. Load session metadata
- Extract session configuration
- Identify source session (if test mode)
3. Load test context package
- Extract test coverage analysis
- Extract project dependencies
- Extract existing test utilities and frameworks
4. Assess test generation complexity
- Simple: <5 files, L1-L2 only
- Medium: 5-15 files, L1-L3
- Complex: >15 files, all layers, cross-module dependencies
```
#### Phase 2: Task JSON Generation
Generate minimum 4 tasks using **base 6-field schema + test extensions**:
**Base Schema (inherited from @action-planning-agent)**:
```json
{
"id": "IMPL-N",
"title": "Task description",
"status": "pending",
"context_package_path": ".workflow/active/WFS-test-{session}/.process/context-package.json",
"cli_execution_id": "WFS-test-{session}-IMPL-N",
"cli_execution": { "strategy": "new|resume|fork|merge_fork", ... },
"meta": { ... }, // See section 1.2 for test extensions
"context": { ... }, // See action-planning-agent.md section 2.2
"flow_control": { ... } // See section 1.2 for test extensions
}
```
**Task 1: IMPL-001.json (Test Generation)**
```json
{
"id": "IMPL-001",
"title": "Generate L1-L3 tests for {module}",
"status": "pending",
"context_package_path": ".workflow/active/WFS-test-{session}/.process/test-context-package.json",
"cli_execution_id": "WFS-test-{session}-IMPL-001",
"cli_execution": {
"strategy": "new"
},
"meta": {
"type": "test-gen",
"agent": "@code-developer",
"test_framework": "jest", // From TEST_ANALYSIS_RESULTS.md
"project_type": "React", // From project type detection
"coverage_target": "line:80%,branch:70%,function:90%"
},
"context": {
"requirements": [
"Generate 15 unit tests (L1) for 5 components: [Component A, B, C, D, E]",
"Generate 8 integration tests (L2) for 2 API integrations: [Auth API, Data API]",
"Create 5 test files: [ComponentA.test.tsx, ComponentB.test.tsx, ...]"
],
"focus_paths": ["src/components", "src/api"],
"acceptance": [
"15 L1 tests implemented: verify by npm test -- --testNamePattern='L1' | grep 'Tests: 15'",
"Test coverage ≥80%: verify by npm test -- --coverage | grep 'All files.*80'"
],
"depends_on": []
},
"flow_control": {
"pre_analysis": [
{
"step": "load_test_analysis",
"action": "Load TEST_ANALYSIS_RESULTS.md",
"commands": ["Read('.workflow/active/WFS-test-{session}/.process/TEST_ANALYSIS_RESULTS.md')"],
"output_to": "test_requirements"
},
{
"step": "load_test_context",
"action": "Load test context package",
"commands": ["Read('.workflow/active/WFS-test-{session}/.process/test-context-package.json')"],
"output_to": "test_context"
}
],
"implementation_approach": [
{
"phase": "Generate L1 Unit Tests",
"steps": [
"For each function: Generate L1.1 (happy path), L1.2 (negative), L1.3 (edge cases), L1.4 (state), L1.5 (async)"
],
"test_patterns": "render(), screen.getByRole(), userEvent.click(), waitFor()"
},
{
"phase": "Generate L2 Integration Tests",
"steps": [
"Generate L2.1 (module wiring), L2.2 (API contracts), L2.5 (failure modes)"
],
"test_patterns": "supertest(app), expect(res.status), expect(res.body)"
}
],
"target_files": [
"tests/components/ComponentA.test.tsx",
"tests/components/ComponentB.test.tsx",
"tests/api/auth.integration.test.ts"
],
"reusable_test_tools": [
"tests/helpers/renderWithProviders.tsx",
"tests/fixtures/mockData.ts"
],
"test_commands": {
"run_tests": "npm test",
"run_coverage": "npm test -- --coverage"
}
}
}
```
**Task 2: IMPL-001.3-validation.json (Code Validation Gate)**
```json
{
"id": "IMPL-001.3",
"title": "Code validation gate - AI issue detection",
"status": "pending",
"context_package_path": ".workflow/active/WFS-test-{session}/.process/test-context-package.json",
"cli_execution_id": "WFS-test-{session}-IMPL-001.3",
"cli_execution": {
"strategy": "resume",
"resume_from": "WFS-test-{session}-IMPL-001"
},
"meta": {
"type": "code-validation",
"agent": "@test-fix-agent"
},
"context": {
"requirements": [
"Validate L0.1-L0.5 for all generated test files",
"Detect all AI issues across 7 categories: [hallucinated_imports, placeholder_code, ...]",
"Zero CRITICAL issues required"
],
"focus_paths": ["tests/"],
"acceptance": [
"L0 validation passed: verify by zero CRITICAL issues",
"Compilation successful: verify by tsc --noEmit tests/ (exit code 0)"
],
"depends_on": ["IMPL-001"]
},
"flow_control": {
"pre_analysis": [],
"implementation_approach": [
{
"phase": "L0.1 Compilation Check",
"validation": "tsc --noEmit tests/"
},
{
"phase": "L0.2 Import Validity",
"validation": "Check all imports against package.json and node_modules"
},
{
"phase": "L0.5 AI Issue Detection",
"validation": "Scan for all 7 AI issue categories with severity levels"
}
],
"target_files": [],
"ai_issue_scan": {
"categories": [
"hallucinated_imports",
"placeholder_code",
"mock_leakage",
"type_abuse",
"naming_issues",
"control_flow",
"resource_leaks",
"security_issues"
],
"severity_levels": ["CRITICAL", "ERROR", "WARNING"],
"auto_fix_enabled": true,
"max_retries": 2,
"thresholds": {
"critical": 0,
"error": 3,
"warning": 10
}
}
}
}
```
**Task 3: IMPL-001.5-review.json (Test Quality Gate)**
```json
{
"id": "IMPL-001.5",
"title": "Test quality gate - anti-patterns and coverage",
"status": "pending",
"context_package_path": ".workflow/active/WFS-test-{session}/.process/test-context-package.json",
"cli_execution_id": "WFS-test-{session}-IMPL-001.5",
"cli_execution": {
"strategy": "resume",
"resume_from": "WFS-test-{session}-IMPL-001.3"
},
"meta": {
"type": "test-quality-review",
"agent": "@test-fix-agent"
},
"context": {
"requirements": [
"Validate layer completeness: L1.1 100%, L1.2 80%, L1.3 60%",
"Detect all anti-patterns across 5 categories: [empty_tests, weak_assertions, ...]",
"Verify coverage: line ≥80%, branch ≥70%, function ≥90%"
],
"focus_paths": ["tests/"],
"acceptance": [
"Coverage ≥80%: verify by npm test -- --coverage | grep 'All files.*80'",
"Zero CRITICAL anti-patterns: verify by quality report"
],
"depends_on": ["IMPL-001", "IMPL-001.3"]
},
"flow_control": {
"pre_analysis": [],
"implementation_approach": [
{
"phase": "Static Analysis",
"validation": "Lint test files, check anti-patterns"
},
{
"phase": "Coverage Analysis",
"validation": "Calculate coverage percentage, identify gaps"
},
{
"phase": "Quality Metrics",
"validation": "Verify thresholds, layer completeness"
}
],
"target_files": [],
"quality_gates": {
"layer_completeness": {
"L1.1": "100%",
"L1.2": "80%",
"L1.3": "60%",
"L1.4": "80%",
"L1.5": "100%",
"L2": "70%"
},
"anti_patterns": [
"empty_tests",
"weak_assertions",
"test_isolation",
"incomplete_coverage",
"ai_generated_issues"
],
"coverage_thresholds": {
"line": "80%",
"branch": "70%",
"function": "90%"
}
}
}
}
```
**Task 4: IMPL-002.json (Test Execution & Fix)**
```json
{
"id": "IMPL-002",
"title": "Test execution and fix cycle",
"status": "pending",
"context_package_path": ".workflow/active/WFS-test-{session}/.process/test-context-package.json",
"cli_execution_id": "WFS-test-{session}-IMPL-002",
"cli_execution": {
"strategy": "resume",
"resume_from": "WFS-test-{session}-IMPL-001.5"
},
"meta": {
"type": "test-fix",
"agent": "@test-fix-agent"
},
"context": {
"requirements": [
"Execute all tests and fix failures until pass rate ≥95%",
"Maximum 5 fix iterations",
"Use Gemini for diagnosis, agent for fixes"
],
"focus_paths": ["tests/", "src/"],
"acceptance": [
"All tests pass: verify by npm test (exit code 0)",
"Pass rate ≥95%: verify by test output"
],
"depends_on": ["IMPL-001", "IMPL-001.3", "IMPL-001.5"]
},
"flow_control": {
"pre_analysis": [],
"implementation_approach": [
{
"phase": "Initial Test Execution",
"command": "npm test"
},
{
"phase": "Iterative Fix Cycle",
"steps": [
"Diagnose failures with Gemini",
"Apply fixes via agent or CLI",
"Re-run tests",
"Repeat until pass rate ≥95% or max iterations"
],
"max_iterations": 5
}
],
"target_files": [],
"test_fix_cycle": {
"max_iterations": 5,
"diagnosis_tool": "gemini",
"fix_mode": "agent",
"exit_conditions": ["all_tests_pass", "max_iterations_reached"]
}
}
}
```
#### Phase 3: Document Generation
```
1. Create IMPL_PLAN.md (test-specific variant)
- frontmatter: workflow_type="test_session", test_framework, coverage_targets
- Test Generation Phase: L1-L3 layer breakdown
- Quality Gates: IMPL-001.3 and IMPL-001.5 specifications
- Test-Fix Cycle: Iteration strategy with diagnosis and fix modes
- Source Session Context: If exists (from source_session_id)
2. Create TODO_LIST.md
- Hierarchical structure with test phase containers
- Links to task JSONs with status markers
- Test layer indicators (L0, L1, L2, L3)
- Quality gate indicators (validation, review)
```
---
## 2. Output Validation
### Task JSON Validation
**IMPL-001 Requirements**:
- All L1.1-L1.5 tests explicitly defined for each target function
- Project type template correctly applied
- Reusable test tools and test commands included
- Implementation approach includes all 3 phases (L1, L2, L3)
**IMPL-001.3 Requirements**:
- All 7 AI issue categories included
- Severity levels properly assigned
- Auto-fix logic for ERROR and below
- Acceptance criteria references zero CRITICAL rule
**IMPL-001.5 Requirements**:
- Layer completeness thresholds: L1.1 100%, L1.2 80%, L1.3 60%
- All 5 anti-pattern categories included
- Coverage metrics: Line 80%, Branch 70%, Function 90%
- Acceptance criteria references all thresholds
**IMPL-002 Requirements**:
- Depends on: IMPL-001, IMPL-001.3, IMPL-001.5 (sequential)
- Max iterations: 5
- Diagnosis tool: Gemini
- Exit conditions: all_tests_pass OR max_iterations_reached
### Quality Standards
Hard Constraints:
- Task count: minimum 4, maximum 18
- All requirements quantified from TEST_ANALYSIS_RESULTS.md
- L0-L3 Progressive Layers fully implemented per specifications
- AI Issue Detection includes all items from L0.5 checklist
- Project Type Template correctly applied
- Test Anti-Patterns validation rules implemented
- Layer Completeness Thresholds met
- Quality Metrics targets: Line 80%, Branch 70%, Function 90%
---
## 3. Success Criteria
- All test planning documents generated successfully
- Task count reported: minimum 4
- Test framework correctly detected and reported
- Coverage targets clearly specified: L0 zero errors, L1 80%+, L2 70%+
- L0-L3 layers explicitly defined in IMPL-001 task
- AI issue detection configured in IMPL-001.3
- Quality gates with measurable thresholds in IMPL-001.5
- Source session status reported (if applicable)

View File

@@ -1,200 +1,264 @@
---
name: test-fix-gen
description: Create test-fix workflow session from session ID, description, or file path with test strategy generation and task planning
description: Create test-fix workflow session with progressive test layers (L0-L3), AI code validation, and test task generation
argument-hint: "(source-session-id | \"feature description\" | /path/to/file.md)"
allowed-tools: SlashCommand(*), TodoWrite(*), Read(*), Bash(*)
group: workflow
---
# Workflow Test-Fix Generation Command (/workflow:test-fix-gen)
## Quick Reference
## Coordinator Role
### Command Scope
**This command is a pure orchestrator**: Execute 5 slash commands in sequence, parse their outputs, pass context between them, and ensure complete execution through **automatic continuation**.
| Aspect | Description |
|--------|-------------|
| **Purpose** | Generate test-fix workflow session with task JSON files |
| **Output** | IMPL-001.json, IMPL-001.3-validation.json, IMPL-001.5-review.json, IMPL-002.json |
| **Does NOT** | Execute tests, apply fixes, handle test failures |
| **Next Step** | Must call `/workflow:test-cycle-execute` after this command |
**Execution Model - Auto-Continue Workflow**:
### Task Pipeline
This workflow runs **fully autonomously** once triggered. Phase 3 (test analysis) and Phase 4 (task generation) are delegated to specialized agents.
```
IMPL-001 (Test Generation) → IMPL-001.3 (Code Validation) → IMPL-001.5 (Test Quality) → IMPL-002 (Test Execution)
@code-developer @test-fix-agent @test-fix-agent @test-fix-agent
```
1. **User triggers**: `/workflow:test-fix-gen "task"` or `/workflow:test-fix-gen WFS-source-session`
2. **Phase 1 executes** → Test session created → Auto-continues
3. **Phase 2 executes** → Context gathering → Auto-continues
4. **Phase 3 executes** → Test generation analysis (Gemini) → Auto-continues
5. **Phase 4 executes** → Task generation (test-task-generate) → Reports final summary
### Coordinator Role
**Task Attachment Model**:
- SlashCommand execute **expands workflow** by attaching sub-tasks to current TodoWrite
- When a sub-command is executed, its internal tasks are attached to the orchestrator's TodoWrite
- Orchestrator **executes these attached tasks** sequentially
- After completion, attached tasks are **collapsed** back to high-level phase summary
- This is **task expansion**, not external delegation
This command is a **pure planning coordinator**:
- ONLY coordinates slash commands to generate task JSON files
- Does NOT analyze code, generate tests, execute tests, or apply fixes
- All execution delegated to `/workflow:test-cycle-execute`
**Auto-Continue Mechanism**:
- TodoList tracks current phase status and dynamically manages task attachment/collapse
- When each phase finishes executing, automatically execute next pending phase
- All phases run autonomously without user interaction
- **⚠️ CONTINUOUS EXECUTION** - Do not stop until all phases complete
### Core Principles
## Core Rules
| Principle | Description |
|-----------|-------------|
| **Session Isolation** | Creates independent `WFS-test-[slug]` session |
| **Context-First** | Gathers implementation context via appropriate method |
| **Format Reuse** | Creates standard `IMPL-*.json` tasks with `meta.type: "test-fix"` |
| **Semantic CLI Selection** | CLI tool usage determined from user's task description |
| **Automatic Detection** | Input pattern determines execution mode |
1. **Start Immediately**: First action is TodoWrite initialization, second action is Phase 1 command execution
2. **No Preliminary Analysis**: Do not read files, analyze structure, or gather context before Phase 1
3. **Parse Every Output**: Extract required data from each command output for next phase
4. **Auto-Continue via TodoList**: Check TodoList status to execute next pending phase automatically
5. **Track Progress**: Update TodoWrite dynamically with task attachment/collapse pattern
6. **Task Attachment Model**: SlashCommand execute **attaches** sub-tasks to current workflow. Orchestrator **executes** these attached tasks itself, then **collapses** them after completion
7. **⚠️ CRITICAL: DO NOT STOP**: Continuous multi-phase workflow. After executing all attached tasks, immediately collapse them and execute next phase
---
## Usage
## Test Strategy Overview
### Command Syntax
This workflow generates tests using **Progressive Test Layers (L0-L3)**:
```bash
/workflow:test-fix-gen <INPUT>
| Layer | Name | Focus |
|-------|------|-------|
| **L0** | Static Analysis | Compilation, imports, types, AI code issues |
| **L1** | Unit Tests | Function/class behavior (happy/negative/edge cases) |
| **L2** | Integration Tests | Component interactions, API contracts, failure modes |
| **L3** | E2E Tests | User journeys, critical paths (optional) |
**Key Features**:
- **AI Code Issue Detection** - Validates against common AI-generated code problems (hallucinated imports, placeholder code, mock leakage, etc.)
- **Project Type Detection** - Applies appropriate test templates (React, Node API, CLI, Library, etc.)
- **Quality Gates** - IMPL-001.3 (code validation) and IMPL-001.5 (test quality) ensure high standards
**Detailed specifications**: See `/workflow:tools:test-task-generate` for complete L0-L3 requirements and quality thresholds.
---
## Execution Process
# INPUT can be:
# - Session ID: WFS-user-auth-v2
# - Description: "Test the user authentication API"
# - File path: ./docs/api-requirements.md
```
Input Parsing:
├─ Detect input type: Session ID (WFS-*) | Description | File path
└─ Set MODE: session | prompt
### Mode Detection
Phase 1: Create Test Session
└─ /workflow:session:start --type test --new "structured-description"
└─ Output: testSessionId (WFS-test-xxx)
**Automatic mode detection** based on input pattern:
Phase 2: Gather Test Context
├─ MODE=session → /workflow:tools:test-context-gather --session testSessionId
└─ MODE=prompt → /workflow:tools:context-gather --session testSessionId "description"
└─ Output: contextPath (context-package.json)
```bash
if [[ "$input" == WFS-* ]]; then
MODE="session" # Use test-context-gather
else
MODE="prompt" # Use context-gather
fi
```
Phase 3: Test Generation Analysis
└─ /workflow:tools:test-concept-enhanced --session testSessionId --context contextPath
└─ Output: TEST_ANALYSIS_RESULTS.md (L0-L3 requirements)
| Mode | Input Pattern | Context Source | Use Case |
|------|--------------|----------------|----------|
| **Session** | `WFS-xxx` | Source session summaries | Test validation for completed workflow |
| **Prompt** | Text or file path | Direct codebase analysis | Ad-hoc test generation |
Phase 4: Generate Test Tasks
└─ /workflow:tools:test-task-generate --session testSessionId
└─ Output: IMPL_PLAN.md, IMPL-*.json (4+ tasks), TODO_LIST.md
### Examples
```bash
# Session Mode - test validation for completed implementation
/workflow:test-fix-gen WFS-user-auth-v2
# Prompt Mode - text description
/workflow:test-fix-gen "Test the user authentication API endpoints in src/auth/api.ts"
# Prompt Mode - file reference
/workflow:test-fix-gen ./docs/api-requirements.md
# With CLI tool preference (semantic detection)
/workflow:test-fix-gen "Test user registration, use Codex for automated fixes"
Phase 5: Return Summary
└─ Summary with next steps → /workflow:test-cycle-execute
```
---
## Execution Phases
### Execution Rules
1. **Start Immediately**: First action is TodoWrite, second is Phase 1 execution
2. **No Preliminary Analysis**: Do not read files before Phase 1
3. **Parse Every Output**: Extract required data from each phase for next phase
4. **Sequential Execution**: Each phase depends on previous phase's output
5. **Complete All Phases**: Do not return until Phase 5 completes
6. **⚠️ CONTINUOUS EXECUTION**: Do not stop between phases
## 5-Phase Execution
### Phase 1: Create Test Session
**Execute**:
```javascript
// Session Mode - preserve original task description
Read(".workflow/active/[sourceSessionId]/workflow-session.json")
SlashCommand("/workflow:session:start --type test --new \"Test validation for [sourceSessionId]: [originalTaskDescription]\"")
**Step 1.0: Detect Input Mode**
// Prompt Mode - use user's description directly
SlashCommand("/workflow:session:start --type test --new \"Test generation for: [description]\"")
```javascript
// Automatic mode detection based on input pattern
if (input.startsWith("WFS-")) {
MODE = "session"
// Load source session to preserve original task description
Read(".workflow/active/[sourceSessionId]/workflow-session.json")
} else {
MODE = "prompt"
}
```
**Output**: `testSessionId` (pattern: `WFS-test-[slug]`)
**Step 1.1: Execute** - Create test workflow session
```javascript
// Session Mode - preserve original task description
SlashCommand(command="/workflow:session:start --type test --new \"Test validation for [sourceSessionId]: [originalTaskDescription]\"")
// Prompt Mode - use user's description directly
SlashCommand(command="/workflow:session:start --type test --new \"Test generation for: [description]\"")
```
**Parse Output**:
- Extract: `SESSION_ID: WFS-test-[slug]` (store as `testSessionId`)
**Validation**:
- Session Mode: Source session exists with completed IMPL tasks
- Session Mode: Source session `.workflow/active/[sourceSessionId]/` exists with completed IMPL tasks
- Both Modes: New test session directory created with metadata
**TodoWrite**: Mark phase 1 completed, phase 2 in_progress
---
### Phase 2: Gather Test Context
**Execute**:
```javascript
// Session Mode
SlashCommand("/workflow:tools:test-context-gather --session [testSessionId]")
**Step 2.1: Execute** - Gather context based on mode
// Prompt Mode
SlashCommand("/workflow:tools:context-gather --session [testSessionId] \"[task_description]\"")
```javascript
// Session Mode - gather from source session
SlashCommand(command="/workflow:tools:test-context-gather --session [testSessionId]")
// Prompt Mode - gather from codebase
SlashCommand(command="/workflow:tools:context-gather --session [testSessionId] \"[task_description]\"")
```
**Expected Behavior**:
- **Session Mode**: Load source session summaries, analyze test coverage
- **Prompt Mode**: Analyze codebase from description
- Both: Detect test framework, generate context package
**Input**: `testSessionId` from Phase 1
**Output**: `contextPath` (pattern: `.workflow/[testSessionId]/.process/[test-]context-package.json`)
**Parse Output**:
- Extract: context package path (store as `contextPath`)
- Pattern: `.workflow/active/[testSessionId]/.process/[test-]context-package.json`
**Validation**:
- Context package file exists and is valid JSON
- Contains coverage analysis (session mode) or codebase analysis (prompt mode)
- Test framework detected
**TodoWrite Update (tasks attached)**:
```json
[
{"content": "Phase 1: Create Test Session", "status": "completed"},
{"content": "Phase 2: Gather Test Context", "status": "in_progress"},
{"content": " → Load source/codebase context", "status": "in_progress"},
{"content": " → Analyze test coverage", "status": "pending"},
{"content": " → Generate context package", "status": "pending"},
{"content": "Phase 3: Test Generation Analysis", "status": "pending"},
{"content": "Phase 4: Generate Test Tasks", "status": "pending"},
{"content": "Phase 5: Return Summary", "status": "pending"}
]
```
**TodoWrite Update (tasks collapsed)**:
```json
[
{"content": "Phase 1: Create Test Session", "status": "completed"},
{"content": "Phase 2: Gather Test Context", "status": "completed"},
{"content": "Phase 3: Test Generation Analysis", "status": "pending"},
{"content": "Phase 4: Generate Test Tasks", "status": "pending"},
{"content": "Phase 5: Return Summary", "status": "pending"}
]
```
---
### Phase 3: Test Generation Analysis
**Execute**:
**Step 3.1: Execute** - Analyze test requirements with Gemini
```javascript
SlashCommand("/workflow:tools:test-concept-enhanced --session [testSessionId] --context [contextPath]")
SlashCommand(command="/workflow:tools:test-concept-enhanced --session [testSessionId] --context [contextPath]")
```
**Input**:
- `testSessionId` from Phase 1
- `contextPath` from Phase 2
**Expected Behavior**:
- Use Gemini to analyze coverage gaps
- Generate **multi-layered test requirements**:
- L0: Static Analysis (linting, type checking, anti-pattern detection)
- L1: Unit Tests (happy path, negative path, edge cases: null/undefined/empty)
- L2: Integration Tests (component interactions, failure scenarios: timeout/unavailable)
- L3: E2E Tests (user journeys, if applicable)
- Detect project type and apply appropriate test templates
- Generate **multi-layered test requirements** (L0-L3)
- Scan for AI code issues
- Generate `TEST_ANALYSIS_RESULTS.md`
**Output**: `.workflow/[testSessionId]/.process/TEST_ANALYSIS_RESULTS.md`
**Validation** - TEST_ANALYSIS_RESULTS.md must include:
- Coverage Assessment
- Project Type Detection (with confidence)
- Coverage Assessment (current vs target)
- Test Framework & Conventions
- Multi-Layered Test Plan (L0-L3)
- AI Issue Scan Results
- Test Requirements by File (with layer annotations)
- Test Generation Strategy
- Implementation Targets
- Quality Assurance Criteria:
- Minimum coverage thresholds
- Required test types per function
- Acceptance criteria for test quality
- Quality Assurance Criteria
- Success Criteria
**Note**: Detailed specifications for project types, L0-L3 layers, and AI issue detection are defined in `/workflow:tools:test-concept-enhanced`.
---
### Phase 4: Generate Test Tasks
**Execute**:
**Step 4.1: Execute** - Generate test planning documents
```javascript
SlashCommand("/workflow:tools:test-task-generate --session [testSessionId]")
SlashCommand(command="/workflow:tools:test-task-generate --session [testSessionId]")
```
**Expected Behavior**:
- Parse TEST_ANALYSIS_RESULTS.md
- Generate **minimum 4 task JSON files**:
- IMPL-001.json (Test Generation)
- IMPL-001.3-validation.json (Code Validation Gate)
- IMPL-001.5-review.json (Test Quality Gate)
- IMPL-002.json (Test Execution & Fix)
- Generate IMPL_PLAN.md and TODO_LIST.md
**Input**: `testSessionId` from Phase 1
**Output Validation**:
- Verify all `.task/IMPL-*.json` files exist
- Verify `IMPL_PLAN.md` and `TODO_LIST.md` created
**Note**: test-task-generate invokes action-planning-agent to generate test-specific IMPL_PLAN.md and task JSONs based on TEST_ANALYSIS_RESULTS.md.
**Expected Output** (minimum 4 tasks):
| Task | Type | Agent | Purpose |
|------|------|-------|---------|
| IMPL-001 | test-gen | @code-developer | Test understanding & generation (L1-L3) |
| IMPL-001.3 | code-validation | @test-fix-agent | Code validation gate (L0 + AI issues) |
| IMPL-001.5 | test-quality-review | @test-fix-agent | Test quality gate |
| IMPL-002 | test-fix | @test-fix-agent | Test execution & fix cycle |
**Validation**:
- `.workflow/active/[testSessionId]/.task/IMPL-001.json` exists
- `.workflow/active/[testSessionId]/.task/IMPL-001.3-validation.json` exists
- `.workflow/active/[testSessionId]/.task/IMPL-001.5-review.json` exists
- `.workflow/active/[testSessionId]/.task/IMPL-002.json` exists
- `.workflow/active/[testSessionId]/IMPL_PLAN.md` exists
- `.workflow/active/[testSessionId]/TODO_LIST.md` exists
**TodoWrite Update (agent task attached)**:
```json
[
{"content": "Phase 1: Create Test Session", "status": "completed"},
{"content": "Phase 2: Gather Test Context", "status": "completed"},
{"content": "Phase 3: Test Generation Analysis", "status": "completed"},
{"content": "Phase 4: Generate Test Tasks", "status": "in_progress"},
{"content": "Phase 5: Return Summary", "status": "pending"}
]
```
---
@@ -202,7 +266,7 @@ SlashCommand("/workflow:tools:test-task-generate --session [testSessionId]")
**Return to User**:
```
Independent test-fix workflow created successfully!
Test-fix workflow created successfully!
Input: [original input]
Mode: [Session|Prompt]
@@ -215,148 +279,104 @@ Tasks Created:
- IMPL-002: Test Execution & Fix Cycle (@test-fix-agent)
Quality Thresholds:
- Code Validation: Zero compilation/import/variable errors
- Minimum Coverage: 80%
- Static Analysis: Zero critical issues
- Code Validation: Zero CRITICAL issues, zero compilation errors
- Minimum Coverage: 80% line, 70% branch
- Static Analysis: Zero critical anti-patterns
- Max Fix Iterations: 5
Review artifacts:
- Test plan: .workflow/[testSessionId]/IMPL_PLAN.md
- Task list: .workflow/[testSessionId]/TODO_LIST.md
- Validation config: ~/.claude/workflows/test-quality-config.json
- Analysis: .workflow/[testSessionId]/.process/TEST_ANALYSIS_RESULTS.md
CRITICAL - Next Steps:
1. Review IMPL_PLAN.md
2. **MUST execute: /workflow:test-cycle-execute**
CRITICAL - Next Step:
/workflow:test-cycle-execute --session [testSessionId]
```
---
## Task Specifications
## Data Flow
Generates minimum 4 tasks (expandable for complex projects):
### IMPL-001: Test Understanding & Generation
| Field | Value |
|-------|-------|
| **Agent** | `@code-developer` |
| **Type** | `test-gen` |
| **Depends On** | None |
**Purpose**: Understand source implementation and generate test files following multi-layered test strategy
**Execution Flow**:
1. **Understand**: Load TEST_ANALYSIS_RESULTS.md, analyze requirements (L0-L3)
2. **Generate**: Create test files (unit, integration, E2E as applicable)
3. **Verify**: Check test completeness, meaningful assertions, no anti-patterns
---
### IMPL-001.3: Code Validation Gate
| Field | Value |
|-------|-------|
| **Agent** | `@test-fix-agent` |
| **Type** | `code-validation` |
| **Depends On** | `["IMPL-001"]` |
| **Config** | `~/.claude/workflows/test-quality-config.json` |
**Purpose**: Validate AI-generated code for common errors before test execution
**Validation Phases**:
| Phase | Checks |
|-------|--------|
| L0.1 Compilation | `tsc --noEmit` - syntax errors, module resolution |
| L0.2 Imports | Unresolved/hallucinated packages, circular deps, duplicates |
| L0.3 Variables | Redeclaration, scope conflicts, undefined/unused vars |
| L0.4 Types | Type mismatches, missing definitions, `any` abuse |
| L0.5 AI-Specific | Placeholder code, mock in production, naming inconsistency |
**Gate Decision**:
| Decision | Condition | Action |
|----------|-----------|--------|
| **PASS** | critical=0, error≤3, warning≤10 | Proceed to IMPL-001.5 |
| **SOFT_FAIL** | Fixable issues | Auto-fix and retry (max 2) |
| **HARD_FAIL** | critical>0 OR max retries | Block with report |
**Acceptance Criteria**:
- Zero compilation errors
- All imports resolvable
- No variable redeclarations
- No undefined variable usage
**Output**: `.process/code-validation-report.md`, `.process/code-validation-report.json`
---
### IMPL-001.5: Test Quality Gate
| Field | Value |
|-------|-------|
| **Agent** | `@test-fix-agent` |
| **Type** | `test-quality-review` |
| **Depends On** | `["IMPL-001", "IMPL-001.3"]` |
| **Config** | `~/.claude/workflows/test-quality-config.json` |
**Purpose**: Validate test quality before entering fix cycle
**Execution Flow**:
1. **Static Analysis**: Lint test files, check anti-patterns (empty tests, missing assertions)
2. **Coverage Analysis**: Calculate coverage percentage, identify gaps
3. **Quality Metrics**: Verify thresholds, negative test coverage
4. **Gate Decision**: PASS (proceed) or FAIL (loop back to IMPL-001)
**Acceptance Criteria**:
- Coverage ≥ 80%
- Zero critical anti-patterns
- All targeted functions have unit tests
- Each public API has error handling test
**Failure Handling**:
If quality gate fails:
1. Generate detailed feedback report (`.process/test-quality-report.md`)
2. Update IMPL-001 task with specific improvement requirements
3. Trigger IMPL-001 re-execution with enhanced context
4. Maximum 2 quality gate retries before escalating to user
**Output**: `.process/test-quality-report.md`
---
### IMPL-002: Test Execution & Fix Cycle
| Field | Value |
|-------|-------|
| **Agent** | `@test-fix-agent` |
| **Type** | `test-fix` |
| **Depends On** | `["IMPL-001", "IMPL-001.3", "IMPL-001.5"]` |
**Purpose**: Execute tests and trigger orchestrator-managed fix cycles
**Note**: The agent executes tests and reports results. The `test-cycle-execute` orchestrator manages all fix iterations.
**Cycle Pattern** (orchestrator-managed):
```
test → gemini_diagnose → fix (agent or CLI) → retest
User Input (session ID | description | file path)
[Detect Mode: session | prompt]
Phase 1: session:start --type test --new "description"
↓ Output: testSessionId
Phase 2: test-context-gather | context-gather
↓ Input: testSessionId
↓ Output: contextPath (context-package.json)
Phase 3: test-concept-enhanced
↓ Input: testSessionId + contextPath
↓ Output: TEST_ANALYSIS_RESULTS.md (L0-L3 requirements + AI issues)
Phase 4: test-task-generate
↓ Input: testSessionId + TEST_ANALYSIS_RESULTS.md
↓ Output: IMPL_PLAN.md, IMPL-*.json (4+), TODO_LIST.md
Phase 5: Return summary to user
Next: /workflow:test-cycle-execute
```
**Tools Configuration** (orchestrator-controlled):
- Gemini for analysis with bug-fix template → surgical fix suggestions
- Agent fix application (default) OR CLI if `command` field present in implementation_approach
**Exit Conditions**:
- Success: All tests pass
- Failure: Max iterations reached (5)
---
### IMPL-003+: Additional Tasks (Optional)
## Execution Flow Diagram
**Scenarios**:
- Large projects requiring per-module test generation
- Separate integration vs unit test tasks
- Specialized test types (performance, security)
```
User triggers: /workflow:test-fix-gen "Test user authentication"
[Input Detection] → MODE: prompt
[TodoWrite Init] 5 orchestrator-level tasks
Phase 1: Create Test Session
→ /workflow:session:start --type test
→ testSessionId extracted (WFS-test-user-auth)
Phase 2: Gather Test Context (SlashCommand executed)
→ ATTACH 3 sub-tasks: ← ATTACHED
- → Load codebase context
- → Analyze test coverage
- → Generate context package
→ Execute sub-tasks sequentially
→ COLLAPSE tasks ← COLLAPSED
→ contextPath extracted
Phase 3: Test Generation Analysis (SlashCommand executed)
→ ATTACH 3 sub-tasks: ← ATTACHED
- → Analyze coverage gaps with Gemini
- → Detect AI code issues (L0.5)
- → Generate L0-L3 test requirements
→ Execute sub-tasks sequentially
→ COLLAPSE tasks ← COLLAPSED
→ TEST_ANALYSIS_RESULTS.md created
Phase 4: Generate Test Tasks (SlashCommand executed)
→ Single agent task (test-task-generate → action-planning-agent)
→ Agent autonomously generates:
- IMPL-001.json (test generation)
- IMPL-001.3-validation.json (code validation)
- IMPL-001.5-review.json (test quality)
- IMPL-002.json (test execution)
- IMPL_PLAN.md
- TODO_LIST.md
Phase 5: Return Summary
→ Display summary with next steps
→ Command ends
Task Pipeline (for execution):
┌──────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐
│ IMPL-001 │───→│ IMPL-001.3 │───→│ IMPL-001.5 │───→│ IMPL-002 │
│ Test Gen │ │ Code Validate │ │ Quality Gate │ │ Test & Fix │
│ L1-L3 │ │ L0 + AI Issues │ │ Coverage 80%+ │ │ Max 5 iter │
│@code-developer│ │ @test-fix-agent │ │ @test-fix-agent │ │@test-fix-agent│
└──────────────┘ └─────────────────┘ └─────────────────┘ └──────────────┘
```
---
@@ -377,10 +397,7 @@ test → gemini_diagnose → fix (agent or CLI) → retest
│ └── IMPL-*.json # Additional tasks (if applicable)
└── .process/
├── [test-]context-package.json # Context and coverage analysis
── TEST_ANALYSIS_RESULTS.md # Test requirements and strategy
├── code-validation-report.md # Code validation findings
├── code-validation-report.json # Machine-readable findings
└── test-quality-report.md # Test quality gate findings
── TEST_ANALYSIS_RESULTS.md # Test requirements and strategy (L0-L3)
```
### Session Metadata
@@ -394,101 +411,67 @@ test → gemini_diagnose → fix (agent or CLI) → retest
---
## Orchestration Patterns
### TodoWrite Pattern
**Initial Structure**:
```json
[
{"content": "Phase 1: Create Test Session", "status": "in_progress", "activeForm": "Creating test session"},
{"content": "Phase 2: Gather Test Context", "status": "pending", "activeForm": "Gathering test context"},
{"content": "Phase 3: Test Generation Analysis", "status": "pending", "activeForm": "Analyzing test generation"},
{"content": "Phase 4: Generate Test Tasks", "status": "pending", "activeForm": "Generating test tasks"},
{"content": "Phase 5: Return Summary", "status": "pending", "activeForm": "Completing"}
]
```
### Task Attachment Model
SlashCommand execution follows **attach → execute → collapse** pattern:
1. **Attach**: Sub-command's tasks are attached to orchestrator's TodoWrite
2. **Execute**: Orchestrator executes attached tasks sequentially
3. **Collapse**: After completion, sub-tasks collapse to phase summary
**Example - Phase 2 Expanded**:
```json
[
{"content": "Phase 1: Create Test Session", "status": "completed"},
{"content": "Phase 2: Gather Test Context", "status": "in_progress"},
{"content": " → Load context and analyze coverage", "status": "in_progress"},
{"content": " → Detect test framework and conventions", "status": "pending"},
{"content": " → Generate context package", "status": "pending"},
{"content": "Phase 3: Test Generation Analysis", "status": "pending"},
...
]
```
### Auto-Continue Mechanism
- TodoList tracks current phase status
- When phase completes, automatically execute next pending phase
- All phases run autonomously without user interaction
- **⚠️ Do not stop until all phases complete**
---
## Reference
### Error Handling
## Error Handling
| Phase | Error Condition | Action |
|-------|----------------|--------|
| 1 | Source session not found | Return error with session ID |
| 1 | No completed IMPL tasks | Return error, source incomplete |
| 1 | Source session not found (session mode) | Return error with session ID |
| 1 | No completed IMPL tasks (session mode) | Return error, source incomplete |
| 2 | Context gathering failed | Return error, check source artifacts |
| 3 | Gemini analysis failed | Return error, check context package |
| 4 | Task generation failed | Retry once, then return error |
### Best Practices
---
**Before Running**:
- Ensure implementation is complete (session mode: check summaries exist)
- Commit all implementation changes
## Coordinator Checklist
**After Running**:
- Review `IMPL_PLAN.md` before execution
- Check `TEST_ANALYSIS_RESULTS.md` for completeness
- Verify task dependencies in `TODO_LIST.md`
- Detect input type (session ID / description / file path)
- Initialize TodoWrite before any command
- Execute Phase 1 immediately with structured description
- Parse test session ID from Phase 1 output, store in memory
- Execute Phase 2 with appropriate context-gather command based on mode
- Parse context path from Phase 2 output, store in memory
- Execute Phase 3 test-concept-enhanced with session and context
- Verify TEST_ANALYSIS_RESULTS.md created with L0-L3 requirements
- Execute Phase 4 test-task-generate with session ID
- Verify all Phase 4 outputs (4 task JSONs, IMPL_PLAN.md, TODO_LIST.md)
- Return summary with next step: `/workflow:test-cycle-execute`
- Update TodoWrite after each phase
**During Execution** (in test-cycle-execute):
- Monitor iteration logs in `.process/fix-iteration-*`
- Track progress with `/workflow:status`
- Review Gemini diagnostic outputs
---
**Mode Selection**:
- **Session Mode**: For completed workflow validation
- **Prompt Mode**: For ad-hoc test generation
- Include "use Codex" in description for autonomous fix application
## Usage Examples
### Related Commands
```bash
# Session Mode - test validation for completed implementation
/workflow:test-fix-gen WFS-user-auth-v2
**Prerequisites**:
# Prompt Mode - text description
/workflow:test-fix-gen "Test the user authentication API endpoints in src/auth/api.ts"
# Prompt Mode - file reference
/workflow:test-fix-gen ./docs/api-requirements.md
# With CLI tool preference (semantic detection)
/workflow:test-fix-gen "Test user registration, use Codex for automated fixes"
```
---
## Related Commands
**Prerequisite Commands**:
- `/workflow:plan` or `/workflow:execute` - Complete implementation (Session Mode)
- None for Prompt Mode
**Called by This Command**:
- `/workflow:session:start` - Phase 1
- `/workflow:tools:test-context-gather` - Phase 2 (Session Mode)
- `/workflow:tools:context-gather` - Phase 2 (Prompt Mode)
- `/workflow:tools:test-concept-enhanced` - Phase 3
- `/workflow:tools:test-task-generate` - Phase 4
**Validation Commands** (invoked during test-cycle-execute):
- `/workflow:tools:code-validation-gate` - IMPL-001.3
**Called by This Command** (5 phases):
- `/workflow:session:start` - Phase 1: Create test workflow session
- `/workflow:tools:test-context-gather` - Phase 2 (Session Mode): Analyze test coverage
- `/workflow:tools:context-gather` - Phase 2 (Prompt Mode): Analyze codebase
- `/workflow:tools:test-concept-enhanced` - Phase 3: Generate test requirements with Gemini
- `/workflow:tools:test-task-generate` - Phase 4: Generate test task JSONs via action-planning-agent
**Follow-up Commands**:
- `/workflow:status` - Review generated tasks
- `/workflow:test-cycle-execute` - Execute test workflow
- `/workflow:execute` - Standard task execution
- `/workflow:test-cycle-execute` - Execute test workflow (REQUIRED next step)
- `/workflow:execute` - Alternative: Standard task execution

View File

@@ -1,529 +0,0 @@
---
name: test-gen
description: Create independent test-fix workflow session from completed implementation session, analyzes code to generate test tasks
argument-hint: "source-session-id"
allowed-tools: SlashCommand(*), TodoWrite(*), Read(*), Bash(*)
---
# Workflow Test Generation Command (/workflow:test-gen)
## Coordinator Role
**This command is a pure orchestrator**: Creates an independent test-fix workflow session for validating a completed implementation. It reuses the standard planning toolchain with automatic cross-session context gathering.
**Core Principles**:
- **Session Isolation**: Creates new `WFS-test-[source]` session to keep verification separate from implementation
- **Context-First**: Prioritizes gathering code changes and summaries from source session
- **Format Reuse**: Creates standard `IMPL-*.json` task, using `meta.type: "test-fix"` for agent assignment
- **Parameter Simplification**: Tools auto-detect test session type via metadata, no manual cross-session parameters needed
- **Semantic CLI Selection**: CLI tool usage is determined by user's task description (e.g., "use Codex for fixes")
**Task Attachment Model**:
- SlashCommand dispatch **expands workflow** by attaching sub-tasks to current TodoWrite
- When a sub-command is executed (e.g., `/workflow:tools:test-context-gather`), its internal tasks are attached to the orchestrator's TodoWrite
- Orchestrator **executes these attached tasks** sequentially
- After completion, attached tasks are **collapsed** back to high-level phase summary
- This is **task expansion**, not external delegation
**Auto-Continue Mechanism**:
- TodoList tracks current phase status and dynamically manages task attachment/collapse
- When each phase finishes executing, automatically execute next pending phase
- All phases run autonomously without user interaction
- **⚠️ CONTINUOUS EXECUTION** - Do not stop until all phases complete
**Execution Flow**:
1. Initialize TodoWrite → Create test session → Parse session ID
2. Gather cross-session context (automatic) → Parse context path
3. Analyze implementation with concept-enhanced → Parse ANALYSIS_RESULTS.md
4. Generate test task from analysis → Return summary
**Command Scope**: This command ONLY prepares test workflow artifacts. It does NOT execute tests or implementation. Task execution requires separate user action.
## Core Rules
1. **Start Immediately**: First action is TodoWrite initialization, second action is Phase 1 test session creation
2. **No Preliminary Analysis**: Do not read files or analyze before Phase 1
3. **Parse Every Output**: Extract required data from each phase for next phase
4. **Sequential Execution**: Each phase depends on previous phase's output
5. **Complete All Phases**: Do not return to user until Phase 5 completes (summary returned)
6. **Track Progress**: Update TodoWrite dynamically with task attachment/collapse pattern
7. **Automatic Detection**: context-gather auto-detects test session and gathers source session context
8. **Semantic CLI Selection**: CLI tool usage determined from user's task description, passed to Phase 4
9. **Command Boundary**: This command ends at Phase 5 summary. Test execution is NOT part of this command.
10. **Task Attachment Model**: SlashCommand dispatch **attaches** sub-tasks to current workflow. Orchestrator **executes** these attached tasks itself, then **collapses** them after completion
11. **⚠️ CRITICAL: DO NOT STOP**: Continuous multi-phase workflow. After executing all attached tasks, immediately collapse them and execute next phase
## 5-Phase Execution
### Phase 1: Create Test Session
**Step 1.0: Load Source Session Intent** - Preserve user's original task description for semantic CLI selection
```javascript
// Read source session metadata to get original task description
Read(".workflow/active/[sourceSessionId]/workflow-session.json")
// OR if context-package exists:
Read(".workflow/active/[sourceSessionId]/.process/context-package.json")
// Extract: metadata.task_description or project/description field
// This preserves user's CLI tool preferences (e.g., "use Codex for fixes")
```
**Step 1.1: Execute** - Create new test workflow session with preserved intent
```javascript
// Include original task description to enable semantic CLI selection
SlashCommand(command="/workflow:session:start --new \"Test validation for [sourceSessionId]: [originalTaskDescription]\"")
```
**Input**:
- `sourceSessionId` from user argument (e.g., `WFS-user-auth`)
- `originalTaskDescription` from source session metadata (preserves CLI tool preferences)
**Expected Behavior**:
- Creates new session with pattern `WFS-test-[source-slug]` (e.g., `WFS-test-user-auth`)
- Writes metadata to `workflow-session.json`:
- `workflow_type: "test_session"`
- `source_session_id: "[sourceSessionId]"`
- Description includes original user intent for semantic CLI selection
- Returns new session ID for subsequent phases
**Parse Output**:
- Extract: new test session ID (store as `testSessionId`)
- Pattern: `WFS-test-[slug]`
**Validation**:
- Source session `.workflow/[sourceSessionId]/` exists
- Source session has completed IMPL tasks (`.summaries/IMPL-*-summary.md`)
- New test session directory created
- Metadata includes `workflow_type` and `source_session_id`
**TodoWrite**: Mark phase 1 completed, phase 2 in_progress
---
### Phase 2: Gather Test Context
**Step 2.1: Execute** - Gather test coverage context from source session
```javascript
SlashCommand(command="/workflow:tools:test-context-gather --session [testSessionId]")
```
**Input**: `testSessionId` from Phase 1 (e.g., `WFS-test-user-auth`)
**Expected Behavior**:
- Load source session implementation context and summaries
- Analyze test coverage using MCP tools (find existing tests)
- Identify files requiring tests (coverage gaps)
- Detect test framework and conventions
- Generate `test-context-package.json`
**Parse Output**:
- Extract: test context package path (store as `testContextPath`)
- Pattern: `.workflow/[testSessionId]/.process/test-context-package.json`
**Validation**:
- Test context package created
- Contains source session summaries
- Includes coverage gap analysis
- Test framework detected
- Test conventions documented
<!-- TodoWrite: When test-context-gather executed, INSERT 3 test-context-gather tasks -->
**TodoWrite Update (Phase 2 SlashCommand executed - tasks attached)**:
```json
[
{"content": "Create independent test session", "status": "completed", "activeForm": "Creating test session"},
{"content": "Phase 2.1: Load source session summaries (test-context-gather)", "status": "in_progress", "activeForm": "Loading source session summaries"},
{"content": "Phase 2.2: Analyze test coverage with MCP tools (test-context-gather)", "status": "pending", "activeForm": "Analyzing test coverage"},
{"content": "Phase 2.3: Identify coverage gaps and framework (test-context-gather)", "status": "pending", "activeForm": "Identifying coverage gaps"},
{"content": "Analyze test requirements with Gemini", "status": "pending", "activeForm": "Analyzing test requirements"},
{"content": "Generate test generation and execution tasks", "status": "pending", "activeForm": "Generating test tasks"},
{"content": "Return workflow summary", "status": "pending", "activeForm": "Returning workflow summary"}
]
```
**Note**: SlashCommand dispatch **attaches** test-context-gather's 3 tasks. Orchestrator **executes** these tasks.
**Next Action**: Tasks attached → **Execute Phase 2.1-2.3** sequentially
<!-- TodoWrite: After Phase 2 tasks complete, REMOVE Phase 2.1-2.3, restore to orchestrator view -->
**TodoWrite Update (Phase 2 completed - tasks collapsed)**:
```json
[
{"content": "Create independent test session", "status": "completed", "activeForm": "Creating test session"},
{"content": "Gather test coverage context", "status": "completed", "activeForm": "Gathering test coverage context"},
{"content": "Analyze test requirements with Gemini", "status": "pending", "activeForm": "Analyzing test requirements"},
{"content": "Generate test generation and execution tasks", "status": "pending", "activeForm": "Generating test tasks"},
{"content": "Return workflow summary", "status": "pending", "activeForm": "Returning workflow summary"}
]
```
**Note**: Phase 2 tasks completed and collapsed to summary.
---
### Phase 3: Test Generation Analysis
**Step 3.1: Execute** - Analyze test requirements with Gemini
```javascript
SlashCommand(command="/workflow:tools:test-concept-enhanced --session [testSessionId] --context [testContextPath]")
```
**Input**:
- `testSessionId` from Phase 1
- `testContextPath` from Phase 2
**Expected Behavior**:
- Use Gemini to analyze coverage gaps and implementation context
- Study existing test patterns and conventions
- Generate test requirements for each missing test file
- Design test generation strategy
- Generate `TEST_ANALYSIS_RESULTS.md`
**Parse Output**:
- Verify `.workflow/[testSessionId]/.process/TEST_ANALYSIS_RESULTS.md` created
- Contains test requirements and generation strategy
- Lists test files to create with specifications
**Validation**:
- TEST_ANALYSIS_RESULTS.md exists with complete sections:
- Coverage Assessment
- Test Framework & Conventions
- Test Requirements by File
- Test Generation Strategy
- Implementation Targets (test files to create)
- Success Criteria
<!-- TodoWrite: When test-concept-enhanced executed, INSERT 3 concept-enhanced tasks -->
**TodoWrite Update (Phase 3 SlashCommand executed - tasks attached)**:
```json
[
{"content": "Create independent test session", "status": "completed", "activeForm": "Creating test session"},
{"content": "Gather test coverage context", "status": "completed", "activeForm": "Gathering test coverage context"},
{"content": "Phase 3.1: Analyze coverage gaps with Gemini (test-concept-enhanced)", "status": "in_progress", "activeForm": "Analyzing coverage gaps"},
{"content": "Phase 3.2: Study existing test patterns (test-concept-enhanced)", "status": "pending", "activeForm": "Studying test patterns"},
{"content": "Phase 3.3: Generate test generation strategy (test-concept-enhanced)", "status": "pending", "activeForm": "Generating test strategy"},
{"content": "Generate test generation and execution tasks", "status": "pending", "activeForm": "Generating test tasks"},
{"content": "Return workflow summary", "status": "pending", "activeForm": "Returning workflow summary"}
]
```
**Note**: SlashCommand dispatch **attaches** test-concept-enhanced's 3 tasks. Orchestrator **executes** these tasks.
**Next Action**: Tasks attached → **Execute Phase 3.1-3.3** sequentially
<!-- TodoWrite: After Phase 3 tasks complete, REMOVE Phase 3.1-3.3, restore to orchestrator view -->
**TodoWrite Update (Phase 3 completed - tasks collapsed)**:
```json
[
{"content": "Create independent test session", "status": "completed", "activeForm": "Creating test session"},
{"content": "Gather test coverage context", "status": "completed", "activeForm": "Gathering test coverage context"},
{"content": "Analyze test requirements with Gemini", "status": "completed", "activeForm": "Analyzing test requirements"},
{"content": "Generate test generation and execution tasks", "status": "pending", "activeForm": "Generating test tasks"},
{"content": "Return workflow summary", "status": "pending", "activeForm": "Returning workflow summary"}
]
```
**Note**: Phase 3 tasks completed and collapsed to summary.
---
### Phase 4: Generate Test Tasks
**Step 4.1: Execute** - Generate test task JSON files and planning documents
```javascript
SlashCommand(command="/workflow:tools:test-task-generate --session [testSessionId]")
```
**Input**:
- `testSessionId` from Phase 1
**Note**: CLI tool usage for fixes is determined semantically from user's task description (e.g., "use Codex for automated fixes").
**Expected Behavior**:
- Parse TEST_ANALYSIS_RESULTS.md from Phase 3
- Extract test requirements and generation strategy
- Generate **TWO task JSON files**:
- **IMPL-001.json**: Test Generation task (calls @code-developer)
- **IMPL-002.json**: Test Execution and Fix Cycle task (calls @test-fix-agent)
- Generate IMPL_PLAN.md with test generation and execution strategy
- Generate TODO_LIST.md with both tasks
**Parse Output**:
- Verify `.workflow/[testSessionId]/.task/IMPL-001.json` exists (test generation)
- Verify `.workflow/[testSessionId]/.task/IMPL-002.json` exists (test execution & fix)
- Verify `.workflow/[testSessionId]/IMPL_PLAN.md` created
- Verify `.workflow/[testSessionId]/TODO_LIST.md` created
**Validation - IMPL-001.json (Test Generation)**:
- Task ID: `IMPL-001`
- `meta.type: "test-gen"`
- `meta.agent: "@code-developer"`
- `context.requirements`: Generate tests based on TEST_ANALYSIS_RESULTS.md
- `flow_control.pre_analysis`: Load TEST_ANALYSIS_RESULTS.md and test context
- `flow_control.implementation_approach`: Test generation steps
- `flow_control.target_files`: Test files to create from analysis section 5
**Validation - IMPL-002.json (Test Execution & Fix)**:
- Task ID: `IMPL-002`
- `meta.type: "test-fix"`
- `meta.agent: "@test-fix-agent"`
- `context.depends_on: ["IMPL-001"]`
- `context.requirements`: Execute and fix tests
- `flow_control.implementation_approach.test_fix_cycle`: Complete cycle specification
- **Cycle pattern**: test → gemini_diagnose → fix (agent or CLI based on `command` field) → retest
- **Tools configuration**: Gemini for analysis with bug-fix template, agent or CLI for fixes
- **Exit conditions**: Success (all pass) or failure (max iterations)
- `flow_control.implementation_approach.modification_points`: 3-phase execution flow
- Phase 1: Initial test execution
- Phase 2: Iterative Gemini diagnosis + fixes (agent or CLI based on step's `command` field)
- Phase 3: Final validation and certification
<!-- TodoWrite: When test-task-generate executed, INSERT 3 test-task-generate tasks -->
**TodoWrite Update (Phase 4 SlashCommand executed - tasks attached)**:
```json
[
{"content": "Create independent test session", "status": "completed", "activeForm": "Creating test session"},
{"content": "Gather test coverage context", "status": "completed", "activeForm": "Gathering test coverage context"},
{"content": "Analyze test requirements with Gemini", "status": "completed", "activeForm": "Analyzing test requirements"},
{"content": "Phase 4.1: Parse TEST_ANALYSIS_RESULTS.md (test-task-generate)", "status": "in_progress", "activeForm": "Parsing test analysis"},
{"content": "Phase 4.2: Generate IMPL-001.json and IMPL-002.json (test-task-generate)", "status": "pending", "activeForm": "Generating task JSONs"},
{"content": "Phase 4.3: Generate IMPL_PLAN.md and TODO_LIST.md (test-task-generate)", "status": "pending", "activeForm": "Generating plan documents"},
{"content": "Return workflow summary", "status": "pending", "activeForm": "Returning workflow summary"}
]
```
**Note**: SlashCommand dispatch **attaches** test-task-generate's 3 tasks. Orchestrator **executes** these tasks.
**Next Action**: Tasks attached → **Execute Phase 4.1-4.3** sequentially
<!-- TodoWrite: After Phase 4 tasks complete, REMOVE Phase 4.1-4.3, restore to orchestrator view -->
**TodoWrite Update (Phase 4 completed - tasks collapsed)**:
```json
[
{"content": "Create independent test session", "status": "completed", "activeForm": "Creating test session"},
{"content": "Gather test coverage context", "status": "completed", "activeForm": "Gathering test coverage context"},
{"content": "Analyze test requirements with Gemini", "status": "completed", "activeForm": "Analyzing test requirements"},
{"content": "Generate test generation and execution tasks", "status": "completed", "activeForm": "Generating test tasks"},
{"content": "Return workflow summary", "status": "in_progress", "activeForm": "Returning workflow summary"}
]
```
**Note**: Phase 4 tasks completed and collapsed to summary.
---
### Phase 5: Return Summary (Command Ends Here)
**Important**: This is the final phase of `/workflow:test-gen`. The command completes and returns control to the user. No automatic execution occurs.
**Return to User**:
```
Test workflow preparation complete!
Source Session: [sourceSessionId]
Test Session: [testSessionId]
Artifacts Created:
- Test context analysis
- Test generation strategy
- Task definitions (IMPL-001, IMPL-002)
- Implementation plan
Test Framework: [detected framework]
Test Files to Generate: [count]
Fix Mode: [Agent|CLI] (based on `command` field in implementation_approach steps)
Review Generated Artifacts:
- Test plan: .workflow/[testSessionId]/IMPL_PLAN.md
- Task list: .workflow/[testSessionId]/TODO_LIST.md
- Analysis: .workflow/[testSessionId]/.process/TEST_ANALYSIS_RESULTS.md
Ready for execution. Use appropriate workflow commands to proceed.
```
**TodoWrite**: Mark phase 5 completed
**Command Boundary**: After this phase, the command terminates and returns to user prompt.
---
## TodoWrite Pattern
**Core Concept**: Dynamic task attachment and collapse for test-gen workflow with cross-session context gathering and test generation strategy.
### Key Principles
1. **Task Attachment** (when SlashCommand executed):
- Sub-command's internal tasks are **attached** to orchestrator's TodoWrite
- Example: `/workflow:tools:test-context-gather` attaches 3 sub-tasks (Phase 2.1, 2.2, 2.3)
- First attached task marked as `in_progress`, others as `pending`
- Orchestrator **executes** these attached tasks sequentially
2. **Task Collapse** (after sub-tasks complete):
- Remove detailed sub-tasks from TodoWrite
- **Collapse** to high-level phase summary
- Example: Phase 2.1-2.3 collapse to "Gather test coverage context: completed"
- Maintains clean orchestrator-level view
3. **Continuous Execution**:
- After collapse, automatically proceed to next pending phase
- No user intervention required between phases
- TodoWrite dynamically reflects current execution state
**Lifecycle Summary**: Initial pending tasks → Phase executed (tasks ATTACHED) → Sub-tasks executed sequentially → Phase completed (tasks COLLAPSED to summary) → Next phase begins → Repeat until all phases complete.
### Test-Gen Specific Features
- **Phase 2**: Cross-session context gathering from source implementation session
- **Phase 3**: Test requirements analysis with Gemini for generation strategy
- **Phase 4**: Dual-task generation (IMPL-001 for test generation, IMPL-002 for test execution)
- **Fix Mode Configuration**: CLI tool usage determined semantically from user's task description
**Note**: See individual Phase descriptions (Phase 2, 3, 4) for detailed TodoWrite Update examples with full JSON structures.
## Execution Flow Diagram
```
Test-Gen Workflow Orchestrator
├─ Phase 1: Create Test Session
│ └─ /workflow:session:start --new
│ └─ Returns: testSessionId (WFS-test-[source])
├─ Phase 2: Gather Test Context ← ATTACHED (3 tasks)
│ └─ /workflow:tools:test-context-gather
│ ├─ Phase 2.1: Load source session summaries
│ ├─ Phase 2.2: Analyze test coverage with MCP tools
│ └─ Phase 2.3: Identify coverage gaps and framework
│ └─ Returns: test-context-package.json ← COLLAPSED
├─ Phase 3: Test Generation Analysis ← ATTACHED (3 tasks)
│ └─ /workflow:tools:test-concept-enhanced
│ ├─ Phase 3.1: Analyze coverage gaps with Gemini
│ ├─ Phase 3.2: Study existing test patterns
│ └─ Phase 3.3: Generate test generation strategy
│ └─ Returns: TEST_ANALYSIS_RESULTS.md ← COLLAPSED
├─ Phase 4: Generate Test Tasks ← ATTACHED (3 tasks)
│ └─ /workflow:tools:test-task-generate
│ ├─ Phase 4.1: Parse TEST_ANALYSIS_RESULTS.md
│ ├─ Phase 4.2: Generate IMPL-001.json and IMPL-002.json
│ └─ Phase 4.3: Generate IMPL_PLAN.md and TODO_LIST.md
│ └─ Returns: Task JSONs and plans ← COLLAPSED
└─ Phase 5: Return Summary
└─ Command ends, control returns to user
Artifacts Created:
├── .workflow/active/WFS-test-[session]/
│ ├── workflow-session.json
│ ├── IMPL_PLAN.md
│ ├── TODO_LIST.md
│ ├── .task/
│ │ ├── IMPL-001.json (test generation task)
│ │ └── IMPL-002.json (test execution task)
│ └── .process/
│ ├── test-context-package.json
│ └── TEST_ANALYSIS_RESULTS.md
Key Points:
• ← ATTACHED: SlashCommand attaches sub-tasks to orchestrator TodoWrite
• ← COLLAPSED: Sub-tasks executed and collapsed to phase summary
```
## Session Metadata
Test session includes `workflow_type: "test_session"` and `source_session_id` for automatic context gathering.
## Task Output
Generates two task definition files:
- **IMPL-001.json**: Test generation task specification
- Agent: @code-developer
- Input: TEST_ANALYSIS_RESULTS.md
- Output: Test files based on analysis
- **IMPL-002.json**: Test execution and fix cycle specification
- Agent: @test-fix-agent
- Dependency: IMPL-001 must complete first
- Max iterations: 5
- Fix mode: Agent or CLI (based on `command` field in implementation_approach)
See `/workflow:tools:test-task-generate` for complete task JSON schemas.
## Error Handling
| Phase | Error | Action |
|-------|-------|--------|
| 1 | Source session not found | Return error with source session ID |
| 1 | No completed IMPL tasks | Return error, source incomplete |
| 2 | Context gathering failed | Return error, check source artifacts |
| 3 | Analysis failed | Return error, check context package |
| 4 | Task generation failed | Retry once, then error with details |
## Output Files
Created in `.workflow/active/WFS-test-[session]/`:
- `workflow-session.json` - Session metadata
- `.process/test-context-package.json` - Coverage analysis
- `.process/TEST_ANALYSIS_RESULTS.md` - Test requirements
- `.task/IMPL-001.json` - Test generation task
- `.task/IMPL-002.json` - Test execution & fix task
- `IMPL_PLAN.md` - Test plan
- `TODO_LIST.md` - Task checklist
## Task Specifications
**IMPL-001.json Structure**:
- `meta.type: "test-gen"`
- `meta.agent: "@code-developer"`
- `context.requirements`: Generate tests based on TEST_ANALYSIS_RESULTS.md
- `flow_control.target_files`: Test files to create
- `flow_control.implementation_approach`: Test generation strategy
**IMPL-002.json Structure**:
- `meta.type: "test-fix"`
- `meta.agent: "@test-fix-agent"`
- `context.depends_on: ["IMPL-001"]`
- `flow_control.implementation_approach.test_fix_cycle`: Complete cycle specification
- Gemini diagnosis template
- Fix application mode (agent or CLI based on `command` field)
- Max iterations: 5
- `flow_control.implementation_approach.modification_points`: 3-phase flow
See `/workflow:tools:test-task-generate` for complete JSON schemas.
## Best Practices
1. **Prerequisites**: Ensure source session has completed IMPL tasks with summaries
2. **Clean State**: Commit implementation changes before running test-gen
3. **Review Artifacts**: Check generated IMPL_PLAN.md and TODO_LIST.md before proceeding
4. **Understand Scope**: This command only prepares artifacts; it does not execute tests
## Related Commands
**Prerequisite Commands**:
- `/workflow:plan` or `/workflow:execute` - Complete implementation session that needs test validation
**Executed by This Command** (4 phases):
- `/workflow:session:start` - Phase 1: Create independent test workflow session
- `/workflow:tools:test-context-gather` - Phase 2: Analyze test coverage and gather source session context
- `/workflow:tools:test-concept-enhanced` - Phase 3: Generate test requirements and strategy using Gemini
- `/workflow:tools:test-task-generate` - Phase 4: Generate test task JSONs (CLI tool usage determined semantically)
**Follow-up Commands**:
- `/workflow:status` - Review generated test tasks
- `/workflow:test-cycle-execute` - Execute test generation and fix cycles
- `/workflow:execute` - Execute generated test tasks

View File

@@ -1,6 +1,6 @@
---
name: test-task-generate
description: Generate test planning documents (IMPL_PLAN.md, test task JSONs, TODO_LIST.md) using action-planning-agent - produces test planning artifacts, does NOT execute tests
description: Generate test planning documents (IMPL_PLAN.md, test task JSONs, TODO_LIST.md) by invoking test-action-planning-agent
argument-hint: "--session WFS-test-session-id"
examples:
- /workflow:tools:test-task-generate --session WFS-test-auth
@@ -9,33 +9,29 @@ examples:
# Generate Test Planning Documents Command
## Overview
Generate test planning documents (IMPL_PLAN.md, test task JSONs, TODO_LIST.md) using action-planning-agent. This command produces **test planning artifacts only** - it does NOT execute tests or implement code. Actual test execution requires separate execution command (e.g., /workflow:test-cycle-execute).
## Core Philosophy
- **Planning Only**: Generate test planning documents (IMPL_PLAN.md, task JSONs, TODO_LIST.md) - does NOT execute tests
- **Agent-Driven Document Generation**: Delegate test plan generation to action-planning-agent
- **Two-Phase Flow**: Context Preparation (command) → Test Document Generation (agent)
- **Memory-First**: Reuse loaded documents from conversation memory
- **MCP-Enhanced**: Use MCP tools for test pattern research and analysis
- **Path Clarity**: All `focus_paths` prefer absolute paths (e.g., `D:\\project\\src\\module`), or clear relative paths from project root
- **Leverage Existing Test Infrastructure**: Prioritize using established testing frameworks and tools present in the project
Generate test planning documents (IMPL_PLAN.md, test task JSONs, TODO_LIST.md) by invoking **test-action-planning-agent**.
## Test-Specific Execution Modes
This command produces **test planning artifacts only** - it does NOT execute tests or implement code. Actual test execution requires separate execution command (e.g., /workflow:test-cycle-execute).
### Test Generation (IMPL-001)
- **Agent Mode** (default): @code-developer generates tests within agent context
- **CLI Mode**: Use CLI tools when `command` field present in implementation_approach (determined semantically)
### Agent Specialization
### Test Execution & Fix (IMPL-002+)
- **Agent Mode** (default): Gemini diagnosis → agent applies fixes
- **CLI Mode**: Gemini diagnosis → CLI applies fixes (when `command` field present in implementation_approach)
This command invokes `@test-action-planning-agent` - a specialized variant of action-planning-agent with:
- Progressive L0-L3 test layers (Static, Unit, Integration, E2E)
- AI code issue detection (L0.5) with severity levels
- Project type templates (React, Node API, CLI, Library, Monorepo)
- Test anti-pattern detection with quality gates
- Layer completeness thresholds and coverage targets
**See**: `d:\Claude_dms3\.claude\agents\test-action-planning-agent.md` for complete test specifications.
---
## Execution Process
```
Input Parsing:
─ Parse flags: --session
└─ Validation: session_id REQUIRED
─ Parse flags: --session
Phase 1: Context Preparation (Command)
├─ Assemble test session paths
@@ -47,78 +43,33 @@ Phase 1: Context Preparation (Command)
Phase 2: Test Document Generation (Agent)
├─ Load TEST_ANALYSIS_RESULTS.md as primary requirements source
├─ Generate Test Task JSON Files (.task/IMPL-*.json)
│ ├─ IMPL-001: Test generation (meta.type: "test-gen")
─ IMPL-002+: Test execution & fix (meta.type: "test-fix")
│ ├─ IMPL-001: Test generation (L1-L3 layers, project-specific templates)
─ IMPL-001.3: Code validation gate (L0 + AI issue detection)
│ ├─ IMPL-001.5: Test quality gate (anti-patterns + coverage)
│ └─ IMPL-002: Test execution & fix cycle
├─ Create IMPL_PLAN.md (test_session variant)
└─ Generate TODO_LIST.md with test phase indicators
```
## Document Generation Lifecycle
---
### Phase 1: Context Preparation (Command Responsibility)
## Agent Invocation
**Command prepares test session paths and metadata for planning document generation.**
**Test Session Path Structure**:
```
.workflow/active/WFS-test-{session-id}/
├── workflow-session.json # Test session metadata
├── .process/
│ ├── TEST_ANALYSIS_RESULTS.md # Test requirements and strategy
│ ├── test-context-package.json # Test patterns and coverage
│ └── context-package.json # General context artifacts
├── .task/ # Output: Test task JSON files
├── IMPL_PLAN.md # Output: Test implementation plan
└── TODO_LIST.md # Output: Test TODO list
```
**Command Preparation**:
1. **Assemble Test Session Paths** for agent prompt:
- `session_metadata_path`
- `test_analysis_results_path` (REQUIRED)
- `test_context_package_path`
- Output directory paths
2. **Provide Metadata** (simple values):
- `session_id`
- `source_session_id` (if exists)
- `mcp_capabilities` (available MCP tools)
**Note**: CLI tool usage is now determined semantically from user's task description, not by flags.
### Phase 2: Test Document Generation (Agent Responsibility)
**Purpose**: Generate test-specific IMPL_PLAN.md, task JSONs, and TODO_LIST.md - planning documents only, NOT test execution.
**Agent Invocation**:
```javascript
Task(
subagent_type="action-planning-agent",
subagent_type="test-action-planning-agent",
run_in_background=false,
description="Generate test planning documents (IMPL_PLAN.md, task JSONs, TODO_LIST.md)",
description="Generate test planning documents",
prompt=`
## TASK OBJECTIVE
Generate test planning documents (IMPL_PLAN.md, task JSONs, TODO_LIST.md) for test workflow session
IMPORTANT: This is TEST PLANNING ONLY - you are generating planning documents, NOT executing tests.
CRITICAL:
- Use existing test frameworks and utilities from the project
- Follow the progressive loading strategy defined in your agent specification (load context incrementally from memory-first approach)
## AGENT CONFIGURATION REFERENCE
Refer to your specification for:
- Test Task JSON Schema (6-field structure with test-specific metadata)
- Test IMPL_PLAN.md Structure (test_session variant with test-fix cycle)
- TODO_LIST.md Format (with test phase indicators)
- Progressive Loading Strategy (memory-first, load TEST_ANALYSIS_RESULTS.md as primary source)
- Quality Validation Rules (task count limits, requirement quantification)
## SESSION PATHS
Input:
- Session Metadata: .workflow/active/{test-session-id}/workflow-session.json
- TEST_ANALYSIS_RESULTS: .workflow/active/{test-session-id}/.process/TEST_ANALYSIS_RESULTS.md (REQUIRED - primary requirements source)
- TEST_ANALYSIS_RESULTS: .workflow/active/{test-session-id}/.process/TEST_ANALYSIS_RESULTS.md (REQUIRED)
- Test Context Package: .workflow/active/{test-session-id}/.process/test-context-package.json
- Context Package: .workflow/active/{test-session-id}/.process/context-package.json
- Source Session Summaries: .workflow/active/{source-session-id}/.summaries/IMPL-*.md (if exists)
@@ -134,130 +85,95 @@ Workflow Type: test_session
Source Session: {source-session-id} (if exists)
MCP Capabilities: {exa_code, exa_web, code_index}
## CLI TOOL SELECTION
Determine CLI tool usage per-step based on user's task description:
- If user specifies "use Codex/Gemini/Qwen for X" → Add command field to relevant steps
- Default: Agent execution (no command field) unless user explicitly requests CLI
## YOUR SPECIFICATIONS
You are @test-action-planning-agent. Your complete test specifications are defined in:
d:\Claude_dms3\.claude\agents\test-action-planning-agent.md
## TEST-SPECIFIC REQUIREMENTS SUMMARY
(Detailed specifications in your agent definition)
This includes:
- Progressive Test Layers (L0-L3) with L0.1-L0.5, L1.1-L1.5, L2.1-L2.5, L3.1-L3.4
- AI Code Issue Detection (L0.5) with 7 categories and severity levels
- Project Type Detection & Templates (6 project types)
- Test Anti-Pattern Detection (5 categories)
- Layer Completeness & Quality Metrics (thresholds and gate decisions)
- Task JSON structure requirements (minimum 4 tasks)
- Quality validation rules
### Task Structure Requirements
- Minimum 4 tasks: IMPL-001 (test generation) + IMPL-001.3 (code validation) + IMPL-001.5 (test quality) + IMPL-002 (test execution & fix)
- Expandable for complex projects: Add IMPL-003+ (per-module, integration, E2E tests)
Task Configuration:
IMPL-001 (Test Generation):
- meta.type: "test-gen"
- meta.agent: "@code-developer"
- meta.test_framework: Specify existing framework (e.g., "jest", "vitest", "pytest")
- flow_control: Test generation strategy from TEST_ANALYSIS_RESULTS.md
- CLI execution: Add `command` field when user requests (determined semantically)
IMPL-001.3 (Code Validation Gate) ← NEW:
- meta.type: "code-validation"
- meta.agent: "@test-fix-agent"
- context.depends_on: ["IMPL-001"]
- context.validation_config: "~/.claude/workflows/test-quality-config.json"
- flow_control.validation_phases: ["compilation", "imports", "variables", "types", "ai_specific"]
- flow_control.auto_fix_enabled: true
- flow_control.max_retries: 2
- flow_control.severity_thresholds: { critical: 0, error: 3, warning: 10 }
- acceptance_criteria: Zero compilation errors, all imports resolvable, no variable redeclarations
IMPL-001.5 (Test Quality Gate):
- meta.type: "test-quality-review"
- meta.agent: "@test-fix-agent"
- context.depends_on: ["IMPL-001", "IMPL-001.3"]
- context.quality_config: "~/.claude/workflows/test-quality-config.json"
- flow_control: Static analysis, coverage analysis, anti-pattern detection
- acceptance_criteria: Coverage ≥ 80%, zero critical anti-patterns
IMPL-002+ (Test Execution & Fix):
- meta.type: "test-fix"
- meta.agent: "@test-fix-agent"
- context.depends_on: ["IMPL-001", "IMPL-001.3", "IMPL-001.5"]
- flow_control: Test-fix cycle with iteration limits and diagnosis configuration
- CLI execution: Add `command` field when user requests (determined semantically)
### Test-Fix Cycle Specification (IMPL-002+)
Required flow_control fields:
- max_iterations: 5
- diagnosis_tool: "gemini"
- diagnosis_template: "~/.claude/workflows/cli-templates/prompts/analysis/01-diagnose-bug-root-cause.txt"
- cycle_pattern: "test → gemini_diagnose → fix → retest"
- exit_conditions: ["all_tests_pass", "max_iterations_reached"]
- auto_revert_on_failure: true
- CLI fix: Add `command` field when user specifies CLI tool usage
### Automation Framework Configuration
Select automation tools based on test requirements from TEST_ANALYSIS_RESULTS.md:
- UI interaction testing → E2E browser automation (meta.e2e_framework)
- API/database integration → integration test tools (meta.test_tools)
- Performance metrics → load testing tools (meta.perf_framework)
- Logic verification → unit test framework (meta.test_framework)
**Tool Selection**: Detect from project config > suggest based on requirements
### TEST_ANALYSIS_RESULTS.md Mapping
PRIMARY requirements source - extract and map to task JSONs:
- Test framework config → meta.test_framework (use existing framework from project)
- Existing test utilities → flow_control.reusable_test_tools (discovered test helpers, fixtures, mocks)
- Test runner commands → flow_control.test_commands (from package.json or pytest config)
- Coverage targets → meta.coverage_target
- Test requirements → context.requirements (quantified with explicit counts)
- Test generation strategy → IMPL-001 flow_control.implementation_approach
- Implementation targets → context.files_to_test (absolute paths)
**Follow your specification exactly** when generating test task JSONs.
## EXPECTED DELIVERABLES
1. Test Task JSON Files (.task/IMPL-*.json) - Minimum 4 required:
- IMPL-001.json: Test generation task
- IMPL-001.3-validation.json: Code validation gate (AI error detection) ← NEW
- IMPL-001.5-review.json: Test quality gate
1. Test Task JSON Files (.task/IMPL-*.json) - Minimum 4:
- IMPL-001.json: Test generation (L1-L3 layers per spec)
- IMPL-001.3-validation.json: Code validation gate (L0 + AI issues per spec)
- IMPL-001.5-review.json: Test quality gate (anti-patterns + coverage per spec)
- IMPL-002.json: Test execution & fix cycle
Each task includes:
- 6-field schema with quantified requirements from TEST_ANALYSIS_RESULTS.md
- Test-specific metadata: type, agent, test_framework, coverage_target
- flow_control includes: reusable_test_tools, test_commands (from project config)
- Validation config reference for IMPL-001.3: ~/.claude/workflows/test-quality-config.json
- CLI execution via `command` field when user requests (determined semantically)
- Artifact references from test-context-package.json
- Absolute paths in context.files_to_test
2. IMPL_PLAN.md: Test implementation plan with quality gates
2. Test Implementation Plan (IMPL_PLAN.md)
- Template: ~/.claude/workflows/cli-templates/prompts/workflow/impl-plan-template.txt
- Test-specific frontmatter: workflow_type="test_session", test_framework, source_session_id
- Test-Fix-Retest Cycle section with diagnosis configuration
- Source session context integration (if applicable)
3. TODO List (TODO_LIST.md)
- Hierarchical structure with test phase containers
- Links to task JSONs with status markers
- Matches task JSON hierarchy
## QUALITY STANDARDS
Hard Constraints:
- Task count: minimum 4, maximum 18 (IMPL-001, IMPL-001.3, IMPL-001.5, IMPL-002 required)
- All requirements quantified from TEST_ANALYSIS_RESULTS.md
- Test framework matches existing project framework
- flow_control includes reusable_test_tools and test_commands from project
- Absolute paths for all focus_paths
- Acceptance criteria include verification commands
- CLI `command` field added only when user explicitly requests CLI tool usage
3. TODO_LIST.md: Hierarchical task list with test phase indicators
## SUCCESS CRITERIA
- All test planning documents generated successfully
- Return completion status: task count, test framework, coverage targets, source session status
- Task count: minimum 4 (expandable for complex projects)
- Test framework: {detected from project}
- Coverage targets: L0 zero errors, L1 80%+, L2 70%+
- L0-L3 layers explicitly defined per spec
- AI issue detection configured per spec
- Quality gates with measurable thresholds
`
)
```
---
## Test-Specific Execution Modes
### Test Generation (IMPL-001)
- **Agent Mode** (default): @code-developer generates tests within agent context
- **CLI Mode**: Use CLI tools when `command` field present in implementation_approach
### Test Execution & Fix (IMPL-002+)
- **Agent Mode** (default): Gemini diagnosis → agent applies fixes
- **CLI Mode**: Gemini diagnosis → CLI applies fixes (when `command` field present)
**CLI Tool Selection**: Determined semantically from user's task description (e.g., "use Codex for fixes")
---
## Output
### Directory Structure
```
.workflow/active/WFS-test-[session]/
├── workflow-session.json # Session metadata
├── IMPL_PLAN.md # Test implementation plan
├── TODO_LIST.md # Task checklist
├── .task/
│ ├── IMPL-001.json # Test generation (L1-L3)
│ ├── IMPL-001.3-validation.json # Code validation gate (L0 + AI)
│ ├── IMPL-001.5-review.json # Test quality gate
│ └── IMPL-002.json # Test execution & fix cycle
└── .process/
├── test-context-package.json # Test coverage and patterns
└── TEST_ANALYSIS_RESULTS.md # L0-L3 requirements (source for agent)
```
### Task Summary
| Task | Type | Agent | Purpose |
|------|------|-------|---------|
| IMPL-001 | test-gen | @code-developer | Generate L1-L3 tests with project templates |
| IMPL-001.3 | code-validation | @test-fix-agent | Validate L0 + detect AI issues (CRITICAL/ERROR/WARNING) |
| IMPL-001.5 | test-quality-review | @test-fix-agent | Check anti-patterns, layer completeness, coverage |
| IMPL-002 | test-fix | @test-fix-agent | Execute tests, diagnose failures, apply fixes |
---
## Integration & Usage
### Command Chain
- **Called By**: `/workflow:test-gen` (Phase 4), `/workflow:test-fix-gen` (Phase 4)
- **Invokes**: `action-planning-agent` for test planning document generation
- **Called By**: `/workflow:test-fix-gen` (Phase 4)
- **Invokes**: `@test-action-planning-agent` for test planning document generation
- **Followed By**: `/workflow:test-cycle-execute` or `/workflow:execute` (user-triggered)
### Usage Examples
@@ -265,22 +181,31 @@ Hard Constraints:
# Standard execution
/workflow:tools:test-task-generate --session WFS-test-auth
# With semantic CLI request (include in task description)
# With semantic CLI request (include in task description when calling /workflow:test-fix-gen)
# e.g., "Generate tests, use Codex for implementation and fixes"
```
### CLI Tool Selection
CLI tool usage is determined semantically from user's task description:
- Include "use Codex" for automated fixes
- Include "use Gemini" for analysis
- Default: Agent execution (no `command` field)
### Output Validation
### Output
- Test task JSON files in `.task/` directory (minimum 4):
- IMPL-001.json (test generation)
- IMPL-001.3-validation.json (code validation gate)
- IMPL-001.5-review.json (test quality gate)
- IMPL-002.json (test execution & fix)
- IMPL_PLAN.md with test strategy, validation gates, and fix cycle specification
- TODO_LIST.md with test phase indicators
- Session ready for test execution
**Minimum Requirements**:
- 4 task JSON files created
- IMPL_PLAN.md exists with test-specific sections
- TODO_LIST.md exists with test phase hierarchy
- All tasks reference TEST_ANALYSIS_RESULTS.md specifications
- L0-L3 layers explicitly defined in IMPL-001
- AI issue detection configured in IMPL-001.3
- Quality gates with thresholds in IMPL-001.5
---
## Related Commands
**Called By**:
- `/workflow:test-fix-gen` - Phase 4: Generate test planning documents
**Prerequisite**:
- `/workflow:tools:test-concept-enhanced` - Must generate TEST_ANALYSIS_RESULTS.md first
**Follow-Up**:
- `/workflow:test-cycle-execute` - Execute generated test tasks
- `/workflow:execute` - Alternative: Standard task execution

1
ccw/frontend/.gitignore vendored Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,17 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"validate:i18n": "tsx scripts/validate-translations.ts"
},
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.11.4",
"@hello-pangea/dnd": "^18.0.1",
"@radix-ui/react-dialog": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.0",
@@ -18,26 +26,36 @@
"@radix-ui/react-toast": "^1.2.0",
"@radix-ui/react-tooltip": "^1.1.0",
"@tanstack/react-query": "^5.60.0",
"@xyflow/react": "^12.3.0",
"@xyflow/react": "^12.10.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.460.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intl": "^6.8.9",
"react-router-dom": "^6.28.0",
"tailwind-merge": "^2.5.0",
"zod": "^3.23.8",
"zustand": "^5.0.0"
},
"devDependencies": {
"@playwright/test": "^1.40.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-v8": "^2.0.0",
"@vitest/ui": "^2.0.0",
"autoprefixer": "^10.4.20",
"jsdom": "^25.0.0",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"tsx": "^4.19.0",
"typescript": "^5.6.0",
"vite": "^6.0.0"
"vite": "^6.0.0",
"vitest": "^2.0.0"
}
}

View File

@@ -0,0 +1,128 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33] [cursor=pointer]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39] [cursor=pointer]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44] [cursor=pointer]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49] [cursor=pointer]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57] [cursor=pointer]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63] [cursor=pointer]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70] [cursor=pointer]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78] [cursor=pointer]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83] [cursor=pointer]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88] [cursor=pointer]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93] [cursor=pointer]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106] [cursor=pointer]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112] [cursor=pointer]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- heading "memory.title" [level=1] [ref=e127]:
- img [ref=e128]
- text: memory.title
- paragraph [ref=e138]: memory.description
- generic [ref=e139]:
- button "common.actions.refresh" [disabled]:
- img
- text: common.actions.refresh
- button "memory.actions.add" [ref=e140] [cursor=pointer]:
- img [ref=e141]
- text: memory.actions.add
- generic [ref=e142]:
- generic [ref=e144]:
- img [ref=e146]
- generic [ref=e150]:
- generic [ref=e151]: "0"
- paragraph [ref=e152]: memory.stats.count
- generic [ref=e154]:
- img [ref=e156]
- generic [ref=e159]:
- generic [ref=e160]: "0"
- paragraph [ref=e161]: memory.stats.claudeMdCount
- generic [ref=e163]:
- img [ref=e165]
- generic [ref=e175]:
- generic [ref=e176]: 0 B
- paragraph [ref=e177]: memory.stats.totalSize
- generic [ref=e179]:
- img [ref=e180]
- textbox "memory.filters.search" [ref=e183]
```

View File

@@ -0,0 +1,144 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇺🇸
- generic: English
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- heading "home.title" [level=1] [ref=e127]
- paragraph [ref=e128]: home.description
- button "common.actions.refresh" [ref=e129] [cursor=pointer]:
- img [ref=e130]
- text: common.actions.refresh
- generic [ref=e135]:
- heading "home.sections.statistics" [level=2] [ref=e136]
- generic [ref=e137]:
- generic [ref=e140]:
- generic [ref=e141]:
- paragraph [ref=e142]: home.stats.activeSessions
- paragraph [ref=e144]: "0"
- img [ref=e146]
- generic [ref=e150]:
- generic [ref=e151]:
- paragraph [ref=e152]: home.stats.totalTasks
- paragraph [ref=e154]: "0"
- img [ref=e156]
- generic [ref=e161]:
- generic [ref=e162]:
- paragraph [ref=e163]: home.stats.completedTasks
- paragraph [ref=e165]: "0"
- img [ref=e167]
- generic [ref=e172]:
- generic [ref=e173]:
- paragraph [ref=e174]: home.stats.pendingTasks
- paragraph [ref=e176]: "0"
- img [ref=e178]
- generic [ref=e183]:
- generic [ref=e184]:
- paragraph [ref=e185]: common.status.failed
- paragraph [ref=e187]: "0"
- img [ref=e189]
- generic [ref=e195]:
- generic [ref=e196]:
- paragraph [ref=e197]: common.stats.todayActivity
- paragraph [ref=e199]: "0"
- img [ref=e201]
- generic [ref=e203]:
- generic [ref=e204]:
- heading "home.sections.recentSessions" [level=2] [ref=e205]
- button "common.actions.viewAll" [ref=e206] [cursor=pointer]
- generic [ref=e207]:
- img [ref=e208]
- heading "home.emptyState.noSessions.title" [level=3] [ref=e210]
- paragraph [ref=e211]: home.emptyState.noSessions.message
```

View File

@@ -0,0 +1,144 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e21]
- button "common.aria.switchToDarkMode" [ref=e23] [cursor=pointer]:
- img [ref=e24]
- button "common.aria.userMenu" [ref=e27] [cursor=pointer]:
- img [ref=e28]
- generic [ref=e31]:
- navigation "Claude Code Workflow" [ref=e32]:
- navigation [ref=e33]:
- list [ref=e34]:
- listitem [ref=e35]:
- link "navigation.main.home" [ref=e36] [cursor=pointer]:
- /url: /
- img [ref=e37]
- generic [ref=e40]: navigation.main.home
- listitem [ref=e41]:
- link "navigation.main.sessions" [ref=e42] [cursor=pointer]:
- /url: /sessions
- img [ref=e43]
- generic [ref=e48]: navigation.main.sessions
- listitem [ref=e49]:
- link "navigation.main.liteTasks" [ref=e50] [cursor=pointer]:
- /url: /lite-tasks
- img [ref=e51]
- generic [ref=e53]: navigation.main.liteTasks
- listitem [ref=e54]:
- link "navigation.main.project" [ref=e55] [cursor=pointer]:
- /url: /project
- img [ref=e56]
- generic [ref=e61]: navigation.main.project
- listitem [ref=e62]:
- link "navigation.main.history" [ref=e63] [cursor=pointer]:
- /url: /history
- img [ref=e64]
- generic [ref=e67]: navigation.main.history
- listitem [ref=e68]:
- link "navigation.main.orchestrator" [ref=e69] [cursor=pointer]:
- /url: /orchestrator
- img [ref=e70]
- generic [ref=e74]: navigation.main.orchestrator
- listitem [ref=e75]:
- link "navigation.main.loops" [ref=e76] [cursor=pointer]:
- /url: /loops
- img [ref=e77]
- generic [ref=e82]: navigation.main.loops
- listitem [ref=e83]:
- link "navigation.main.issues" [ref=e84] [cursor=pointer]:
- /url: /issues
- img [ref=e85]
- generic [ref=e89]: navigation.main.issues
- listitem [ref=e90]:
- link "navigation.main.skills" [ref=e91] [cursor=pointer]:
- /url: /skills
- img [ref=e92]
- generic [ref=e98]: navigation.main.skills
- listitem [ref=e99]:
- link "navigation.main.commands" [ref=e100] [cursor=pointer]:
- /url: /commands
- img [ref=e101]
- generic [ref=e104]: navigation.main.commands
- listitem [ref=e105]:
- link "navigation.main.memory" [ref=e106] [cursor=pointer]:
- /url: /memory
- img [ref=e107]
- generic [ref=e117]: navigation.main.memory
- listitem [ref=e118]:
- link "navigation.main.settings" [ref=e119] [cursor=pointer]:
- /url: /settings
- img [ref=e120]
- generic [ref=e123]: navigation.main.settings
- listitem [ref=e124]:
- link "navigation.main.help" [ref=e125] [cursor=pointer]:
- /url: /help
- img [ref=e126]
- generic [ref=e130]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e132] [cursor=pointer]:
- img [ref=e133]
- generic [ref=e137]: navigation.sidebar.collapse
- main [ref=e138]:
- generic [ref=e139]:
- generic [ref=e140]:
- generic [ref=e141]:
- heading "home.title" [level=1] [ref=e142]
- paragraph [ref=e143]: home.description
- button "common.actions.refresh" [ref=e144] [cursor=pointer]:
- img [ref=e145]
- text: common.actions.refresh
- generic [ref=e150]:
- heading "home.sections.statistics" [level=2] [ref=e151]
- generic [ref=e152]:
- generic [ref=e155]:
- generic [ref=e156]:
- paragraph [ref=e157]: home.stats.activeSessions
- paragraph [ref=e159]: "0"
- img [ref=e161]
- generic [ref=e168]:
- generic [ref=e169]:
- paragraph [ref=e170]: home.stats.totalTasks
- paragraph [ref=e172]: "0"
- img [ref=e174]
- generic [ref=e182]:
- generic [ref=e183]:
- paragraph [ref=e184]: home.stats.completedTasks
- paragraph [ref=e186]: "0"
- img [ref=e188]
- generic [ref=e193]:
- generic [ref=e194]:
- paragraph [ref=e195]: home.stats.pendingTasks
- paragraph [ref=e197]: "0"
- img [ref=e199]
- generic [ref=e204]:
- generic [ref=e205]:
- paragraph [ref=e206]: common.status.failed
- paragraph [ref=e208]: "0"
- img [ref=e210]
- generic [ref=e216]:
- generic [ref=e217]:
- paragraph [ref=e218]: common.stats.todayActivity
- paragraph [ref=e220]: "0"
- img [ref=e222]
- generic [ref=e224]:
- generic [ref=e225]:
- heading "home.sections.recentSessions" [level=2] [ref=e226]
- button "common.actions.viewAll" [ref=e227] [cursor=pointer]
- generic [ref=e228]:
- img [ref=e229]
- heading "home.emptyState.noSessions.title" [level=3] [ref=e234]
- paragraph [ref=e235]: home.emptyState.noSessions.message
```

View File

@@ -0,0 +1,265 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- heading "settings.title" [level=1] [ref=e126]:
- img [ref=e127]
- text: settings.title
- paragraph [ref=e130]: settings.description
- generic [ref=e131]:
- heading "settings.sections.appearance" [level=2] [ref=e132]:
- img [ref=e133]
- text: settings.sections.appearance
- generic [ref=e136]:
- generic [ref=e137]:
- paragraph [ref=e138]: settings.appearance.theme
- paragraph [ref=e139]: settings.appearance.description
- generic [ref=e140]:
- button "settings.appearance.themeOptions.light" [ref=e141] [cursor=pointer]:
- img [ref=e142]
- text: settings.appearance.themeOptions.light
- button "settings.appearance.themeOptions.dark" [ref=e148] [cursor=pointer]:
- img [ref=e149]
- text: settings.appearance.themeOptions.dark
- button "settings.appearance.themeOptions.system" [ref=e151] [cursor=pointer]
- generic [ref=e152]:
- heading "settings.sections.language" [level=2] [ref=e153]:
- img [ref=e154]
- text: settings.sections.language
- generic [ref=e159]:
- generic [ref=e160]:
- paragraph [ref=e161]: settings.language.displayLanguage
- paragraph [ref=e162]: settings.language.chooseLanguage
- combobox "Select language" [ref=e163] [cursor=pointer]:
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e164]
- generic [ref=e166]:
- heading "settings.sections.cliTools" [level=2] [ref=e167]:
- img [ref=e168]
- text: settings.sections.cliTools
- paragraph [ref=e171]:
- text: settings.cliTools.description
- strong [ref=e172]: gemini
- generic [ref=e173]:
- generic [ref=e175] [cursor=pointer]:
- generic [ref=e176]:
- generic [ref=e177]:
- img [ref=e179]
- generic [ref=e182]:
- generic [ref=e183]:
- generic [ref=e184]: gemini
- generic [ref=e185]: settings.cliTools.default
- generic [ref=e186]: builtin
- paragraph [ref=e187]: gemini-2.5-pro
- generic [ref=e188]:
- button "settings.cliTools.enabled" [ref=e189]:
- img [ref=e190]
- text: settings.cliTools.enabled
- img [ref=e192]
- generic [ref=e194]:
- generic [ref=e195]: analysis
- generic [ref=e196]: debug
- generic [ref=e199] [cursor=pointer]:
- generic [ref=e200]:
- img [ref=e202]
- generic [ref=e205]:
- generic [ref=e206]:
- generic [ref=e207]: qwen
- generic [ref=e208]: builtin
- paragraph [ref=e209]: coder-model
- generic [ref=e210]:
- button "settings.cliTools.enabled" [ref=e211]:
- img [ref=e212]
- text: settings.cliTools.enabled
- img [ref=e214]
- generic [ref=e218] [cursor=pointer]:
- generic [ref=e219]:
- img [ref=e221]
- generic [ref=e224]:
- generic [ref=e225]:
- generic [ref=e226]: codex
- generic [ref=e227]: builtin
- paragraph [ref=e228]: gpt-5.2
- generic [ref=e229]:
- button "settings.cliTools.enabled" [ref=e230]:
- img [ref=e231]
- text: settings.cliTools.enabled
- img [ref=e233]
- generic [ref=e237] [cursor=pointer]:
- generic [ref=e238]:
- img [ref=e240]
- generic [ref=e243]:
- generic [ref=e244]:
- generic [ref=e245]: claude
- generic [ref=e246]: builtin
- paragraph [ref=e247]: sonnet
- generic [ref=e248]:
- button "settings.cliTools.enabled" [ref=e249]:
- img [ref=e250]
- text: settings.cliTools.enabled
- img [ref=e252]
- generic [ref=e254]:
- heading "settings.dataRefresh.title" [level=2] [ref=e255]:
- img [ref=e256]
- text: settings.dataRefresh.title
- generic [ref=e261]:
- generic [ref=e262]:
- generic [ref=e263]:
- paragraph [ref=e264]: settings.dataRefresh.autoRefresh
- paragraph [ref=e265]: settings.dataRefresh.autoRefreshDesc
- button "settings.dataRefresh.enabled" [ref=e266] [cursor=pointer]
- generic [ref=e267]:
- generic [ref=e268]:
- paragraph [ref=e269]: settings.dataRefresh.refreshInterval
- paragraph [ref=e270]: settings.dataRefresh.refreshIntervalDesc
- generic [ref=e271]:
- button "15s" [ref=e272] [cursor=pointer]
- button "30s" [ref=e273] [cursor=pointer]
- button "60s" [ref=e274] [cursor=pointer]
- button "120s" [ref=e275] [cursor=pointer]
- generic [ref=e276]:
- heading "settings.notifications.title" [level=2] [ref=e277]:
- img [ref=e278]
- text: settings.notifications.title
- generic [ref=e281]:
- generic [ref=e282]:
- generic [ref=e283]:
- paragraph [ref=e284]: settings.notifications.enableNotifications
- paragraph [ref=e285]: settings.notifications.enableNotificationsDesc
- button "settings.dataRefresh.enabled" [ref=e286] [cursor=pointer]
- generic [ref=e287]:
- generic [ref=e288]:
- paragraph [ref=e289]: settings.notifications.soundEffects
- paragraph [ref=e290]: settings.notifications.soundEffectsDesc
- button "settings.notifications.off" [ref=e291] [cursor=pointer]
- generic [ref=e292]:
- heading "settings.sections.display" [level=2] [ref=e293]:
- img [ref=e294]
- text: settings.sections.display
- generic [ref=e298]:
- generic [ref=e299]:
- paragraph [ref=e300]: settings.display.showCompletedTasks
- paragraph [ref=e301]: settings.display.showCompletedTasksDesc
- button "settings.display.show" [ref=e302] [cursor=pointer]
- generic [ref=e303]:
- generic [ref=e304]:
- heading "settings.sections.hooks" [level=2] [ref=e305]:
- img [ref=e306]
- text: settings.sections.hooks
- generic [ref=e312]: 0/0 cliHooks.stats.enabled
- generic [ref=e313]:
- img [ref=e314]
- textbox "cliHooks.filters.searchPlaceholder" [ref=e317]
- generic [ref=e323]:
- generic [ref=e324]:
- heading "settings.sections.rules" [level=2] [ref=e325]:
- img [ref=e326]
- text: settings.sections.rules
- generic [ref=e331]: 0/0 cliRules.stats.enabled
- generic [ref=e332]:
- img [ref=e333]
- textbox "cliRules.filters.searchPlaceholder" [ref=e336]
- generic [ref=e342]:
- heading "common.actions.reset" [level=2] [ref=e343]:
- img [ref=e344]
- text: common.actions.reset
- paragraph [ref=e347]: settings.reset.description
- button "common.actions.resetToDefaults" [ref=e348] [cursor=pointer]:
- img [ref=e349]
- text: common.actions.resetToDefaults
```

View File

@@ -0,0 +1,153 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- heading "skills.title" [level=1] [ref=e127]:
- img [ref=e128]
- text: skills.title
- paragraph [ref=e130]: skills.description
- generic [ref=e131]:
- button "common.actions.refresh" [disabled]:
- img
- text: common.actions.refresh
- button "skills.actions.install" [ref=e132] [cursor=pointer]:
- img [ref=e133]
- text: skills.actions.install
- generic [ref=e134]:
- generic [ref=e135]:
- generic [ref=e136]:
- img [ref=e137]
- generic [ref=e139]: "0"
- paragraph [ref=e140]: common.stats.totalSkills
- generic [ref=e141]:
- generic [ref=e142]:
- img [ref=e143]
- generic [ref=e145]: "0"
- paragraph [ref=e146]: skills.state.enabled
- generic [ref=e147]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: "0"
- paragraph [ref=e154]: skills.state.disabled
- generic [ref=e155]:
- generic [ref=e156]:
- img [ref=e157]
- generic [ref=e160]: "0"
- paragraph [ref=e161]: skills.card.category
- generic [ref=e162]:
- generic [ref=e163]:
- img [ref=e164]
- textbox "skills.filters.searchPlaceholder" [ref=e167]
- generic [ref=e168]:
- combobox [ref=e169] [cursor=pointer]:
- generic: skills.filters.all
- img [ref=e170]
- combobox [ref=e172] [cursor=pointer]:
- generic: skills.filters.allSources
- img [ref=e173]
- combobox [ref=e175] [cursor=pointer]:
- generic: skills.filters.all
- img [ref=e176]
- generic [ref=e178]:
- button "skills.filters.all (0)" [ref=e179] [cursor=pointer]
- button "skills.state.enabled (0)" [ref=e180] [cursor=pointer]:
- img [ref=e181]
- text: skills.state.enabled (0)
- button "skills.state.disabled (0)" [ref=e183] [cursor=pointer]:
- img [ref=e184]
- text: skills.state.disabled (0)
- button "skills.view.compact" [ref=e189] [cursor=pointer]
```

View File

@@ -0,0 +1,135 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- heading "memory.title" [level=1] [ref=e127]:
- img [ref=e128]
- text: memory.title
- paragraph [ref=e138]: memory.description
- generic [ref=e139]:
- button "common.actions.refresh" [ref=e140] [cursor=pointer]:
- img [ref=e141]
- text: common.actions.refresh
- button "memory.actions.add" [ref=e146] [cursor=pointer]:
- img [ref=e147]
- text: memory.actions.add
- generic [ref=e148]:
- generic [ref=e150]:
- img [ref=e152]
- generic [ref=e156]:
- generic [ref=e157]: "0"
- paragraph [ref=e158]: memory.stats.count
- generic [ref=e160]:
- img [ref=e162]
- generic [ref=e165]:
- generic [ref=e166]: "0"
- paragraph [ref=e167]: memory.stats.claudeMdCount
- generic [ref=e169]:
- img [ref=e171]
- generic [ref=e181]:
- generic [ref=e182]: 0 B
- paragraph [ref=e183]: memory.stats.totalSize
- generic [ref=e185]:
- img [ref=e186]
- textbox "memory.filters.search" [ref=e189]
- generic [ref=e190]:
- img [ref=e191]
- heading "memory.emptyState.title" [level=3] [ref=e201]
- paragraph [ref=e202]: memory.emptyState.message
- button "memory.emptyState.createFirst" [ref=e203] [cursor=pointer]:
- img [ref=e204]
- text: memory.emptyState.createFirst
```

View File

@@ -0,0 +1,265 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e21]
- button "common.aria.switchToDarkMode" [ref=e23] [cursor=pointer]:
- img [ref=e24]
- button "common.aria.userMenu" [ref=e27] [cursor=pointer]:
- img [ref=e28]
- generic [ref=e31]:
- navigation "Claude Code Workflow" [ref=e32]:
- navigation [ref=e33]:
- list [ref=e34]:
- listitem [ref=e35]:
- link "navigation.main.home" [ref=e36] [cursor=pointer]:
- /url: /
- img [ref=e37]
- generic [ref=e40]: navigation.main.home
- listitem [ref=e41]:
- link "navigation.main.sessions" [ref=e42] [cursor=pointer]:
- /url: /sessions
- img [ref=e43]
- generic [ref=e48]: navigation.main.sessions
- listitem [ref=e49]:
- link "navigation.main.liteTasks" [ref=e50] [cursor=pointer]:
- /url: /lite-tasks
- img [ref=e51]
- generic [ref=e53]: navigation.main.liteTasks
- listitem [ref=e54]:
- link "navigation.main.project" [ref=e55] [cursor=pointer]:
- /url: /project
- img [ref=e56]
- generic [ref=e61]: navigation.main.project
- listitem [ref=e62]:
- link "navigation.main.history" [ref=e63] [cursor=pointer]:
- /url: /history
- img [ref=e64]
- generic [ref=e67]: navigation.main.history
- listitem [ref=e68]:
- link "navigation.main.orchestrator" [ref=e69] [cursor=pointer]:
- /url: /orchestrator
- img [ref=e70]
- generic [ref=e74]: navigation.main.orchestrator
- listitem [ref=e75]:
- link "navigation.main.loops" [ref=e76] [cursor=pointer]:
- /url: /loops
- img [ref=e77]
- generic [ref=e82]: navigation.main.loops
- listitem [ref=e83]:
- link "navigation.main.issues" [ref=e84] [cursor=pointer]:
- /url: /issues
- img [ref=e85]
- generic [ref=e89]: navigation.main.issues
- listitem [ref=e90]:
- link "navigation.main.skills" [ref=e91] [cursor=pointer]:
- /url: /skills
- img [ref=e92]
- generic [ref=e98]: navigation.main.skills
- listitem [ref=e99]:
- link "navigation.main.commands" [ref=e100] [cursor=pointer]:
- /url: /commands
- img [ref=e101]
- generic [ref=e104]: navigation.main.commands
- listitem [ref=e105]:
- link "navigation.main.memory" [ref=e106] [cursor=pointer]:
- /url: /memory
- img [ref=e107]
- generic [ref=e117]: navigation.main.memory
- listitem [ref=e118]:
- link "navigation.main.settings" [ref=e119] [cursor=pointer]:
- /url: /settings
- img [ref=e120]
- generic [ref=e123]: navigation.main.settings
- listitem [ref=e124]:
- link "navigation.main.help" [ref=e125] [cursor=pointer]:
- /url: /help
- img [ref=e126]
- generic [ref=e130]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e132] [cursor=pointer]:
- img [ref=e133]
- generic [ref=e137]: navigation.sidebar.collapse
- main [ref=e138]:
- generic [ref=e139]:
- generic [ref=e140]:
- heading "settings.title" [level=1] [ref=e141]:
- img [ref=e142]
- text: settings.title
- paragraph [ref=e145]: settings.description
- generic [ref=e146]:
- heading "settings.sections.appearance" [level=2] [ref=e147]:
- img [ref=e148]
- text: settings.sections.appearance
- generic [ref=e151]:
- generic [ref=e152]:
- paragraph [ref=e153]: settings.appearance.theme
- paragraph [ref=e154]: settings.appearance.description
- generic [ref=e155]:
- button "settings.appearance.themeOptions.light" [ref=e156] [cursor=pointer]:
- img [ref=e157]
- text: settings.appearance.themeOptions.light
- button "settings.appearance.themeOptions.dark" [ref=e167] [cursor=pointer]:
- img [ref=e168]
- text: settings.appearance.themeOptions.dark
- button "settings.appearance.themeOptions.system" [ref=e170] [cursor=pointer]
- generic [ref=e171]:
- heading "settings.sections.language" [level=2] [ref=e172]:
- img [ref=e173]
- text: settings.sections.language
- generic [ref=e181]:
- generic [ref=e182]:
- paragraph [ref=e183]: settings.language.displayLanguage
- paragraph [ref=e184]: settings.language.chooseLanguage
- combobox "Select language" [ref=e185] [cursor=pointer]:
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e186]
- generic [ref=e188]:
- heading "settings.sections.cliTools" [level=2] [ref=e189]:
- img [ref=e190]
- text: settings.sections.cliTools
- paragraph [ref=e201]:
- text: settings.cliTools.description
- strong [ref=e202]: gemini
- generic [ref=e203]:
- generic [ref=e205] [cursor=pointer]:
- generic [ref=e206]:
- generic [ref=e207]:
- img [ref=e209]
- generic [ref=e220]:
- generic [ref=e221]:
- generic [ref=e222]: gemini
- generic [ref=e223]: settings.cliTools.default
- generic [ref=e224]: builtin
- paragraph [ref=e225]: gemini-2.5-pro
- generic [ref=e226]:
- button "settings.cliTools.enabled" [ref=e227]:
- img [ref=e228]
- text: settings.cliTools.enabled
- img [ref=e230]
- generic [ref=e232]:
- generic [ref=e233]: analysis
- generic [ref=e234]: debug
- generic [ref=e237] [cursor=pointer]:
- generic [ref=e238]:
- img [ref=e240]
- generic [ref=e251]:
- generic [ref=e252]:
- generic [ref=e253]: qwen
- generic [ref=e254]: builtin
- paragraph [ref=e255]: coder-model
- generic [ref=e256]:
- button "settings.cliTools.enabled" [ref=e257]:
- img [ref=e258]
- text: settings.cliTools.enabled
- img [ref=e260]
- generic [ref=e264] [cursor=pointer]:
- generic [ref=e265]:
- img [ref=e267]
- generic [ref=e278]:
- generic [ref=e279]:
- generic [ref=e280]: codex
- generic [ref=e281]: builtin
- paragraph [ref=e282]: gpt-5.2
- generic [ref=e283]:
- button "settings.cliTools.enabled" [ref=e284]:
- img [ref=e285]
- text: settings.cliTools.enabled
- img [ref=e287]
- generic [ref=e291] [cursor=pointer]:
- generic [ref=e292]:
- img [ref=e294]
- generic [ref=e305]:
- generic [ref=e306]:
- generic [ref=e307]: claude
- generic [ref=e308]: builtin
- paragraph [ref=e309]: sonnet
- generic [ref=e310]:
- button "settings.cliTools.enabled" [ref=e311]:
- img [ref=e312]
- text: settings.cliTools.enabled
- img [ref=e314]
- generic [ref=e316]:
- heading "settings.dataRefresh.title" [level=2] [ref=e317]:
- img [ref=e318]
- text: settings.dataRefresh.title
- generic [ref=e323]:
- generic [ref=e324]:
- generic [ref=e325]:
- paragraph [ref=e326]: settings.dataRefresh.autoRefresh
- paragraph [ref=e327]: settings.dataRefresh.autoRefreshDesc
- button "settings.dataRefresh.enabled" [ref=e328] [cursor=pointer]
- generic [ref=e329]:
- generic [ref=e330]:
- paragraph [ref=e331]: settings.dataRefresh.refreshInterval
- paragraph [ref=e332]: settings.dataRefresh.refreshIntervalDesc
- generic [ref=e333]:
- button "15s" [ref=e334] [cursor=pointer]
- button "30s" [ref=e335] [cursor=pointer]
- button "60s" [ref=e336] [cursor=pointer]
- button "120s" [ref=e337] [cursor=pointer]
- generic [ref=e338]:
- heading "settings.notifications.title" [level=2] [ref=e339]:
- img [ref=e340]
- text: settings.notifications.title
- generic [ref=e343]:
- generic [ref=e344]:
- generic [ref=e345]:
- paragraph [ref=e346]: settings.notifications.enableNotifications
- paragraph [ref=e347]: settings.notifications.enableNotificationsDesc
- button "settings.dataRefresh.enabled" [ref=e348] [cursor=pointer]
- generic [ref=e349]:
- generic [ref=e350]:
- paragraph [ref=e351]: settings.notifications.soundEffects
- paragraph [ref=e352]: settings.notifications.soundEffectsDesc
- button "settings.notifications.off" [ref=e353] [cursor=pointer]
- generic [ref=e354]:
- heading "settings.sections.display" [level=2] [ref=e355]:
- img [ref=e356]
- text: settings.sections.display
- generic [ref=e360]:
- generic [ref=e361]:
- paragraph [ref=e362]: settings.display.showCompletedTasks
- paragraph [ref=e363]: settings.display.showCompletedTasksDesc
- button "settings.display.show" [ref=e364] [cursor=pointer]
- generic [ref=e365]:
- generic [ref=e366]:
- heading "settings.sections.hooks" [level=2] [ref=e367]:
- img [ref=e368]
- text: settings.sections.hooks
- generic [ref=e375]: 0/0 cliHooks.stats.enabled
- generic [ref=e376]:
- img [ref=e377]
- textbox "cliHooks.filters.searchPlaceholder" [ref=e380]
- generic [ref=e386]:
- generic [ref=e387]:
- heading "settings.sections.rules" [level=2] [ref=e388]:
- img [ref=e389]
- text: settings.sections.rules
- generic [ref=e396]: 0/0 cliRules.stats.enabled
- generic [ref=e397]:
- img [ref=e398]
- textbox "cliRules.filters.searchPlaceholder" [ref=e401]
- generic [ref=e407]:
- heading "common.actions.reset" [level=2] [ref=e408]:
- img [ref=e409]
- text: common.actions.reset
- paragraph [ref=e412]: settings.reset.description
- button "common.actions.resetToDefaults" [ref=e413] [cursor=pointer]:
- img [ref=e414]
- text: common.actions.resetToDefaults
```

View File

@@ -0,0 +1,183 @@
# Page snapshot
```yaml
- generic:
- generic:
- generic:
- banner:
- generic:
- link:
- /url: /
- img
- generic: navigation.header.brand
- generic:
- combobox [expanded]:
- img
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img
- button:
- img
- generic:
- button:
- img
- generic:
- navigation:
- navigation:
- list:
- listitem:
- link:
- /url: /
- img
- generic: navigation.main.home
- listitem:
- link:
- /url: /sessions
- img
- generic: navigation.main.sessions
- listitem:
- link:
- /url: /lite-tasks
- img
- generic: navigation.main.liteTasks
- listitem:
- link:
- /url: /project
- img
- generic: navigation.main.project
- listitem:
- link:
- /url: /history
- img
- generic: navigation.main.history
- listitem:
- link:
- /url: /orchestrator
- img
- generic: navigation.main.orchestrator
- listitem:
- link:
- /url: /loops
- img
- generic: navigation.main.loops
- listitem:
- link:
- /url: /issues
- img
- generic: navigation.main.issues
- listitem:
- link:
- /url: /skills
- img
- generic: navigation.main.skills
- listitem:
- link:
- /url: /commands
- img
- generic: navigation.main.commands
- listitem:
- link:
- /url: /memory
- img
- generic: navigation.main.memory
- listitem:
- link:
- /url: /settings
- img
- generic: navigation.main.settings
- listitem:
- link:
- /url: /help
- img
- generic: navigation.main.help
- generic:
- button:
- img
- generic: navigation.sidebar.collapse
- main:
- generic:
- generic:
- generic:
- heading [level=1]: home.title
- paragraph: home.description
- button:
- img
- text: common.actions.refresh
- generic:
- heading [level=2]: home.sections.statistics
- generic:
- generic:
- generic:
- generic:
- generic:
- paragraph: home.stats.activeSessions
- generic:
- paragraph: "0"
- generic:
- img
- generic:
- generic:
- generic:
- generic:
- paragraph: home.stats.totalTasks
- generic:
- paragraph: "0"
- generic:
- img
- generic:
- generic:
- generic:
- generic:
- paragraph: home.stats.completedTasks
- generic:
- paragraph: "0"
- generic:
- img
- generic:
- generic:
- generic:
- generic:
- paragraph: home.stats.pendingTasks
- generic:
- paragraph: "0"
- generic:
- img
- generic:
- generic:
- generic:
- generic:
- paragraph: common.status.failed
- generic:
- paragraph: "0"
- generic:
- img
- generic:
- generic:
- generic:
- generic:
- paragraph: common.stats.todayActivity
- generic:
- paragraph: "0"
- generic:
- img
- generic:
- generic:
- heading [level=2]: home.sections.recentSessions
- button: common.actions.viewAll
- generic:
- img
- heading [level=3]: home.emptyState.noSessions.title
- paragraph: home.emptyState.noSessions.message
- listbox [ref=e1]:
- option "🇺🇸 English" [ref=e2]:
- generic [ref=e5]:
- generic [ref=e6]: 🇺🇸
- generic [ref=e7]: English
- option "🇨🇳 中文" [active] [selected] [ref=e8]:
- img [ref=e11]
- generic [ref=e14]:
- generic [ref=e15]: 🇨🇳
- generic [ref=e16]: 中文
```

View File

@@ -0,0 +1,144 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33] [cursor=pointer]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39] [cursor=pointer]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44] [cursor=pointer]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49] [cursor=pointer]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57] [cursor=pointer]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63] [cursor=pointer]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70] [cursor=pointer]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78] [cursor=pointer]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83] [cursor=pointer]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88] [cursor=pointer]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93] [cursor=pointer]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106] [cursor=pointer]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112] [cursor=pointer]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- heading "home.title" [level=1] [ref=e127]
- paragraph [ref=e128]: home.description
- button "common.actions.refresh" [ref=e129] [cursor=pointer]:
- img [ref=e130]
- text: common.actions.refresh
- generic [ref=e135]:
- heading "home.sections.statistics" [level=2] [ref=e136]
- generic [ref=e137]:
- generic [ref=e140]:
- generic [ref=e141]:
- paragraph [ref=e142]: home.stats.activeSessions
- paragraph [ref=e144]: "0"
- img [ref=e146]
- generic [ref=e150]:
- generic [ref=e151]:
- paragraph [ref=e152]: home.stats.totalTasks
- paragraph [ref=e154]: "0"
- img [ref=e156]
- generic [ref=e161]:
- generic [ref=e162]:
- paragraph [ref=e163]: home.stats.completedTasks
- paragraph [ref=e165]: "0"
- img [ref=e167]
- generic [ref=e172]:
- generic [ref=e173]:
- paragraph [ref=e174]: home.stats.pendingTasks
- paragraph [ref=e176]: "0"
- img [ref=e178]
- generic [ref=e183]:
- generic [ref=e184]:
- paragraph [ref=e185]: common.status.failed
- paragraph [ref=e187]: "0"
- img [ref=e189]
- generic [ref=e195]:
- generic [ref=e196]:
- paragraph [ref=e197]: common.stats.todayActivity
- paragraph [ref=e199]: "0"
- img [ref=e201]
- generic [ref=e203]:
- generic [ref=e204]:
- heading "home.sections.recentSessions" [level=2] [ref=e205]
- button "common.actions.viewAll" [ref=e206] [cursor=pointer]
- generic [ref=e207]:
- img [ref=e208]
- heading "home.emptyState.noSessions.title" [level=3] [ref=e210]
- paragraph [ref=e211]: home.emptyState.noSessions.message
```

View File

@@ -0,0 +1,128 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e21]
- button "common.aria.switchToDarkMode" [ref=e23] [cursor=pointer]:
- img [ref=e24]
- button "common.aria.userMenu" [ref=e27] [cursor=pointer]:
- img [ref=e28]
- generic [ref=e31]:
- navigation "Claude Code Workflow" [ref=e32]:
- navigation [ref=e33]:
- list [ref=e34]:
- listitem [ref=e35]:
- link "navigation.main.home" [ref=e36] [cursor=pointer]:
- /url: /
- img [ref=e37]
- generic [ref=e40]: navigation.main.home
- listitem [ref=e41]:
- link "navigation.main.sessions" [ref=e42] [cursor=pointer]:
- /url: /sessions
- img [ref=e43]
- generic [ref=e48]: navigation.main.sessions
- listitem [ref=e49]:
- link "navigation.main.liteTasks" [ref=e50] [cursor=pointer]:
- /url: /lite-tasks
- img [ref=e51]
- generic [ref=e53]: navigation.main.liteTasks
- listitem [ref=e54]:
- link "navigation.main.project" [ref=e55] [cursor=pointer]:
- /url: /project
- img [ref=e56]
- generic [ref=e61]: navigation.main.project
- listitem [ref=e62]:
- link "navigation.main.history" [ref=e63] [cursor=pointer]:
- /url: /history
- img [ref=e64]
- generic [ref=e67]: navigation.main.history
- listitem [ref=e68]:
- link "navigation.main.orchestrator" [ref=e69] [cursor=pointer]:
- /url: /orchestrator
- img [ref=e70]
- generic [ref=e74]: navigation.main.orchestrator
- listitem [ref=e75]:
- link "navigation.main.loops" [ref=e76] [cursor=pointer]:
- /url: /loops
- img [ref=e77]
- generic [ref=e82]: navigation.main.loops
- listitem [ref=e83]:
- link "navigation.main.issues" [ref=e84] [cursor=pointer]:
- /url: /issues
- img [ref=e85]
- generic [ref=e89]: navigation.main.issues
- listitem [ref=e90]:
- link "navigation.main.skills" [ref=e91] [cursor=pointer]:
- /url: /skills
- img [ref=e92]
- generic [ref=e98]: navigation.main.skills
- listitem [ref=e99]:
- link "navigation.main.commands" [ref=e100] [cursor=pointer]:
- /url: /commands
- img [ref=e101]
- generic [ref=e104]: navigation.main.commands
- listitem [ref=e105]:
- link "navigation.main.memory" [ref=e106] [cursor=pointer]:
- /url: /memory
- img [ref=e107]
- generic [ref=e117]: navigation.main.memory
- listitem [ref=e118]:
- link "navigation.main.settings" [ref=e119] [cursor=pointer]:
- /url: /settings
- img [ref=e120]
- generic [ref=e123]: navigation.main.settings
- listitem [ref=e124]:
- link "navigation.main.help" [ref=e125] [cursor=pointer]:
- /url: /help
- img [ref=e126]
- generic [ref=e130]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e132] [cursor=pointer]:
- img [ref=e133]
- generic [ref=e137]: navigation.sidebar.collapse
- main [ref=e138]:
- generic [ref=e139]:
- generic [ref=e140]:
- generic [ref=e141]:
- heading "memory.title" [level=1] [ref=e142]:
- img [ref=e143]
- text: memory.title
- paragraph [ref=e153]: memory.description
- generic [ref=e154]:
- button "common.actions.refresh" [disabled]:
- img
- text: common.actions.refresh
- button "memory.actions.add" [ref=e155] [cursor=pointer]:
- img [ref=e156]
- text: memory.actions.add
- generic [ref=e159]:
- generic [ref=e161]:
- img [ref=e163]
- generic [ref=e167]:
- generic [ref=e168]: "0"
- paragraph [ref=e169]: memory.stats.count
- generic [ref=e171]:
- img [ref=e173]
- generic [ref=e179]:
- generic [ref=e180]: "0"
- paragraph [ref=e181]: memory.stats.claudeMdCount
- generic [ref=e183]:
- img [ref=e185]
- generic [ref=e195]:
- generic [ref=e196]: 0 B
- paragraph [ref=e197]: memory.stats.totalSize
- generic [ref=e199]:
- img [ref=e200]
- textbox "memory.filters.search" [ref=e203]
```

View File

@@ -0,0 +1,153 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33] [cursor=pointer]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39] [cursor=pointer]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44] [cursor=pointer]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49] [cursor=pointer]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57] [cursor=pointer]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63] [cursor=pointer]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70] [cursor=pointer]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78] [cursor=pointer]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83] [cursor=pointer]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88] [cursor=pointer]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93] [cursor=pointer]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106] [cursor=pointer]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112] [cursor=pointer]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- heading "skills.title" [level=1] [ref=e127]:
- img [ref=e128]
- text: skills.title
- paragraph [ref=e130]: skills.description
- generic [ref=e131]:
- button "common.actions.refresh" [disabled]:
- img
- text: common.actions.refresh
- button "skills.actions.install" [ref=e132] [cursor=pointer]:
- img [ref=e133]
- text: skills.actions.install
- generic [ref=e134]:
- generic [ref=e135]:
- generic [ref=e136]:
- img [ref=e137]
- generic [ref=e139]: "0"
- paragraph [ref=e140]: common.stats.totalSkills
- generic [ref=e141]:
- generic [ref=e142]:
- img [ref=e143]
- generic [ref=e145]: "0"
- paragraph [ref=e146]: skills.state.enabled
- generic [ref=e147]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: "0"
- paragraph [ref=e154]: skills.state.disabled
- generic [ref=e155]:
- generic [ref=e156]:
- img [ref=e157]
- generic [ref=e160]: "0"
- paragraph [ref=e161]: skills.card.category
- generic [ref=e162]:
- generic [ref=e163]:
- img [ref=e164]
- textbox "skills.filters.searchPlaceholder" [ref=e167]
- generic [ref=e168]:
- combobox [ref=e169] [cursor=pointer]:
- generic: skills.filters.all
- img [ref=e170]
- combobox [ref=e172] [cursor=pointer]:
- generic: skills.filters.allSources
- img [ref=e173]
- combobox [ref=e175] [cursor=pointer]:
- generic: skills.filters.all
- img [ref=e176]
- generic [ref=e178]:
- button "skills.filters.all (0)" [ref=e179] [cursor=pointer]
- button "skills.state.enabled (0)" [ref=e180] [cursor=pointer]:
- img [ref=e181]
- text: skills.state.enabled (0)
- button "skills.state.disabled (0)" [ref=e183] [cursor=pointer]:
- img [ref=e184]
- text: skills.state.disabled (0)
- button "skills.view.compact" [ref=e189] [cursor=pointer]
```

View File

@@ -0,0 +1,144 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- generic [ref=e126]:
- heading "home.title" [level=1] [ref=e127]
- paragraph [ref=e128]: home.description
- button "common.actions.refresh" [ref=e129] [cursor=pointer]:
- img [ref=e130]
- text: common.actions.refresh
- generic [ref=e135]:
- heading "home.sections.statistics" [level=2] [ref=e136]
- generic [ref=e137]:
- generic [ref=e140]:
- generic [ref=e141]:
- paragraph [ref=e142]: home.stats.activeSessions
- paragraph [ref=e144]: "0"
- img [ref=e146]
- generic [ref=e150]:
- generic [ref=e151]:
- paragraph [ref=e152]: home.stats.totalTasks
- paragraph [ref=e154]: "0"
- img [ref=e156]
- generic [ref=e161]:
- generic [ref=e162]:
- paragraph [ref=e163]: home.stats.completedTasks
- paragraph [ref=e165]: "0"
- img [ref=e167]
- generic [ref=e172]:
- generic [ref=e173]:
- paragraph [ref=e174]: home.stats.pendingTasks
- paragraph [ref=e176]: "0"
- img [ref=e178]
- generic [ref=e183]:
- generic [ref=e184]:
- paragraph [ref=e185]: common.status.failed
- paragraph [ref=e187]: "0"
- img [ref=e189]
- generic [ref=e195]:
- generic [ref=e196]:
- paragraph [ref=e197]: common.stats.todayActivity
- paragraph [ref=e199]: "0"
- img [ref=e201]
- generic [ref=e203]:
- generic [ref=e204]:
- heading "home.sections.recentSessions" [level=2] [ref=e205]
- button "common.actions.viewAll" [ref=e206] [cursor=pointer]
- generic [ref=e207]:
- img [ref=e208]
- heading "home.emptyState.noSessions.title" [level=3] [ref=e210]
- paragraph [ref=e211]: home.emptyState.noSessions.message
```

View File

@@ -0,0 +1,153 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e21]
- button "common.aria.switchToDarkMode" [ref=e23] [cursor=pointer]:
- img [ref=e24]
- button "common.aria.userMenu" [ref=e27] [cursor=pointer]:
- img [ref=e28]
- generic [ref=e31]:
- navigation "Claude Code Workflow" [ref=e32]:
- navigation [ref=e33]:
- list [ref=e34]:
- listitem [ref=e35]:
- link "navigation.main.home" [ref=e36] [cursor=pointer]:
- /url: /
- img [ref=e37]
- generic [ref=e40]: navigation.main.home
- listitem [ref=e41]:
- link "navigation.main.sessions" [ref=e42] [cursor=pointer]:
- /url: /sessions
- img [ref=e43]
- generic [ref=e48]: navigation.main.sessions
- listitem [ref=e49]:
- link "navigation.main.liteTasks" [ref=e50] [cursor=pointer]:
- /url: /lite-tasks
- img [ref=e51]
- generic [ref=e53]: navigation.main.liteTasks
- listitem [ref=e54]:
- link "navigation.main.project" [ref=e55] [cursor=pointer]:
- /url: /project
- img [ref=e56]
- generic [ref=e61]: navigation.main.project
- listitem [ref=e62]:
- link "navigation.main.history" [ref=e63] [cursor=pointer]:
- /url: /history
- img [ref=e64]
- generic [ref=e67]: navigation.main.history
- listitem [ref=e68]:
- link "navigation.main.orchestrator" [ref=e69] [cursor=pointer]:
- /url: /orchestrator
- img [ref=e70]
- generic [ref=e74]: navigation.main.orchestrator
- listitem [ref=e75]:
- link "navigation.main.loops" [ref=e76] [cursor=pointer]:
- /url: /loops
- img [ref=e77]
- generic [ref=e82]: navigation.main.loops
- listitem [ref=e83]:
- link "navigation.main.issues" [ref=e84] [cursor=pointer]:
- /url: /issues
- img [ref=e85]
- generic [ref=e89]: navigation.main.issues
- listitem [ref=e90]:
- link "navigation.main.skills" [ref=e91] [cursor=pointer]:
- /url: /skills
- img [ref=e92]
- generic [ref=e98]: navigation.main.skills
- listitem [ref=e99]:
- link "navigation.main.commands" [ref=e100] [cursor=pointer]:
- /url: /commands
- img [ref=e101]
- generic [ref=e104]: navigation.main.commands
- listitem [ref=e105]:
- link "navigation.main.memory" [ref=e106] [cursor=pointer]:
- /url: /memory
- img [ref=e107]
- generic [ref=e117]: navigation.main.memory
- listitem [ref=e118]:
- link "navigation.main.settings" [ref=e119] [cursor=pointer]:
- /url: /settings
- img [ref=e120]
- generic [ref=e123]: navigation.main.settings
- listitem [ref=e124]:
- link "navigation.main.help" [ref=e125] [cursor=pointer]:
- /url: /help
- img [ref=e126]
- generic [ref=e130]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e132] [cursor=pointer]:
- img [ref=e133]
- generic [ref=e137]: navigation.sidebar.collapse
- main [ref=e138]:
- generic [ref=e139]:
- generic [ref=e140]:
- generic [ref=e141]:
- heading "skills.title" [level=1] [ref=e142]:
- img [ref=e143]
- text: skills.title
- paragraph [ref=e149]: skills.description
- generic [ref=e150]:
- button "common.actions.refresh" [disabled]:
- img
- text: common.actions.refresh
- button "skills.actions.install" [ref=e151] [cursor=pointer]:
- img [ref=e152]
- text: skills.actions.install
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- img [ref=e158]
- generic [ref=e164]: "0"
- paragraph [ref=e165]: common.stats.totalSkills
- generic [ref=e166]:
- generic [ref=e167]:
- img [ref=e168]
- generic [ref=e171]: "0"
- paragraph [ref=e172]: skills.state.enabled
- generic [ref=e173]:
- generic [ref=e174]:
- img [ref=e175]
- generic [ref=e180]: "0"
- paragraph [ref=e181]: skills.state.disabled
- generic [ref=e182]:
- generic [ref=e183]:
- img [ref=e184]
- generic [ref=e187]: "0"
- paragraph [ref=e188]: skills.card.category
- generic [ref=e189]:
- generic [ref=e190]:
- img [ref=e191]
- textbox "skills.filters.searchPlaceholder" [ref=e194]
- generic [ref=e195]:
- combobox [ref=e196] [cursor=pointer]:
- generic: skills.filters.all
- img [ref=e197]
- combobox [ref=e199] [cursor=pointer]:
- generic: skills.filters.allSources
- img [ref=e200]
- combobox [ref=e202] [cursor=pointer]:
- generic: skills.filters.all
- img [ref=e203]
- generic [ref=e205]:
- button "skills.filters.all (0)" [ref=e206] [cursor=pointer]
- button "skills.state.enabled (0)" [ref=e207] [cursor=pointer]:
- img [ref=e208]
- text: skills.state.enabled (0)
- button "skills.state.disabled (0)" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- text: skills.state.disabled (0)
- button "skills.view.compact" [ref=e218] [cursor=pointer]
```

View File

@@ -0,0 +1,265 @@
# Page snapshot
```yaml
- generic [ref=e3]:
- banner [ref=e4]:
- link "navigation.header.brand" [ref=e6] [cursor=pointer]:
- /url: /
- img [ref=e7]
- generic [ref=e11]: navigation.header.brand
- generic [ref=e12]:
- combobox "Select language" [active] [ref=e13] [cursor=pointer]:
- img [ref=e14]
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e18]
- button "common.aria.switchToDarkMode" [ref=e20] [cursor=pointer]:
- img [ref=e21]
- button "common.aria.userMenu" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- generic [ref=e28]:
- navigation "Claude Code Workflow" [ref=e29]:
- navigation [ref=e30]:
- list [ref=e31]:
- listitem [ref=e32]:
- link "navigation.main.home" [ref=e33] [cursor=pointer]:
- /url: /
- img [ref=e34]
- generic [ref=e37]: navigation.main.home
- listitem [ref=e38]:
- link "navigation.main.sessions" [ref=e39] [cursor=pointer]:
- /url: /sessions
- img [ref=e40]
- generic [ref=e42]: navigation.main.sessions
- listitem [ref=e43]:
- link "navigation.main.liteTasks" [ref=e44] [cursor=pointer]:
- /url: /lite-tasks
- img [ref=e45]
- generic [ref=e47]: navigation.main.liteTasks
- listitem [ref=e48]:
- link "navigation.main.project" [ref=e49] [cursor=pointer]:
- /url: /project
- img [ref=e50]
- generic [ref=e55]: navigation.main.project
- listitem [ref=e56]:
- link "navigation.main.history" [ref=e57] [cursor=pointer]:
- /url: /history
- img [ref=e58]
- generic [ref=e61]: navigation.main.history
- listitem [ref=e62]:
- link "navigation.main.orchestrator" [ref=e63] [cursor=pointer]:
- /url: /orchestrator
- img [ref=e64]
- generic [ref=e68]: navigation.main.orchestrator
- listitem [ref=e69]:
- link "navigation.main.loops" [ref=e70] [cursor=pointer]:
- /url: /loops
- img [ref=e71]
- generic [ref=e76]: navigation.main.loops
- listitem [ref=e77]:
- link "navigation.main.issues" [ref=e78] [cursor=pointer]:
- /url: /issues
- img [ref=e79]
- generic [ref=e81]: navigation.main.issues
- listitem [ref=e82]:
- link "navigation.main.skills" [ref=e83] [cursor=pointer]:
- /url: /skills
- img [ref=e84]
- generic [ref=e86]: navigation.main.skills
- listitem [ref=e87]:
- link "navigation.main.commands" [ref=e88] [cursor=pointer]:
- /url: /commands
- img [ref=e89]
- generic [ref=e91]: navigation.main.commands
- listitem [ref=e92]:
- link "navigation.main.memory" [ref=e93] [cursor=pointer]:
- /url: /memory
- img [ref=e94]
- generic [ref=e104]: navigation.main.memory
- listitem [ref=e105]:
- link "navigation.main.settings" [ref=e106] [cursor=pointer]:
- /url: /settings
- img [ref=e107]
- generic [ref=e110]: navigation.main.settings
- listitem [ref=e111]:
- link "navigation.main.help" [ref=e112] [cursor=pointer]:
- /url: /help
- img [ref=e113]
- generic [ref=e116]: navigation.main.help
- button "navigation.sidebar.collapseAria" [ref=e118] [cursor=pointer]:
- img [ref=e119]
- generic [ref=e122]: navigation.sidebar.collapse
- main [ref=e123]:
- generic [ref=e124]:
- generic [ref=e125]:
- heading "settings.title" [level=1] [ref=e126]:
- img [ref=e127]
- text: settings.title
- paragraph [ref=e130]: settings.description
- generic [ref=e131]:
- heading "settings.sections.appearance" [level=2] [ref=e132]:
- img [ref=e133]
- text: settings.sections.appearance
- generic [ref=e136]:
- generic [ref=e137]:
- paragraph [ref=e138]: settings.appearance.theme
- paragraph [ref=e139]: settings.appearance.description
- generic [ref=e140]:
- button "settings.appearance.themeOptions.light" [ref=e141] [cursor=pointer]:
- img [ref=e142]
- text: settings.appearance.themeOptions.light
- button "settings.appearance.themeOptions.dark" [ref=e148] [cursor=pointer]:
- img [ref=e149]
- text: settings.appearance.themeOptions.dark
- button "settings.appearance.themeOptions.system" [ref=e151] [cursor=pointer]
- generic [ref=e152]:
- heading "settings.sections.language" [level=2] [ref=e153]:
- img [ref=e154]
- text: settings.sections.language
- generic [ref=e159]:
- generic [ref=e160]:
- paragraph [ref=e161]: settings.language.displayLanguage
- paragraph [ref=e162]: settings.language.chooseLanguage
- combobox "Select language" [ref=e163] [cursor=pointer]:
- generic:
- generic:
- generic: 🇨🇳
- generic: 中文
- img [ref=e164]
- generic [ref=e166]:
- heading "settings.sections.cliTools" [level=2] [ref=e167]:
- img [ref=e168]
- text: settings.sections.cliTools
- paragraph [ref=e171]:
- text: settings.cliTools.description
- strong [ref=e172]: gemini
- generic [ref=e173]:
- generic [ref=e175] [cursor=pointer]:
- generic [ref=e176]:
- generic [ref=e177]:
- img [ref=e179]
- generic [ref=e182]:
- generic [ref=e183]:
- generic [ref=e184]: gemini
- generic [ref=e185]: settings.cliTools.default
- generic [ref=e186]: builtin
- paragraph [ref=e187]: gemini-2.5-pro
- generic [ref=e188]:
- button "settings.cliTools.enabled" [ref=e189]:
- img [ref=e190]
- text: settings.cliTools.enabled
- img [ref=e192]
- generic [ref=e194]:
- generic [ref=e195]: analysis
- generic [ref=e196]: debug
- generic [ref=e199] [cursor=pointer]:
- generic [ref=e200]:
- img [ref=e202]
- generic [ref=e205]:
- generic [ref=e206]:
- generic [ref=e207]: qwen
- generic [ref=e208]: builtin
- paragraph [ref=e209]: coder-model
- generic [ref=e210]:
- button "settings.cliTools.enabled" [ref=e211]:
- img [ref=e212]
- text: settings.cliTools.enabled
- img [ref=e214]
- generic [ref=e218] [cursor=pointer]:
- generic [ref=e219]:
- img [ref=e221]
- generic [ref=e224]:
- generic [ref=e225]:
- generic [ref=e226]: codex
- generic [ref=e227]: builtin
- paragraph [ref=e228]: gpt-5.2
- generic [ref=e229]:
- button "settings.cliTools.enabled" [ref=e230]:
- img [ref=e231]
- text: settings.cliTools.enabled
- img [ref=e233]
- generic [ref=e237] [cursor=pointer]:
- generic [ref=e238]:
- img [ref=e240]
- generic [ref=e243]:
- generic [ref=e244]:
- generic [ref=e245]: claude
- generic [ref=e246]: builtin
- paragraph [ref=e247]: sonnet
- generic [ref=e248]:
- button "settings.cliTools.enabled" [ref=e249]:
- img [ref=e250]
- text: settings.cliTools.enabled
- img [ref=e252]
- generic [ref=e254]:
- heading "settings.dataRefresh.title" [level=2] [ref=e255]:
- img [ref=e256]
- text: settings.dataRefresh.title
- generic [ref=e261]:
- generic [ref=e262]:
- generic [ref=e263]:
- paragraph [ref=e264]: settings.dataRefresh.autoRefresh
- paragraph [ref=e265]: settings.dataRefresh.autoRefreshDesc
- button "settings.dataRefresh.enabled" [ref=e266] [cursor=pointer]
- generic [ref=e267]:
- generic [ref=e268]:
- paragraph [ref=e269]: settings.dataRefresh.refreshInterval
- paragraph [ref=e270]: settings.dataRefresh.refreshIntervalDesc
- generic [ref=e271]:
- button "15s" [ref=e272] [cursor=pointer]
- button "30s" [ref=e273] [cursor=pointer]
- button "60s" [ref=e274] [cursor=pointer]
- button "120s" [ref=e275] [cursor=pointer]
- generic [ref=e276]:
- heading "settings.notifications.title" [level=2] [ref=e277]:
- img [ref=e278]
- text: settings.notifications.title
- generic [ref=e281]:
- generic [ref=e282]:
- generic [ref=e283]:
- paragraph [ref=e284]: settings.notifications.enableNotifications
- paragraph [ref=e285]: settings.notifications.enableNotificationsDesc
- button "settings.dataRefresh.enabled" [ref=e286] [cursor=pointer]
- generic [ref=e287]:
- generic [ref=e288]:
- paragraph [ref=e289]: settings.notifications.soundEffects
- paragraph [ref=e290]: settings.notifications.soundEffectsDesc
- button "settings.notifications.off" [ref=e291] [cursor=pointer]
- generic [ref=e292]:
- heading "settings.sections.display" [level=2] [ref=e293]:
- img [ref=e294]
- text: settings.sections.display
- generic [ref=e298]:
- generic [ref=e299]:
- paragraph [ref=e300]: settings.display.showCompletedTasks
- paragraph [ref=e301]: settings.display.showCompletedTasksDesc
- button "settings.display.show" [ref=e302] [cursor=pointer]
- generic [ref=e303]:
- generic [ref=e304]:
- heading "settings.sections.hooks" [level=2] [ref=e305]:
- img [ref=e306]
- text: settings.sections.hooks
- generic [ref=e312]: 0/0 cliHooks.stats.enabled
- generic [ref=e313]:
- img [ref=e314]
- textbox "cliHooks.filters.searchPlaceholder" [ref=e317]
- generic [ref=e323]:
- generic [ref=e324]:
- heading "settings.sections.rules" [level=2] [ref=e325]:
- img [ref=e326]
- text: settings.sections.rules
- generic [ref=e331]: 0/0 cliRules.stats.enabled
- generic [ref=e332]:
- img [ref=e333]
- textbox "cliRules.filters.searchPlaceholder" [ref=e336]
- generic [ref=e342]:
- heading "common.actions.reset" [level=2] [ref=e343]:
- img [ref=e344]
- text: common.actions.reset
- paragraph [ref=e347]: settings.reset.description
- button "common.actions.resetToDefaults" [ref=e348] [cursor=pointer]:
- img [ref=e349]
- text: common.actions.resetToDefaults
```

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,34 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { browserName: 'firefox' },
},
{
name: 'webkit',
use: { browserName: 'webkit' },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});

View File

@@ -0,0 +1,197 @@
// ========================================
// Translation Validation Script
// ========================================
// Checks that en/ and zh/ translation files have matching keys
import { readFileSync, readdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
interface TranslationEntry {
key: string;
path: string[];
}
interface ValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
missingKeys: {
en: string[];
zh: string[];
};
extraKeys: {
en: string[];
zh: string[];
};
}
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const LOCALES_DIR = join(__dirname, '../src/locales');
const SUPPORTED_LOCALES = ['en', 'zh'] as const;
/**
* Recursively get all translation keys from a nested object
*/
function flattenObject(obj: Record<string, unknown>, prefix = ''): string[] {
const keys: string[] = [];
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
keys.push(...flattenObject(value as Record<string, unknown>, fullKey));
} else if (typeof value === 'string') {
keys.push(fullKey);
}
}
return keys;
}
/**
* Load and parse a JSON file
*/
function loadJsonFile(filePath: string): Record<string, unknown> {
try {
const content = readFileSync(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error(`Error loading ${filePath}:`, error);
return {};
}
}
/**
* Get all translation keys for a locale
*/
function getLocaleKeys(locale: string): string[] {
const localeDir = join(LOCALES_DIR, locale);
const keys: string[] = [];
try {
const files = readdirSync(localeDir).filter((f) => f.endsWith('.json'));
for (const file of files) {
const filePath = join(localeDir, file);
const content = loadJsonFile(filePath);
keys.push(...flattenObject(content));
}
} catch (error) {
console.error(`Error reading locale directory for ${locale}:`, error);
}
return keys;
}
/**
* Compare translation keys between locales
*/
function compareTranslations(): ValidationResult {
const result: ValidationResult = {
isValid: true,
errors: [],
warnings: [],
missingKeys: { en: [], zh: [] },
extraKeys: { en: [], zh: [] },
};
// Get keys for each locale
const enKeys = getLocaleKeys('en');
const zhKeys = getLocaleKeys('zh');
// Sort for comparison
enKeys.sort();
zhKeys.sort();
// Find keys missing in Chinese
for (const key of enKeys) {
if (!zhKeys.includes(key)) {
result.missingKeys.zh.push(key);
result.isValid = false;
}
}
// Find keys missing in English
for (const key of zhKeys) {
if (!enKeys.includes(key)) {
result.missingKeys.en.push(key);
result.isValid = false;
}
}
return result;
}
/**
* Display validation results
*/
function displayResults(result: ValidationResult): void {
console.log('\n=== Translation Validation Report ===\n');
if (result.isValid) {
console.log('Status: PASSED');
console.log('All translation keys are synchronized between en/ and zh/ locales.\n');
} else {
console.log('Status: FAILED');
console.log('Translation keys are not synchronized.\n');
}
// Display missing keys
if (result.missingKeys.zh.length > 0) {
console.log(`Keys missing in zh/ (${result.missingKeys.zh.length}):`);
result.missingKeys.zh.forEach((key) => console.log(` - ${key}`));
console.log('');
}
if (result.missingKeys.en.length > 0) {
console.log(`Keys missing in en/ (${result.missingKeys.en.length}):`);
result.missingKeys.en.forEach((key) => console.log(` - ${key}`));
console.log('');
}
// Display warnings
if (result.warnings.length > 0) {
console.log('Warnings:');
result.warnings.forEach((warning) => console.log(` ⚠️ ${warning}`));
console.log('');
}
// Display errors
if (result.errors.length > 0) {
console.log('Errors:');
result.errors.forEach((error) => console.log(`${error}`));
console.log('');
}
console.log('=====================================\n');
}
/**
* Main validation function
*/
function main(): void {
console.log('Validating translations...\n');
// Check if locale directories exist
for (const locale of SUPPORTED_LOCALES) {
const localePath = join(LOCALES_DIR, locale);
// Note: In a real script, you'd use fs.existsSync here
// For now, we'll let the error be caught in getLocaleKeys
}
// Compare translations
const result = compareTranslations();
// Display results
displayResults(result);
// Exit with appropriate code
process.exit(result.isValid ? 0 : 1);
}
// Run the validation
main();

1
ccw/frontend/src/.gitignore vendored Normal file
View File

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

View File

@@ -3,15 +3,30 @@
// ========================================
// Root application component with Router provider
import { QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider } from 'react-router-dom';
import { IntlProvider } from 'react-intl';
import { router } from './router';
import queryClient from './lib/query-client';
import type { Locale } from './lib/i18n';
interface AppProps {
locale: Locale;
messages: Record<string, string>;
}
/**
* Root App component
* Provides routing and global providers
*/
function App() {
return <RouterProvider router={router} />;
function App({ locale, messages }: AppProps) {
return (
<IntlProvider locale={locale} messages={messages}>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</IntlProvider>
);
}
export default App;

View File

@@ -0,0 +1,203 @@
// ========================================
// Header Component Tests - i18n Focus
// ========================================
// Tests for the header component with internationalization
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, waitFor } from '@/test/i18n';
import { Header } from './Header';
import { useAppStore } from '@/stores/appStore';
import userEvent from '@testing-library/user-event';
// Mock useTheme hook
vi.mock('@/hooks', () => ({
useTheme: () => ({
isDark: false,
toggleTheme: vi.fn(),
}),
}));
describe('Header Component - i18n Tests', () => {
beforeEach(() => {
// Reset store state before each test
useAppStore.setState({ locale: 'en' });
vi.clearAllMocks();
});
describe('language switcher visibility', () => {
it('should render language switcher', () => {
render(<Header />);
const languageSwitcher = screen.getByRole('combobox', { name: /select language/i });
expect(languageSwitcher).toBeInTheDocument();
});
it('should render language switcher in compact mode', () => {
render(<Header />);
const languageSwitcher = screen.getByRole('combobox', { name: /select language/i });
expect(languageSwitcher).toHaveClass('w-[110px]');
});
});
describe('translated aria-labels', () => {
it('should have translated aria-label for menu toggle', () => {
render(<Header onMenuClick={vi.fn()} />);
const menuButton = screen.getByRole('button', { name: /toggle navigation/i });
expect(menuButton).toBeInTheDocument();
expect(menuButton).toHaveAttribute('aria-label');
});
it('should have translated aria-label for theme toggle', () => {
render(<Header />);
const themeButton = screen.getByRole('button', { name: /switch to dark mode/i });
expect(themeButton).toBeInTheDocument();
expect(themeButton).toHaveAttribute('aria-label');
});
it('should have translated aria-label for user menu', () => {
render(<Header />);
const userMenuButton = screen.getByRole('button', { name: /user menu/i });
expect(userMenuButton).toBeInTheDocument();
expect(userMenuButton).toHaveAttribute('aria-label');
});
it('should have translated aria-label for refresh button', () => {
render(<Header onRefresh={vi.fn()} />);
const refreshButton = screen.getByRole('button', { name: /refresh workspace/i });
expect(refreshButton).toBeInTheDocument();
expect(refreshButton).toHaveAttribute('aria-label');
});
});
describe('translated text content', () => {
it('should display translated brand name', () => {
render(<Header />);
const brandLink = screen.getByRole('link', { name: /ccw/i });
expect(brandLink).toBeInTheDocument();
});
it('should update aria-label when locale changes', async () => {
const { rerender } = render(<Header />);
// Initial locale is English
const themeButtonEn = screen.getByRole('button', { name: /switch to dark mode/i });
expect(themeButtonEn).toBeInTheDocument();
// Change locale to Chinese and re-render
useAppStore.setState({ locale: 'zh' });
rerender(<Header />);
// After locale change, the theme button should be updated
// In Chinese, it should say "切换到深色模式"
const themeButtonZh = screen.getByRole('button', { name: /切换到深色模式|switch to dark mode/i });
expect(themeButtonZh).toBeInTheDocument();
});
});
describe('translated navigation items', () => {
it('should display translated settings link in user menu', async () => {
const user = userEvent.setup();
render(<Header />);
// Click user menu to show dropdown
const userMenuButton = screen.getByRole('button', { name: /user menu/i });
await user.click(userMenuButton);
// Wait for dropdown to appear
await waitFor(() => {
const settingsLink = screen.getByRole('link', { name: /settings/i });
expect(settingsLink).toBeInTheDocument();
});
});
it('should display translated logout button in user menu', async () => {
const user = userEvent.setup();
render(<Header />);
// Click user menu to show dropdown
const userMenuButton = screen.getByRole('button', { name: /user menu/i });
await user.click(userMenuButton);
// Wait for dropdown to appear
await waitFor(() => {
const logoutButton = screen.getByRole('button', { name: /logout/i });
expect(logoutButton).toBeInTheDocument();
});
});
});
describe('locale switching integration', () => {
it('should reflect locale change in language switcher', async () => {
const { rerender } = render(<Header />);
const languageSwitcher = screen.getByRole('combobox', { name: /select language/i });
expect(languageSwitcher).toHaveTextContent('English');
// Change locale in store
useAppStore.setState({ locale: 'zh' });
// Re-render header
rerender(<Header />);
expect(languageSwitcher).toHaveTextContent('中文');
});
});
describe('translated project path display', () => {
it('should display translated fallback when no project path', () => {
render(<Header projectPath="" />);
// Header should render correctly even without project path
const header = screen.getByRole('banner');
expect(header).toBeInTheDocument();
// Brand link should still be present
const brandLink = screen.getByRole('link', { name: /ccw/i });
expect(brandLink).toBeInTheDocument();
});
it('should display project path when provided', () => {
render(<Header projectPath="/test/path" />);
// Should show the path indicator
const pathDisplay = screen.getByTitle('/test/path');
expect(pathDisplay).toBeInTheDocument();
});
});
describe('accessibility with i18n', () => {
it('should maintain accessible labels across locales', () => {
render(<Header />);
// Check specific buttons have proper aria-labels
const themeButton = screen.getByRole('button', { name: /switch to dark mode/i });
expect(themeButton).toHaveAttribute('aria-label');
const userMenuButton = screen.getByRole('button', { name: /user menu/i });
expect(userMenuButton).toHaveAttribute('aria-label');
});
it('should have translated title attributes', () => {
render(<Header />);
// Theme button should have title attribute
const themeButton = screen.getByRole('button', { name: /switch to dark mode/i });
expect(themeButton).toHaveAttribute('title');
});
});
describe('header role with i18n', () => {
it('should have banner role for accessibility', () => {
render(<Header />);
const header = screen.getByRole('banner');
expect(header).toBeInTheDocument();
});
});
});

View File

@@ -5,6 +5,7 @@
import { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
Workflow,
Menu,
@@ -18,6 +19,7 @@ import {
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
import { useTheme } from '@/hooks';
import { LanguageSwitcher } from './LanguageSwitcher';
export interface HeaderProps {
/** Callback to toggle mobile sidebar */
@@ -36,6 +38,7 @@ export function Header({
onRefresh,
isRefreshing = false,
}: HeaderProps) {
const { formatMessage } = useIntl();
const { isDark, toggleTheme } = useTheme();
const handleRefresh = useCallback(() => {
@@ -47,7 +50,7 @@ export function Header({
// Get display path (truncate if too long)
const displayPath = projectPath.length > 40
? '...' + projectPath.slice(-37)
: projectPath || 'No project selected';
: projectPath || formatMessage({ id: 'navigation.header.noProject' });
return (
<header
@@ -62,7 +65,7 @@ export function Header({
size="icon"
className="md:hidden"
onClick={onMenuClick}
aria-label="Toggle navigation menu"
aria-label={formatMessage({ id: 'common.aria.toggleNavigation' })}
>
<Menu className="w-5 h-5" />
</Button>
@@ -73,8 +76,8 @@ export function Header({
className="flex items-center gap-2 text-lg font-semibold text-primary hover:opacity-80 transition-opacity"
>
<Workflow className="w-6 h-6" />
<span className="hidden sm:inline">Claude Code Workflow</span>
<span className="sm:hidden">CCW</span>
<span className="hidden sm:inline">{formatMessage({ id: 'navigation.header.brand' })}</span>
<span className="sm:hidden">{formatMessage({ id: 'navigation.header.brandShort' })}</span>
</Link>
</div>
@@ -96,8 +99,8 @@ export function Header({
size="icon"
onClick={handleRefresh}
disabled={isRefreshing}
aria-label="Refresh workspace"
title="Refresh workspace"
aria-label={formatMessage({ id: 'common.aria.refreshWorkspace' })}
title={formatMessage({ id: 'common.aria.refreshWorkspace' })}
>
<RefreshCw
className={cn('w-5 h-5', isRefreshing && 'animate-spin')}
@@ -105,13 +108,22 @@ export function Header({
</Button>
)}
{/* Language switcher */}
<LanguageSwitcher compact />
{/* Theme toggle */}
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
aria-label={isDark
? formatMessage({ id: 'common.aria.switchToLightMode' })
: formatMessage({ id: 'common.aria.switchToDarkMode' })
}
title={isDark
? formatMessage({ id: 'common.aria.switchToLightMode' })
: formatMessage({ id: 'common.aria.switchToDarkMode' })
}
>
{isDark ? (
<Sun className="w-5 h-5" />
@@ -126,8 +138,8 @@ export function Header({
variant="ghost"
size="icon"
className="rounded-full"
aria-label="User menu"
title="User menu"
aria-label={formatMessage({ id: 'common.aria.userMenu' })}
title={formatMessage({ id: 'common.aria.userMenu' })}
>
<User className="w-5 h-5" />
</Button>
@@ -140,7 +152,7 @@ export function Header({
className="flex items-center gap-2 px-4 py-2 text-sm text-foreground hover:bg-hover transition-colors"
>
<Settings className="w-4 h-4" />
<span>Settings</span>
<span>{formatMessage({ id: 'navigation.header.settings' })}</span>
</Link>
<hr className="my-1 border-border" />
<button
@@ -151,7 +163,7 @@ export function Header({
}}
>
<LogOut className="w-4 h-4" />
<span>Exit Dashboard</span>
<span>{formatMessage({ id: 'navigation.header.logout' })}</span>
</button>
</div>
</div>

View File

@@ -0,0 +1,245 @@
// ========================================
// LanguageSwitcher Component Tests
// ========================================
// Tests for the language switcher component
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, waitFor } from '@/test/i18n';
import { LanguageSwitcher } from './LanguageSwitcher';
import { useAppStore } from '@/stores/appStore';
import userEvent from '@testing-library/user-event';
describe('LanguageSwitcher Component', () => {
beforeEach(() => {
// Reset store state before each test
useAppStore.setState({ locale: 'en' });
vi.clearAllMocks();
});
describe('rendering', () => {
it('should render the select component', () => {
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
});
it('should display current locale value', () => {
useAppStore.setState({ locale: 'en' });
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
expect(select).toHaveTextContent('English');
});
it('should display Chinese locale when set', () => {
useAppStore.setState({ locale: 'zh' });
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
expect(select).toHaveTextContent('中文');
});
it('should have aria-label for accessibility', () => {
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
expect(select).toHaveAttribute('aria-label', 'Select language');
});
it('should render in compact mode', () => {
render(<LanguageSwitcher compact />);
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
expect(select).toHaveClass('w-[110px]');
});
it('should render in default mode', () => {
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
expect(select).toHaveClass('w-[160px]');
});
});
describe('language options', () => {
it('should display English option', async () => {
const user = userEvent.setup();
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
await user.click(select);
// Wait for dropdown to appear and check for option role
await waitFor(() => {
const englishOption = screen.getByRole('option', { name: /English/ });
expect(englishOption).toBeInTheDocument();
});
});
it('should display Chinese option', async () => {
const user = userEvent.setup();
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
await user.click(select);
// Wait for dropdown to appear
await waitFor(() => {
const chineseOption = screen.getByRole('option', { name: /中文/ });
expect(chineseOption).toBeInTheDocument();
});
});
it('should display flag icons for options', async () => {
const user = userEvent.setup();
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
await user.click(select);
// Check for flag emojis in options
await waitFor(() => {
const options = screen.getAllByRole('option');
expect(options.length).toBe(2);
const optionsText = options.map(opt => opt.textContent).join(' ');
expect(optionsText).toContain('🇺🇸');
expect(optionsText).toContain('🇨🇳');
});
});
});
describe('language switching behavior', () => {
it('should call setLocale when option is selected', async () => {
const user = userEvent.setup();
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
await user.click(select);
// Wait for Chinese option and click it
await waitFor(() => {
const chineseOption = screen.getByText('中文');
user.click(chineseOption);
});
// Verify locale was updated in store
await waitFor(() => {
expect(useAppStore.getState().locale).toBe('zh');
});
});
it('should switch to English when selected', async () => {
const user = userEvent.setup();
useAppStore.setState({ locale: 'zh' });
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
expect(select).toHaveTextContent('中文');
await user.click(select);
// Wait for English option and click it
await waitFor(() => {
const englishOption = screen.getByText('English');
user.click(englishOption);
});
// Verify locale was updated in store
await waitFor(() => {
expect(useAppStore.getState().locale).toBe('en');
});
});
it('should persist locale selection to store', async () => {
const user = userEvent.setup();
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
await user.click(select);
await waitFor(() => {
const chineseOption = screen.getByText('中文');
user.click(chineseOption);
});
// Check that store was updated
await waitFor(() => {
const storeLocale = useAppStore.getState().locale;
expect(storeLocale).toBe('zh');
});
});
});
describe('custom className', () => {
it('should apply custom className', () => {
render(<LanguageSwitcher className="custom-class" />);
const select = screen.getByRole('combobox');
expect(select).toHaveClass('custom-class');
});
});
describe('accessibility', () => {
it('should be keyboard navigable', async () => {
const user = userEvent.setup();
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
select.focus();
expect(select).toHaveFocus();
// Open with Enter
await user.keyboard('{Enter}');
// Should show options after opening
await waitFor(() => {
const englishOption = screen.getByRole('option', { name: /English/ });
expect(englishOption).toBeInTheDocument();
});
});
it('should maintain focus management', async () => {
const user = userEvent.setup();
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
await user.click(select);
// Focus should remain on select or move to options
await waitFor(() => {
const options = screen.getAllByRole('option');
expect(options.length).toBeGreaterThan(0);
});
});
});
describe('integration with useLocale hook', () => {
it('should reflect current locale from store', () => {
useAppStore.setState({ locale: 'zh' });
render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
expect(select).toHaveTextContent('中文');
});
it('should update when store locale changes externally', async () => {
const { rerender } = render(<LanguageSwitcher />);
const select = screen.getByRole('combobox');
expect(select).toHaveTextContent('English');
// Update store externally
useAppStore.setState({ locale: 'zh' });
// Re-render to reflect change
rerender(<LanguageSwitcher />);
expect(select).toHaveTextContent('中文');
});
});
});

View File

@@ -0,0 +1,70 @@
// ========================================
// Language Switcher Component
// ========================================
// Language selection dropdown with flag icons
import { Languages } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/Select';
import { useLocale } from '@/hooks/useLocale';
import { cn } from '@/lib/utils';
export interface LanguageSwitcherProps {
/** Compact variant for header (smaller, icon-only trigger) */
compact?: boolean;
/** Additional CSS classes */
className?: string;
}
// Language options with flag emojis and labels
const LANGUAGE_OPTIONS = [
{ value: 'en' as const, label: 'English', flag: '🇺🇸' },
{ value: 'zh' as const, label: '中文', flag: '🇨🇳' },
] as const;
/**
* Language switcher component
* Allows users to switch between English and Chinese
*/
export function LanguageSwitcher({ compact = false, className }: LanguageSwitcherProps) {
const { locale, setLocale } = useLocale();
return (
<Select value={locale} onValueChange={setLocale}>
<SelectTrigger
className={cn(
compact ? 'w-[110px]' : 'w-[160px]',
'gap-2',
className
)}
aria-label="Select language"
>
{compact ? (
<>
<Languages className="w-4 h-4" />
<SelectValue />
</>
) : (
<SelectValue />
)}
</SelectTrigger>
<SelectContent>
{LANGUAGE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<span className="flex items-center gap-2">
<span className="text-base">{option.flag}</span>
<span>{option.label}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export default LanguageSwitcher;

View File

@@ -3,8 +3,9 @@
// ========================================
// Collapsible navigation sidebar with route links
import { useState, useCallback } from 'react';
import { useState, useCallback, useMemo } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
Home,
FolderKanban,
@@ -18,6 +19,9 @@ import {
HelpCircle,
PanelLeftClose,
PanelLeftOpen,
LayoutDashboard,
Clock,
Zap,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/Button';
@@ -41,17 +45,21 @@ interface NavItem {
badgeVariant?: 'default' | 'success' | 'warning' | 'info';
}
const navItems: NavItem[] = [
{ path: '/', label: 'Home', icon: Home },
{ path: '/sessions', label: 'Sessions', icon: FolderKanban },
{ path: '/orchestrator', label: 'Orchestrator', icon: Workflow },
{ path: '/loops', label: 'Loop Monitor', icon: RefreshCw },
{ path: '/issues', label: 'Issues', icon: AlertCircle },
{ path: '/skills', label: 'Skills', icon: Sparkles },
{ path: '/commands', label: 'Commands', icon: Terminal },
{ path: '/memory', label: 'Memory', icon: Brain },
{ path: '/settings', label: 'Settings', icon: Settings },
{ path: '/help', label: 'Help', icon: HelpCircle },
// Navigation item definitions (without labels for i18n)
const navItemDefinitions: Omit<NavItem, 'label'>[] = [
{ path: '/', icon: Home },
{ path: '/sessions', icon: FolderKanban },
{ path: '/lite-tasks', icon: Zap },
{ path: '/project', icon: LayoutDashboard },
{ path: '/history', icon: Clock },
{ path: '/orchestrator', icon: Workflow },
{ path: '/loops', icon: RefreshCw },
{ path: '/issues', icon: AlertCircle },
{ path: '/skills', icon: Sparkles },
{ path: '/commands', icon: Terminal },
{ path: '/memory', icon: Brain },
{ path: '/settings', icon: Settings },
{ path: '/help', icon: HelpCircle },
];
export function Sidebar({
@@ -60,6 +68,7 @@ export function Sidebar({
mobileOpen = false,
onMobileClose,
}: SidebarProps) {
const { formatMessage } = useIntl();
const location = useLocation();
const [internalCollapsed, setInternalCollapsed] = useState(collapsed);
@@ -80,6 +89,29 @@ export function Sidebar({
}
}, [onMobileClose]);
// Build nav items with translated labels
const navItems = useMemo(() => {
const keyMap: Record<string, string> = {
'/': 'main.home',
'/sessions': 'main.sessions',
'/lite-tasks': 'main.liteTasks',
'/project': 'main.project',
'/history': 'main.history',
'/orchestrator': 'main.orchestrator',
'/loops': 'main.loops',
'/issues': 'main.issues',
'/skills': 'main.skills',
'/commands': 'main.commands',
'/memory': 'main.memory',
'/settings': 'main.settings',
'/help': 'main.help',
};
return navItemDefinitions.map((item) => ({
...item,
label: formatMessage({ id: `navigation.${keyMap[item.path]}` }),
}));
}, [formatMessage]);
return (
<>
{/* Mobile overlay */}
@@ -103,7 +135,7 @@ export function Sidebar({
mobileOpen && 'fixed left-0 top-14 flex translate-x-0 z-50 h-[calc(100vh-56px)] w-64 shadow-lg'
)}
role="navigation"
aria-label="Main navigation"
aria-label={formatMessage({ id: 'header.brand' })}
>
<nav className="flex-1 py-3 overflow-y-auto">
<ul className="space-y-1 px-2">
@@ -164,14 +196,17 @@ export function Sidebar({
'w-full flex items-center gap-2 text-muted-foreground hover:text-foreground',
isCollapsed && 'justify-center'
)}
aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
aria-label={isCollapsed
? formatMessage({ id: 'navigation.sidebar.expand' })
: formatMessage({ id: 'navigation.sidebar.collapseAria' })
}
>
{isCollapsed ? (
<PanelLeftOpen className="w-4 h-4" />
) : (
<>
<PanelLeftClose className="w-4 h-4" />
<span>Collapse</span>
<span>{formatMessage({ id: 'navigation.sidebar.collapse' })}</span>
</>
)}
</Button>

View File

@@ -0,0 +1,245 @@
// ========================================
// ConversationCard Component
// ========================================
// Card component for displaying CLI execution history items
import * as React from 'react';
import { useIntl } from 'react-intl';
import { cn } from '@/lib/utils';
import { Card, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button';
import {
MoreVertical,
Eye,
Trash2,
Copy,
Clock,
Timer,
Hash,
MessagesSquare,
Folder,
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from '@/components/ui/Dropdown';
import type { CliExecution } from '@/lib/api';
export interface ConversationCardProps {
/** Execution data */
execution: CliExecution;
/** Called when view action is triggered */
onView?: (execution: CliExecution) => void;
/** Called when delete action is triggered */
onDelete?: (id: string) => void;
/** Called when card is clicked */
onClick?: (execution: CliExecution) => void;
/** Optional className */
className?: string;
/** Disabled state for actions */
actionsDisabled?: boolean;
}
// Status configuration
const statusConfig = {
success: {
variant: 'success' as const,
icon: 'check-circle',
},
error: {
variant: 'destructive' as const,
icon: 'x-circle',
},
timeout: {
variant: 'warning' as const,
icon: 'clock',
},
};
/**
* Format duration to human readable string
*/
function formatDuration(ms: number): string {
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds}s`;
}
/**
* Get time ago string
*/
function getTimeAgo(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (seconds < 60) return 'just now';
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
/**
* ConversationCard component for displaying CLI execution history
*/
export function ConversationCard({
execution,
onView,
onDelete,
onClick,
className,
actionsDisabled = false,
}: ConversationCardProps) {
const { formatMessage } = useIntl();
const [copied, setCopied] = React.useState(false);
const status = statusConfig[execution.status] || statusConfig.error;
const handleCopyId = async (e: React.MouseEvent) => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(execution.id);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
console.error('Failed to copy ID');
}
};
const handleCardClick = (e: React.MouseEvent) => {
// Don't trigger if clicking on dropdown
if ((e.target as HTMLElement).closest('[data-radix-popper-content-wrapper]')) {
return;
}
onClick?.(execution);
};
const handleAction = (
e: React.MouseEvent,
action: 'view' | 'delete' | 'copy'
) => {
e.stopPropagation();
switch (action) {
case 'view':
onView?.(execution);
break;
case 'delete':
onDelete?.(execution.id);
break;
case 'copy':
handleCopyId(e);
break;
}
};
return (
<Card
className={cn(
'group cursor-pointer transition-all duration-200 hover:shadow-md',
className
)}
onClick={handleCardClick}
>
<CardContent className="p-4">
<div className="flex items-start gap-3">
{/* Main content */}
<div className="flex-1 min-w-0">
{/* Header row */}
<div className="flex flex-wrap items-center gap-2 mb-2">
<Badge variant="secondary" className="text-xs">
{execution.tool}
</Badge>
<Badge variant="outline" className="text-xs">
{execution.mode || 'analysis'}
</Badge>
{execution.turn_count && execution.turn_count > 1 && (
<Badge variant="info" className="gap-1 text-xs">
<MessagesSquare className="h-3 w-3" />
{execution.turn_count}
</Badge>
)}
{execution.sourceDir && execution.sourceDir !== '.' && (
<Badge variant="outline" className="gap-1 text-xs">
<Folder className="h-3 w-3" />
{execution.sourceDir}
</Badge>
)}
<Badge variant={status.variant} className="gap-1 text-xs ml-auto">
{status.icon === 'check-circle' && '✓'}
{status.icon === 'x-circle' && '✗'}
{status.icon === 'clock' && '⏱'}
{execution.status}
</Badge>
</div>
{/* Prompt preview */}
<p className="text-sm text-foreground line-clamp-2 mb-2">
{execution.prompt_preview}
</p>
{/* Meta info */}
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{getTimeAgo(execution.timestamp)}
</span>
<span className="flex items-center gap-1">
<Timer className="h-3 w-3" />
{formatDuration(execution.duration_ms)}
</span>
<span className="flex items-center gap-1 font-mono" title={execution.id}>
<Hash className="h-3 w-3" />
{execution.id.substring(0, 8)}...
</span>
</div>
</div>
{/* Actions dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => e.stopPropagation()}
disabled={actionsDisabled}
>
<MoreVertical className="h-4 w-4" />
<span className="sr-only">{formatMessage({ id: 'common.aria.actions' })}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => handleAction(e, 'copy')}>
<Copy className="mr-2 h-4 w-4" />
{copied
? formatMessage({ id: 'history.actions.copied' })
: formatMessage({ id: 'history.actions.copyId' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={(e) => handleAction(e, 'view')}>
<Eye className="mr-2 h-4 w-4" />
{formatMessage({ id: 'history.actions.view' })}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={(e) => handleAction(e, 'delete')}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
{formatMessage({ id: 'history.actions.delete' })}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,304 @@
// ========================================
// Flowchart Component
// ========================================
// Interactive flowchart component using @xyflow/react
import * as React from 'react';
import {
ReactFlow,
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
type Node,
type Edge,
type Connection,
type NodeTypes,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import type { FlowControl } from '@/lib/api';
// Custom node types
interface FlowchartNodeData extends Record<string, unknown> {
label: string;
description?: string;
step?: number | string;
output?: string;
type: 'pre-analysis' | 'implementation' | 'section';
dependsOn?: string[];
}
// Custom node component
const CustomNode: React.FC<{ data: FlowchartNodeData }> = ({ data }) => {
const isPreAnalysis = data.type === 'pre-analysis';
const isSection = data.type === 'section';
if (isSection) {
return (
<div className="px-4 py-2 bg-muted rounded border-2 border-border">
<span className="text-sm font-semibold text-foreground">{data.label}</span>
</div>
);
}
return (
<div
className={`px-4 py-3 rounded-lg border-2 shadow-sm min-w-[280px] max-w-[400px] ${
isPreAnalysis
? 'bg-amber-50 border-amber-500 dark:bg-amber-950/30'
: 'bg-blue-50 border-blue-500 dark:bg-blue-950/30'
}`}
>
<div className="flex items-start gap-2">
<span
className={`flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
isPreAnalysis ? 'bg-amber-500 text-white' : 'bg-blue-500 text-white'
}`}
>
{data.step}
</span>
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-foreground">{data.label}</div>
{data.description && (
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">{data.description}</div>
)}
{data.output && (
<div className="text-xs text-green-600 dark:text-green-400 mt-1">
{'->'} {data.output}
</div>
)}
</div>
</div>
</div>
);
};
const nodeTypes: NodeTypes = {
custom: CustomNode,
};
export interface FlowchartProps {
flowControl: FlowControl;
className?: string;
}
/**
* Flowchart component for visualizing implementation approach
*/
export function Flowchart({ flowControl, className = '' }: FlowchartProps) {
const preAnalysis = flowControl.pre_analysis || [];
const implSteps = flowControl.implementation_approach || [];
// Build nodes and edges
const initialNodes: Node[] = [];
const initialEdges: Edge[] = [];
let currentY = 0;
const nodeHeight = 100;
const verticalGap = 80;
const sectionGap = 60;
// Add Pre-Analysis section
if (preAnalysis.length > 0) {
// Section header node
initialNodes.push({
id: 'pre-section',
type: 'custom',
position: { x: 0, y: currentY },
data: {
label: 'Pre-Analysis Steps',
type: 'section' as const,
},
});
currentY += sectionGap;
preAnalysis.forEach((step, idx) => {
const nodeId = `pre-${idx}`;
initialNodes.push({
id: nodeId,
type: 'custom',
position: { x: 0, y: currentY },
data: {
label: step.step || step.action || `Pre-step ${idx + 1}`,
description: step.action,
step: `P${idx + 1}`,
output: step.output_to,
type: 'pre-analysis' as const,
},
});
// Edge from previous node
if (idx === 0) {
initialEdges.push({
id: `pre-section-${idx}`,
source: 'pre-section',
target: nodeId,
type: 'smoothstep',
animated: false,
});
} else {
initialEdges.push({
id: `pre-${idx - 1}-${idx}`,
source: `pre-${idx - 1}`,
target: nodeId,
type: 'smoothstep',
animated: false,
});
}
currentY += nodeHeight + verticalGap;
});
currentY += sectionGap;
}
// Add Implementation section
if (implSteps.length > 0) {
// Section header node
const implSectionId = `impl-section-${Date.now()}`;
initialNodes.push({
id: implSectionId,
type: 'custom',
position: { x: 0, y: currentY },
data: {
label: 'Implementation Steps',
type: 'section' as const,
},
});
// Edge from pre-analysis to impl section (if both exist)
if (preAnalysis.length > 0) {
initialEdges.push({
id: `pre-impl-conn`,
source: `pre-${preAnalysis.length - 1}`,
target: implSectionId,
type: 'smoothstep',
animated: true,
style: { stroke: 'hsl(var(--primary))' },
});
}
currentY += sectionGap;
implSteps.forEach((step, idx) => {
const nodeId = `impl-${idx}`;
initialNodes.push({
id: nodeId,
type: 'custom',
position: { x: 0, y: currentY },
data: {
label: step.title || `Step ${step.step}`,
description: step.description,
step: step.step,
type: 'implementation' as const,
dependsOn: step.depends_on?.map(d => `impl-${d - 1}`),
},
});
// Edge from section header to first step
if (idx === 0) {
initialEdges.push({
id: `impl-section-${idx}`,
source: implSectionId,
target: nodeId,
type: 'smoothstep',
animated: false,
});
} else {
// Sequential edge
initialEdges.push({
id: `impl-${idx - 1}-${idx}`,
source: `impl-${idx - 1}`,
target: nodeId,
type: 'smoothstep',
animated: false,
});
}
// Dependency edges
if (step.depends_on && step.depends_on.length > 0) {
step.depends_on.forEach(depIdx => {
const depNodeId = `impl-${depIdx - 1}`;
initialEdges.push({
id: `dep-${depIdx}-${idx}`,
source: depNodeId,
target: nodeId,
type: 'smoothstep',
animated: false,
style: { strokeDasharray: '5,5', stroke: 'hsl(var(--warning))' },
});
});
}
currentY += nodeHeight + verticalGap;
});
}
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
// Handle new connections (disabled for this use case)
const onConnect = React.useCallback(
(connection: Connection) => {
setEdges((eds) => [
...eds,
{
...connection,
id: `edge-${Date.now()}`,
type: 'smoothstep',
},
]);
},
[setEdges]
);
// If no data, show empty state
if (preAnalysis.length === 0 && implSteps.length === 0) {
return (
<div className={`flex items-center justify-center p-8 text-center ${className}`}>
<div>
<p className="text-sm text-muted-foreground">No flowchart data available</p>
</div>
</div>
);
}
return (
<div className={className} style={{ height: `${currentY + 100}px` }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.2 }}
minZoom={0.3}
maxZoom={1.5}
defaultViewport={{ x: 0, y: 0, zoom: 0.8 }}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={true}
selectNodesOnDrag={false}
zoomOnScroll={true}
panOnScroll={true}
>
<Background />
<Controls />
<MiniMap
nodeColor={(node) => {
const data = node.data as FlowchartNodeData;
if (data.type === 'section') return '#e5e7eb';
if (data.type === 'pre-analysis') return '#f59e0b';
return '#3b82f6';
}}
className="!bg-background !border-border"
/>
</ReactFlow>
</div>
);
}
export default Flowchart;

View File

@@ -4,6 +4,7 @@
// Card component for displaying issues with actions
import { useState } from 'react';
import { useIntl } from 'react-intl';
import {
AlertCircle,
AlertTriangle,
@@ -41,31 +42,64 @@ export interface IssueCardProps {
// ========== Priority Helpers ==========
const priorityConfig: Record<Issue['priority'], { icon: React.ElementType; color: string; label: string }> = {
critical: { icon: AlertCircle, color: 'destructive', label: 'Critical' },
high: { icon: AlertTriangle, color: 'warning', label: 'High' },
medium: { icon: Info, color: 'info', label: 'Medium' },
low: { icon: Info, color: 'secondary', label: 'Low' },
// Priority icon and color configuration (without labels for i18n)
const priorityVariantConfig: Record<Issue['priority'], { icon: React.ElementType; color: string }> = {
critical: { icon: AlertCircle, color: 'destructive' },
high: { icon: AlertTriangle, color: 'warning' },
medium: { icon: Info, color: 'info' },
low: { icon: Info, color: 'secondary' },
};
const statusConfig: Record<Issue['status'], { icon: React.ElementType; color: string; label: string }> = {
open: { icon: AlertCircle, color: 'info', label: 'Open' },
in_progress: { icon: Clock, color: 'warning', label: 'In Progress' },
resolved: { icon: CheckCircle, color: 'success', label: 'Resolved' },
closed: { icon: XCircle, color: 'muted', label: 'Closed' },
completed: { icon: CheckCircle, color: 'success', label: 'Completed' },
// Priority label keys for i18n
const priorityLabelKeys: Record<Issue['priority'], string> = {
critical: 'issues.priority.critical',
high: 'issues.priority.high',
medium: 'issues.priority.medium',
low: 'issues.priority.low',
};
// Status icon and color configuration (without labels for i18n)
const statusVariantConfig: Record<Issue['status'], { icon: React.ElementType; color: string }> = {
open: { icon: AlertCircle, color: 'info' },
in_progress: { icon: Clock, color: 'warning' },
resolved: { icon: CheckCircle, color: 'success' },
closed: { icon: XCircle, color: 'muted' },
completed: { icon: CheckCircle, color: 'success' },
};
// Status label keys for i18n
const statusLabelKeys: Record<Issue['status'], string> = {
open: 'issues.status.open',
in_progress: 'issues.status.inProgress',
resolved: 'issues.status.resolved',
closed: 'issues.status.closed',
completed: 'issues.status.completed',
};
// ========== Priority Badge ==========
export function PriorityBadge({ priority }: { priority: Issue['priority'] }) {
const config = priorityConfig[priority];
const { formatMessage } = useIntl();
const config = priorityVariantConfig[priority];
// Defensive check: handle unknown priority values
if (!config) {
return (
<Badge variant="secondary" className="gap-1">
{priority}
</Badge>
);
}
const Icon = config.icon;
const label = priorityLabelKeys[priority]
? formatMessage({ id: priorityLabelKeys[priority] })
: priority;
return (
<Badge variant={config.color as 'default' | 'secondary' | 'destructive' | 'outline'} className="gap-1">
<Icon className="w-3 h-3" />
{config.label}
{label}
</Badge>
);
}
@@ -73,13 +107,27 @@ export function PriorityBadge({ priority }: { priority: Issue['priority'] }) {
// ========== Status Badge ==========
export function StatusBadge({ status }: { status: Issue['status'] }) {
const config = statusConfig[status];
const { formatMessage } = useIntl();
const config = statusVariantConfig[status];
// Defensive check: handle unknown status values
if (!config) {
return (
<Badge variant="outline" className="gap-1">
{status}
</Badge>
);
}
const Icon = config.icon;
const label = statusLabelKeys[status]
? formatMessage({ id: statusLabelKeys[status] })
: status;
return (
<Badge variant="outline" className="gap-1">
<Icon className="w-3 h-3" />
{config.label}
{label}
</Badge>
);
}
@@ -99,6 +147,7 @@ export function IssueCard({
dragHandleProps,
innerRef,
}: IssueCardProps) {
const { formatMessage } = useIntl();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const handleClick = () => {
@@ -176,19 +225,19 @@ export function IssueCard({
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleEdit}>
<Edit className="w-4 h-4 mr-2" />
Edit
{formatMessage({ id: 'issues.actions.edit' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onStatusChange?.(issue, 'in_progress')}>
<Clock className="w-4 h-4 mr-2" />
Start Progress
{formatMessage({ id: 'issues.actions.startProgress' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onStatusChange?.(issue, 'resolved')}>
<CheckCircle className="w-4 h-4 mr-2" />
Mark Resolved
{formatMessage({ id: 'issues.actions.markResolved' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDelete} className="text-destructive">
<Trash2 className="w-4 h-4 mr-2" />
Delete
{formatMessage({ id: 'issues.actions.delete' })}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -228,7 +277,10 @@ export function IssueCard({
{issue.solutions && issue.solutions.length > 0 && (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<ExternalLink className="w-3 h-3" />
{issue.solutions.length} solution{issue.solutions.length !== 1 ? 's' : ''}
{issue.solutions.length} {formatMessage(
{ id: 'issues.card.solutions' },
{ count: issue.solutions.length }
)}
</div>
)}
</Card>

View File

@@ -4,6 +4,7 @@
// Session card with status badge and action menu
import * as React from 'react';
import { useIntl } from 'react-intl';
import { cn } from '@/lib/utils';
import { Card, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
@@ -44,16 +45,25 @@ export interface SessionCardProps {
actionsDisabled?: boolean;
}
// Status badge configuration
const statusConfig: Record<
// Status variant configuration (without labels for i18n)
const statusVariantConfig: Record<
SessionMetadata['status'],
{ label: string; variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info' }
{ variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info' }
> = {
planning: { label: 'Planning', variant: 'info' },
in_progress: { label: 'In Progress', variant: 'warning' },
completed: { label: 'Completed', variant: 'success' },
archived: { label: 'Archived', variant: 'secondary' },
paused: { label: 'Paused', variant: 'default' },
planning: { variant: 'info' },
in_progress: { variant: 'warning' },
completed: { variant: 'success' },
archived: { variant: 'secondary' },
paused: { variant: 'default' },
};
// Status label keys for i18n
const statusLabelKeys: Record<SessionMetadata['status'], string> = {
planning: 'sessions.status.planning',
in_progress: 'sessions.status.inProgress',
completed: 'sessions.status.completed',
archived: 'sessions.status.archived',
paused: 'sessions.status.paused',
};
/**
@@ -116,10 +126,14 @@ export function SessionCard({
showActions = true,
actionsDisabled = false,
}: SessionCardProps) {
const { label: statusLabel, variant: statusVariant } = statusConfig[session.status] || {
label: 'Unknown',
const { formatMessage } = useIntl();
const { variant: statusVariant } = statusVariantConfig[session.status] || {
variant: 'default' as const,
};
const statusLabel = statusLabelKeys[session.status]
? formatMessage({ id: statusLabelKeys[session.status] })
: formatMessage({ id: 'common.status.unknown' });
const progress = calculateProgress(session.tasks);
const isPlanning = session.status === 'planning';
@@ -186,20 +200,20 @@ export function SessionCard({
disabled={actionsDisabled}
>
<MoreVertical className="h-4 w-4" />
<span className="sr-only">Actions</span>
<span className="sr-only">{formatMessage({ id: 'common.aria.actions' })}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={(e) => handleAction(e, 'view')}>
<Eye className="mr-2 h-4 w-4" />
View Details
{formatMessage({ id: 'sessions.actions.viewDetails' })}
</DropdownMenuItem>
{!isArchived && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={(e) => handleAction(e, 'archive')}>
<Archive className="mr-2 h-4 w-4" />
Archive
{formatMessage({ id: 'sessions.actions.archive' })}
</DropdownMenuItem>
</>
)}
@@ -209,7 +223,7 @@ export function SessionCard({
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
{formatMessage({ id: 'sessions.actions.delete' })}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -225,7 +239,7 @@ export function SessionCard({
</span>
<span className="flex items-center gap-1">
<ListChecks className="h-3.5 w-3.5" />
{progress.total} tasks
{progress.total} {formatMessage({ id: 'sessions.card.tasks' })}
</span>
</div>
@@ -233,7 +247,7 @@ export function SessionCard({
{progress.total > 0 && !isPlanning && (
<div className="mt-3">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground">Progress</span>
<span className="text-muted-foreground">{formatMessage({ id: 'sessions.card.progress' })}</span>
<span className="text-card-foreground font-medium">
{progress.completed}/{progress.total} ({progress.percentage}%)
</span>

View File

@@ -4,6 +4,7 @@
// Card component for displaying skills with enable/disable toggle
import { useState } from 'react';
import { useIntl } from 'react-intl';
import {
Sparkles,
MoreVertical,
@@ -36,17 +37,29 @@ export interface SkillCardProps {
// ========== Source Badge ==========
const sourceConfig: Record<NonNullable<Skill['source']>, { color: string; label: string }> = {
builtin: { color: 'default', label: 'Built-in' },
custom: { color: 'secondary', label: 'Custom' },
community: { color: 'outline', label: 'Community' },
// Source color configuration (without labels for i18n)
const sourceColorConfig: Record<NonNullable<Skill['source']>, { color: string }> = {
builtin: { color: 'default' },
custom: { color: 'secondary' },
community: { color: 'outline' },
};
// Source label keys for i18n
const sourceLabelKeys: Record<NonNullable<Skill['source']>, string> = {
builtin: 'skills.source.builtin',
custom: 'skills.source.custom',
community: 'skills.source.community',
};
export function SourceBadge({ source }: { source?: Skill['source'] }) {
const config = sourceConfig[source ?? 'builtin'];
const { formatMessage } = useIntl();
const config = sourceColorConfig[source ?? 'builtin'];
const label = sourceLabelKeys[source ?? 'builtin']
? formatMessage({ id: sourceLabelKeys[source ?? 'builtin'] })
: source ?? 'builtin';
return (
<Badge variant={config.color as 'default' | 'secondary' | 'destructive' | 'outline'}>
{config.label}
{label}
</Badge>
);
}
@@ -63,6 +76,7 @@ export function SkillCard({
showActions = true,
isToggling = false,
}: SkillCardProps) {
const { formatMessage } = useIntl();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const handleClick = () => {
@@ -108,12 +122,12 @@ export function SkillCard({
{skill.enabled ? (
<>
<Power className="w-3 h-3 mr-1" />
On
{formatMessage({ id: 'skills.state.on' })}
</>
) : (
<>
<PowerOff className="w-3 h-3 mr-1" />
Off
{formatMessage({ id: 'skills.state.off' })}
</>
)}
</Button>
@@ -162,22 +176,22 @@ export function SkillCard({
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onClick?.(skill)}>
<Info className="w-4 h-4 mr-2" />
View Details
{formatMessage({ id: 'skills.actions.viewDetails' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleConfigure}>
<Settings className="w-4 h-4 mr-2" />
Configure
{formatMessage({ id: 'skills.actions.configure' })}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleToggle}>
{skill.enabled ? (
<>
<PowerOff className="w-4 h-4 mr-2" />
Disable
{formatMessage({ id: 'skills.actions.disable' })}
</>
) : (
<>
<Power className="w-4 h-4 mr-2" />
Enable
{formatMessage({ id: 'skills.actions.enable' })}
</>
)}
</DropdownMenuItem>
@@ -196,7 +210,7 @@ export function SkillCard({
<div className="mt-3">
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<Tag className="w-3 h-3" />
Triggers
{formatMessage({ id: 'skills.card.triggers' })}
</div>
<div className="flex flex-wrap gap-1">
{skill.triggers.slice(0, 4).map((trigger) => (
@@ -232,12 +246,12 @@ export function SkillCard({
{skill.enabled ? (
<>
<Power className="w-4 h-4 mr-1" />
Enabled
{formatMessage({ id: 'skills.state.enabled' })}
</>
) : (
<>
<PowerOff className="w-4 h-4 mr-1" />
Disabled
{formatMessage({ id: 'skills.state.disabled' })}
</>
)}
</Button>

View File

@@ -119,3 +119,50 @@ export type {
UseUpdateMemoryReturn,
UseDeleteMemoryReturn,
} from './useMemory';
// ========== MCP Servers ==========
export {
useMcpServers,
useUpdateMcpServer,
useCreateMcpServer,
useDeleteMcpServer,
useToggleMcpServer,
useMcpServerMutations,
mcpServersKeys,
} from './useMcpServers';
export type {
UseMcpServersOptions,
UseMcpServersReturn,
UseUpdateMcpServerReturn,
UseCreateMcpServerReturn,
UseDeleteMcpServerReturn,
UseToggleMcpServerReturn,
} from './useMcpServers';
// ========== CLI ==========
export {
useCliEndpoints,
useToggleCliEndpoint,
cliEndpointsKeys,
useCliInstallations,
useInstallCliTool,
useUninstallCliTool,
useUpgradeCliTool,
cliInstallationsKeys,
useHooks,
useToggleHook,
hooksKeys,
useRules,
useToggleRule,
rulesKeys,
} from './useCli';
export type {
UseCliEndpointsOptions,
UseCliEndpointsReturn,
UseCliInstallationsOptions,
UseCliInstallationsReturn,
UseHooksOptions,
UseHooksReturn,
UseRulesOptions,
UseRulesReturn,
} from './useCli';

View File

@@ -0,0 +1,448 @@
// ========================================
// useCliEndpoints Hook
// ========================================
// TanStack Query hooks for CLI endpoint management
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
fetchCliEndpoints,
toggleCliEndpoint,
type CliEndpoint,
type CliEndpointsResponse,
} from '../lib/api';
// Query key factory
export const cliEndpointsKeys = {
all: ['cliEndpoints'] as const,
lists: () => [...cliEndpointsKeys.all, 'list'] as const,
};
const STALE_TIME = 2 * 60 * 1000;
export interface UseCliEndpointsOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseCliEndpointsReturn {
endpoints: CliEndpoint[];
litellmEndpoints: CliEndpoint[];
customEndpoints: CliEndpoint[];
wrapperEndpoints: CliEndpoint[];
totalCount: number;
enabledCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useCliEndpoints(options: UseCliEndpointsOptions = {}): UseCliEndpointsReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: cliEndpointsKeys.lists(),
queryFn: fetchCliEndpoints,
staleTime,
enabled,
retry: 2,
});
const endpoints = query.data?.endpoints ?? [];
const enabledEndpoints = endpoints.filter((e) => e.enabled);
const litellmEndpoints = endpoints.filter((e) => e.type === 'litellm');
const customEndpoints = endpoints.filter((e) => e.type === 'custom');
const wrapperEndpoints = endpoints.filter((e) => e.type === 'wrapper');
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: cliEndpointsKeys.all });
};
return {
endpoints,
litellmEndpoints,
customEndpoints,
wrapperEndpoints,
totalCount: endpoints.length,
enabledCount: enabledEndpoints.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useToggleCliEndpoint() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ endpointId, enabled }: { endpointId: string; enabled: boolean }) =>
toggleCliEndpoint(endpointId, enabled),
onMutate: async ({ endpointId, enabled }) => {
await queryClient.cancelQueries({ queryKey: cliEndpointsKeys.all });
const previousEndpoints = queryClient.getQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists());
queryClient.setQueryData<CliEndpointsResponse>(cliEndpointsKeys.lists(), (old) => {
if (!old) return old;
return {
endpoints: old.endpoints.map((e) => (e.id === endpointId ? { ...e, enabled } : e)),
};
});
return { previousEndpoints };
},
onError: (_error, _vars, context) => {
if (context?.previousEndpoints) {
queryClient.setQueryData(cliEndpointsKeys.lists(), context.previousEndpoints);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: cliEndpointsKeys.all });
},
});
return {
toggleEndpoint: (endpointId: string, enabled: boolean) => mutation.mutateAsync({ endpointId, enabled }),
isToggling: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// useCliInstallations Hook
// ========================================
import {
fetchCliInstallations,
installCliTool,
uninstallCliTool,
upgradeCliTool,
type CliInstallation,
} from '../lib/api';
export const cliInstallationsKeys = {
all: ['cliInstallations'] as const,
lists: () => [...cliInstallationsKeys.all, 'list'] as const,
};
export interface UseCliInstallationsOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseCliInstallationsReturn {
installations: CliInstallation[];
installedTools: CliInstallation[];
totalCount: number;
installedCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useCliInstallations(options: UseCliInstallationsOptions = {}): UseCliInstallationsReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: cliInstallationsKeys.lists(),
queryFn: fetchCliInstallations,
staleTime,
enabled,
retry: 2,
});
const installations = query.data?.tools ?? [];
const installedTools = installations.filter((t) => t.installed);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: cliInstallationsKeys.all });
};
return {
installations,
installedTools,
totalCount: installations.length,
installedCount: installedTools.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useInstallCliTool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (toolName: string) => installCliTool(toolName),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: cliInstallationsKeys.all });
},
});
return {
installTool: mutation.mutateAsync,
isInstalling: mutation.isPending,
error: mutation.error,
};
}
export function useUninstallCliTool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (toolName: string) => uninstallCliTool(toolName),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: cliInstallationsKeys.all });
},
});
return {
uninstallTool: mutation.mutateAsync,
isUninstalling: mutation.isPending,
error: mutation.error,
};
}
export function useUpgradeCliTool() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (toolName: string) => upgradeCliTool(toolName),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: cliInstallationsKeys.all });
},
});
return {
upgradeTool: mutation.mutateAsync,
isUpgrading: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// useHooks Hook
// ========================================
import {
fetchHooks,
toggleHook,
type Hook,
type HooksResponse,
} from '../lib/api';
export const hooksKeys = {
all: ['hooks'] as const,
lists: () => [...hooksKeys.all, 'list'] as const,
};
export interface UseHooksOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseHooksReturn {
hooks: Hook[];
enabledHooks: Hook[];
totalCount: number;
enabledCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useHooks(options: UseHooksOptions = {}): UseHooksReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: hooksKeys.lists(),
queryFn: fetchHooks,
staleTime,
enabled,
retry: 2,
});
const hooks = query.data?.hooks ?? [];
const enabledHooks = hooks.filter((h) => h.enabled);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: hooksKeys.all });
};
return {
hooks,
enabledHooks,
totalCount: hooks.length,
enabledCount: enabledHooks.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useToggleHook() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ hookName, enabled }: { hookName: string; enabled: boolean }) =>
toggleHook(hookName, enabled),
onMutate: async ({ hookName, enabled }) => {
await queryClient.cancelQueries({ queryKey: hooksKeys.all });
const previousHooks = queryClient.getQueryData<HooksResponse>(hooksKeys.lists());
queryClient.setQueryData<HooksResponse>(hooksKeys.lists(), (old) => {
if (!old) return old;
return {
hooks: old.hooks.map((h) => (h.name === hookName ? { ...h, enabled } : h)),
};
});
return { previousHooks };
},
onError: (_error, _vars, context) => {
if (context?.previousHooks) {
queryClient.setQueryData(hooksKeys.lists(), context.previousHooks);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: hooksKeys.all });
},
});
return {
toggleHook: (hookName: string, enabled: boolean) => mutation.mutateAsync({ hookName, enabled }),
isToggling: mutation.isPending,
error: mutation.error,
};
}
// ========================================
// useRules Hook
// ========================================
import {
fetchRules,
toggleRule,
type Rule,
type RulesResponse,
} from '../lib/api';
export const rulesKeys = {
all: ['rules'] as const,
lists: () => [...rulesKeys.all, 'list'] as const,
};
export interface UseRulesOptions {
staleTime?: number;
enabled?: boolean;
}
export interface UseRulesReturn {
rules: Rule[];
enabledRules: Rule[];
totalCount: number;
enabledCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
export function useRules(options: UseRulesOptions = {}): UseRulesReturn {
const { staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: rulesKeys.lists(),
queryFn: fetchRules,
staleTime,
enabled,
retry: 2,
});
const rules = query.data?.rules ?? [];
const enabledRules = rules.filter((r) => r.enabled);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: rulesKeys.all });
};
return {
rules,
enabledRules,
totalCount: rules.length,
enabledCount: enabledRules.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
export function useToggleRule() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ ruleId, enabled }: { ruleId: string; enabled: boolean }) =>
toggleRule(ruleId, enabled),
onMutate: async ({ ruleId, enabled }) => {
await queryClient.cancelQueries({ queryKey: rulesKeys.all });
const previousRules = queryClient.getQueryData<RulesResponse>(rulesKeys.lists());
queryClient.setQueryData<RulesResponse>(rulesKeys.lists(), (old) => {
if (!old) return old;
return {
rules: old.rules.map((r) => (r.id === ruleId ? { ...r, enabled } : r)),
};
});
return { previousRules };
},
onError: (_error, _vars, context) => {
if (context?.previousRules) {
queryClient.setQueryData(rulesKeys.lists(), context.previousRules);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: rulesKeys.all });
},
});
return {
toggleRule: (ruleId: string, enabled: boolean) => mutation.mutateAsync({ ruleId, enabled }),
isToggling: mutation.isPending,
error: mutation.error,
};
}

View File

@@ -0,0 +1,148 @@
// ========================================
// useHistory Hook
// ========================================
// TanStack Query hook for CLI execution history
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
fetchHistory,
deleteExecution,
deleteExecutionsByTool,
deleteAllHistory,
type HistoryResponse,
} from '../lib/api';
// Query key factory
export const historyKeys = {
all: ['history'] as const,
lists: () => [...historyKeys.all, 'list'] as const,
list: (filter?: HistoryFilter) => [...historyKeys.lists(), filter] as const,
};
export interface HistoryFilter {
search?: string;
tool?: string;
}
// Default stale time: 30 seconds
const STALE_TIME = 30 * 1000;
export interface UseHistoryOptions {
/** Filter options */
filter?: HistoryFilter;
/** Override default stale time (ms) */
staleTime?: number;
/** Enable/disable the query */
enabled?: boolean;
}
export interface UseHistoryReturn {
/** All executions data */
executions: HistoryResponse['executions'];
/** Loading state for initial fetch */
isLoading: boolean;
/** Fetching state (initial or refetch) */
isFetching: boolean;
/** Error object if query failed */
error: Error | null;
/** Manually refetch data */
refetch: () => Promise<void>;
/** Delete a single execution */
deleteExecution: (id: string) => Promise<void>;
/** Delete executions by tool */
deleteByTool: (tool: string) => Promise<void>;
/** Delete all history */
deleteAll: () => Promise<void>;
/** Is any mutation in progress */
isDeleting: boolean;
}
/**
* Hook for fetching CLI execution history
*
* @example
* ```tsx
* const { executions, isLoading, deleteExecution } = useHistory();
* ```
*/
export function useHistory(options: UseHistoryOptions = {}): UseHistoryReturn {
const { filter, staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: historyKeys.list(filter),
queryFn: fetchHistory,
staleTime,
enabled,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
// Apply client-side filtering
const executions = React.useMemo(() => {
let executions = query.data?.executions ?? [];
// Apply search filter
if (filter?.search) {
const searchLower = filter.search.toLowerCase();
executions = executions.filter(
(exec) =>
exec.prompt_preview.toLowerCase().includes(searchLower) ||
exec.tool.toLowerCase().includes(searchLower)
);
}
// Apply tool filter
if (filter?.tool) {
executions = executions.filter((exec) => exec.tool === filter.tool);
}
return executions;
}, [query.data, filter]);
const refetch = async () => {
await query.refetch();
};
// Delete single execution
const deleteSingleMutation = useMutation({
mutationFn: deleteExecution,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: historyKeys.all });
},
});
// Delete by tool
const deleteByToolMutation = useMutation({
mutationFn: deleteExecutionsByTool,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: historyKeys.all });
},
});
// Delete all
const deleteAllMutation = useMutation({
mutationFn: deleteAllHistory,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: historyKeys.all });
},
});
const isDeleting =
deleteSingleMutation.isPending ||
deleteByToolMutation.isPending ||
deleteAllMutation.isPending;
return {
executions,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
deleteExecution: deleteSingleMutation.mutateAsync,
deleteByTool: deleteByToolMutation.mutateAsync,
deleteAll: deleteAllMutation.mutateAsync,
isDeleting,
};
}

View File

@@ -125,7 +125,10 @@ export function useIssues(options: UseIssuesOptions = {}): UseIssuesReturn {
};
for (const issue of allIssues) {
issuesByStatus[issue.status].push(issue);
// Defensive check: only push if the status key exists
if (issue.status in issuesByStatus) {
issuesByStatus[issue.status].push(issue);
}
}
// Group by priority
@@ -137,7 +140,10 @@ export function useIssues(options: UseIssuesOptions = {}): UseIssuesReturn {
};
for (const issue of allIssues) {
issuesByPriority[issue.priority].push(issue);
// Defensive check: only push if the priority key exists
if (issue.priority in issuesByPriority) {
issuesByPriority[issue.priority].push(issue);
}
}
const refetch = async () => {

View File

@@ -0,0 +1,98 @@
// ========================================
// useLiteTasks Hook
// ========================================
// Custom hook for fetching and managing lite tasks data
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { fetchLiteTasks, fetchLiteTaskSession, type LiteTaskSession, type LiteTasksResponse } from '@/lib/api';
type LiteTaskType = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
interface UseLiteTasksOptions {
enabled?: boolean;
refetchInterval?: number;
}
/**
* Hook for fetching all lite tasks sessions
*/
export function useLiteTasks(options: UseLiteTasksOptions = {}) {
const queryClient = useQueryClient();
const {
data = { litePlan: [], liteFix: [], multiCliPlan: [] },
isLoading,
error,
refetch,
} = useQuery<LiteTasksResponse>({
queryKey: ['liteTasks'],
queryFn: fetchLiteTasks,
staleTime: 30000,
refetchInterval: options.refetchInterval,
enabled: options.enabled ?? true,
});
// Get all sessions flattened
const allSessions = [
...(data.litePlan || []).map(s => ({ ...s, _type: 'lite-plan' as LiteTaskType })),
...(data.liteFix || []).map(s => ({ ...s, _type: 'lite-fix' as LiteTaskType })),
...(data.multiCliPlan || []).map(s => ({ ...s, _type: 'multi-cli-plan' as LiteTaskType })),
];
// Get sessions by type
const getSessionsByType = (type: LiteTaskType): LiteTaskSession[] => {
switch (type) {
case 'lite-plan':
return data.litePlan || [];
case 'lite-fix':
return data.liteFix || [];
case 'multi-cli-plan':
return data.multiCliPlan || [];
}
};
// Prefetch a specific session
const prefetchSession = (sessionId: string, type: LiteTaskType) => {
queryClient.prefetchQuery({
queryKey: ['liteTask', sessionId, type],
queryFn: () => fetchLiteTaskSession(sessionId, type),
staleTime: 60000,
});
};
return {
litePlan: data.litePlan || [],
liteFix: data.liteFix || [],
multiCliPlan: data.multiCliPlan || [],
allSessions,
getSessionsByType,
prefetchSession,
isLoading,
error,
refetch,
};
}
/**
* Hook for fetching a single lite task session
*/
export function useLiteTaskSession(sessionId: string | undefined, type: LiteTaskType) {
const {
data: session,
isLoading,
error,
refetch,
} = useQuery<LiteTaskSession | null>({
queryKey: ['liteTask', sessionId, type],
queryFn: () => (sessionId ? fetchLiteTaskSession(sessionId, type) : Promise.resolve(null)),
enabled: !!sessionId && !!type,
staleTime: 60000,
});
return {
session,
isLoading,
error,
refetch,
};
}

View File

@@ -0,0 +1,149 @@
// ========================================
// useLocale Hook Tests
// ========================================
// Tests for the useLocale hook
import { describe, it, expect, beforeEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useLocale } from './useLocale';
import { useAppStore } from '../stores/appStore';
describe('useLocale Hook', () => {
beforeEach(() => {
// Reset store state before each test
useAppStore.setState({ locale: 'en' });
});
describe('returns current locale', () => {
it('should return current locale from store', () => {
useAppStore.setState({ locale: 'en' });
const { result } = renderHook(() => useLocale());
expect(result.current.locale).toBe('en');
});
it('should return zh when locale is Chinese', () => {
useAppStore.setState({ locale: 'zh' });
const { result } = renderHook(() => useLocale());
expect(result.current.locale).toBe('zh');
});
});
describe('returns setLocale function', () => {
it('should provide setLocale function', () => {
const { result } = renderHook(() => useLocale());
expect(typeof result.current.setLocale).toBe('function');
});
it('should update locale when setLocale is called', () => {
const { result } = renderHook(() => useLocale());
act(() => {
result.current.setLocale('zh');
});
expect(result.current.locale).toBe('zh');
});
it('should persist locale change to store', () => {
const { result } = renderHook(() => useLocale());
act(() => {
result.current.setLocale('zh');
});
const storeLocale = useAppStore.getState().locale;
expect(storeLocale).toBe('zh');
});
});
describe('returns available locales', () => {
it('should provide availableLocales object', () => {
const { result } = renderHook(() => useLocale());
expect(typeof result.current.availableLocales).toBe('object');
expect(result.current.availableLocales).toBeDefined();
});
it('should include English in available locales', () => {
const { result } = renderHook(() => useLocale());
expect(result.current.availableLocales.en).toBe('English');
});
it('should include Chinese in available locales', () => {
const { result } = renderHook(() => useLocale());
expect(result.current.availableLocales.zh).toBe('中文');
});
});
describe('locale switching behavior', () => {
it('should switch between en and zh', () => {
const { result } = renderHook(() => useLocale());
expect(result.current.locale).toBe('en');
act(() => {
result.current.setLocale('zh');
});
expect(result.current.locale).toBe('zh');
act(() => {
result.current.setLocale('en');
});
expect(result.current.locale).toBe('en');
});
});
describe('return type integrity', () => {
it('should match UseLocaleReturn interface', () => {
const { result } = renderHook(() => useLocale());
expect(result.current).toHaveProperty('locale');
expect(result.current).toHaveProperty('setLocale');
expect(result.current).toHaveProperty('availableLocales');
});
it('should have correct types for properties', () => {
const { result } = renderHook(() => useLocale());
// locale should be 'en' or 'zh'
expect(['en', 'zh']).toContain(result.current.locale);
// setLocale should be a function
expect(typeof result.current.setLocale).toBe('function');
// availableLocales should be a record
expect(typeof result.current.availableLocales).toBe('object');
});
});
describe('integration with appStore', () => {
it('should reflect store changes in hook output', () => {
const { result } = renderHook(() => useLocale());
act(() => {
useAppStore.getState().setLocale('zh');
});
expect(result.current.locale).toBe('zh');
});
it('should update store when setLocale is called', () => {
const { result } = renderHook(() => useLocale());
act(() => {
result.current.setLocale('zh');
});
expect(useAppStore.getState().locale).toBe('zh');
});
});
});

View File

@@ -0,0 +1,53 @@
// ========================================
// useLocale Hook
// ========================================
// Convenient hook for locale management
import { useCallback } from 'react';
import { useAppStore, selectLocale } from '../stores/appStore';
import type { Locale } from '../types/store';
import { availableLocales } from '../lib/i18n';
export interface UseLocaleReturn {
/** Current locale ('en' or 'zh') */
locale: Locale;
/** Set locale preference */
setLocale: (locale: Locale) => void;
/** Available locales with display names */
availableLocales: Record<Locale, string>;
}
/**
* Hook for managing locale state
* @returns Locale state and actions
*
* @example
* ```tsx
* const { locale, setLocale, availableLocales } = useLocale();
*
* return (
* <select value={locale} onChange={(e) => setLocale(e.target.value as Locale)}>
* {Object.entries(availableLocales).map(([key, label]) => (
* <option key={key} value={key}>{label}</option>
* ))}
* </select>
* );
* ```
*/
export function useLocale(): UseLocaleReturn {
const locale = useAppStore(selectLocale);
const setLocaleAction = useAppStore((state) => state.setLocale);
const setLocale = useCallback(
(newLocale: Locale) => {
setLocaleAction(newLocale);
},
[setLocaleAction]
);
return {
locale,
setLocale,
availableLocales,
};
}

View File

@@ -0,0 +1,227 @@
// ========================================
// useMcpServers Hook
// ========================================
// TanStack Query hooks for MCP server management
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
fetchMcpServers,
updateMcpServer,
createMcpServer,
deleteMcpServer,
toggleMcpServer,
type McpServer,
type McpServersResponse,
} from '../lib/api';
// Query key factory
export const mcpServersKeys = {
all: ['mcpServers'] as const,
lists: () => [...mcpServersKeys.all, 'list'] as const,
list: (scope?: 'project' | 'global') => [...mcpServersKeys.lists(), scope] as const,
};
// Default stale time: 2 minutes (MCP servers change occasionally)
const STALE_TIME = 2 * 60 * 1000;
export interface UseMcpServersOptions {
scope?: 'project' | 'global';
staleTime?: number;
enabled?: boolean;
}
export interface UseMcpServersReturn {
servers: McpServer[];
projectServers: McpServer[];
globalServers: McpServer[];
totalCount: number;
enabledCount: number;
isLoading: boolean;
isFetching: boolean;
error: Error | null;
refetch: () => Promise<void>;
invalidate: () => Promise<void>;
}
/**
* Hook for fetching MCP servers
*/
export function useMcpServers(options: UseMcpServersOptions = {}): UseMcpServersReturn {
const { scope, staleTime = STALE_TIME, enabled = true } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: mcpServersKeys.list(scope),
queryFn: fetchMcpServers,
staleTime,
enabled,
retry: 2,
});
const projectServers = query.data?.project ?? [];
const globalServers = query.data?.global ?? [];
const allServers = scope === 'project' ? projectServers : scope === 'global' ? globalServers : [...projectServers, ...globalServers];
const enabledServers = allServers.filter((s) => s.enabled);
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
};
return {
servers: allServers,
projectServers,
globalServers,
totalCount: allServers.length,
enabledCount: enabledServers.length,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
// ========== Mutations ==========
export interface UseUpdateMcpServerReturn {
updateServer: (serverName: string, config: Partial<McpServer>) => Promise<McpServer>;
isUpdating: boolean;
error: Error | null;
}
export function useUpdateMcpServer(): UseUpdateMcpServerReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ serverName, config }: { serverName: string; config: Partial<McpServer> }) =>
updateMcpServer(serverName, config),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
},
});
return {
updateServer: (serverName, config) => mutation.mutateAsync({ serverName, config }),
isUpdating: mutation.isPending,
error: mutation.error,
};
}
export interface UseCreateMcpServerReturn {
createServer: (server: Omit<McpServer, 'name'>) => Promise<McpServer>;
isCreating: boolean;
error: Error | null;
}
export function useCreateMcpServer(): UseCreateMcpServerReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (server: Omit<McpServer, 'name'>) => createMcpServer(server),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
},
});
return {
createServer: mutation.mutateAsync,
isCreating: mutation.isPending,
error: mutation.error,
};
}
export interface UseDeleteMcpServerReturn {
deleteServer: (serverName: string) => Promise<void>;
isDeleting: boolean;
error: Error | null;
}
export function useDeleteMcpServer(): UseDeleteMcpServerReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (serverName: string) => deleteMcpServer(serverName),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
},
});
return {
deleteServer: mutation.mutateAsync,
isDeleting: mutation.isPending,
error: mutation.error,
};
}
export interface UseToggleMcpServerReturn {
toggleServer: (serverName: string, enabled: boolean) => Promise<McpServer>;
isToggling: boolean;
error: Error | null;
}
export function useToggleMcpServer(): UseToggleMcpServerReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ serverName, enabled }: { serverName: string; enabled: boolean }) =>
toggleMcpServer(serverName, enabled),
onMutate: async ({ serverName, enabled }) => {
await queryClient.cancelQueries({ queryKey: mcpServersKeys.all });
const previousServers = queryClient.getQueryData<McpServersResponse>(mcpServersKeys.list());
// Optimistic update
queryClient.setQueryData<McpServersResponse>(mcpServersKeys.list(), (old) => {
if (!old) return old;
const updateServer = (servers: McpServer[]) =>
servers.map((s) => (s.name === serverName ? { ...s, enabled } : s));
return {
project: updateServer(old.project),
global: updateServer(old.global),
};
});
return { previousServers };
},
onError: (_error, _vars, context) => {
if (context?.previousServers) {
queryClient.setQueryData(mcpServersKeys.list(), context.previousServers);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: mcpServersKeys.all });
},
});
return {
toggleServer: (serverName, enabled) => mutation.mutateAsync({ serverName, enabled }),
isToggling: mutation.isPending,
error: mutation.error,
};
}
/**
* Combined hook for all MCP server mutations
*/
export function useMcpServerMutations() {
const update = useUpdateMcpServer();
const create = useCreateMcpServer();
const remove = useDeleteMcpServer();
const toggle = useToggleMcpServer();
return {
updateServer: update.updateServer,
isUpdating: update.isUpdating,
createServer: create.createServer,
isCreating: create.isCreating,
deleteServer: remove.deleteServer,
isDeleting: remove.isDeleting,
toggleServer: toggle.toggleServer,
isToggling: toggle.isToggling,
isMutating: update.isUpdating || create.isCreating || remove.isDeleting || toggle.isToggling,
};
}

View File

@@ -0,0 +1,51 @@
// ========================================
// useProjectOverview Hook
// ========================================
// TanStack Query hook for project overview data
import { useQuery } from '@tanstack/react-query';
import { fetchProjectOverview } from '../lib/api';
// Query key factory
export const projectOverviewKeys = {
all: ['projectOverview'] as const,
detail: (path?: string) => [...projectOverviewKeys.all, 'detail', path] as const,
};
// Default stale time: 5 minutes
const STALE_TIME = 5 * 60 * 1000;
export interface UseProjectOverviewOptions {
/** Override default stale time (ms) */
staleTime?: number;
/** Enable/disable the query */
enabled?: boolean;
}
/**
* Hook for fetching project overview data
*
* @example
* ```tsx
* const { projectOverview, isLoading } = useProjectOverview();
* ```
*/
export function useProjectOverview(options: UseProjectOverviewOptions = {}) {
const { staleTime = STALE_TIME, enabled = true } = options;
const query = useQuery({
queryKey: projectOverviewKeys.detail(),
queryFn: fetchProjectOverview,
staleTime,
enabled,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
return {
projectOverview: query.data,
isLoading: query.isLoading,
error: query.error,
refetch: query.refetch,
};
}

View File

@@ -0,0 +1,101 @@
// ========================================
// useReviewSession Hook
// ========================================
// Custom hook for fetching and managing review session data
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { fetchReviewSessions, fetchReviewSession, type ReviewSession, type ReviewFinding } from '@/lib/api';
interface UseReviewSessionsOptions {
enabled?: boolean;
refetchInterval?: number;
}
/**
* Hook for fetching all review sessions
*/
export function useReviewSessions(options: UseReviewSessionsOptions = {}) {
const queryClient = useQueryClient();
const {
data = [],
isLoading,
error,
refetch,
} = useQuery<ReviewSession[]>({
queryKey: ['reviewSessions'],
queryFn: fetchReviewSessions,
staleTime: 30000,
refetchInterval: options.refetchInterval,
enabled: options.enabled ?? true,
});
// Prefetch a specific session
const prefetchSession = (sessionId: string) => {
queryClient.prefetchQuery({
queryKey: ['reviewSession', sessionId],
queryFn: () => fetchReviewSession(sessionId),
staleTime: 60000,
});
};
return {
reviewSessions: data,
isLoading,
error,
refetch,
prefetchSession,
};
}
/**
* Hook for fetching a single review session
*/
export function useReviewSession(sessionId: string | undefined) {
const {
data: reviewSession,
isLoading,
error,
refetch,
} = useQuery<ReviewSession | null>({
queryKey: ['reviewSession', sessionId],
queryFn: () => (sessionId ? fetchReviewSession(sessionId) : Promise.resolve(null)),
enabled: !!sessionId,
staleTime: 60000,
});
// Flatten findings with dimension info
const flattenedFindings = React.useMemo(() => {
if (!reviewSession?.reviewDimensions) return [];
const findings: Array<ReviewFinding & { dimension: string }> = [];
reviewSession.reviewDimensions.forEach(dim => {
(dim.findings || []).forEach(f => {
findings.push({ ...f, dimension: dim.name });
});
});
return findings;
}, [reviewSession]);
// Get severity counts
const severityCounts = React.useMemo(() => {
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
flattenedFindings.forEach(f => {
const sev = (f.severity || 'medium').toLowerCase() as keyof typeof counts;
if (counts[sev] !== undefined) {
counts[sev]++;
}
});
return counts;
}, [flattenedFindings]);
return {
reviewSession,
flattenedFindings,
severityCounts,
isLoading,
error,
refetch,
};
}
import React from 'react';

View File

@@ -0,0 +1,51 @@
// ========================================
// useSessionDetail Hook
// ========================================
// TanStack Query hook for session detail data
import { useQuery } from '@tanstack/react-query';
import { fetchSessionDetail } from '../lib/api';
// Query key factory
export const sessionDetailKeys = {
all: ['sessionDetail'] as const,
detail: (id: string) => [...sessionDetailKeys.all, 'detail', id] as const,
};
// Default stale time: 30 seconds
const STALE_TIME = 30 * 1000;
export interface UseSessionDetailOptions {
/** Override default stale time (ms) */
staleTime?: number;
/** Enable/disable the query */
enabled?: boolean;
}
/**
* Hook for fetching session detail data
*
* @example
* ```tsx
* const { sessionDetail, isLoading } = useSessionDetail(sessionId);
* ```
*/
export function useSessionDetail(sessionId: string, options: UseSessionDetailOptions = {}) {
const { staleTime = STALE_TIME, enabled = true } = options;
const query = useQuery({
queryKey: sessionDetailKeys.detail(sessionId),
queryFn: () => fetchSessionDetail(sessionId),
staleTime,
enabled: enabled && !!sessionId,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
return {
sessionDetail: query.data,
isLoading: query.isLoading,
error: query.error,
refetch: query.refetch,
};
}

View File

@@ -7,6 +7,62 @@ import type { SessionMetadata, TaskData } from '../types/store';
// ========== Types ==========
/**
* Raw backend session data structure matching the backend API response.
*
* @remarks
* This interface represents the exact schema returned by the backend `/api/data` endpoint.
* It is used internally during transformation to `SessionMetadata` in the frontend.
*
* **Field mappings to frontend SessionMetadata:**
* - `project` → `title` and `description` (split on ':' separator)
* - `status: 'active'` → `status: 'in_progress'` (other statuses remain unchanged)
* - `location` is added based on which array (activeSessions/archivedSessions) the data comes from
*
* **Backend schema location:** `ccw/src/data-aggregator.ts`
* **Transformation function:** {@link transformBackendSession}
* **Frontend type:** {@link SessionMetadata}
*
* @warning If backend schema changes, update this interface AND the transformation logic in {@link transformBackendSession}
*/
interface BackendSessionData {
session_id: string;
project?: string;
status: 'active' | 'completed' | 'archived' | 'planning' | 'paused';
type?: string;
created_at: string;
updated_at?: string;
[key: string]: unknown;
}
/**
* Dashboard statistics mapped from backend statistics response.
*
* @remarks
* This interface represents the frontend statistics type displayed on the dashboard.
* The data is extracted from the backend `/api/data` response's `statistics` field.
*
* **Backend response structure:**
* ```json
* {
* "statistics": {
* "totalSessions": number,
* "activeSessions": number,
* "archivedSessions": number,
* "totalTasks": number,
* "completedTasks": number,
* "pendingTasks": number,
* "failedTasks": number,
* "todayActivity": number
* }
* }
* ```
*
* **Mapping function:** {@link fetchDashboardStats}
* **Fallback:** Returns zero-initialized stats on error via {@link getEmptyDashboardStats}
*
* @see {@link fetchDashboardStats} for the transformation logic
*/
export interface DashboardStats {
totalSessions: number;
activeSessions: number;
@@ -92,8 +148,9 @@ async function fetchApi<T>(
const body = await response.json();
if (body.message) error.message = body.message;
if (body.code) error.code = body.code;
} catch {
// Ignore JSON parse errors
} catch (parseError) {
// Log parse errors instead of silently ignoring
console.warn('[API] Failed to parse error response:', parseError);
}
throw error;
@@ -104,7 +161,68 @@ async function fetchApi<T>(
return undefined as T;
}
return response.json();
// Wrap response.json() with try-catch for better error messages
try {
return await response.json();
} catch (parseError) {
const message = parseError instanceof Error ? parseError.message : 'Unknown error';
throw new Error(`Failed to parse JSON response: ${message}`);
}
}
// ========== Transformation Helpers ==========
/**
* Transform backend session data to frontend SessionMetadata interface
* Maps backend schema (project, status: 'active') to frontend schema (title, description, status: 'in_progress', location)
*
* @param backendSession - Raw session data from backend
* @param location - Whether this session is from active or archived list
* @returns Transformed SessionMetadata object
*/
function transformBackendSession(
backendSession: BackendSessionData,
location: 'active' | 'archived'
): SessionMetadata {
// Map backend 'active' status to frontend 'in_progress'
// Other statuses remain the same
const statusMap: Record<string, SessionMetadata['status']> = {
'active': 'in_progress',
'completed': 'completed',
'archived': 'archived',
'planning': 'planning',
'paused': 'paused',
};
const transformedStatus = statusMap[backendSession.status] || backendSession.status as SessionMetadata['status'];
// Extract title and description from project field
// Backend sends 'project' as a string, frontend expects 'title' and optional 'description'
let title = backendSession.project || backendSession.session_id;
let description: string | undefined;
if (backendSession.project && backendSession.project.includes(':')) {
const parts = backendSession.project.split(':');
title = parts[0].trim();
description = parts.slice(1).join(':').trim();
}
return {
session_id: backendSession.session_id,
title,
description,
status: transformedStatus,
created_at: backendSession.created_at,
updated_at: backendSession.updated_at,
location,
// Preserve additional fields if they exist
has_plan: (backendSession as unknown as { has_plan?: boolean }).has_plan,
plan_updated_at: (backendSession as unknown as { plan_updated_at?: string }).plan_updated_at,
has_review: (backendSession as unknown as { has_review?: boolean }).has_review,
review: (backendSession as unknown as { review?: SessionMetadata['review'] }).review,
summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries,
tasks: (backendSession as unknown as { tasks?: TaskData[] }).tasks,
};
}
// ========== Dashboard API ==========
@@ -113,18 +231,45 @@ async function fetchApi<T>(
* Fetch dashboard statistics
*/
export async function fetchDashboardStats(): Promise<DashboardStats> {
const data = await fetchApi<{ statistics?: DashboardStats }>('/api/data');
try {
const data = await fetchApi<{ statistics?: DashboardStats }>('/api/data');
// Extract statistics from response, with defaults
// Validate response structure
if (!data) {
console.warn('[API] No data received from /api/data for dashboard stats');
return getEmptyDashboardStats();
}
// Extract statistics from response, with defaults
return {
totalSessions: data.statistics?.totalSessions ?? 0,
activeSessions: data.statistics?.activeSessions ?? 0,
archivedSessions: data.statistics?.archivedSessions ?? 0,
totalTasks: data.statistics?.totalTasks ?? 0,
completedTasks: data.statistics?.completedTasks ?? 0,
pendingTasks: data.statistics?.pendingTasks ?? 0,
failedTasks: data.statistics?.failedTasks ?? 0,
todayActivity: data.statistics?.todayActivity ?? 0,
};
} catch (error) {
console.error('[API] Failed to fetch dashboard stats:', error);
return getEmptyDashboardStats();
}
}
/**
* Get empty dashboard stats with zero values
*/
function getEmptyDashboardStats(): DashboardStats {
return {
totalSessions: data.statistics?.totalSessions ?? 0,
activeSessions: data.statistics?.activeSessions ?? 0,
archivedSessions: data.statistics?.archivedSessions ?? 0,
totalTasks: data.statistics?.totalTasks ?? 0,
completedTasks: data.statistics?.completedTasks ?? 0,
pendingTasks: data.statistics?.pendingTasks ?? 0,
failedTasks: data.statistics?.failedTasks ?? 0,
todayActivity: data.statistics?.todayActivity ?? 0,
totalSessions: 0,
activeSessions: 0,
archivedSessions: 0,
totalTasks: 0,
completedTasks: 0,
pendingTasks: 0,
failedTasks: 0,
todayActivity: 0,
};
}
@@ -132,17 +277,61 @@ export async function fetchDashboardStats(): Promise<DashboardStats> {
/**
* Fetch all sessions (active and archived)
* Applies transformation layer to map backend data to frontend SessionMetadata interface
*/
export async function fetchSessions(): Promise<SessionsResponse> {
const data = await fetchApi<{
activeSessions?: SessionMetadata[];
archivedSessions?: SessionMetadata[];
}>('/api/data');
try {
const data = await fetchApi<{
activeSessions?: BackendSessionData[];
archivedSessions?: BackendSessionData[];
}>('/api/data');
return {
activeSessions: data.activeSessions ?? [],
archivedSessions: data.archivedSessions ?? [],
};
// Validate response structure
if (!data) {
console.warn('[API] No data received from /api/data for sessions');
return { activeSessions: [], archivedSessions: [] };
}
// Transform active sessions with location = 'active'
const activeSessions = (data.activeSessions ?? []).map((session) => {
try {
return transformBackendSession(session, 'active');
} catch (error) {
console.error('[API] Failed to transform active session:', session, error);
// Return a minimal valid session to prevent crashes
return {
session_id: session.session_id,
title: session.project || session.session_id,
status: 'in_progress' as const,
created_at: session.created_at,
location: 'active' as const,
};
}
});
// Transform archived sessions with location = 'archived'
const archivedSessions = (data.archivedSessions ?? []).map((session) => {
try {
return transformBackendSession(session, 'archived');
} catch (error) {
console.error('[API] Failed to transform archived session:', session, error);
// Return a minimal valid session to prevent crashes
return {
session_id: session.session_id,
title: session.project || session.session_id,
status: session.status === 'active' ? 'in_progress' : session.status as SessionMetadata['status'],
created_at: session.created_at,
location: 'archived' as const,
};
}
});
return { activeSessions, archivedSessions };
} catch (error) {
console.error('[API] Failed to fetch sessions:', error);
// Return empty arrays on error to prevent crashes
return { activeSessions: [], archivedSessions: [] };
}
}
/**
@@ -571,6 +760,177 @@ export async function deleteMemory(memoryId: string): Promise<void> {
});
}
// ========== Project Overview API ==========
export interface TechnologyStack {
languages: Array<{ name: string; file_count: number; primary?: boolean }>;
frameworks: string[];
build_tools: string[];
test_frameworks?: string[];
}
export interface Architecture {
style: string;
layers: string[];
patterns: string[];
}
export interface KeyComponent {
name: string;
description?: string;
importance: 'high' | 'medium' | 'low';
responsibility?: string[];
path?: string;
}
export interface DevelopmentIndexEntry {
title: string;
description?: string;
sessionId?: string;
sub_feature?: string;
status?: string;
tags?: string[];
archivedAt?: string;
date?: string;
implemented_at?: string;
}
export interface GuidelineEntry {
rule: string;
scope: string;
enforced_by?: string;
}
export interface LearningEntry {
insight: string;
category?: string;
session_id?: string;
context?: string;
date: string;
}
export interface ProjectGuidelines {
conventions?: Record<string, string[]>;
constraints?: Record<string, string[]>;
quality_rules?: GuidelineEntry[];
learnings?: LearningEntry[];
}
export interface ProjectOverviewMetadata {
analysis_mode?: string;
[key: string]: unknown;
}
export interface ProjectOverview {
projectName: string;
description?: string;
initializedAt: string;
technologyStack: TechnologyStack;
architecture: Architecture;
keyComponents: KeyComponent[];
developmentIndex?: {
feature?: DevelopmentIndexEntry[];
enhancement?: DevelopmentIndexEntry[];
bugfix?: DevelopmentIndexEntry[];
refactor?: DevelopmentIndexEntry[];
docs?: DevelopmentIndexEntry[];
[key: string]: DevelopmentIndexEntry[] | undefined;
};
guidelines?: ProjectGuidelines;
metadata?: ProjectOverviewMetadata;
}
/**
* Fetch project overview
*/
export async function fetchProjectOverview(): Promise<ProjectOverview | null> {
const data = await fetchApi<{ projectOverview?: ProjectOverview }>('/api/ccw');
return data.projectOverview ?? null;
}
// ========== Session Detail API ==========
export interface SessionDetailContext {
requirements?: string[];
focus_paths?: string[];
artifacts?: string[];
shared_context?: {
tech_stack?: string[];
conventions?: string[];
};
}
export interface SessionDetailResponse {
session: SessionMetadata;
context?: SessionDetailContext;
summary?: string;
implPlan?: unknown;
conflicts?: unknown[];
review?: unknown;
}
/**
* Fetch session detail
*/
export async function fetchSessionDetail(sessionId: string): Promise<SessionDetailResponse> {
return fetchApi<SessionDetailResponse>(`/api/sessions/${encodeURIComponent(sessionId)}/detail`);
}
// ========== History / CLI Execution API ==========
export interface CliExecution {
id: string;
tool: 'gemini' | 'qwen' | 'codex' | string;
mode?: string;
status: 'success' | 'error' | 'timeout';
prompt_preview: string;
timestamp: string;
duration_ms: number;
sourceDir?: string;
turn_count?: number;
}
export interface HistoryResponse {
executions: CliExecution[];
}
/**
* Fetch CLI execution history
*/
export async function fetchHistory(): Promise<HistoryResponse> {
const data = await fetchApi<{ executions?: CliExecution[] }>('/api/cli/history');
return {
executions: data.executions ?? [],
};
}
/**
* Delete a CLI execution record
*/
export async function deleteExecution(executionId: string): Promise<void> {
await fetchApi<void>(`/api/cli/history/${encodeURIComponent(executionId)}`, {
method: 'DELETE',
});
}
/**
* Delete CLI executions by tool
*/
export async function deleteExecutionsByTool(tool: string): Promise<void> {
await fetchApi<void>(`/api/cli/history/tool/${encodeURIComponent(tool)}`, {
method: 'DELETE',
});
}
/**
* Delete all CLI execution history
*/
export async function deleteAllHistory(): Promise<void> {
await fetchApi<void>('/api/cli/history', {
method: 'DELETE',
});
}
// ========== CLI Tools Config API ==========
export interface CliToolsConfigResponse {
@@ -602,3 +962,461 @@ export async function updateCliToolsConfig(
body: JSON.stringify(config),
});
}
// ========== Lite Tasks API ==========
export interface ImplementationStep {
step: number;
title?: string;
description?: string;
modification_points?: string[];
logic_flow?: string[];
depends_on?: number[];
output?: string;
}
export interface FlowControl {
pre_analysis?: Array<{
step: string;
action: string;
commands?: string[];
output_to: string;
on_error?: 'fail' | 'continue' | 'skip';
}>;
implementation_approach?: ImplementationStep[];
target_files?: string[];
}
export interface LiteTask {
id: string;
task_id?: string;
title?: string;
description?: string;
status: 'pending' | 'in_progress' | 'completed' | 'blocked' | 'failed';
priority?: string;
flow_control?: FlowControl;
meta?: {
type?: string;
scope?: string;
};
context?: {
focus_paths?: string[];
acceptance?: string[];
depends_on?: string[];
};
created_at?: string;
updated_at?: string;
}
export interface LiteTaskSession {
id: string;
session_id?: string;
type: 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
title?: string;
description?: string;
tasks?: LiteTask[];
metadata?: Record<string, unknown>;
latestSynthesis?: {
title?: string | { en?: string; zh?: string };
status?: string;
};
roundCount?: number;
status?: string;
createdAt?: string;
updatedAt?: string;
}
export interface LiteTasksResponse {
litePlan?: LiteTaskSession[];
liteFix?: LiteTaskSession[];
multiCliPlan?: LiteTaskSession[];
}
/**
* Fetch all lite tasks sessions
*/
export async function fetchLiteTasks(): Promise<LiteTasksResponse> {
const data = await fetchApi<{ liteTasks?: LiteTasksResponse }>('/api/data');
return data.liteTasks || {};
}
/**
* Fetch a single lite task session by ID
*/
export async function fetchLiteTaskSession(
sessionId: string,
type: 'lite-plan' | 'lite-fix' | 'multi-cli-plan'
): Promise<LiteTaskSession | null> {
const data = await fetchLiteTasks();
const sessions = type === 'lite-plan' ? (data.litePlan || []) :
type === 'lite-fix' ? (data.liteFix || []) :
(data.multiCliPlan || []);
return sessions.find(s => s.id === sessionId || s.session_id === sessionId) || null;
}
// ========== Review Session API ==========
export interface ReviewFinding {
id?: string;
title: string;
description?: string;
severity: 'critical' | 'high' | 'medium' | 'low';
category?: string;
file?: string;
line?: string;
code_context?: string;
snippet?: string;
recommendations?: string[];
recommendation?: string;
root_cause?: string;
impact?: string;
references?: string[];
metadata?: Record<string, unknown>;
fix_status?: string | null;
}
export interface ReviewDimension {
name: string;
findings: ReviewFinding[];
}
export interface ReviewSession {
session_id: string;
title?: string;
description?: string;
type: 'review';
phase?: string;
reviewDimensions?: ReviewDimension[];
_isActive?: boolean;
created_at?: string;
updated_at?: string;
status?: string;
}
export interface ReviewSessionsResponse {
reviewSessions?: ReviewSession[];
}
/**
* Fetch all review sessions
*/
export async function fetchReviewSessions(): Promise<ReviewSession[]> {
const data = await fetchApi<ReviewSessionsResponse>('/api/data');
return data.reviewSessions || [];
}
/**
* Fetch a single review session by ID
*/
export async function fetchReviewSession(sessionId: string): Promise<ReviewSession | null> {
const sessions = await fetchReviewSessions();
return sessions.find(s => s.session_id === sessionId) || null;
}
// ========== MCP API ==========
export interface McpServer {
name: string;
command: string;
args?: string[];
env?: Record<string, string>;
enabled: boolean;
scope: 'project' | 'global';
}
export interface McpServersResponse {
project: McpServer[];
global: McpServer[];
}
/**
* Fetch all MCP servers (project and global scope)
*/
export async function fetchMcpServers(): Promise<McpServersResponse> {
const data = await fetchApi<{ project?: McpServer[]; global?: McpServer[] }>('/api/mcp/servers');
return {
project: data.project ?? [],
global: data.global ?? [],
};
}
/**
* Update MCP server configuration
*/
export async function updateMcpServer(
serverName: string,
config: Partial<McpServer>
): Promise<McpServer> {
return fetchApi<McpServer>(`/api/mcp/servers/${encodeURIComponent(serverName)}`, {
method: 'PATCH',
body: JSON.stringify(config),
});
}
/**
* Create a new MCP server
*/
export async function createMcpServer(
server: Omit<McpServer, 'name'>
): Promise<McpServer> {
return fetchApi<McpServer>('/api/mcp/servers', {
method: 'POST',
body: JSON.stringify(server),
});
}
/**
* Delete an MCP server
*/
export async function deleteMcpServer(serverName: string): Promise<void> {
await fetchApi<void>(`/api/mcp/servers/${encodeURIComponent(serverName)}`, {
method: 'DELETE',
});
}
/**
* Toggle MCP server enabled status
*/
export async function toggleMcpServer(
serverName: string,
enabled: boolean
): Promise<McpServer> {
return fetchApi<McpServer>(`/api/mcp/servers/${encodeURIComponent(serverName)}/toggle`, {
method: 'POST',
body: JSON.stringify({ enabled }),
});
}
// ========== CLI Endpoints API ==========
export interface CliEndpoint {
id: string;
name: string;
type: 'litellm' | 'custom' | 'wrapper';
enabled: boolean;
config: Record<string, unknown>;
}
export interface CliEndpointsResponse {
endpoints: CliEndpoint[];
}
/**
* Fetch all CLI endpoints
*/
export async function fetchCliEndpoints(): Promise<CliEndpointsResponse> {
const data = await fetchApi<{ endpoints?: CliEndpoint[] }>('/api/cli/endpoints');
return {
endpoints: data.endpoints ?? [],
};
}
/**
* Update CLI endpoint configuration
*/
export async function updateCliEndpoint(
endpointId: string,
config: Partial<CliEndpoint>
): Promise<CliEndpoint> {
return fetchApi<CliEndpoint>(`/api/cli/endpoints/${encodeURIComponent(endpointId)}`, {
method: 'PATCH',
body: JSON.stringify(config),
});
}
/**
* Create a new CLI endpoint
*/
export async function createCliEndpoint(
endpoint: Omit<CliEndpoint, 'id'>
): Promise<CliEndpoint> {
return fetchApi<CliEndpoint>('/api/cli/endpoints', {
method: 'POST',
body: JSON.stringify(endpoint),
});
}
/**
* Delete a CLI endpoint
*/
export async function deleteCliEndpoint(endpointId: string): Promise<void> {
await fetchApi<void>(`/api/cli/endpoints/${encodeURIComponent(endpointId)}`, {
method: 'DELETE',
});
}
/**
* Toggle CLI endpoint enabled status
*/
export async function toggleCliEndpoint(
endpointId: string,
enabled: boolean
): Promise<CliEndpoint> {
return fetchApi<CliEndpoint>(`/api/cli/endpoints/${encodeURIComponent(endpointId)}/toggle`, {
method: 'POST',
body: JSON.stringify({ enabled }),
});
}
// ========== CLI Installations API ==========
export interface CliInstallation {
name: string;
version: string;
installed: boolean;
path?: string;
status: 'active' | 'inactive' | 'error';
lastChecked?: string;
}
export interface CliInstallationsResponse {
tools: CliInstallation[];
}
/**
* Fetch all CLI tool installations
*/
export async function fetchCliInstallations(): Promise<CliInstallationsResponse> {
const data = await fetchApi<{ tools?: CliInstallation[] }>('/api/cli/installations');
return {
tools: data.tools ?? [],
};
}
/**
* Install a CLI tool
*/
export async function installCliTool(toolName: string): Promise<CliInstallation> {
return fetchApi<CliInstallation>(`/api/cli/installations/${encodeURIComponent(toolName)}/install`, {
method: 'POST',
});
}
/**
* Uninstall a CLI tool
*/
export async function uninstallCliTool(toolName: string): Promise<void> {
await fetchApi<void>(`/api/cli/installations/${encodeURIComponent(toolName)}/uninstall`, {
method: 'POST',
});
}
/**
* Upgrade a CLI tool
*/
export async function upgradeCliTool(toolName: string): Promise<CliInstallation> {
return fetchApi<CliInstallation>(`/api/cli/installations/${encodeURIComponent(toolName)}/upgrade`, {
method: 'POST',
});
}
/**
* Check CLI tool installation status
*/
export async function checkCliToolStatus(toolName: string): Promise<CliInstallation> {
return fetchApi<CliInstallation>(`/api/cli/installations/${encodeURIComponent(toolName)}/check`, {
method: 'POST',
});
}
// ========== Hooks API ==========
export interface Hook {
name: string;
description?: string;
enabled: boolean;
script?: string;
trigger: 'pre-commit' | 'post-commit' | 'pre-push' | 'custom';
}
export interface HooksResponse {
hooks: Hook[];
}
/**
* Fetch all hooks
*/
export async function fetchHooks(): Promise<HooksResponse> {
const data = await fetchApi<{ hooks?: Hook[] }>('/api/hooks');
return {
hooks: data.hooks ?? [],
};
}
/**
* Update hook configuration
*/
export async function updateHook(
hookName: string,
config: Partial<Hook>
): Promise<Hook> {
return fetchApi<Hook>(`/api/hooks/${encodeURIComponent(hookName)}`, {
method: 'PATCH',
body: JSON.stringify(config),
});
}
/**
* Toggle hook enabled status
*/
export async function toggleHook(
hookName: string,
enabled: boolean
): Promise<Hook> {
return fetchApi<Hook>(`/api/hooks/${encodeURIComponent(hookName)}/toggle`, {
method: 'POST',
body: JSON.stringify({ enabled }),
});
}
// ========== Rules API ==========
export interface Rule {
id: string;
name: string;
description?: string;
enabled: boolean;
category?: string;
pattern?: string;
severity?: 'error' | 'warning' | 'info';
}
export interface RulesResponse {
rules: Rule[];
}
/**
* Fetch all rules
*/
export async function fetchRules(): Promise<RulesResponse> {
const data = await fetchApi<{ rules?: Rule[] }>('/api/rules');
return {
rules: data.rules ?? [],
};
}
/**
* Update rule configuration
*/
export async function updateRule(
ruleId: string,
config: Partial<Rule>
): Promise<Rule> {
return fetchApi<Rule>(`/api/rules/${encodeURIComponent(ruleId)}`, {
method: 'PATCH',
body: JSON.stringify(config),
});
}
/**
* Toggle rule enabled status
*/
export async function toggleRule(
ruleId: string,
enabled: boolean
): Promise<Rule> {
return fetchApi<Rule>(`/api/rules/${encodeURIComponent(ruleId)}/toggle`, {
method: 'POST',
body: JSON.stringify({ enabled }),
});
}

View File

@@ -0,0 +1,156 @@
// ========================================
// i18n Configuration
// ========================================
// Internationalization setup with react-intl
import { createIntl, createIntlCache } from '@formatjs/intl';
// Supported locales
export type Locale = 'en' | 'zh';
// Available locales with display names
export const availableLocales: Record<Locale, string> = {
en: 'English',
zh: '中文',
};
// Browser language detection
function getBrowserLocale(): Locale {
if (typeof window === 'undefined') return 'zh';
const browserLang = navigator.language.toLowerCase();
if (browserLang.startsWith('zh')) return 'zh';
if (browserLang.startsWith('en')) return 'en';
// Default to Chinese for unsupported languages
return 'zh';
}
// Get initial locale from localStorage or browser detection
export function getInitialLocale(): Locale {
if (typeof window === 'undefined') return 'zh';
try {
const stored = localStorage.getItem('ccw-app-store');
if (stored) {
const parsed = JSON.parse(stored);
if (parsed.state?.locale && (parsed.state.locale === 'en' || parsed.state.locale === 'zh')) {
return parsed.state.locale as Locale;
}
}
} catch {
// Ignore storage errors
}
return getBrowserLocale();
}
/**
* Load translation messages for a locale
* Dynamically imports the consolidated translation file
* NOTE: This dynamic import relies on Vite's glob import feature
* to bundle the locale index.ts files.
*/
async function loadMessages(locale: Locale): Promise<Record<string, string>> {
try {
// Dynamic import with .ts extension for Vite compatibility
const messagesModule = await import(`../locales/${locale}/index.ts`);
return messagesModule.default || {};
} catch (error) {
console.error(`Failed to load messages for locale "${locale}":`, error);
return {};
}
}
// Translation messages (will be populated by loading message files)
const messages: Record<Locale, Record<string, string>> = {
en: {},
zh: {},
};
/**
* Initialize translation messages for all locales
* Call this during app initialization
*/
export async function initMessages(): Promise<void> {
// Load messages for both locales in parallel
const [enMessages, zhMessages] = await Promise.all([
loadMessages('en'),
loadMessages('zh'),
]);
messages.en = enMessages;
messages.zh = zhMessages;
// Update current intl instance with loaded messages
const currentLocale = getInitialLocale();
updateIntl(currentLocale);
}
// Cache for intl instances to avoid recreating on every render
const intlCache = createIntlCache();
// Current intl instance (will be updated when locale changes)
let currentIntl = createIntl(
{
locale: getInitialLocale(),
messages: messages[getInitialLocale()],
},
intlCache
);
/**
* Get translation messages for a locale
* This will be used to load messages dynamically
*/
export function getMessages(locale: Locale): Record<string, string> {
return messages[locale];
}
/**
* Update the current intl instance with a new locale
*/
export function updateIntl(locale: Locale): void {
currentIntl = createIntl(
{
locale,
messages: messages[locale],
},
intlCache
);
// Update document lang attribute
if (typeof document !== 'undefined') {
document.documentElement.lang = locale;
}
}
/**
* Get the current intl instance
*/
export function getIntl() {
return currentIntl;
}
/**
* Register messages for a locale
* This can be used to dynamically load translation files
*/
export function registerMessages(locale: Locale, newMessages: Record<string, string>): void {
messages[locale] = { ...messages[locale], ...newMessages };
// Update current intl if this is the active locale
if (currentIntl.locale === locale) {
updateIntl(locale);
}
}
/**
* Format a message using the current intl instance
*/
export function formatMessage(
id: string,
values?: Record<string, string | number | boolean | Date | null | undefined>
): string {
return currentIntl.formatMessage({ id }, values);
}

View File

@@ -0,0 +1,30 @@
// ========================================
// Query Client Configuration
// ========================================
// TanStack Query client configuration for React
import { QueryClient } from '@tanstack/react-query';
/**
* Query client instance with default configuration
*/
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Time in milliseconds that data remains fresh
staleTime: 1000 * 60 * 5, // 5 minutes
// Time in milliseconds that unused data is cached
gcTime: 1000 * 60 * 10, // 10 minutes
// Number of times to retry failed queries
retry: 1,
// Disable refetch on window focus for better UX
refetchOnWindowFocus: false,
},
mutations: {
// Number of times to retry failed mutations
retry: 1,
},
},
});
export default queryClient;

View File

@@ -0,0 +1,127 @@
{
"cliEndpoints": {
"title": "CLI Endpoints",
"description": "Manage LiteLLM endpoints, custom CLI endpoints, and CLI wrapper configurations",
"type": {
"litellm": "LiteLLM",
"custom": "Custom",
"wrapper": "Wrapper"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled"
},
"stats": {
"total": "Total Endpoints",
"enabled": "Enabled"
},
"id": "ID",
"config": "Configuration",
"filters": {
"type": "Type",
"allTypes": "All Types",
"searchPlaceholder": "Search endpoints by name or ID..."
},
"actions": {
"add": "Add Endpoint",
"edit": "Edit Endpoint",
"delete": "Delete Endpoint",
"toggle": "Toggle Endpoint"
},
"deleteConfirm": "Are you sure you want to delete the endpoint \"{id}\"?",
"emptyState": {
"title": "No CLI Endpoints Found",
"message": "Add a CLI endpoint to configure custom API endpoints and wrappers."
}
},
"cliInstallations": {
"title": "CLI Installations",
"description": "Manage CCW CLI tool installations, upgrades, and removals",
"status": {
"active": "Active",
"inactive": "Inactive",
"error": "Error"
},
"stats": {
"total": "Total Tools",
"installed": "Installed",
"available": "Available"
},
"installed": "Installed",
"lastChecked": "Last Checked",
"filters": {
"status": "Status",
"all": "All",
"installed": "Installed",
"notInstalled": "Not Installed",
"searchPlaceholder": "Search tools by name or version..."
},
"actions": {
"install": "Install",
"uninstall": "Uninstall",
"upgrade": "Upgrade"
},
"uninstallConfirm": "Are you sure you want to uninstall \"{name}\"?",
"emptyState": {
"title": "No CLI Tools Found",
"message": "No CLI tools are available for installation at this time."
}
},
"cliHooks": {
"title": "Git Hooks",
"description": "Manage Git hooks for automated workflows",
"trigger": {
"pre-commit": "Pre-commit",
"post-commit": "Post-commit",
"pre-push": "Pre-push",
"custom": "Custom"
},
"stats": {
"total": "Total Hooks",
"enabled": "Enabled"
},
"filters": {
"trigger": "Trigger",
"allTriggers": "All Triggers",
"searchPlaceholder": "Search hooks by name..."
},
"actions": {
"add": "Add Hook",
"edit": "Edit Hook",
"delete": "Delete Hook",
"toggle": "Toggle Hook"
},
"emptyState": {
"title": "No Git Hooks Found",
"message": "Add a Git hook to automate tasks during Git workflows."
}
},
"cliRules": {
"title": "Rules",
"description": "Manage code quality rules and linting configurations",
"severity": {
"error": "Error",
"warning": "Warning",
"info": "Info"
},
"stats": {
"total": "Total Rules",
"enabled": "Enabled"
},
"filters": {
"severity": "Severity",
"allSeverities": "All Severities",
"searchPlaceholder": "Search rules by name..."
},
"actions": {
"add": "Add Rule",
"edit": "Edit Rule",
"delete": "Delete Rule",
"toggle": "Toggle Rule"
},
"emptyState": {
"title": "No Rules Found",
"message": "Add a rule to enforce code quality standards."
}
}
}

View File

@@ -0,0 +1,43 @@
{
"title": "Commands Manager",
"description": "Manage custom slash commands for Claude Code",
"actions": {
"create": "New Command",
"edit": "Edit Command",
"delete": "Delete Command",
"refresh": "Refresh",
"expandAll": "Expand All",
"collapseAll": "Collapse All",
"copy": "Copy"
},
"source": {
"builtin": "Built-in",
"custom": "Custom"
},
"filters": {
"allCategories": "All Categories",
"allSources": "All Sources",
"category": "Category",
"source": "Source",
"searchPlaceholder": "Search commands by name, description, or alias..."
},
"card": {
"name": "Name",
"description": "Description",
"usage": "Usage",
"examples": "Examples",
"aliases": "Aliases",
"triggers": "Triggers",
"noDescription": "No description"
},
"emptyState": {
"title": "No Commands Found",
"message": "Try adjusting your search or filters."
},
"table": {
"name": "Name",
"description": "Description",
"scope": "Scope",
"status": "Status"
}
}

View File

@@ -0,0 +1,180 @@
{
"aria": {
"toggleNavigation": "Toggle navigation menu",
"refreshWorkspace": "Refresh workspace",
"switchToLightMode": "Switch to light mode",
"switchToDarkMode": "Switch to dark mode",
"userMenu": "User menu",
"actions": "Actions"
},
"actions": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"refresh": "Refresh",
"loading": "Loading...",
"retry": "Retry",
"search": "Search...",
"searchIssues": "Search issues...",
"searchLoops": "Search loops...",
"clear": "Clear",
"close": "Close",
"copy": "Copy",
"view": "View",
"viewAll": "View All",
"update": "Update",
"add": "Add",
"new": "New",
"remove": "Remove",
"confirm": "Confirm",
"back": "Back",
"next": "Next",
"previous": "Previous",
"submit": "Submit",
"reset": "Reset",
"resetDesc": "Reset all user preferences to their default values. This cannot be undone.",
"resetConfirm": "Reset all settings to defaults?",
"resetToDefaults": "Reset to Defaults",
"enable": "Enable",
"disable": "Disable",
"expand": "Expand All",
"expandAll": "Expand All",
"collapse": "Collapse All",
"collapseAll": "Collapse All",
"filter": "Filter",
"filters": "Filters",
"clearFilters": "Clear filters",
"clearAll": "Clear all",
"select": "Select",
"selectAll": "Select All",
"deselectAll": "Deselect All"
},
"status": {
"active": "Active",
"inactive": "Inactive",
"pending": "Pending",
"inProgress": "In Progress",
"completed": "Completed",
"failed": "Failed",
"blocked": "Blocked",
"cancelled": "Cancelled",
"paused": "Paused",
"archived": "Archived",
"unknown": "Unknown",
"draft": "Draft",
"published": "Published",
"creating": "Creating...",
"deleting": "Deleting...",
"label": "Status",
"openIssues": "Open Issues"
},
"priority": {
"low": "Low",
"medium": "Medium",
"high": "High",
"critical": "Critical",
"label": "Priority"
},
"time": {
"seconds": "seconds",
"minutes": "minutes",
"hours": "hours",
"days": "days",
"weeks": "weeks",
"months": "months",
"years": "years",
"ago": "ago",
"justNow": "just now"
},
"buttons": {
"new": "New",
"create": "Create",
"edit": "Edit",
"delete": "Delete",
"save": "Save",
"cancel": "Cancel",
"confirm": "Confirm",
"retry": "Retry",
"refresh": "Refresh",
"close": "Close",
"back": "Back",
"next": "Next",
"submit": "Submit"
},
"form": {
"required": "Required",
"optional": "optional",
"placeholder": "Enter...",
"search": "Search...",
"select": "Select...",
"noResults": "No results found",
"loading": "Loading...",
"sessionId": "Session ID",
"title": "Title",
"description": "Description",
"sessionIdPlaceholder": "e.g., WFS-feature-auth",
"titlePlaceholder": "e.g., Authentication System",
"descriptionPlaceholder": "Brief description of the session"
},
"emptyState": {
"noData": "No data found",
"noResults": "No results found",
"noItems": "No items",
"createFirst": "Create your first item to get started",
"searchEmpty": "Try adjusting your search or filters",
"filterEmpty": "No items match your current filters"
},
"errors": {
"generic": "An error occurred",
"network": "Network error. Please check your connection.",
"timeout": "Request timed out. Please try again.",
"notFound": "Resource not found",
"unauthorized": "Unauthorized access",
"forbidden": "Access forbidden",
"validation": "Validation error",
"server": "Server error. Please try again later.",
"loadingFailed": "Failed to load data",
"loadFailed": "Failed to load data",
"saveFailed": "Failed to save",
"deleteFailed": "Failed to delete",
"updateFailed": "Failed to update",
"unknownError": "An unexpected error occurred"
},
"success": {
"saved": "Saved successfully",
"created": "Created successfully",
"updated": "Updated successfully",
"deleted": "Deleted successfully",
"copied": "Copied to clipboard"
},
"messages": {
"confirmDelete": "Are you sure you want to delete this item?",
"confirmArchive": "Are you sure you want to archive this item?",
"unsavedChanges": "You have unsaved changes. Are you sure you want to leave?",
"noPermission": "You don't have permission to perform this action"
},
"stats": {
"todayActivity": "Today's Activity",
"totalCommands": "Total Commands",
"totalSkills": "Total Skills",
"categories": "Categories"
},
"dialog": {
"createSession": "Create New Session",
"createSessionDesc": "Create a new workflow session to track your development tasks.",
"deleteSession": "Delete Session",
"deleteConfirm": "Are you sure you want to delete this session? This action cannot be undone."
},
"help": {
"title": "Help & Documentation",
"description": "Learn how to use CCW Dashboard and get the most out of your workflows",
"support": {
"title": "Need more help?",
"description": "Check the project documentation or reach out for support.",
"documentation": "Documentation",
"tutorials": "Tutorials"
}
}
}

View File

@@ -0,0 +1,44 @@
{
"progress": {
"title": "Fix Progress"
},
"tasks": {
"title": "Fix Tasks"
},
"stats": {
"total": "Total",
"fixed": "Fixed",
"failed": "Failed",
"pending": "Pending"
},
"status": {
"fixed": "Fixed",
"failed": "Failed",
"inProgress": "In Progress",
"pending": "Pending"
},
"filter": {
"all": "All",
"pending": "Pending",
"inProgress": "In Progress",
"fixed": "Fixed",
"failed": "Failed"
},
"task": {
"untitled": "Untitled Task",
"attempts": "{count} attempts"
},
"info": {
"created": "Created",
"updated": "Updated",
"description": "Description"
},
"notFound": {
"title": "Session Not Found",
"message": "The fix session you're looking for doesn't exist or has been deleted."
},
"empty": {
"title": "No Tasks Found",
"message": "No fix tasks match the current filter."
}
}

View File

@@ -0,0 +1,29 @@
{
"title": "CLI Execution History",
"description": "View and manage your CLI execution history",
"searchPlaceholder": "Search executions...",
"filterAllTools": "All Tools",
"deleteOptions": "Delete Options",
"deleteBy": "Delete By Tool",
"deleteAllTool": "Delete All {tool}",
"deleteAll": "Delete All History",
"actions": {
"view": "View Details",
"delete": "Delete",
"copyId": "Copy ID",
"copied": "Copied!"
},
"dialog": {
"deleteTitle": "Confirm Delete",
"deleteAllTitle": "Delete All History",
"deleteMessage": "Are you sure you want to delete this execution? This action cannot be undone.",
"deleteToolMessage": "Are you sure you want to delete all {tool} executions? This action cannot be undone.",
"deleteAllMessage": "Are you sure you want to delete all execution history? This action cannot be undone."
},
"empty": {
"title": "No Executions Found",
"message": "CLI execution history will appear here when you run CLI commands.",
"filtered": "No Matching Results",
"filteredMessage": "No executions match your current filter. Try adjusting your search or filter."
}
}

View File

@@ -0,0 +1,37 @@
{
"title": "Home",
"description": "Dashboard overview and statistics",
"stats": {
"activeSessions": "Active Sessions",
"totalTasks": "Total Tasks",
"completedTasks": "Completed",
"pendingTasks": "Pending",
"runningLoops": "Running Loops",
"openIssues": "Open Issues"
},
"sections": {
"statistics": "Statistics",
"recentSessions": "Recent Sessions",
"activeLoops": "Active Loops",
"openIssues": "Open Issues",
"quickActions": "Quick Actions"
},
"emptyState": {
"noSessions": {
"title": "No Sessions Found",
"message": "No workflow sessions match your current filter."
},
"noLoops": {
"title": "No Active Loops",
"message": "Start a new development loop to begin monitoring progress."
},
"noIssues": {
"title": "No Open Issues",
"message": "Create an issue to track bugs or feature requests."
}
},
"errors": {
"loadFailed": "Failed to load dashboard data",
"retry": "Retry"
}
}

View File

@@ -0,0 +1,64 @@
/**
* English translations
* Consolidated exports for all English translation files
*/
import common from './common.json';
import navigation from './navigation.json';
import sessions from './sessions.json';
import issues from './issues.json';
import home from './home.json';
import orchestrator from './orchestrator.json';
import loops from './loops.json';
import commands from './commands.json';
import memory from './memory.json';
import settings from './settings.json';
import fixSession from './fix-session.json';
import history from './history.json';
import liteTasks from './lite-tasks.json';
import projectOverview from './project-overview.json';
import reviewSession from './review-session.json';
import sessionDetail from './session-detail.json';
/**
* Flattens nested JSON object to dot-separated keys
* e.g., { actions: { save: 'Save' } } => { 'actions.save': 'Save' }
*/
function flattenMessages(obj: Record<string, unknown>, prefix = ''): Record<string, string> {
const result: Record<string, string> = {};
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(result, flattenMessages(value as Record<string, unknown>, fullKey));
} else if (typeof value === 'string') {
result[fullKey] = value;
}
}
return result;
}
/**
* Consolidated and flattened English messages
*/
export default {
...flattenMessages(common),
...flattenMessages(navigation),
...flattenMessages(sessions),
...flattenMessages(issues),
...flattenMessages(home),
...flattenMessages(orchestrator),
...flattenMessages(loops),
...flattenMessages(commands),
...flattenMessages(memory),
...flattenMessages(settings),
...flattenMessages(fixSession),
...flattenMessages(history),
...flattenMessages(liteTasks),
...flattenMessages(projectOverview),
...flattenMessages(reviewSession),
...flattenMessages(sessionDetail),
} as Record<string, string>;

View File

@@ -0,0 +1,64 @@
{
"title": "Issues",
"description": "Track and manage issues",
"status": {
"open": "Open",
"inProgress": "In Progress",
"resolved": "Resolved",
"closed": "Closed",
"completed": "Completed"
},
"priority": {
"low": "Low",
"medium": "Medium",
"high": "High",
"critical": "Critical"
},
"actions": {
"create": "New Issue",
"edit": "Edit",
"delete": "Delete",
"viewDetails": "View Details",
"changeStatus": "Change Status",
"changePriority": "Change Priority",
"startProgress": "Start Progress",
"markResolved": "Mark Resolved",
"github": "Pull from GitHub"
},
"filters": {
"all": "All",
"open": "Open",
"inProgress": "In Progress",
"resolved": "Resolved",
"closed": "Closed",
"byPriority": "By Priority"
},
"emptyState": {
"title": "No Issues Found",
"message": "No issues match your current filter.",
"createFirst": "Create your first issue to get started"
},
"createDialog": {
"title": "Create New Issue",
"labels": {
"title": "Title",
"context": "Context",
"priority": "Priority"
},
"placeholders": {
"title": "Enter issue title...",
"context": "Describe the issue context..."
},
"buttons": {
"create": "Create",
"cancel": "Cancel",
"creating": "Creating..."
}
},
"card": {
"id": "ID",
"createdAt": "Created",
"updatedAt": "Updated",
"solutions": "{count, plural, one {solution} other {solutions}}"
}
}

View File

@@ -0,0 +1,26 @@
{
"title": "Lite Tasks",
"subtitle": "{count} session(s)",
"type": {
"plan": "Lite Plan",
"fix": "Lite Fix",
"multiCli": "Multi-CLI"
},
"rounds": "rounds",
"empty": {
"title": "No {type} sessions",
"message": "Create a new session to get started."
},
"flowchart": "Flowchart",
"implementationFlow": "Implementation Flow",
"focusPaths": "Focus Paths",
"acceptanceCriteria": "Acceptance Criteria",
"emptyDetail": {
"title": "No tasks in this session",
"message": "This session does not contain any tasks yet."
},
"notFound": {
"title": "Lite Task Not Found",
"message": "The requested lite task session could not be found."
}
}

View File

@@ -0,0 +1,68 @@
{
"title": "Loop Monitor",
"description": "Monitor and control running development loops",
"status": {
"created": "Pending",
"running": "Running",
"paused": "Paused",
"completed": "Completed",
"failed": "Failed"
},
"actions": {
"create": "New Loop",
"pause": "Pause",
"resume": "Resume",
"stop": "Stop",
"restart": "Restart",
"viewDetails": "View Details"
},
"emptyState": {
"title": "No Active Loops",
"message": "Start a new development loop to begin monitoring progress.",
"createFirst": "Start New Loop"
},
"card": {
"step": "Step",
"of": "of",
"progress": "Progress",
"prompt": "Prompt",
"tool": "Tool",
"iteration": "Iteration",
"error": "Error"
},
"createDialog": {
"title": "Start New Loop",
"labels": {
"prompt": "Prompt",
"tool": "CLI Tool (optional)"
},
"placeholders": {
"prompt": "Enter your development loop prompt...",
"tool": "e.g., gemini, qwen, codex"
},
"buttons": {
"create": "Start",
"cancel": "Cancel"
}
},
"monitor": {
"title": "Loop Monitor",
"loops": "Loops",
"tasks": "Tasks",
"iterations": "Iterations",
"timeline": "Timeline"
},
"taskStatus": {
"pending": "Pending",
"inProgress": "In Progress",
"blocked": "Blocked",
"done": "Done"
},
"columns": {
"pending": "Pending",
"running": "Running",
"paused": "Paused",
"completed": "Completed",
"failed": "Failed"
}
}

View File

@@ -0,0 +1,37 @@
{
"title": "MCP Servers",
"description": "Manage Model Context Protocol (MCP) servers for cross-CLI integration",
"scope": {
"global": "Global",
"project": "Project"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled"
},
"stats": {
"total": "Total Servers",
"enabled": "Enabled",
"global": "Global",
"project": "Project"
},
"command": "Command",
"args": "Arguments",
"env": "Environment Variables",
"filters": {
"all": "All",
"searchPlaceholder": "Search servers by name or command..."
},
"actions": {
"add": "Add Server",
"edit": "Edit Server",
"delete": "Delete Server",
"toggle": "Toggle Server",
"expand": "View Details"
},
"deleteConfirm": "Are you sure you want to delete the MCP server \"{name}\"?",
"emptyState": {
"title": "No MCP Servers Found",
"message": "Add an MCP server to enable cross-CLI integration with tools like Claude, Codex, and Qwen."
}
}

View File

@@ -0,0 +1,65 @@
{
"title": "Memory",
"description": "Manage core memory, context, and knowledge base",
"actions": {
"add": "Add Memory",
"edit": "Edit",
"delete": "Delete",
"copy": "Copy",
"refresh": "Refresh",
"expand": "Expand",
"collapse": "Collapse"
},
"stats": {
"totalSize": "Total Size",
"count": "Count",
"claudeMdCount": "CLAUDE.md Files",
"totalEntries": "Total Entries"
},
"filters": {
"search": "Search memories...",
"tags": "Tags",
"clear": "Clear",
"all": "All"
},
"card": {
"id": "ID",
"content": "Content",
"summary": "Summary",
"tags": "Tags",
"createdAt": "Created",
"updatedAt": "Updated",
"size": "Size",
"favorite": "Favorite",
"archived": "Archived"
},
"emptyState": {
"title": "No Memories Stored",
"message": "Add context and knowledge to help Claude understand your project better.",
"createFirst": "Add First Memory"
},
"createDialog": {
"title": "Add Memory",
"editTitle": "Edit Memory",
"labels": {
"content": "Content",
"tags": "Tags"
},
"placeholders": {
"content": "Enter memory content...",
"tags": "e.g., project, config, api"
},
"buttons": {
"create": "Add Memory",
"update": "Update Memory",
"cancel": "Cancel",
"creating": "Creating...",
"updating": "Updating..."
}
},
"types": {
"coreMemory": "Core Memory",
"workflow": "Workflow",
"cliHistory": "CLI History"
}
}

View File

@@ -0,0 +1,38 @@
{
"main": {
"home": "Home",
"sessions": "Sessions",
"liteTasks": "Lite Tasks",
"project": "Project",
"history": "History",
"orchestrator": "Orchestrator",
"loops": "Loop Monitor",
"issues": "Issues",
"skills": "Skills",
"commands": "Commands",
"memory": "Memory",
"settings": "Settings",
"mcp": "MCP Servers",
"endpoints": "CLI Endpoints",
"installations": "Installations",
"help": "Help"
},
"sidebar": {
"collapse": "Collapse",
"expand": "Expand sidebar",
"collapseAria": "Collapse sidebar"
},
"header": {
"brand": "Claude Code Workflow",
"brandShort": "CCW",
"noProject": "No project selected",
"settings": "Settings",
"logout": "Logout"
},
"breadcrumbs": {
"home": "Home",
"sessions": "Sessions",
"detail": "Details",
"settings": "Settings"
}
}

View File

@@ -0,0 +1,63 @@
{
"title": "Orchestrator",
"description": "Manage and execute workflow flows",
"flow": {
"title": "Flow",
"flows": "Flows",
"create": "New Flow",
"edit": "Edit Flow",
"delete": "Delete Flow",
"duplicate": "Duplicate Flow",
"export": "Export Flow",
"import": "Import Flow"
},
"execution": {
"title": "Execution",
"status": "Status",
"start": "Start",
"pause": "Pause",
"resume": "Resume",
"stop": "Stop",
"restart": "Restart",
"viewLogs": "View Logs"
},
"status": {
"pending": "Pending",
"running": "Running",
"paused": "Paused",
"completed": "Completed",
"failed": "Failed"
},
"node": {
"title": "Node",
"nodes": "Nodes",
"add": "Add Node",
"edit": "Edit Node",
"delete": "Delete Node",
"status": "Node Status",
"result": "Result"
},
"actions": {
"execute": "Execute",
"validate": "Validate",
"save": "Save",
"cancel": "Cancel"
},
"emptyState": {
"noFlows": {
"title": "No Flows Found",
"message": "Create your first workflow flow to get started."
},
"noExecution": {
"title": "No Execution History",
"message": "Execute a flow to see the execution history."
}
},
"monitor": {
"title": "Execution Monitor",
"logs": "Logs",
"timeline": "Timeline",
"variables": "Variables",
"realtime": "Real-time Updates"
}
}

View File

@@ -0,0 +1,52 @@
{
"noDescription": "No description available",
"header": {
"initialized": "Initialized"
},
"techStack": {
"title": "Technology Stack",
"languages": "Languages",
"frameworks": "Frameworks",
"buildTools": "Build Tools",
"testFrameworks": "Test Frameworks",
"primary": "Primary",
"noLanguages": "No languages detected",
"noFrameworks": "No frameworks detected"
},
"architecture": {
"title": "Architecture",
"style": "Style",
"layers": "Layers",
"patterns": "Patterns"
},
"components": {
"title": "Key Components",
"importance": {
"high": "High",
"medium": "Medium",
"low": "Low"
}
},
"devIndex": {
"title": "Development History",
"categories": "Categories",
"timeline": "Timeline"
},
"guidelines": {
"title": "Project Guidelines",
"conventions": "Conventions",
"constraints": "Constraints",
"qualityRules": "Quality Rules",
"learnings": "Session Learnings",
"scope": "Scope"
},
"stats": {
"title": "Statistics",
"totalFeatures": "Total Features",
"lastUpdated": "Last Updated"
},
"empty": {
"title": "No Project Overview",
"message": "Run /workflow:init to initialize project analysis"
}
}

View File

@@ -0,0 +1,41 @@
{
"title": "Review Session",
"type": "Review",
"severity": {
"critical": "Critical",
"high": "High",
"medium": "Medium",
"low": "Low"
},
"stats": {
"total": "Total Findings",
"dimensions": "Dimensions"
},
"search": {
"placeholder": "Search findings..."
},
"sort": {
"severity": "By Severity",
"dimension": "By Dimension",
"file": "By File"
},
"selection": {
"count": "{count} selected",
"selectAll": "Select All",
"clearAll": "Clear All",
"clear": "Clear"
},
"export": "Export Fix JSON",
"codeContext": "Code Context",
"rootCause": "Root Cause",
"impact": "Impact",
"recommendations": "Recommendations",
"empty": {
"title": "No findings found",
"message": "Try adjusting your filters or search query."
},
"notFound": {
"title": "Review Session Not Found",
"message": "The requested review session could not be found."
}
}

View File

@@ -0,0 +1,54 @@
{
"notFound": {
"title": "Session Not Found",
"message": "The session you're looking for doesn't exist or has been deleted."
},
"tabs": {
"tasks": "Tasks",
"context": "Context",
"summary": "Summary"
},
"tasks": {
"completed": "completed",
"inProgress": "in progress",
"pending": "pending",
"blocked": "blocked",
"status": {
"pending": "Pending",
"inProgress": "In Progress",
"completed": "Completed",
"blocked": "Blocked",
"skipped": "Skipped"
},
"untitled": "Untitled Task",
"empty": {
"title": "No Tasks Found",
"message": "This session has no tasks yet."
}
},
"context": {
"requirements": "Requirements",
"focusPaths": "Focus Paths",
"artifacts": "Artifacts",
"sharedContext": "Shared Context",
"techStack": "Tech Stack",
"conventions": "Conventions",
"empty": {
"title": "No Context Available",
"message": "This session has no context information."
}
},
"summary": {
"title": "Session Summary",
"empty": {
"title": "No Summary Available",
"message": "This session has no summary yet."
}
},
"info": {
"created": "Created",
"updated": "Updated",
"tasks": "Tasks",
"description": "Description"
}
}

View File

@@ -0,0 +1,48 @@
{
"title": "Sessions",
"description": "Manage your workflow sessions and track progress",
"status": {
"planning": "Planning",
"inProgress": "In Progress",
"completed": "Completed",
"archived": "Archived",
"paused": "Paused"
},
"actions": {
"viewDetails": "View Details",
"archive": "Archive",
"delete": "Delete",
"restore": "Restore",
"pause": "Pause",
"resume": "Resume"
},
"filters": {
"all": "All",
"active": "Active",
"planning": "Planning",
"completed": "Completed",
"archived": "Archived",
"paused": "Paused"
},
"searchPlaceholder": "Search sessions...",
"emptyState": {
"title": "No Sessions Found",
"message": "No workflow sessions match your current filter.",
"createFirst": "Create your first session to get started"
},
"card": {
"tasks": "tasks",
"findings": "findings",
"dimensions": "dimensions",
"progress": "Progress",
"createdAt": "Created",
"updatedAt": "Updated"
},
"detail": {
"overview": "Overview",
"tasks": "Tasks",
"summary": "Summary",
"metadata": "Metadata",
"timeline": "Timeline"
}
}

View File

@@ -0,0 +1,42 @@
{
"title": "Skills",
"description": "Manage and configure skills",
"source": {
"builtin": "Built-in",
"custom": "Custom",
"community": "Community"
},
"actions": {
"viewDetails": "View Details",
"configure": "Configure",
"enable": "Enable",
"disable": "Disable",
"toggle": "Toggle",
"install": "Install Skill"
},
"state": {
"enabled": "Enabled",
"disabled": "Disabled",
"on": "On",
"off": "Off"
},
"card": {
"triggers": "Triggers",
"category": "Category",
"author": "Author",
"version": "Version"
},
"filters": {
"all": "All",
"enabled": "Enabled",
"disabled": "Disabled"
},
"view": {
"grid": "Grid View",
"compact": "Compact View"
},
"emptyState": {
"title": "No Skills Found",
"message": "No skills match your current filter."
}
}

View File

@@ -0,0 +1,127 @@
{
"cliEndpoints": {
"title": "CLI 端点",
"description": "管理 LiteLLM 端点、自定义 CLI 端点和 CLI 包装器配置",
"type": {
"litellm": "LiteLLM",
"custom": "自定义",
"wrapper": "包装器"
},
"status": {
"enabled": "已启用",
"disabled": "已禁用"
},
"stats": {
"total": "端点总数",
"enabled": "已启用"
},
"id": "ID",
"config": "配置",
"filters": {
"type": "类型",
"allTypes": "全部类型",
"searchPlaceholder": "按名称或 ID 搜索端点..."
},
"actions": {
"add": "添加端点",
"edit": "编辑端点",
"delete": "删除端点",
"toggle": "切换端点"
},
"deleteConfirm": "确定要删除端点 \"{id}\" 吗?",
"emptyState": {
"title": "未找到 CLI 端点",
"message": "添加 CLI 端点以配置自定义 API 端点和包装器。"
}
},
"cliInstallations": {
"title": "CLI 安装",
"description": "管理 CCW CLI 工具的安装、升级和卸载",
"status": {
"active": "活动",
"inactive": "未活动",
"error": "错误"
},
"stats": {
"total": "工具总数",
"installed": "已安装",
"available": "可用"
},
"installed": "已安装",
"lastChecked": "上次检查",
"filters": {
"status": "状态",
"all": "全部",
"installed": "已安装",
"notInstalled": "未安装",
"searchPlaceholder": "按名称或版本搜索工具..."
},
"actions": {
"install": "安装",
"uninstall": "卸载",
"upgrade": "升级"
},
"uninstallConfirm": "确定要卸载 \"{name}\" 吗?",
"emptyState": {
"title": "未找到 CLI 工具",
"message": "目前没有可用于安装的 CLI 工具。"
}
},
"cliHooks": {
"title": "Git 钩子",
"description": "管理用于自动化工作流的 Git 钩子",
"trigger": {
"pre-commit": "提交前",
"post-commit": "提交后",
"pre-push": "推送前",
"custom": "自定义"
},
"stats": {
"total": "钩子总数",
"enabled": "已启用"
},
"filters": {
"trigger": "触发器",
"allTriggers": "全部触发器",
"searchPlaceholder": "按名称搜索钩子..."
},
"actions": {
"add": "添加钩子",
"edit": "编辑钩子",
"delete": "删除钩子",
"toggle": "切换钩子"
},
"emptyState": {
"title": "未找到 Git 钩子",
"message": "添加 Git 钩子以在 Git 工作流期间自动化任务。"
}
},
"cliRules": {
"title": "规则",
"description": "管理代码质量规则和检查配置",
"severity": {
"error": "错误",
"warning": "警告",
"info": "信息"
},
"stats": {
"total": "规则总数",
"enabled": "已启用"
},
"filters": {
"severity": "严重程度",
"allSeverities": "全部严重程度",
"searchPlaceholder": "按名称搜索规则..."
},
"actions": {
"add": "添加规则",
"edit": "编辑规则",
"delete": "删除规则",
"toggle": "切换规则"
},
"emptyState": {
"title": "未找到规则",
"message": "添加规则以强制执行代码质量标准。"
}
}
}

View File

@@ -0,0 +1,43 @@
{
"title": "命令管理",
"description": "管理 Claude Code 自定义斜杠命令",
"actions": {
"create": "新建命令",
"edit": "编辑命令",
"delete": "删除命令",
"refresh": "刷新",
"expandAll": "全部展开",
"collapseAll": "全部收起",
"copy": "复制"
},
"source": {
"builtin": "内置",
"custom": "自定义"
},
"filters": {
"allCategories": "所有类别",
"allSources": "所有来源",
"category": "类别",
"source": "来源",
"searchPlaceholder": "按名称、描述或别名搜索命令..."
},
"card": {
"name": "名称",
"description": "描述",
"usage": "用法",
"examples": "示例",
"aliases": "别名",
"triggers": "触发器",
"noDescription": "无描述"
},
"emptyState": {
"title": "未找到命令",
"message": "尝试调整搜索或筛选条件。"
},
"table": {
"name": "名称",
"description": "描述",
"scope": "作用域",
"status": "状态"
}
}

View File

@@ -0,0 +1,180 @@
{
"aria": {
"toggleNavigation": "切换导航菜单",
"refreshWorkspace": "刷新工作区",
"switchToLightMode": "切换到浅色模式",
"switchToDarkMode": "切换到深色模式",
"userMenu": "用户菜单",
"actions": "操作"
},
"actions": {
"save": "保存",
"cancel": "取消",
"delete": "删除",
"edit": "编辑",
"create": "创建",
"refresh": "刷新",
"loading": "加载中...",
"retry": "重试",
"search": "搜索...",
"searchIssues": "搜索问题...",
"searchLoops": "搜索循环...",
"clear": "清除",
"close": "关闭",
"copy": "复制",
"view": "查看",
"viewAll": "查看全部",
"update": "更新",
"add": "添加",
"new": "新建",
"remove": "移除",
"confirm": "确认",
"back": "返回",
"next": "下一步",
"previous": "上一步",
"submit": "提交",
"reset": "重置",
"resetDesc": "将所有用户偏好重置为默认值。此操作无法撤销。",
"resetConfirm": "确定要将所有设置重置为默认值吗?",
"resetToDefaults": "重置为默认值",
"enable": "启用",
"disable": "禁用",
"expand": "全部展开",
"expandAll": "全部展开",
"collapse": "全部收起",
"collapseAll": "全部收起",
"filter": "筛选",
"filters": "筛选",
"clearFilters": "清除筛选",
"clearAll": "清除全部",
"select": "选择",
"selectAll": "全选",
"deselectAll": "取消全选"
},
"status": {
"active": "活跃",
"inactive": "未激活",
"pending": "待处理",
"inProgress": "进行中",
"completed": "已完成",
"failed": "失败",
"blocked": "已阻塞",
"cancelled": "已取消",
"paused": "已暂停",
"archived": "已归档",
"unknown": "未知",
"draft": "草稿",
"published": "已发布",
"creating": "创建中...",
"deleting": "删除中...",
"label": "状态",
"openIssues": "开放问题"
},
"priority": {
"low": "低",
"medium": "中",
"high": "高",
"critical": "紧急",
"label": "优先级"
},
"time": {
"seconds": "秒",
"minutes": "分钟",
"hours": "小时",
"days": "天",
"weeks": "周",
"months": "月",
"years": "年",
"ago": "前",
"justNow": "刚刚"
},
"buttons": {
"new": "新建",
"create": "创建",
"edit": "编辑",
"delete": "删除",
"save": "保存",
"cancel": "取消",
"confirm": "确认",
"retry": "重试",
"refresh": "刷新",
"close": "关闭",
"back": "返回",
"next": "下一步",
"submit": "提交"
},
"form": {
"required": "必填",
"optional": "可选",
"placeholder": "输入...",
"search": "搜索...",
"select": "选择...",
"noResults": "未找到结果",
"loading": "加载中...",
"sessionId": "会话ID",
"title": "标题",
"description": "描述",
"sessionIdPlaceholder": "例如WFS-feature-auth",
"titlePlaceholder": "例如:认证系统",
"descriptionPlaceholder": "会话简短描述"
},
"emptyState": {
"noData": "未找到数据",
"noResults": "未找到结果",
"noItems": "暂无项目",
"createFirst": "创建第一个项目以开始",
"searchEmpty": "尝试调整搜索或筛选条件",
"filterEmpty": "没有项目符合当前筛选条件"
},
"errors": {
"generic": "发生错误",
"network": "网络错误,请检查连接。",
"timeout": "请求超时,请重试。",
"notFound": "未找到资源",
"unauthorized": "未授权访问",
"forbidden": "访问被禁止",
"validation": "验证错误",
"server": "服务器错误,请稍后重试。",
"loadingFailed": "加载数据失败",
"loadFailed": "加载数据失败",
"saveFailed": "保存失败",
"deleteFailed": "删除失败",
"updateFailed": "更新失败",
"unknownError": "发生意外错误"
},
"success": {
"saved": "保存成功",
"created": "创建成功",
"updated": "更新成功",
"deleted": "删除成功",
"copied": "已复制到剪贴板"
},
"messages": {
"confirmDelete": "确定要删除此项目吗?",
"confirmArchive": "确定要归档此项目吗?",
"unsavedChanges": "您有未保存的更改,确定要离开吗?",
"noPermission": "您没有执行此操作的权限"
},
"stats": {
"todayActivity": "今日活动",
"totalCommands": "总命令数",
"totalSkills": "总技能数",
"categories": "分类"
},
"dialog": {
"createSession": "创建新会话",
"createSessionDesc": "创建新的工作流会话以跟踪您的开发任务。",
"deleteSession": "删除会话",
"deleteConfirm": "确定要删除此会话吗?此操作无法撤销。"
},
"help": {
"title": "帮助与文档",
"description": "了解如何使用 CCW 仪表板并充分利用您的工作流",
"support": {
"title": "需要更多帮助?",
"description": "查看项目文档或联系支持。",
"documentation": "文档",
"tutorials": "教程"
}
}
}

View File

@@ -0,0 +1,44 @@
{
"progress": {
"title": "修复进度"
},
"tasks": {
"title": "修复任务"
},
"stats": {
"total": "总计",
"fixed": "已修复",
"failed": "失败",
"pending": "待处理"
},
"status": {
"fixed": "已修复",
"failed": "失败",
"inProgress": "进行中",
"pending": "待处理"
},
"filter": {
"all": "全部",
"pending": "待处理",
"inProgress": "进行中",
"fixed": "已修复",
"failed": "失败"
},
"task": {
"untitled": "无标题任务",
"attempts": "{count} 次尝试"
},
"info": {
"created": "创建时间",
"updated": "更新时间",
"description": "描述"
},
"notFound": {
"title": "会话未找到",
"message": "您要查找的修复会话不存在或已被删除。"
},
"empty": {
"title": "未找到任务",
"message": "没有匹配当前筛选条件的修复任务。"
}
}

View File

@@ -0,0 +1,29 @@
{
"title": "CLI 执行历史",
"description": "查看和管理 CLI 执行历史",
"searchPlaceholder": "搜索执行记录...",
"filterAllTools": "全部工具",
"deleteOptions": "删除选项",
"deleteBy": "按工具删除",
"deleteAllTool": "删除所有 {tool}",
"deleteAll": "删除全部历史",
"actions": {
"view": "查看详情",
"delete": "删除",
"copyId": "复制 ID",
"copied": "已复制!"
},
"dialog": {
"deleteTitle": "确认删除",
"deleteAllTitle": "删除全部历史",
"deleteMessage": "确定要删除此执行记录吗?此操作无法撤销。",
"deleteToolMessage": "确定要删除所有 {tool} 执行记录吗?此操作无法撤销。",
"deleteAllMessage": "确定要删除全部执行历史吗?此操作无法撤销。"
},
"empty": {
"title": "未找到执行记录",
"message": "运行 CLI 命令后,执行历史将显示在这里。",
"filtered": "没有匹配结果",
"filteredMessage": "没有匹配当前筛选条件的执行记录。请尝试调整搜索或筛选条件。"
}
}

View File

@@ -0,0 +1,37 @@
{
"title": "首页",
"description": "仪表板概览与统计",
"stats": {
"activeSessions": "活跃会话",
"totalTasks": "总任务",
"completedTasks": "已完成",
"pendingTasks": "待处理",
"runningLoops": "运行中的循环",
"openIssues": "开放问题"
},
"sections": {
"statistics": "统计",
"recentSessions": "最近会话",
"activeLoops": "活跃循环",
"openIssues": "开放问题",
"quickActions": "快速操作"
},
"emptyState": {
"noSessions": {
"title": "未找到会话",
"message": "没有符合当前筛选条件的工作流会话。"
},
"noLoops": {
"title": "无活跃循环",
"message": "启动新的开发循环以开始监控进度。"
},
"noIssues": {
"title": "无开放问题",
"message": "创建问题以跟踪错误或功能请求。"
}
},
"errors": {
"loadFailed": "加载仪表板数据失败",
"retry": "重试"
}
}

View File

@@ -0,0 +1,64 @@
/**
* Chinese (Simplified) translations
* Consolidated exports for all Chinese translation files
*/
import common from './common.json';
import navigation from './navigation.json';
import sessions from './sessions.json';
import issues from './issues.json';
import home from './home.json';
import orchestrator from './orchestrator.json';
import loops from './loops.json';
import commands from './commands.json';
import memory from './memory.json';
import settings from './settings.json';
import fixSession from './fix-session.json';
import history from './history.json';
import liteTasks from './lite-tasks.json';
import projectOverview from './project-overview.json';
import reviewSession from './review-session.json';
import sessionDetail from './session-detail.json';
/**
* Flattens nested JSON object to dot-separated keys
* e.g., { actions: { save: 'Save' } } => { 'actions.save': 'Save' }
*/
function flattenMessages(obj: Record<string, unknown>, prefix = ''): Record<string, string> {
const result: Record<string, string> = {};
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(result, flattenMessages(value as Record<string, unknown>, fullKey));
} else if (typeof value === 'string') {
result[fullKey] = value;
}
}
return result;
}
/**
* Consolidated and flattened Chinese messages
*/
export default {
...flattenMessages(common),
...flattenMessages(navigation),
...flattenMessages(sessions),
...flattenMessages(issues),
...flattenMessages(home),
...flattenMessages(orchestrator),
...flattenMessages(loops),
...flattenMessages(commands),
...flattenMessages(memory),
...flattenMessages(settings),
...flattenMessages(fixSession),
...flattenMessages(history),
...flattenMessages(liteTasks),
...flattenMessages(projectOverview),
...flattenMessages(reviewSession),
...flattenMessages(sessionDetail),
} as Record<string, string>;

View File

@@ -0,0 +1,64 @@
{
"title": "问题",
"description": "跟踪和管理问题",
"status": {
"open": "开放",
"inProgress": "进行中",
"resolved": "已解决",
"closed": "已关闭",
"completed": "已完成"
},
"priority": {
"low": "低",
"medium": "中",
"high": "高",
"critical": "紧急"
},
"actions": {
"create": "新建问题",
"edit": "编辑",
"delete": "删除",
"viewDetails": "查看详情",
"changeStatus": "更改状态",
"changePriority": "更改优先级",
"startProgress": "开始处理",
"markResolved": "标记为已解决",
"github": "从 GitHub 拉取"
},
"filters": {
"all": "全部",
"open": "开放",
"inProgress": "进行中",
"resolved": "已解决",
"closed": "已关闭",
"byPriority": "按优先级"
},
"emptyState": {
"title": "未找到问题",
"message": "没有符合当前筛选条件的问题。",
"createFirst": "创建第一个问题以开始"
},
"createDialog": {
"title": "创建新问题",
"labels": {
"title": "标题",
"context": "上下文",
"priority": "优先级"
},
"placeholders": {
"title": "输入问题标题...",
"context": "描述问题上下文..."
},
"buttons": {
"create": "创建",
"cancel": "取消",
"creating": "创建中..."
}
},
"card": {
"id": "ID",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"solutions": "{count, plural, one {解决方案} other {解决方案}}"
}
}

View File

@@ -0,0 +1,26 @@
{
"title": "轻量任务",
"subtitle": "{count} 个会话",
"type": {
"plan": "轻量规划",
"fix": "轻量修复",
"multiCli": "多 CLI 规划"
},
"rounds": "轮",
"empty": {
"title": "没有 {type} 会话",
"message": "创建新会话以开始使用。"
},
"flowchart": "流程图",
"implementationFlow": "实现流程",
"focusPaths": "关注路径",
"acceptanceCriteria": "验收标准",
"emptyDetail": {
"title": "此会话中没有任务",
"message": "此会话尚不包含任何任务。"
},
"notFound": {
"title": "未找到轻量任务",
"message": "无法找到请求的轻量任务会话。"
}
}

View File

@@ -0,0 +1,68 @@
{
"title": "循环监控",
"description": "监控和控制运行中的开发循环",
"status": {
"created": "待处理",
"running": "运行中",
"paused": "已暂停",
"completed": "已完成",
"failed": "失败"
},
"actions": {
"create": "新建循环",
"pause": "暂停",
"resume": "继续",
"stop": "停止",
"restart": "重新开始",
"viewDetails": "查看详情"
},
"emptyState": {
"title": "无活跃循环",
"message": "启动新的开发循环以开始监控进度。",
"createFirst": "启动新循环"
},
"card": {
"step": "步骤",
"of": "/",
"progress": "进度",
"prompt": "提示词",
"tool": "工具",
"iteration": "迭代",
"error": "错误"
},
"createDialog": {
"title": "启动新循环",
"labels": {
"prompt": "提示词",
"tool": "CLI 工具(可选)"
},
"placeholders": {
"prompt": "输入开发循环提示词...",
"tool": "例如gemini、qwen、codex"
},
"buttons": {
"create": "启动",
"cancel": "取消"
}
},
"monitor": {
"title": "循环监控",
"loops": "循环",
"tasks": "任务",
"iterations": "迭代",
"timeline": "时间线"
},
"taskStatus": {
"pending": "待处理",
"inProgress": "进行中",
"blocked": "已阻塞",
"done": "完成"
},
"columns": {
"pending": "待处理",
"running": "运行中",
"paused": "已暂停",
"completed": "已完成",
"failed": "失败"
}
}

View File

@@ -0,0 +1,37 @@
{
"title": "MCP 服务器",
"description": "管理模型上下文协议 (MCP) 服务器以实现跨 CLI 集成",
"scope": {
"global": "全局",
"project": "项目"
},
"status": {
"enabled": "已启用",
"disabled": "已禁用"
},
"stats": {
"total": "服务器总数",
"enabled": "已启用",
"global": "全局",
"project": "项目"
},
"command": "命令",
"args": "参数",
"env": "环境变量",
"filters": {
"all": "全部",
"searchPlaceholder": "按名称或命令搜索服务器..."
},
"actions": {
"add": "添加服务器",
"edit": "编辑服务器",
"delete": "删除服务器",
"toggle": "切换服务器",
"expand": "查看详情"
},
"deleteConfirm": "确定要删除 MCP 服务器 \"{name}\" 吗?",
"emptyState": {
"title": "未找到 MCP 服务器",
"message": "添加 MCP 服务器以启用与 Claude、Codex 和 Qwen 等工具的跨 CLI 集成。"
}
}

View File

@@ -0,0 +1,65 @@
{
"title": "记忆",
"description": "管理核心记忆、上下文和知识库",
"actions": {
"add": "添加记忆",
"edit": "编辑",
"delete": "删除",
"copy": "复制",
"refresh": "刷新",
"expand": "展开",
"collapse": "收起"
},
"stats": {
"totalSize": "总大小",
"count": "数量",
"claudeMdCount": "CLAUDE.md 文件",
"totalEntries": "总条目"
},
"filters": {
"search": "搜索记忆...",
"tags": "标签",
"clear": "清除",
"all": "全部"
},
"card": {
"id": "ID",
"content": "内容",
"summary": "摘要",
"tags": "标签",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"size": "大小",
"favorite": "收藏",
"archived": "已归档"
},
"emptyState": {
"title": "未存储记忆",
"message": "添加上下文和知识以帮助 Claude 更好地理解您的项目。",
"createFirst": "添加第一条记忆"
},
"createDialog": {
"title": "添加记忆",
"editTitle": "编辑记忆",
"labels": {
"content": "内容",
"tags": "标签"
},
"placeholders": {
"content": "输入记忆内容...",
"tags": "例如project、config、api"
},
"buttons": {
"create": "添加记忆",
"update": "更新记忆",
"cancel": "取消",
"creating": "创建中...",
"updating": "更新中..."
}
},
"types": {
"coreMemory": "核心记忆",
"workflow": "工作流",
"cliHistory": "CLI 历史"
}
}

View File

@@ -0,0 +1,38 @@
{
"main": {
"home": "首页",
"sessions": "会话",
"liteTasks": "轻量任务",
"project": "项目",
"history": "历史",
"orchestrator": "编排器",
"loops": "循环监控",
"issues": "问题",
"skills": "技能",
"commands": "命令",
"memory": "记忆",
"settings": "设置",
"mcp": "MCP 服务器",
"endpoints": "CLI 端点",
"installations": "安装",
"help": "帮助"
},
"sidebar": {
"collapse": "收起",
"expand": "展开侧边栏",
"collapseAria": "收起侧边栏"
},
"header": {
"brand": "Claude Code Workflow",
"brandShort": "CCW",
"noProject": "未选择项目",
"settings": "设置",
"logout": "退出"
},
"breadcrumbs": {
"home": "首页",
"sessions": "会话",
"detail": "详情",
"settings": "设置"
}
}

View File

@@ -0,0 +1,63 @@
{
"title": "编排器",
"description": "管理和执行工作流",
"flow": {
"title": "流程",
"flows": "流程列表",
"create": "新建流程",
"edit": "编辑流程",
"delete": "删除流程",
"duplicate": "复制流程",
"export": "导出流程",
"import": "导入流程"
},
"execution": {
"title": "执行",
"status": "状态",
"start": "开始",
"pause": "暂停",
"resume": "继续",
"stop": "停止",
"restart": "重新开始",
"viewLogs": "查看日志"
},
"status": {
"pending": "待处理",
"running": "运行中",
"paused": "已暂停",
"completed": "已完成",
"failed": "失败"
},
"node": {
"title": "节点",
"nodes": "节点列表",
"add": "添加节点",
"edit": "编辑节点",
"delete": "删除节点",
"status": "节点状态",
"result": "结果"
},
"actions": {
"execute": "执行",
"validate": "验证",
"save": "保存",
"cancel": "取消"
},
"emptyState": {
"noFlows": {
"title": "未找到流程",
"message": "创建第一个工作流流程以开始。"
},
"noExecution": {
"title": "无执行历史",
"message": "执行流程以查看执行历史。"
}
},
"monitor": {
"title": "执行监控",
"logs": "日志",
"timeline": "时间线",
"variables": "变量",
"realtime": "实时更新"
}
}

View File

@@ -0,0 +1,52 @@
{
"noDescription": "暂无描述",
"header": {
"initialized": "初始化时间"
},
"techStack": {
"title": "技术栈",
"languages": "编程语言",
"frameworks": "框架",
"buildTools": "构建工具",
"testFrameworks": "测试框架",
"primary": "主要",
"noLanguages": "未检测到编程语言",
"noFrameworks": "未检测到框架"
},
"architecture": {
"title": "架构",
"style": "架构风格",
"layers": "分层",
"patterns": "设计模式"
},
"components": {
"title": "核心组件",
"importance": {
"high": "高",
"medium": "中",
"low": "低"
}
},
"devIndex": {
"title": "开发历史",
"categories": "分类",
"timeline": "时间线"
},
"guidelines": {
"title": "项目规范",
"conventions": "约定",
"constraints": "约束",
"qualityRules": "质量规则",
"learnings": "学习总结",
"scope": "范围"
},
"stats": {
"title": "统计",
"totalFeatures": "总功能数",
"lastUpdated": "最后更新"
},
"empty": {
"title": "暂无项目概览",
"message": "运行 /workflow:init 初始化项目分析"
}
}

View File

@@ -0,0 +1,41 @@
{
"title": "审查会话",
"type": "审查",
"severity": {
"critical": "严重",
"high": "高",
"medium": "中",
"low": "低"
},
"stats": {
"total": "总发现",
"dimensions": "维度"
},
"search": {
"placeholder": "搜索发现..."
},
"sort": {
"severity": "按严重程度",
"dimension": "按维度",
"file": "按文件"
},
"selection": {
"count": "已选择 {count} 项",
"selectAll": "全选",
"clearAll": "清除全部",
"clear": "清除"
},
"export": "导出修复 JSON",
"codeContext": "代码上下文",
"rootCause": "根本原因",
"impact": "影响",
"recommendations": "建议",
"empty": {
"title": "未找到发现",
"message": "尝试调整筛选条件或搜索查询。"
},
"notFound": {
"title": "未找到审查会话",
"message": "无法找到请求的审查会话。"
}
}

View File

@@ -0,0 +1,54 @@
{
"notFound": {
"title": "会话未找到",
"message": "您要查找的会话不存在或已被删除。"
},
"tabs": {
"tasks": "任务",
"context": "上下文",
"summary": "摘要"
},
"tasks": {
"completed": "已完成",
"inProgress": "进行中",
"pending": "待处理",
"blocked": "已阻塞",
"status": {
"pending": "待处理",
"inProgress": "进行中",
"completed": "已完成",
"blocked": "已阻塞",
"skipped": "已跳过"
},
"untitled": "无标题任务",
"empty": {
"title": "未找到任务",
"message": "该会话暂无任务。"
}
},
"context": {
"requirements": "需求",
"focusPaths": "关注路径",
"artifacts": "产物",
"sharedContext": "共享上下文",
"techStack": "技术栈",
"conventions": "约定",
"empty": {
"title": "暂无上下文",
"message": "该会话暂无上下文信息。"
}
},
"summary": {
"title": "会话摘要",
"empty": {
"title": "暂无摘要",
"message": "该会话暂无摘要。"
}
},
"info": {
"created": "创建时间",
"updated": "更新时间",
"tasks": "任务",
"description": "描述"
}
}

View File

@@ -0,0 +1,48 @@
{
"title": "会话",
"description": "管理工作流会话并跟踪进度",
"status": {
"planning": "规划中",
"inProgress": "进行中",
"completed": "已完成",
"archived": "已归档",
"paused": "已暂停"
},
"actions": {
"viewDetails": "查看详情",
"archive": "归档",
"delete": "删除",
"restore": "恢复",
"pause": "暂停",
"resume": "继续"
},
"filters": {
"all": "全部",
"active": "活跃",
"planning": "规划中",
"completed": "已完成",
"archived": "已归档",
"paused": "已暂停"
},
"searchPlaceholder": "搜索会话...",
"emptyState": {
"title": "未找到会话",
"message": "没有符合当前筛选条件的工作流会话。",
"createFirst": "创建第一个会话以开始"
},
"card": {
"tasks": "任务",
"findings": "发现",
"dimensions": "维度",
"progress": "进度",
"createdAt": "创建时间",
"updatedAt": "更新时间"
},
"detail": {
"overview": "概览",
"tasks": "任务",
"summary": "摘要",
"metadata": "元数据",
"timeline": "时间线"
}
}

View File

@@ -0,0 +1,42 @@
{
"title": "技能",
"description": "管理和配置技能",
"source": {
"builtin": "内置",
"custom": "自定义",
"community": "社区"
},
"actions": {
"viewDetails": "查看详情",
"configure": "配置",
"enable": "启用",
"disable": "禁用",
"toggle": "切换",
"install": "安装技能"
},
"state": {
"enabled": "已启用",
"disabled": "已禁用",
"on": "开启",
"off": "关闭"
},
"card": {
"triggers": "触发器",
"category": "类别",
"author": "作者",
"version": "版本"
},
"filters": {
"all": "全部",
"enabled": "已启用",
"disabled": "已禁用"
},
"view": {
"grid": "网格视图",
"compact": "紧凑视图"
},
"emptyState": {
"title": "未找到技能",
"message": "没有符合当前筛选条件的技能。"
}
}

View File

@@ -2,9 +2,29 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { initMessages, getInitialLocale, getMessages, type Locale } from './lib/i18n'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
async function bootstrapApplication() {
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')
// Initialize translation messages
await initMessages()
// Determine initial locale from browser/storage
const locale: Locale = getInitialLocale()
// Get messages for the initial locale
const messages = getMessages(locale)
const root = createRoot(rootElement)
root.render(
<StrictMode>
<App locale={locale} messages={messages} />
</StrictMode>
)
}
bootstrapApplication().catch((error) => {
console.error('Failed to bootstrap application:', error)
})

View File

@@ -4,6 +4,7 @@
// Manage custom slash commands with search/filter
import { useState, useMemo } from 'react';
import { useIntl } from 'react-intl';
import {
Terminal,
Search,
@@ -35,6 +36,8 @@ interface CommandCardProps {
}
function CommandCard({ command, isExpanded, onToggleExpand, onCopy }: CommandCardProps) {
const { formatMessage } = useIntl();
return (
<Card className="overflow-hidden">
{/* Header */}
@@ -59,7 +62,7 @@ function CommandCard({ command, isExpanded, onToggleExpand, onCopy }: CommandCar
)}
</div>
<p className="text-sm text-muted-foreground mt-1">
{command.description}
{command.description || formatMessage({ id: 'commands.card.noDescription' })}
</p>
</div>
</div>
@@ -107,7 +110,7 @@ function CommandCard({ command, isExpanded, onToggleExpand, onCopy }: CommandCar
<div>
<div className="flex items-center gap-2 text-sm font-medium text-foreground mb-2">
<Code className="w-4 h-4" />
Usage
{formatMessage({ id: 'commands.card.usage' })}
</div>
<div className="p-3 bg-background rounded-md font-mono text-sm overflow-x-auto">
<code>{command.usage}</code>
@@ -120,7 +123,7 @@ function CommandCard({ command, isExpanded, onToggleExpand, onCopy }: CommandCar
<div>
<div className="flex items-center gap-2 text-sm font-medium text-foreground mb-2">
<BookOpen className="w-4 h-4" />
Examples
{formatMessage({ id: 'commands.card.examples' })}
</div>
<div className="space-y-2">
{command.examples.map((example, idx) => (
@@ -151,6 +154,7 @@ function CommandCard({ command, isExpanded, onToggleExpand, onCopy }: CommandCar
// ========== Main Page Component ==========
export function CommandsManagerPage() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string>('all');
const [sourceFilter, setSourceFilter] = useState<string>('all');
@@ -217,20 +221,20 @@ export function CommandsManagerPage() {
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<Terminal className="w-6 h-6 text-primary" />
Commands Manager
{formatMessage({ id: 'commands.title' })}
</h1>
<p className="text-muted-foreground mt-1">
Manage custom slash commands for Claude Code
{formatMessage({ id: 'commands.description' })}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
Refresh
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<Button>
<Plus className="w-4 h-4 mr-2" />
New Command
{formatMessage({ id: 'commands.actions.create' })}
</Button>
</div>
</div>
@@ -242,28 +246,28 @@ export function CommandsManagerPage() {
<Terminal className="w-5 h-5 text-primary" />
<span className="text-2xl font-bold">{totalCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Total Commands</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'common.stats.totalCommands' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<Code className="w-5 h-5 text-info" />
<span className="text-2xl font-bold">{builtinCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Built-in</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'commands.source.builtin' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<Plus className="w-5 h-5 text-success" />
<span className="text-2xl font-bold">{customCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Custom</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'commands.source.custom' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<Tag className="w-5 h-5 text-warning" />
<span className="text-2xl font-bold">{categories.length}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Categories</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'common.stats.categories' })}</p>
</Card>
</div>
@@ -272,7 +276,7 @@ export function CommandsManagerPage() {
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search commands by name, description, or alias..."
placeholder={formatMessage({ id: 'commands.filters.searchPlaceholder' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
@@ -281,10 +285,10 @@ export function CommandsManagerPage() {
<div className="flex gap-2">
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Category" />
<SelectValue placeholder={formatMessage({ id: 'commands.filters.category' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="all">{formatMessage({ id: 'commands.filters.allCategories' })}</SelectItem>
{categories.map((cat) => (
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
))}
@@ -292,12 +296,12 @@ export function CommandsManagerPage() {
</Select>
<Select value={sourceFilter} onValueChange={setSourceFilter}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Source" />
<SelectValue placeholder={formatMessage({ id: 'commands.filters.source' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Sources</SelectItem>
<SelectItem value="builtin">Built-in</SelectItem>
<SelectItem value="custom">Custom</SelectItem>
<SelectItem value="all">{formatMessage({ id: 'commands.filters.allSources' })}</SelectItem>
<SelectItem value="builtin">{formatMessage({ id: 'commands.source.builtin' })}</SelectItem>
<SelectItem value="custom">{formatMessage({ id: 'commands.source.custom' })}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -306,10 +310,10 @@ export function CommandsManagerPage() {
{/* Expand/Collapse All */}
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={expandAll}>
Expand All
{formatMessage({ id: 'commands.actions.expandAll' })}
</Button>
<Button variant="ghost" size="sm" onClick={collapseAll}>
Collapse All
{formatMessage({ id: 'commands.actions.collapseAll' })}
</Button>
</div>
@@ -323,9 +327,9 @@ export function CommandsManagerPage() {
) : commands.length === 0 ? (
<Card className="p-8 text-center">
<Terminal className="w-12 h-12 mx-auto text-muted-foreground/50" />
<h3 className="mt-4 text-lg font-medium text-foreground">No commands found</h3>
<h3 className="mt-4 text-lg font-medium text-foreground">{formatMessage({ id: 'commands.emptyState.title' })}</h3>
<p className="mt-2 text-muted-foreground">
Try adjusting your search or filters.
{formatMessage({ id: 'commands.emptyState.message' })}
</p>
</Card>
) : (

View File

@@ -0,0 +1,334 @@
// ========================================
// CLI Endpoints Page
// ========================================
// Manage LiteLLM endpoints, custom CLI endpoints, and CLI wrapper endpoints
import { useState } from 'react';
import { useIntl } from 'react-intl';
import {
Plug,
Plus,
Search,
RefreshCw,
Power,
PowerOff,
Edit,
Trash2,
ChevronDown,
ChevronUp,
Zap,
Code,
Layers,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Badge } from '@/components/ui/Badge';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
import { useCliEndpoints, useToggleCliEndpoint } from '@/hooks';
import type { CliEndpoint } from '@/lib/api';
import { cn } from '@/lib/utils';
// ========== Endpoint Card Component ==========
interface EndpointCardProps {
endpoint: CliEndpoint;
isExpanded: boolean;
onToggleExpand: () => void;
onToggle: (endpointId: string, enabled: boolean) => void;
onEdit: (endpoint: CliEndpoint) => void;
onDelete: (endpointId: string) => void;
}
function EndpointCard({ endpoint, isExpanded, onToggleExpand, onToggle, onEdit, onDelete }: EndpointCardProps) {
const { formatMessage } = useIntl();
const typeConfig = {
litellm: { icon: Zap, color: 'text-blue-600', label: 'cliEndpoints.type.litellm' },
custom: { icon: Code, color: 'text-purple-600', label: 'cliEndpoints.type.custom' },
wrapper: { icon: Layers, color: 'text-orange-600', label: 'cliEndpoints.type.wrapper' },
};
const config = typeConfig[endpoint.type];
const Icon = config.icon;
return (
<Card className={cn('overflow-hidden', !endpoint.enabled && 'opacity-60')}>
{/* Header */}
<div
className="p-4 cursor-pointer hover:bg-muted/50 transition-colors"
onClick={onToggleExpand}
>
<div className="flex items-start justify-between gap-2">
<div className="flex items-start gap-3">
<div className={cn(
'p-2 rounded-lg',
endpoint.enabled ? 'bg-primary/10' : 'bg-muted'
)}>
<Icon className={cn(
'w-5 h-5',
endpoint.enabled ? config.color : 'text-muted-foreground'
)} />
</div>
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">
{endpoint.name}
</span>
<Badge variant="outline" className="text-xs">
{formatMessage({ id: config.label })}
</Badge>
{endpoint.enabled && (
<Badge variant="outline" className="text-xs text-green-600">
{formatMessage({ id: 'cliEndpoints.status.enabled' })}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground mt-1">
{formatMessage({ id: 'cliEndpoints.id' })}: {endpoint.id}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
onToggle(endpoint.id, !endpoint.enabled);
}}
>
{endpoint.enabled ? <Power className="w-4 h-4 text-green-600" /> : <PowerOff className="w-4 h-4" />}
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
onEdit(endpoint);
}}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={(e) => {
e.stopPropagation();
onDelete(endpoint.id);
}}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-muted-foreground" />
) : (
<ChevronDown className="w-5 h-5 text-muted-foreground" />
)}
</div>
</div>
</div>
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-border p-4 space-y-3 bg-muted/30">
{/* Config display */}
<div>
<p className="text-xs text-muted-foreground mb-2">{formatMessage({ id: 'cliEndpoints.config' })}</p>
<div className="bg-background p-3 rounded-md font-mono text-sm overflow-x-auto">
<pre>{JSON.stringify(endpoint.config, null, 2)}</pre>
</div>
</div>
</div>
)}
</Card>
);
}
// ========== Main Page Component ==========
export function EndpointsPage() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState<'all' | 'litellm' | 'custom' | 'wrapper'>('all');
const [expandedEndpoints, setExpandedEndpoints] = useState<Set<string>>(new Set());
const {
endpoints,
litellmEndpoints,
customEndpoints,
totalCount,
enabledCount,
isLoading,
isFetching,
refetch,
} = useCliEndpoints();
const { toggleEndpoint } = useToggleCliEndpoint();
const toggleExpand = (endpointId: string) => {
setExpandedEndpoints((prev) => {
const next = new Set(prev);
if (next.has(endpointId)) {
next.delete(endpointId);
} else {
next.add(endpointId);
}
return next;
});
};
const handleToggle = (endpointId: string, enabled: boolean) => {
toggleEndpoint(endpointId, enabled);
};
const handleDelete = (endpointId: string) => {
if (confirm(formatMessage({ id: 'cliEndpoints.deleteConfirm' }, { id: endpointId }))) {
// TODO: Implement delete functionality
console.log('Delete endpoint:', endpointId);
}
};
const handleEdit = (endpoint: CliEndpoint) => {
// TODO: Implement edit dialog
console.log('Edit endpoint:', endpoint);
};
// Filter endpoints by search query and type
const filteredEndpoints = (() => {
let filtered = endpoints;
if (typeFilter !== 'all') {
filtered = filtered.filter((e) => e.type === typeFilter);
}
if (searchQuery) {
const searchLower = searchQuery.toLowerCase();
filtered = filtered.filter((e) =>
e.name.toLowerCase().includes(searchLower) ||
e.id.toLowerCase().includes(searchLower)
);
}
return filtered;
})();
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<Plug className="w-6 h-6 text-primary" />
{formatMessage({ id: 'cliEndpoints.title' })}
</h1>
<p className="text-muted-foreground mt-1">
{formatMessage({ id: 'cliEndpoints.description' })}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<Button>
<Plus className="w-4 h-4 mr-2" />
{formatMessage({ id: 'cliEndpoints.actions.add' })}
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-4">
<div className="flex items-center gap-2">
<Plug className="w-5 h-5 text-primary" />
<span className="text-2xl font-bold">{totalCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'cliEndpoints.stats.total' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<Power className="w-5 h-5 text-green-600" />
<span className="text-2xl font-bold">{enabledCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'cliEndpoints.stats.enabled' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<Zap className="w-5 h-5 text-blue-600" />
<span className="text-2xl font-bold">{litellmEndpoints.length}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'cliEndpoints.type.litellm' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<Code className="w-5 h-5 text-purple-600" />
<span className="text-2xl font-bold">{customEndpoints.length}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'cliEndpoints.type.custom' })}</p>
</Card>
</div>
{/* Filters and Search */}
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder={formatMessage({ id: 'cliEndpoints.filters.searchPlaceholder' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Select value={typeFilter} onValueChange={(v: typeof typeFilter) => setTypeFilter(v)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={formatMessage({ id: 'cliEndpoints.filters.type' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{formatMessage({ id: 'cliEndpoints.filters.allTypes' })}</SelectItem>
<SelectItem value="litellm">{formatMessage({ id: 'cliEndpoints.type.litellm' })}</SelectItem>
<SelectItem value="custom">{formatMessage({ id: 'cliEndpoints.type.custom' })}</SelectItem>
<SelectItem value="wrapper">{formatMessage({ id: 'cliEndpoints.type.wrapper' })}</SelectItem>
</SelectContent>
</Select>
</div>
{/* Endpoints List */}
{isLoading ? (
<div className="space-y-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-24 bg-muted animate-pulse rounded-lg" />
))}
</div>
) : filteredEndpoints.length === 0 ? (
<Card className="p-8 text-center">
<Plug className="w-12 h-12 mx-auto text-muted-foreground/50" />
<h3 className="mt-4 text-lg font-medium text-foreground">{formatMessage({ id: 'cliEndpoints.emptyState.title' })}</h3>
<p className="mt-2 text-muted-foreground">
{formatMessage({ id: 'cliEndpoints.emptyState.message' })}
</p>
</Card>
) : (
<div className="space-y-3">
{filteredEndpoints.map((endpoint) => (
<EndpointCard
key={endpoint.id}
endpoint={endpoint}
isExpanded={expandedEndpoints.has(endpoint.id)}
onToggleExpand={() => toggleExpand(endpoint.id)}
onToggle={handleToggle}
onEdit={handleEdit}
onDelete={handleDelete}
/>
))}
</div>
)}
</div>
);
}
export default EndpointsPage;

View File

@@ -0,0 +1,364 @@
// ========================================
// FixSessionPage Component
// ========================================
// Fix session detail page for displaying fix session tasks
import * as React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
ArrowLeft,
Wrench,
CheckCircle,
XCircle,
Clock,
File,
Loader2,
} from 'lucide-react';
import { useSessions } from '@/hooks/useSessions';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card, CardContent } from '@/components/ui/Card';
type TaskStatusFilter = 'all' | 'pending' | 'in_progress' | 'fixed' | 'failed';
interface FixTask {
task_id: string;
id?: string;
title?: string;
status: 'pending' | 'in_progress' | 'completed';
result?: 'fixed' | 'failed';
file?: string;
line?: number;
finding_title?: string;
dimension?: string;
attempts?: number;
commit_hash?: string;
}
/**
* FixSessionPage component - Display fix session tasks and progress
*/
export function FixSessionPage() {
const { sessionId } = useParams<{ sessionId: string }>();
const navigate = useNavigate();
const { formatMessage } = useIntl();
const { filteredSessions, isLoading, error, refetch } = useSessions({
filter: { location: 'all' },
});
const [statusFilter, setStatusFilter] = React.useState<TaskStatusFilter>('all');
// Find session
const session = React.useMemo(
() => filteredSessions.find((s) => s.session_id === sessionId),
[filteredSessions, sessionId]
);
const tasks = React.useMemo(() => {
if (!session?.tasks) return [];
return session.tasks as FixTask[];
}, [session?.tasks]);
// Calculate statistics
const stats = React.useMemo(() => {
const total = tasks.length;
const fixed = tasks.filter((t) => t.status === 'completed' && t.result === 'fixed').length;
const failed = tasks.filter((t) => t.status === 'completed' && t.result === 'failed').length;
const pending = tasks.filter((t) => t.status === 'pending').length;
const inProgress = tasks.filter((t) => t.status === 'in_progress').length;
const completed = fixed + failed;
const percentComplete = total > 0 ? Math.round((completed / total) * 100) : 0;
return { total, fixed, failed, pending, inProgress, completed, percentComplete };
}, [tasks]);
// Filter tasks
const filteredTasks = React.useMemo(() => {
if (statusFilter === 'all') return tasks;
if (statusFilter === 'fixed') {
return tasks.filter((t) => t.status === 'completed' && t.result === 'fixed');
}
if (statusFilter === 'failed') {
return tasks.filter((t) => t.status === 'completed' && t.result === 'failed');
}
return tasks.filter((t) => t.status === statusFilter);
}, [tasks, statusFilter]);
// Get status badge props
const getStatusBadge = (task: FixTask) => {
if (task.status === 'completed') {
if (task.result === 'fixed') {
return { variant: 'success' as const, label: formatMessage({ id: 'fixSession.status.fixed' }), icon: CheckCircle };
}
if (task.result === 'failed') {
return { variant: 'destructive' as const, label: formatMessage({ id: 'fixSession.status.failed' }), icon: XCircle };
}
}
if (task.status === 'in_progress') {
return { variant: 'warning' as const, label: formatMessage({ id: 'fixSession.status.inProgress' }), icon: Loader2 };
}
return { variant: 'secondary' as const, label: formatMessage({ id: 'fixSession.status.pending' }), icon: Clock };
};
const handleBack = () => {
navigate('/sessions');
};
const handleFilterChange = (filter: TaskStatusFilter) => {
setStatusFilter(filter);
};
// Loading state
if (isLoading) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" disabled>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.back' })}
</Button>
<div className="h-8 w-48 rounded bg-muted animate-pulse" />
</div>
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-24 rounded-lg bg-muted animate-pulse" />
))}
</div>
</div>
);
}
// Error state
if (error) {
return (
<div className="flex items-center gap-2 p-4 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive">
<XCircle className="h-5 w-5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium">{formatMessage({ id: 'common.errors.loadFailed' })}</p>
<p className="text-xs mt-0.5">{error.message}</p>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
{formatMessage({ id: 'common.actions.retry' })}
</Button>
</div>
);
}
// Session not found
if (!session) {
return (
<div className="flex flex-col items-center justify-center py-16 px-4">
<Wrench className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'fixSession.notFound.title' })}
</h3>
<p className="text-sm text-muted-foreground mb-4">
{formatMessage({ id: 'fixSession.notFound.message' })}
</p>
<Button onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.back' })}
</Button>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.back' })}
</Button>
<div className="flex-1">
<h1 className="text-2xl font-semibold text-foreground">{session.session_id}</h1>
{session.title && (
<p className="text-sm text-muted-foreground mt-0.5">{session.title}</p>
)}
</div>
<Badge variant="warning">
<Wrench className="h-3 w-3 mr-1" />
Fix
</Badge>
</div>
{/* Progress Section */}
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Wrench className="h-5 w-5" />
{formatMessage({ id: 'fixSession.progress.title' })}
</h3>
<Badge variant="secondary">{session.phase || 'Execution'}</Badge>
</div>
{/* Progress Bar */}
<div className="mb-2">
<div className="h-2 w-full rounded-full bg-muted overflow-hidden">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${stats.percentComplete}%` }}
/>
</div>
</div>
<div className="text-sm text-muted-foreground mb-6">
<strong>{stats.completed}</strong>/{stats.total} {formatMessage({ id: 'common.tasks' })} (
{stats.percentComplete}%)
</div>
{/* Summary Cards */}
<div className="grid grid-cols-4 gap-4">
<div className="text-center p-4 bg-background rounded-lg border">
<div className="text-2xl font-semibold text-foreground">{stats.total}</div>
<div className="text-sm text-muted-foreground">{formatMessage({ id: 'fixSession.stats.total' })}</div>
</div>
<div className="text-center p-4 bg-background rounded-lg border border-success/30 bg-success/5">
<div className="text-2xl font-semibold text-success">{stats.fixed}</div>
<div className="text-sm text-muted-foreground">{formatMessage({ id: 'fixSession.stats.fixed' })}</div>
</div>
<div className="text-center p-4 bg-background rounded-lg border border-destructive/30 bg-destructive/5">
<div className="text-2xl font-semibold text-destructive">{stats.failed}</div>
<div className="text-sm text-muted-foreground">{formatMessage({ id: 'fixSession.stats.failed' })}</div>
</div>
<div className="text-center p-4 bg-background rounded-lg border">
<div className="text-2xl font-semibold text-foreground">{stats.pending}</div>
<div className="text-sm text-muted-foreground">{formatMessage({ id: 'fixSession.stats.pending' })}</div>
</div>
</div>
</CardContent>
</Card>
{/* Tasks Section */}
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<File className="h-5 w-5" />
{formatMessage({ id: 'fixSession.tasks.title' })}
</h3>
<div className="flex gap-1">
{[
{ key: 'all' as const, label: formatMessage({ id: 'fixSession.filter.all' }) },
{ key: 'pending' as const, label: formatMessage({ id: 'fixSession.filter.pending' }) },
{ key: 'in_progress' as const, label: formatMessage({ id: 'fixSession.filter.inProgress' }) },
{ key: 'fixed' as const, label: formatMessage({ id: 'fixSession.filter.fixed' }) },
{ key: 'failed' as const, label: formatMessage({ id: 'fixSession.filter.failed' }) },
].map((filter) => (
<Button
key={filter.key}
variant={statusFilter === filter.key ? 'default' : 'outline'}
size="sm"
onClick={() => handleFilterChange(filter.key)}
>
{filter.label}
</Button>
))}
</div>
</div>
{/* Tasks List */}
{filteredTasks.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<File className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'fixSession.empty.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'fixSession.empty.message' })}
</p>
</div>
) : (
<div className="grid gap-3">
{filteredTasks.map((task) => {
const statusBadge = getStatusBadge(task);
const StatusIcon = statusBadge.icon;
return (
<Card
key={task.task_id || task.id}
className={`hover:shadow-sm transition-shadow ${
task.status === 'completed' && task.result === 'failed'
? 'border-destructive/30 bg-destructive/5'
: ''
}`}
>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-muted-foreground">
{task.task_id || task.id || 'N/A'}
</span>
<Badge variant={statusBadge.variant} className="gap-1">
<StatusIcon className="h-3 w-3" />
{statusBadge.label}
</Badge>
</div>
<h4 className="font-medium text-foreground text-sm">
{task.title || formatMessage({ id: 'fixSession.task.untitled' })}
</h4>
{task.finding_title && (
<p className="text-sm text-muted-foreground mt-1">{task.finding_title}</p>
)}
{task.file && (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<File className="h-3 w-3" />
{task.file}
{task.line && `:${task.line}`}
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-1 text-xs">
{task.dimension && (
<span className="px-2 py-0.5 bg-muted rounded text-muted-foreground">
{task.dimension}
</span>
)}
{task.attempts && task.attempts > 1 && (
<span className="px-2 py-0.5 bg-muted rounded text-muted-foreground">
{formatMessage({ id: 'fixSession.task.attempts' }, { count: task.attempts })}
</span>
)}
{task.commit_hash && (
<span className="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">
{task.commit_hash.substring(0, 7)}
</span>
)}
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
</CardContent>
</Card>
{/* Session Info */}
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground p-4 bg-background rounded-lg border">
<div>
<span className="font-medium">{formatMessage({ id: 'fixSession.info.created' })}:</span>{' '}
{new Date(session.created_at).toLocaleString()}
</div>
{session.updated_at && (
<div>
<span className="font-medium">{formatMessage({ id: 'fixSession.info.updated' })}:</span>{' '}
{new Date(session.updated_at).toLocaleString()}
</div>
)}
{session.description && (
<div className="w-full">
<span className="font-medium">{formatMessage({ id: 'fixSession.info.description' })}:</span>{' '}
{session.description}
</div>
)}
</div>
</div>
);
}
export default FixSessionPage;

View File

@@ -14,6 +14,7 @@ import {
Terminal,
} from 'lucide-react';
import { Link } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
@@ -53,16 +54,18 @@ const helpSections: HelpSection[] = [
];
export function HelpPage() {
const { formatMessage } = useIntl();
return (
<div className="max-w-4xl mx-auto space-y-8">
{/* Page Header */}
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<HelpCircle className="w-6 h-6 text-primary" />
Help & Documentation
{formatMessage({ id: 'help.title' })}
</h1>
<p className="text-muted-foreground mt-1">
Learn how to use CCW Dashboard and get the most out of your workflows
{formatMessage({ id: 'help.description' })}
</p>
</div>
@@ -182,19 +185,19 @@ export function HelpPage() {
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-foreground">
Need more help?
{formatMessage({ id: 'help.support.title' })}
</h3>
<p className="text-muted-foreground mt-1 mb-4">
Check the project documentation or reach out for support.
{formatMessage({ id: 'help.support.description' })}
</p>
<div className="flex gap-3">
<Button variant="outline" size="sm">
<Book className="w-4 h-4 mr-2" />
Documentation
{formatMessage({ id: 'help.support.documentation' })}
</Button>
<Button variant="outline" size="sm">
<Video className="w-4 h-4 mr-2" />
Tutorials
{formatMessage({ id: 'help.support.tutorials' })}
</Button>
</div>
</div>

View File

@@ -0,0 +1,314 @@
// ========================================
// HistoryPage Component
// ========================================
// CLI execution history page with filtering and bulk actions
import * as React from 'react';
import { useIntl } from 'react-intl';
import {
Terminal,
SearchX,
RefreshCw,
Trash2,
AlertTriangle,
Search,
X,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useHistory } from '@/hooks/useHistory';
import { ConversationCard } from '@/components/shared/ConversationCard';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/Dialog';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuLabel,
} from '@/components/ui/Dropdown';
/**
* HistoryPage component - Display CLI execution history
*/
export function HistoryPage() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = React.useState('');
const [toolFilter, setToolFilter] = React.useState<string | undefined>(undefined);
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
const [deleteType, setDeleteType] = React.useState<'single' | 'tool' | 'all' | null>(null);
const [deleteTarget, setDeleteTarget] = React.useState<string | null>(null);
const {
executions,
isLoading,
isFetching,
error,
refetch,
deleteExecution,
deleteByTool,
deleteAll,
isDeleting,
} = useHistory({
filter: { search: searchQuery || undefined, tool: toolFilter },
});
const tools = React.useMemo(() => {
const toolSet = new Set(executions.map((e) => e.tool));
return Array.from(toolSet).sort();
}, [executions]);
// Filter handlers
const handleClearSearch = () => {
setSearchQuery('');
};
const handleClearFilters = () => {
setSearchQuery('');
setToolFilter(undefined);
};
const hasActiveFilters = searchQuery.length > 0 || toolFilter !== undefined;
// Delete handlers
const handleDeleteClick = (id: string) => {
setDeleteType('single');
setDeleteTarget(id);
setDeleteDialogOpen(true);
};
const handleDeleteByTool = (tool: string) => {
setDeleteType('tool');
setDeleteTarget(tool);
setDeleteDialogOpen(true);
};
const handleDeleteAll = () => {
setDeleteType('all');
setDeleteTarget(null);
setDeleteDialogOpen(true);
};
const handleConfirmDelete = async () => {
try {
if (deleteType === 'single' && deleteTarget) {
await deleteExecution(deleteTarget);
} else if (deleteType === 'tool' && deleteTarget) {
await deleteByTool(deleteTarget);
} else if (deleteType === 'all') {
await deleteAll();
}
setDeleteDialogOpen(false);
setDeleteType(null);
setDeleteTarget(null);
} catch (err) {
console.error('Failed to delete:', err);
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-semibold text-foreground">
{formatMessage({ id: 'history.title' })}
</h1>
<p className="text-sm text-muted-foreground mt-1">
{formatMessage({ id: 'history.description' })}
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
disabled={isFetching}
>
<RefreshCw className={cn('h-4 w-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Trash2 className="h-4 w-4 mr-2" />
{formatMessage({ id: 'history.deleteOptions' })}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{formatMessage({ id: 'history.deleteBy' })}</DropdownMenuLabel>
<DropdownMenuSeparator />
{tools.map((tool) => (
<DropdownMenuItem key={tool} onClick={() => handleDeleteByTool(tool)}>
{formatMessage({ id: 'history.deleteAllTool' }, { tool })}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleDeleteAll}
className="text-destructive focus:text-destructive"
>
<AlertTriangle className="mr-2 h-4 w-4" />
{formatMessage({ id: 'history.deleteAll' })}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Error alert */}
{error && (
<div className="flex items-center gap-2 p-4 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive">
<Terminal className="h-5 w-5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium">{formatMessage({ id: 'common.errors.loadFailed' })}</p>
<p className="text-xs mt-0.5">{error.message}</p>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
{formatMessage({ id: 'common.actions.retry' })}
</Button>
</div>
)}
{/* Filters */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
{/* Search input */}
<div className="flex-1 max-w-sm relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={formatMessage({ id: 'history.searchPlaceholder' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-9"
/>
{searchQuery && (
<button
onClick={handleClearSearch}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
)}
</div>
{/* Tool filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2 min-w-[160px] justify-between">
{toolFilter || formatMessage({ id: 'history.filterAllTools' })}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem onClick={() => setToolFilter(undefined)}>
{formatMessage({ id: 'history.filterAllTools' })}
</DropdownMenuItem>
<DropdownMenuSeparator />
{tools.map((tool) => (
<DropdownMenuItem
key={tool}
onClick={() => setToolFilter(tool)}
className={toolFilter === tool ? 'bg-accent' : ''}
>
{tool}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Clear filters */}
{hasActiveFilters && (
<Button variant="ghost" size="sm" onClick={handleClearFilters}>
{formatMessage({ id: 'common.actions.clearFilters' })}
</Button>
)}
</div>
{/* Executions list */}
{isLoading ? (
<div className="grid gap-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-28 rounded-lg bg-muted animate-pulse" />
))}
</div>
) : executions.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 px-4">
<SearchX className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{hasActiveFilters
? formatMessage({ id: 'history.empty.filtered' })
: formatMessage({ id: 'history.empty.title' })}
</h3>
<p className="text-sm text-muted-foreground text-center">
{hasActiveFilters
? formatMessage({ id: 'history.empty.filteredMessage' })
: formatMessage({ id: 'history.empty.message' })}
</p>
{hasActiveFilters && (
<Button variant="outline" onClick={handleClearFilters} className="mt-4">
{formatMessage({ id: 'common.actions.clearFilters' })}
</Button>
)}
</div>
) : (
<div className="grid gap-3">
{executions.map((execution) => (
<ConversationCard
key={execution.id}
execution={execution}
onDelete={handleDeleteClick}
actionsDisabled={isDeleting}
/>
))}
</div>
)}
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{deleteType === 'all'
? formatMessage({ id: 'history.dialog.deleteAllTitle' })
: formatMessage({ id: 'history.dialog.deleteTitle' })}
</DialogTitle>
<DialogDescription>
{deleteType === 'all' && formatMessage({ id: 'history.dialog.deleteAllMessage' })}
{deleteType === 'tool' &&
formatMessage({ id: 'history.dialog.deleteToolMessage' }, { tool: deleteTarget })}
{deleteType === 'single' &&
formatMessage({ id: 'history.dialog.deleteMessage' })}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={isDeleting}
>
{formatMessage({ id: 'common.actions.cancel' })}
</Button>
<Button
variant="destructive"
onClick={handleConfirmDelete}
disabled={isDeleting}
>
{isDeleting
? formatMessage({ id: 'common.status.deleting' })
: formatMessage({ id: 'common.actions.delete' })}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
export default HistoryPage;

View File

@@ -5,6 +5,7 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
FolderKanban,
ListChecks,
@@ -22,58 +23,59 @@ import { SessionCard, SessionCardSkeleton } from '@/components/shared/SessionCar
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils';
// Stat card configuration
const statCards = [
{
key: 'activeSessions',
title: 'Active Sessions',
icon: FolderKanban,
variant: 'primary' as const,
getValue: (stats: { activeSessions: number }) => stats.activeSessions,
},
{
key: 'totalTasks',
title: 'Total Tasks',
icon: ListChecks,
variant: 'info' as const,
getValue: (stats: { totalTasks: number }) => stats.totalTasks,
},
{
key: 'completedTasks',
title: 'Completed',
icon: CheckCircle2,
variant: 'success' as const,
getValue: (stats: { completedTasks: number }) => stats.completedTasks,
},
{
key: 'pendingTasks',
title: 'Pending',
icon: Clock,
variant: 'warning' as const,
getValue: (stats: { pendingTasks: number }) => stats.pendingTasks,
},
{
key: 'failedTasks',
title: 'Failed',
icon: XCircle,
variant: 'danger' as const,
getValue: (stats: { failedTasks: number }) => stats.failedTasks,
},
{
key: 'todayActivity',
title: "Today's Activity",
icon: Activity,
variant: 'default' as const,
getValue: (stats: { todayActivity: number }) => stats.todayActivity,
},
];
/**
* HomePage component - Dashboard overview with statistics and recent sessions
*/
export function HomePage() {
const { formatMessage } = useIntl();
const navigate = useNavigate();
// Stat card configuration
const statCards = React.useMemo(() => [
{
key: 'activeSessions',
title: formatMessage({ id: 'home.stats.activeSessions' }),
icon: FolderKanban,
variant: 'primary' as const,
getValue: (stats: { activeSessions: number }) => stats.activeSessions,
},
{
key: 'totalTasks',
title: formatMessage({ id: 'home.stats.totalTasks' }),
icon: ListChecks,
variant: 'info' as const,
getValue: (stats: { totalTasks: number }) => stats.totalTasks,
},
{
key: 'completedTasks',
title: formatMessage({ id: 'home.stats.completedTasks' }),
icon: CheckCircle2,
variant: 'success' as const,
getValue: (stats: { completedTasks: number }) => stats.completedTasks,
},
{
key: 'pendingTasks',
title: formatMessage({ id: 'home.stats.pendingTasks' }),
icon: Clock,
variant: 'warning' as const,
getValue: (stats: { pendingTasks: number }) => stats.pendingTasks,
},
{
key: 'failedTasks',
title: formatMessage({ id: 'common.status.failed' }),
icon: XCircle,
variant: 'danger' as const,
getValue: (stats: { failedTasks: number }) => stats.failedTasks,
},
{
key: 'todayActivity',
title: formatMessage({ id: 'common.stats.todayActivity' }),
icon: Activity,
variant: 'default' as const,
getValue: (stats: { todayActivity: number }) => stats.todayActivity,
},
], [formatMessage]);
// Fetch dashboard stats
const {
stats,
@@ -126,9 +128,9 @@ export function HomePage() {
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-foreground">Dashboard</h1>
<h1 className="text-2xl font-semibold text-foreground">{formatMessage({ id: 'home.title' })}</h1>
<p className="text-sm text-muted-foreground mt-1">
Overview of your workflow sessions and tasks
{formatMessage({ id: 'home.description' })}
</p>
</div>
<Button
@@ -138,7 +140,7 @@ export function HomePage() {
disabled={isFetching}
>
<RefreshCw className={cn('h-4 w-4 mr-2', isFetching && 'animate-spin')} />
Refresh
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
</div>
@@ -147,20 +149,20 @@ export function HomePage() {
<div className="flex items-center gap-2 p-4 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive">
<AlertCircle className="h-5 w-5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium">Failed to load dashboard data</p>
<p className="text-sm font-medium">{formatMessage({ id: 'home.errors.loadFailed' })}</p>
<p className="text-xs mt-0.5">
{(statsError || sessionsError)?.message || 'An unexpected error occurred'}
{(statsError || sessionsError)?.message || formatMessage({ id: 'common.errors.unknownError' })}
</p>
</div>
<Button variant="outline" size="sm" onClick={handleRefresh}>
Retry
{formatMessage({ id: 'home.errors.retry' })}
</Button>
</div>
)}
{/* Stats Grid */}
<section>
<h2 className="text-lg font-medium text-foreground mb-4">Statistics</h2>
<h2 className="text-lg font-medium text-foreground mb-4">{formatMessage({ id: 'home.sections.statistics' })}</h2>
<div className="grid gap-4 grid-cols-2 md:grid-cols-3 lg:grid-cols-6">
{isLoading
? // Loading skeletons
@@ -182,9 +184,9 @@ export function HomePage() {
{/* Recent Sessions */}
<section>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-medium text-foreground">Recent Sessions</h2>
<h2 className="text-lg font-medium text-foreground">{formatMessage({ id: 'home.sections.recentSessions' })}</h2>
<Button variant="link" size="sm" onClick={handleViewAllSessions}>
View All
{formatMessage({ id: 'common.actions.viewAll' })}
</Button>
</div>
@@ -199,9 +201,9 @@ export function HomePage() {
// Empty state
<div className="flex flex-col items-center justify-center py-12 px-4 border border-dashed border-border rounded-lg">
<FolderKanban className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-1">No sessions yet</h3>
<h3 className="text-lg font-medium text-foreground mb-1">{formatMessage({ id: 'home.emptyState.noSessions.title' })}</h3>
<p className="text-sm text-muted-foreground text-center max-w-sm">
Start a new workflow session to track your development tasks and progress.
{formatMessage({ id: 'home.emptyState.noSessions.message' })}
</p>
</div>
) : (

View File

@@ -0,0 +1,310 @@
// ========================================
// CLI Installations Page
// ========================================
// Manage CCW CLI tool installations (install, upgrade, uninstall)
import { useState } from 'react';
import { useIntl } from 'react-intl';
import {
Download,
Upload,
Trash2,
Search,
RefreshCw,
CheckCircle,
XCircle,
AlertCircle,
Package,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Badge } from '@/components/ui/Badge';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select';
import {
useCliInstallations,
useInstallCliTool,
useUninstallCliTool,
useUpgradeCliTool,
} from '@/hooks';
import type { CliInstallation } from '@/lib/api';
import { cn } from '@/lib/utils';
// ========== Installation Card Component ==========
interface InstallationCardProps {
installation: CliInstallation;
onInstall: (toolName: string) => void;
onUninstall: (toolName: string) => void;
onUpgrade: (toolName: string) => void;
isInstalling: boolean;
isUninstalling: boolean;
isUpgrading: boolean;
}
function InstallationCard({
installation,
onInstall,
onUninstall,
onUpgrade,
isInstalling,
isUninstalling,
isUpgrading,
}: InstallationCardProps) {
const { formatMessage } = useIntl();
const statusConfig = {
active: { icon: CheckCircle, color: 'text-green-600', label: 'cliInstallations.status.active' },
inactive: { icon: XCircle, color: 'text-muted-foreground', label: 'cliInstallations.status.inactive' },
error: { icon: AlertCircle, color: 'text-destructive', label: 'cliInstallations.status.error' },
};
const config = statusConfig[installation.status];
const StatusIcon = config.icon;
const isLoading = isInstalling || isUninstalling || isUpgrading;
return (
<Card className={cn('p-4', !installation.installed && 'opacity-60')}>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3">
<div className={cn(
'p-2 rounded-lg',
installation.installed ? 'bg-primary/10' : 'bg-muted'
)}>
<Package className={cn(
'w-5 h-5',
installation.installed ? 'text-primary' : 'text-muted-foreground'
)} />
</div>
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">
{installation.name}
</span>
{installation.version && (
<Badge variant="outline" className="text-xs">
v{installation.version}
</Badge>
)}
<Badge variant="outline" className={cn('text-xs', config.color)}>
<StatusIcon className="w-3 h-3 mr-1" />
{formatMessage({ id: config.label })}
</Badge>
{installation.installed && (
<Badge variant="outline" className="text-xs text-green-600">
{formatMessage({ id: 'cliInstallations.installed' })}
</Badge>
)}
</div>
{installation.path && (
<p className="text-xs text-muted-foreground mt-1 font-mono">
{installation.path}
</p>
)}
{installation.lastChecked && (
<p className="text-xs text-muted-foreground mt-1">
{formatMessage({ id: 'cliInstallations.lastChecked' })}: {new Date(installation.lastChecked).toLocaleString()}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
{installation.installed ? (
<>
<Button
variant="outline"
size="sm"
onClick={() => onUpgrade(installation.name)}
disabled={isLoading}
>
<Upload className="w-4 h-4 mr-1" />
{formatMessage({ id: 'cliInstallations.actions.upgrade' })}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onUninstall(installation.name)}
disabled={isLoading}
className="text-destructive hover:text-destructive"
>
<Trash2 className="w-4 h-4 mr-1" />
{formatMessage({ id: 'cliInstallations.actions.uninstall' })}
</Button>
</>
) : (
<Button
variant="default"
size="sm"
onClick={() => onInstall(installation.name)}
disabled={isLoading}
>
<Download className="w-4 h-4 mr-1" />
{formatMessage({ id: 'cliInstallations.actions.install' })}
</Button>
)}
</div>
</div>
</Card>
);
}
// ========== Main Page Component ==========
export function InstallationsPage() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'installed' | 'not-installed'>('all');
const {
installations,
totalCount,
installedCount,
isLoading,
isFetching,
refetch,
} = useCliInstallations();
const { installTool, isInstalling } = useInstallCliTool();
const { uninstallTool, isUninstalling } = useUninstallCliTool();
const { upgradeTool, isUpgrading } = useUpgradeCliTool();
const handleInstall = (toolName: string) => {
installTool(toolName);
};
const handleUninstall = (toolName: string) => {
if (confirm(formatMessage({ id: 'cliInstallations.uninstallConfirm' }, { name: toolName }))) {
uninstallTool(toolName);
}
};
const handleUpgrade = (toolName: string) => {
upgradeTool(toolName);
};
// Filter installations by search query and status
const filteredInstallations = (() => {
let filtered = installations;
if (statusFilter === 'installed') {
filtered = filtered.filter((i) => i.installed);
} else if (statusFilter === 'not-installed') {
filtered = filtered.filter((i) => !i.installed);
}
if (searchQuery) {
const searchLower = searchQuery.toLowerCase();
filtered = filtered.filter((i) =>
i.name.toLowerCase().includes(searchLower) ||
(i.version && i.version.toLowerCase().includes(searchLower))
);
}
return filtered;
})();
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<Package className="w-6 h-6 text-primary" />
{formatMessage({ id: 'cliInstallations.title' })}
</h1>
<p className="text-muted-foreground mt-1">
{formatMessage({ id: 'cliInstallations.description' })}
</p>
</div>
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<Card className="p-4">
<div className="flex items-center gap-2">
<Package className="w-5 h-5 text-primary" />
<span className="text-2xl font-bold">{totalCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'cliInstallations.stats.total' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<span className="text-2xl font-bold">{installedCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'cliInstallations.stats.installed' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<XCircle className="w-5 h-5 text-muted-foreground" />
<span className="text-2xl font-bold">{totalCount - installedCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'cliInstallations.stats.available' })}</p>
</Card>
</div>
{/* Filters and Search */}
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder={formatMessage({ id: 'cliInstallations.filters.searchPlaceholder' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Select value={statusFilter} onValueChange={(v: typeof statusFilter) => setStatusFilter(v)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={formatMessage({ id: 'cliInstallations.filters.status' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">{formatMessage({ id: 'cliInstallations.filters.all' })}</SelectItem>
<SelectItem value="installed">{formatMessage({ id: 'cliInstallations.filters.installed' })}</SelectItem>
<SelectItem value="not-installed">{formatMessage({ id: 'cliInstallations.filters.notInstalled' })}</SelectItem>
</SelectContent>
</Select>
</div>
{/* Installations List */}
{isLoading ? (
<div className="space-y-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-20 bg-muted animate-pulse rounded-lg" />
))}
</div>
) : filteredInstallations.length === 0 ? (
<Card className="p-8 text-center">
<Package className="w-12 h-12 mx-auto text-muted-foreground/50" />
<h3 className="mt-4 text-lg font-medium text-foreground">{formatMessage({ id: 'cliInstallations.emptyState.title' })}</h3>
<p className="mt-2 text-muted-foreground">
{formatMessage({ id: 'cliInstallations.emptyState.message' })}
</p>
</Card>
) : (
<div className="space-y-3">
{filteredInstallations.map((installation) => (
<InstallationCard
key={installation.name}
installation={installation}
onInstall={handleInstall}
onUninstall={handleUninstall}
onUpgrade={handleUpgrade}
isInstalling={isInstalling}
isUninstalling={isUninstalling}
isUpgrading={isUpgrading}
/>
))}
</div>
)}
</div>
);
}
export default InstallationsPage;

View File

@@ -4,6 +4,7 @@
// Track and manage project issues with drag-drop queue
import { useState, useMemo } from 'react';
import { useIntl } from 'react-intl';
import {
AlertCircle,
Plus,
@@ -41,6 +42,7 @@ interface NewIssueDialogProps {
}
function NewIssueDialog({ open, onOpenChange, onSubmit, isCreating }: NewIssueDialogProps) {
const { formatMessage } = useIntl();
const [title, setTitle] = useState('');
const [context, setContext] = useState('');
const [priority, setPriority] = useState<Issue['priority']>('medium');
@@ -59,56 +61,56 @@ function NewIssueDialog({ open, onOpenChange, onSubmit, isCreating }: NewIssueDi
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Issue</DialogTitle>
<DialogTitle>{formatMessage({ id: 'issues.createDialog.title' })}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
<div>
<label className="text-sm font-medium text-foreground">Title</label>
<label className="text-sm font-medium text-foreground">{formatMessage({ id: 'issues.createDialog.labels.title' })}</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Issue title..."
placeholder={formatMessage({ id: 'issues.createDialog.placeholders.title' })}
className="mt-1"
required
/>
</div>
<div>
<label className="text-sm font-medium text-foreground">Context (optional)</label>
<label className="text-sm font-medium text-foreground">{formatMessage({ id: 'issues.createDialog.labels.context' })}</label>
<textarea
value={context}
onChange={(e) => setContext(e.target.value)}
placeholder="Describe the issue..."
placeholder={formatMessage({ id: 'issues.createDialog.placeholders.context' })}
className="mt-1 w-full min-h-[100px] p-3 bg-background border border-input rounded-md text-sm resize-none focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div>
<label className="text-sm font-medium text-foreground">Priority</label>
<label className="text-sm font-medium text-foreground">{formatMessage({ id: 'issues.createDialog.labels.priority' })}</label>
<Select value={priority} onValueChange={(v) => setPriority(v as Issue['priority'])}>
<SelectTrigger className="mt-1">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="critical">Critical</SelectItem>
<SelectItem value="low">{formatMessage({ id: 'issues.priority.low' })}</SelectItem>
<SelectItem value="medium">{formatMessage({ id: 'issues.priority.medium' })}</SelectItem>
<SelectItem value="high">{formatMessage({ id: 'issues.priority.high' })}</SelectItem>
<SelectItem value="critical">{formatMessage({ id: 'issues.priority.critical' })}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
{formatMessage({ id: 'issues.createDialog.buttons.cancel' })}
</Button>
<Button type="submit" disabled={isCreating || !title.trim()}>
{isCreating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating...
{formatMessage({ id: 'issues.createDialog.buttons.creating' })}
</>
) : (
<>
<Plus className="w-4 h-4 mr-2" />
Create Issue
{formatMessage({ id: 'issues.createDialog.buttons.create' })}
</>
)}
</Button>
@@ -138,6 +140,8 @@ function IssueList({
onIssueDelete,
onStatusChange,
}: IssueListProps) {
const { formatMessage } = useIntl();
if (isLoading) {
return (
<div className="space-y-3">
@@ -152,9 +156,9 @@ function IssueList({
return (
<Card className="p-8 text-center">
<AlertCircle className="w-12 h-12 mx-auto text-muted-foreground/50" />
<h3 className="mt-4 text-lg font-medium text-foreground">No issues found</h3>
<h3 className="mt-4 text-lg font-medium text-foreground">{formatMessage({ id: 'issues.emptyState.title' })}</h3>
<p className="mt-2 text-muted-foreground">
Create a new issue or adjust your filters.
{formatMessage({ id: 'issues.emptyState.message' })}
</p>
</Card>
);
@@ -179,6 +183,7 @@ function IssueList({
// ========== Main Page Component ==========
export function IssueManagerPage() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
const [priorityFilter, setPriorityFilter] = useState<PriorityFilter>('all');
@@ -238,24 +243,24 @@ export function IssueManagerPage() {
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<AlertCircle className="w-6 h-6 text-primary" />
Issue Manager
{formatMessage({ id: 'issues.title' })}
</h1>
<p className="text-muted-foreground mt-1">
Track and manage project issues and bugs
{formatMessage({ id: 'issues.description' })}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
Refresh
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<Button variant="outline">
<Github className="w-4 h-4 mr-2" />
Pull from GitHub
{formatMessage({ id: 'issues.actions.github' })}
</Button>
<Button onClick={() => setIsNewIssueOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
New Issue
{formatMessage({ id: 'issues.actions.create' })}
</Button>
</div>
</div>
@@ -267,28 +272,28 @@ export function IssueManagerPage() {
<AlertCircle className="w-5 h-5 text-info" />
<span className="text-2xl font-bold">{openCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Open Issues</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'common.status.openIssues' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<Clock className="w-5 h-5 text-warning" />
<span className="text-2xl font-bold">{issuesByStatus.in_progress?.length || 0}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">In Progress</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.inProgress' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-destructive" />
<span className="text-2xl font-bold">{criticalCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Critical</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.priority.critical' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-success" />
<span className="text-2xl font-bold">{issuesByStatus.resolved?.length || 0}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Resolved</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'issues.status.resolved' })}</p>
</Card>
</div>
@@ -297,7 +302,7 @@ export function IssueManagerPage() {
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search issues..."
placeholder={formatMessage({ id: 'common.actions.searchIssues' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
@@ -306,26 +311,26 @@ export function IssueManagerPage() {
<div className="flex gap-2">
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as StatusFilter)}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Status" />
<SelectValue placeholder={formatMessage({ id: 'common.status.label' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
<SelectItem value="all">{formatMessage({ id: 'issues.filters.all' })}</SelectItem>
<SelectItem value="open">{formatMessage({ id: 'issues.status.open' })}</SelectItem>
<SelectItem value="in_progress">{formatMessage({ id: 'issues.status.inProgress' })}</SelectItem>
<SelectItem value="resolved">{formatMessage({ id: 'issues.status.resolved' })}</SelectItem>
<SelectItem value="closed">{formatMessage({ id: 'issues.status.closed' })}</SelectItem>
</SelectContent>
</Select>
<Select value={priorityFilter} onValueChange={(v) => setPriorityFilter(v as PriorityFilter)}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Priority" />
<SelectValue placeholder={formatMessage({ id: 'issues.priority.label' })} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Priority</SelectItem>
<SelectItem value="critical">Critical</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="all">{formatMessage({ id: 'issues.filters.byPriority' })}</SelectItem>
<SelectItem value="critical">{formatMessage({ id: 'issues.priority.critical' })}</SelectItem>
<SelectItem value="high">{formatMessage({ id: 'issues.priority.high' })}</SelectItem>
<SelectItem value="medium">{formatMessage({ id: 'issues.priority.medium' })}</SelectItem>
<SelectItem value="low">{formatMessage({ id: 'issues.priority.low' })}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -338,7 +343,7 @@ export function IssueManagerPage() {
size="sm"
onClick={() => setStatusFilter('all')}
>
All ({statusCounts.all})
{formatMessage({ id: 'issues.filters.all' })} ({statusCounts.all})
</Button>
<Button
variant={statusFilter === 'open' ? 'default' : 'outline'}
@@ -346,7 +351,7 @@ export function IssueManagerPage() {
onClick={() => setStatusFilter('open')}
>
<Badge variant="info" className="mr-2">{statusCounts.open}</Badge>
Open
{formatMessage({ id: 'issues.status.open' })}
</Button>
<Button
variant={statusFilter === 'in_progress' ? 'default' : 'outline'}
@@ -354,7 +359,7 @@ export function IssueManagerPage() {
onClick={() => setStatusFilter('in_progress')}
>
<Badge variant="warning" className="mr-2">{statusCounts.in_progress}</Badge>
In Progress
{formatMessage({ id: 'issues.status.inProgress' })}
</Button>
<Button
variant={priorityFilter === 'critical' ? 'destructive' : 'outline'}
@@ -365,7 +370,7 @@ export function IssueManagerPage() {
}}
>
<Badge variant="destructive" className="mr-2">{criticalCount}</Badge>
Critical
{formatMessage({ id: 'issues.priority.critical' })}
</Button>
</div>

View File

@@ -0,0 +1,318 @@
// ========================================
// LiteTaskDetailPage Component
// ========================================
// Lite task detail page with flowchart visualization
import * as React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
ArrowLeft,
FileEdit,
Wrench,
Calendar,
Loader2,
XCircle,
CheckCircle,
Clock,
Code,
Zap,
ChevronDown,
ChevronRight,
} from 'lucide-react';
import { useLiteTaskSession } from '@/hooks/useLiteTasks';
import { Flowchart } from '@/components/shared/Flowchart';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card, CardContent } from '@/components/ui/Card';
import type { LiteTask } from '@/lib/api';
/**
* LiteTaskDetailPage component - Display single lite task session with flowchart
*/
export function LiteTaskDetailPage() {
const { sessionId } = useParams<{ sessionId: string }>();
const navigate = useNavigate();
const { formatMessage } = useIntl();
// Determine type from URL or state
const [sessionType, setSessionType] = React.useState<'lite-plan' | 'lite-fix' | 'multi-cli-plan'>('lite-plan');
const { session, isLoading, error, refetch } = useLiteTaskSession(sessionId, sessionType);
// Track expanded tasks
const [expandedTasks, setExpandedTasks] = React.useState<Set<string>>(new Set());
// Try to detect type from session data
React.useEffect(() => {
if (session?.type) {
setSessionType(session.type);
}
}, [session]);
const handleBack = () => {
navigate('/lite-tasks');
};
const toggleTaskExpanded = (taskId: string) => {
setExpandedTasks(prev => {
const next = new Set(prev);
if (next.has(taskId)) {
next.delete(taskId);
} else {
next.add(taskId);
}
return next;
});
};
// Get task status badge
const getTaskStatusBadge = (task: LiteTask) => {
switch (task.status) {
case 'completed':
return { variant: 'success' as const, label: formatMessage({ id: 'sessionDetail.status.completed' }), icon: CheckCircle };
case 'in_progress':
return { variant: 'warning' as const, label: formatMessage({ id: 'sessionDetail.status.inProgress' }), icon: Loader2 };
case 'blocked':
return { variant: 'destructive' as const, label: formatMessage({ id: 'sessionDetail.status.blocked' }), icon: XCircle };
case 'failed':
return { variant: 'destructive' as const, label: formatMessage({ id: 'fixSession.status.failed' }), icon: XCircle };
default:
return { variant: 'secondary' as const, label: formatMessage({ id: 'sessionDetail.status.pending' }), icon: Clock };
}
};
// Loading state
if (isLoading) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" disabled>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.back' })}
</Button>
<div className="h-8 w-64 rounded bg-muted animate-pulse" />
</div>
<div className="h-64 rounded-lg bg-muted animate-pulse" />
</div>
);
}
// Error state
if (error) {
return (
<div className="flex items-center gap-2 p-4 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive">
<XCircle className="h-5 w-5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium">{formatMessage({ id: 'common.errors.loadFailed' })}</p>
<p className="text-xs mt-0.5">{error.message}</p>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
{formatMessage({ id: 'common.actions.retry' })}
</Button>
</div>
);
}
// Session not found
if (!session) {
return (
<div className="flex flex-col items-center justify-center py-16 px-4">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasksDetail.notFound.title' })}
</h3>
<p className="text-sm text-muted-foreground mb-4">
{formatMessage({ id: 'liteTasksDetail.notFound.message' })}
</p>
<Button onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.back' })}
</Button>
</div>
);
}
const tasks = session.tasks || [];
const completedTasks = tasks.filter(t => t.status === 'completed').length;
const isLitePlan = session.type === 'lite-plan';
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.back' })}
</Button>
<div>
<h1 className="text-2xl font-semibold text-foreground">
{session.title || session.id || session.session_id}
</h1>
{(session.title || (session.session_id && session.session_id !== session.id)) && (
<p className="text-sm text-muted-foreground mt-0.5">{session.id || session.session_id}</p>
)}
</div>
</div>
<Badge variant={isLitePlan ? 'info' : 'warning'} className="gap-1">
{isLitePlan ? <FileEdit className="h-3 w-3" /> : <Wrench className="h-3 w-3" />}
{formatMessage({ id: isLitePlan ? 'liteTasks.type.plan' : 'liteTasks.type.fix' })}
</Badge>
</div>
{/* Info Bar */}
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground p-4 bg-background rounded-lg border">
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span className="font-medium">{formatMessage({ id: 'sessionDetail.info.created' })}:</span>{' '}
{session.createdAt ? new Date(session.createdAt).toLocaleString() : 'N/A'}
</div>
<div className="flex items-center gap-1">
<CheckCircle className="h-4 w-4" />
<span className="font-medium">{formatMessage({ id: 'sessionDetail.info.tasks' })}:</span>{' '}
{completedTasks}/{tasks.length}
</div>
</div>
{/* Description (if exists) */}
{session.description && (
<div className="p-4 bg-background rounded-lg border">
<h3 className="text-sm font-semibold text-foreground mb-2">
{formatMessage({ id: 'sessionDetail.info.description' })}
</h3>
<p className="text-sm text-muted-foreground">{session.description}</p>
</div>
)}
{/* Tasks List */}
{tasks.length === 0 ? (
<Card>
<CardContent className="p-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasksDetail.empty.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasksDetail.empty.message' })}
</p>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{tasks.map((task, index) => {
const taskId = task.task_id || task.id || `T${index + 1}`;
const isExpanded = expandedTasks.has(taskId);
const statusBadge = getTaskStatusBadge(task);
const StatusIcon = statusBadge.icon;
const hasFlowchart = task.flow_control?.implementation_approach &&
task.flow_control.implementation_approach.length > 0;
return (
<Card key={taskId} className="overflow-hidden">
<CardContent className="p-4">
{/* Task Header */}
<div
className="flex items-start justify-between gap-3 cursor-pointer"
onClick={() => toggleTaskExpanded(taskId)}
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-mono text-muted-foreground">{taskId}</span>
<Badge variant={statusBadge.variant} className="gap-1">
<StatusIcon className="h-3 w-3" />
{statusBadge.label}
</Badge>
{task.priority && (
<Badge variant="outline" className="text-xs">
{task.priority}
</Badge>
)}
{hasFlowchart && (
<Badge variant="info" className="gap-1">
<Code className="h-3 w-3" />
{formatMessage({ id: 'liteTasksDetail.flowchart' })}
</Badge>
)}
</div>
<h4 className="font-medium text-foreground text-sm">
{task.title || formatMessage({ id: 'sessionDetail.tasks.untitled' })}
</h4>
{task.description && (
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
{task.description}
</p>
)}
{task.context?.depends_on && task.context.depends_on.length > 0 && (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<Code className="h-3 w-3" />
<span>Depends on: {task.context.depends_on.join(', ')}</span>
</div>
)}
</div>
<Button variant="ghost" size="sm" className="flex-shrink-0">
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
</div>
{/* Expanded Content */}
{isExpanded && (
<div className="mt-4 pt-4 border-t border-border">
{/* Flowchart */}
{hasFlowchart && task.flow_control && (
<div className="mb-4">
<h5 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<Code className="h-4 w-4" />
{formatMessage({ id: 'liteTasksDetail.implementationFlow' })}
</h5>
<Flowchart flowControl={task.flow_control} className="border border-border rounded-lg" />
</div>
)}
{/* Focus Paths */}
{task.context?.focus_paths && task.context.focus_paths.length > 0 && (
<div className="mb-4">
<h5 className="text-sm font-semibold text-foreground mb-2">
{formatMessage({ id: 'liteTasksDetail.focusPaths' })}
</h5>
<div className="space-y-1">
{task.context.focus_paths.map((path, idx) => (
<code
key={idx}
className="block text-xs bg-muted px-2 py-1 rounded font-mono"
>
{path}
</code>
))}
</div>
</div>
)}
{/* Acceptance Criteria */}
{task.context?.acceptance && task.context.acceptance.length > 0 && (
<div>
<h5 className="text-sm font-semibold text-foreground mb-2">
{formatMessage({ id: 'liteTasksDetail.acceptanceCriteria' })}
</h5>
<ul className="space-y-1">
{task.context.acceptance.map((criteria, idx) => (
<li key={idx} className="text-xs text-muted-foreground flex items-start gap-2">
<span className="text-primary font-bold">{idx + 1}.</span>
<span>{criteria}</span>
</li>
))}
</ul>
</div>
)}
</div>
)}
</CardContent>
</Card>
);
})}
</div>
)}
</div>
);
}
export default LiteTaskDetailPage;

View File

@@ -0,0 +1,302 @@
// ========================================
// LiteTasksPage Component
// ========================================
// Lite-plan and lite-fix task list page with flowchart rendering
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useIntl } from 'react-intl';
import {
ArrowLeft,
Zap,
Wrench,
FileEdit,
MessagesSquare,
Calendar,
ListChecks,
XCircle,
Activity,
Repeat,
MessageCircle,
} 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';
type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
/**
* Get i18n text from label object (supports {en, zh} format)
*/
function getI18nText(label: string | { en?: string; zh?: string } | undefined, fallback: string): string {
if (!label) return fallback;
if (typeof label === 'string') return label;
return label.en || label.zh || fallback;
}
/**
* LiteTasksPage component - Display lite-plan and lite-fix sessions
*/
export function LiteTasksPage() {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const { litePlan, liteFix, multiCliPlan, isLoading, error, refetch } = useLiteTasks();
const [activeTab, setActiveTab] = React.useState<LiteTaskTab>('lite-plan');
const handleBack = () => {
navigate('/sessions');
};
// Get status badge color
const getStatusColor = (status?: string) => {
const statusColors: Record<string, string> = {
decided: 'success',
converged: 'success',
plan_generated: 'success',
completed: 'success',
exploring: 'info',
initialized: 'info',
analyzing: 'warning',
debating: 'warning',
blocked: 'destructive',
conflict: 'destructive',
};
return statusColors[status || ''] || 'secondary';
};
// Render lite task card
const renderLiteTaskCard = (session: { id: string; type: string; createdAt?: string; tasks?: unknown[] }) => {
const isLitePlan = session.type === 'lite-plan';
const taskCount = session.tasks?.length || 0;
return (
<Card
key={session.id}
className="cursor-pointer hover:shadow-md transition-shadow"
onClick={() => navigate(`/lite-tasks/${session.id}`)}
>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex-1 min-w-0">
<h3 className="font-medium text-foreground text-sm">{session.id}</h3>
</div>
<Badge variant={isLitePlan ? 'info' : 'warning'} className="gap-1">
{isLitePlan ? <FileEdit className="h-3 w-3" /> : <Wrench className="h-3 w-3" />}
{formatMessage({ id: isLitePlan ? 'liteTasks.type.plan' : 'liteTasks.type.fix' })}
</Badge>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{session.createdAt && (
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
{new Date(session.createdAt).toLocaleDateString()}
</span>
)}
<span className="flex items-center gap-1">
<ListChecks className="h-3.5 w-3.5" />
{taskCount} {formatMessage({ id: 'session.tasks' })}
</span>
</div>
</CardContent>
</Card>
);
};
// Render multi-cli plan card
const renderMultiCliCard = (session: {
id: string;
metadata?: Record<string, unknown>;
latestSynthesis?: { title?: string | { en?: string; zh?: string }; status?: string };
roundCount?: number;
status?: string;
createdAt?: string;
}) => {
const metadata = session.metadata || {};
const latestSynthesis = session.latestSynthesis || {};
const roundCount = (metadata.roundId as number) || session.roundCount || 1;
const topicTitle = getI18nText(
latestSynthesis.title as string | { en?: string; zh?: string } | undefined,
'Discussion Topic'
);
const status = latestSynthesis.status || session.status || 'analyzing';
const createdAt = (metadata.timestamp as string) || session.createdAt || '';
return (
<Card
key={session.id}
className="cursor-pointer hover:shadow-md transition-shadow"
onClick={() => navigate(`/lite-tasks/${session.id}`)}
>
<CardContent className="p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex-1 min-w-0">
<h3 className="font-medium text-foreground text-sm">{session.id}</h3>
</div>
<Badge variant="info" className="gap-1">
<MessagesSquare className="h-3 w-3" />
{formatMessage({ id: 'liteTasks.type.multiCli' })}
</Badge>
</div>
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-3">
<MessageCircle className="h-4 w-4" />
<span className="line-clamp-1">{topicTitle}</span>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{createdAt && (
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
{new Date(createdAt).toLocaleDateString()}
</span>
)}
<span className="flex items-center gap-1">
<Repeat className="h-3.5 w-3.5" />
{roundCount} {formatMessage({ id: 'liteTasks.rounds' })}
</span>
<Badge variant={getStatusColor(status) as 'success' | 'info' | 'warning' | 'destructive' | 'secondary'} className="gap-1">
<Activity className="h-3 w-3" />
{status}
</Badge>
</div>
</CardContent>
</Card>
);
};
// Loading state
if (isLoading) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" disabled>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.back' })}
</Button>
<div className="h-8 w-64 rounded bg-muted animate-pulse" />
</div>
<div className="h-64 rounded-lg bg-muted animate-pulse" />
</div>
);
}
// Error state
if (error) {
return (
<div className="flex items-center gap-2 p-4 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive">
<XCircle className="h-5 w-5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium">{formatMessage({ id: 'common.errors.loadFailed' })}</p>
<p className="text-xs mt-0.5">{error.message}</p>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
{formatMessage({ id: 'common.actions.retry' })}
</Button>
</div>
);
}
const totalSessions = litePlan.length + liteFix.length + multiCliPlan.length;
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.back' })}
</Button>
<div>
<h1 className="text-2xl font-semibold text-foreground">
{formatMessage({ id: 'liteTasks.title' })}
</h1>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.subtitle' }, { count: totalSessions })}
</p>
</div>
</div>
</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>
{/* 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>
) : (
<div className="grid gap-3">{litePlan.map(renderLiteTaskCard)}</div>
)}
</TabsContent>
{/* 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>
) : (
<div className="grid gap-3">{liteFix.map(renderLiteTaskCard)}</div>
)}
</TabsContent>
{/* 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>
) : (
<div className="grid gap-3">{multiCliPlan.map(renderMultiCliCard)}</div>
)}
</TabsContent>
</Tabs>
</div>
);
}
export default LiteTasksPage;

View File

@@ -4,6 +4,7 @@
// Monitor running development loops with Kanban board
import { useState, useCallback } from 'react';
import { useIntl } from 'react-intl';
import {
RefreshCw,
Play,
@@ -157,6 +158,7 @@ interface NewLoopDialogProps {
}
function NewLoopDialog({ open, onOpenChange, onSubmit, isCreating }: NewLoopDialogProps) {
const { formatMessage } = useIntl();
const [prompt, setPrompt] = useState('');
const [tool, setTool] = useState('');
@@ -173,42 +175,42 @@ function NewLoopDialog({ open, onOpenChange, onSubmit, isCreating }: NewLoopDial
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Start New Loop</DialogTitle>
<DialogTitle>{formatMessage({ id: 'loops.createDialog.title' })}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
<div>
<label className="text-sm font-medium text-foreground">Prompt</label>
<label className="text-sm font-medium text-foreground">{formatMessage({ id: 'loops.createDialog.labels.prompt' })}</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Enter your development loop prompt..."
placeholder={formatMessage({ id: 'loops.createDialog.placeholders.prompt' })}
className="mt-1 w-full min-h-[100px] p-3 bg-background border border-input rounded-md text-sm resize-none focus:outline-none focus:ring-2 focus:ring-primary"
required
/>
</div>
<div>
<label className="text-sm font-medium text-foreground">CLI Tool (optional)</label>
<label className="text-sm font-medium text-foreground">{formatMessage({ id: 'loops.createDialog.labels.tool' })}</label>
<Input
value={tool}
onChange={(e) => setTool(e.target.value)}
placeholder="e.g., gemini, qwen, codex"
placeholder={formatMessage({ id: 'loops.createDialog.placeholders.tool' })}
className="mt-1"
/>
</div>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
{formatMessage({ id: 'loops.createDialog.buttons.cancel' })}
</Button>
<Button type="submit" disabled={isCreating || !prompt.trim()}>
{isCreating ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating...
{formatMessage({ id: 'common.status.creating' })}
</>
) : (
<>
<Play className="w-4 h-4 mr-2" />
Start Loop
{formatMessage({ id: 'loops.createDialog.buttons.create' })}
</>
)}
</Button>
@@ -222,6 +224,7 @@ function NewLoopDialog({ open, onOpenChange, onSubmit, isCreating }: NewLoopDial
// ========== Main Page Component ==========
export function LoopMonitorPage() {
const { formatMessage } = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const [isNewLoopOpen, setIsNewLoopOpen] = useState(false);
@@ -322,20 +325,20 @@ export function LoopMonitorPage() {
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<RefreshCw className="w-6 h-6 text-primary" />
Loop Monitor
{formatMessage({ id: 'loops.title' })}
</h1>
<p className="text-muted-foreground mt-1">
Monitor and control running development loops
{formatMessage({ id: 'loops.description' })}
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
<RefreshCw className={cn('w-4 h-4 mr-2', isFetching && 'animate-spin')} />
Refresh
{formatMessage({ id: 'common.actions.refresh' })}
</Button>
<Button onClick={() => setIsNewLoopOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
New Loop
{formatMessage({ id: 'loops.actions.create' })}
</Button>
</div>
</div>
@@ -347,28 +350,28 @@ export function LoopMonitorPage() {
<Loader2 className="w-5 h-5 text-primary" />
<span className="text-2xl font-bold">{runningCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Running</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'loops.columns.running' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<Pause className="w-5 h-5 text-warning" />
<span className="text-2xl font-bold">{loopsByStatus.paused?.length || 0}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Paused</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'loops.columns.paused' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-success" />
<span className="text-2xl font-bold">{completedCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Completed</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'loops.columns.completed' })}</p>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<XCircle className="w-5 h-5 text-destructive" />
<span className="text-2xl font-bold">{failedCount}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">Failed</p>
<p className="text-sm text-muted-foreground mt-1">{formatMessage({ id: 'loops.columns.failed' })}</p>
</Card>
</div>
@@ -376,7 +379,7 @@ export function LoopMonitorPage() {
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search loops..."
placeholder={formatMessage({ id: 'common.actions.searchLoops' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
@@ -401,14 +404,14 @@ export function LoopMonitorPage() {
<Card className="p-8 text-center">
<RefreshCw className="w-12 h-12 mx-auto text-muted-foreground/50" />
<h3 className="mt-4 text-lg font-medium text-foreground">
No active loops
{formatMessage({ id: 'loops.emptyState.title' })}
</h3>
<p className="mt-2 text-muted-foreground">
Start a new development loop to begin monitoring progress.
{formatMessage({ id: 'loops.emptyState.message' })}
</p>
<Button className="mt-4" onClick={() => setIsNewLoopOpen(true)}>
<Play className="w-4 h-4 mr-2" />
Start New Loop
{formatMessage({ id: 'loops.emptyState.createFirst' })}
</Button>
</Card>
) : (
@@ -416,7 +419,7 @@ export function LoopMonitorPage() {
columns={columns}
onDragEnd={handleDragEnd}
renderItem={renderLoopItem}
emptyColumnMessage="No loops"
emptyColumnMessage={formatMessage({ id: 'loops.card.error' })}
className="min-h-[400px]"
/>
)}

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