mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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:
684
.claude/agents/test-action-planning-agent.md
Normal file
684
.claude/agents/test-action-planning-agent.md
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
1
ccw/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.ace-tool/
|
||||
4941
ccw/frontend/package-lock.json
generated
4941
ccw/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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]: 中文
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
85
ccw/frontend/playwright-report/index.html
Normal file
85
ccw/frontend/playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
34
ccw/frontend/playwright.config.ts
Normal file
34
ccw/frontend/playwright.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
197
ccw/frontend/scripts/validate-translations.ts
Normal file
197
ccw/frontend/scripts/validate-translations.ts
Normal 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
1
ccw/frontend/src/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.ace-tool/
|
||||
@@ -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;
|
||||
|
||||
203
ccw/frontend/src/components/layout/Header.test.tsx
Normal file
203
ccw/frontend/src/components/layout/Header.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
245
ccw/frontend/src/components/layout/LanguageSwitcher.test.tsx
Normal file
245
ccw/frontend/src/components/layout/LanguageSwitcher.test.tsx
Normal 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('中文');
|
||||
});
|
||||
});
|
||||
});
|
||||
70
ccw/frontend/src/components/layout/LanguageSwitcher.tsx
Normal file
70
ccw/frontend/src/components/layout/LanguageSwitcher.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
245
ccw/frontend/src/components/shared/ConversationCard.tsx
Normal file
245
ccw/frontend/src/components/shared/ConversationCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
304
ccw/frontend/src/components/shared/Flowchart.tsx
Normal file
304
ccw/frontend/src/components/shared/Flowchart.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
448
ccw/frontend/src/hooks/useCli.ts
Normal file
448
ccw/frontend/src/hooks/useCli.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
148
ccw/frontend/src/hooks/useHistory.ts
Normal file
148
ccw/frontend/src/hooks/useHistory.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
98
ccw/frontend/src/hooks/useLiteTasks.ts
Normal file
98
ccw/frontend/src/hooks/useLiteTasks.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
149
ccw/frontend/src/hooks/useLocale.test.ts
Normal file
149
ccw/frontend/src/hooks/useLocale.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
53
ccw/frontend/src/hooks/useLocale.ts
Normal file
53
ccw/frontend/src/hooks/useLocale.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
227
ccw/frontend/src/hooks/useMcpServers.ts
Normal file
227
ccw/frontend/src/hooks/useMcpServers.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
51
ccw/frontend/src/hooks/useProjectOverview.ts
Normal file
51
ccw/frontend/src/hooks/useProjectOverview.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
101
ccw/frontend/src/hooks/useReviewSession.ts
Normal file
101
ccw/frontend/src/hooks/useReviewSession.ts
Normal 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';
|
||||
51
ccw/frontend/src/hooks/useSessionDetail.ts
Normal file
51
ccw/frontend/src/hooks/useSessionDetail.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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 }),
|
||||
});
|
||||
}
|
||||
|
||||
156
ccw/frontend/src/lib/i18n.ts
Normal file
156
ccw/frontend/src/lib/i18n.ts
Normal 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);
|
||||
}
|
||||
30
ccw/frontend/src/lib/query-client.ts
Normal file
30
ccw/frontend/src/lib/query-client.ts
Normal 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;
|
||||
127
ccw/frontend/src/locales/en/cli-manager.json
Normal file
127
ccw/frontend/src/locales/en/cli-manager.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
43
ccw/frontend/src/locales/en/commands.json
Normal file
43
ccw/frontend/src/locales/en/commands.json
Normal 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"
|
||||
}
|
||||
}
|
||||
180
ccw/frontend/src/locales/en/common.json
Normal file
180
ccw/frontend/src/locales/en/common.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
44
ccw/frontend/src/locales/en/fix-session.json
Normal file
44
ccw/frontend/src/locales/en/fix-session.json
Normal 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."
|
||||
}
|
||||
}
|
||||
29
ccw/frontend/src/locales/en/history.json
Normal file
29
ccw/frontend/src/locales/en/history.json
Normal 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."
|
||||
}
|
||||
}
|
||||
37
ccw/frontend/src/locales/en/home.json
Normal file
37
ccw/frontend/src/locales/en/home.json
Normal 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"
|
||||
}
|
||||
}
|
||||
64
ccw/frontend/src/locales/en/index.ts
Normal file
64
ccw/frontend/src/locales/en/index.ts
Normal 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>;
|
||||
64
ccw/frontend/src/locales/en/issues.json
Normal file
64
ccw/frontend/src/locales/en/issues.json
Normal 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}}"
|
||||
}
|
||||
}
|
||||
26
ccw/frontend/src/locales/en/lite-tasks.json
Normal file
26
ccw/frontend/src/locales/en/lite-tasks.json
Normal 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."
|
||||
}
|
||||
}
|
||||
68
ccw/frontend/src/locales/en/loops.json
Normal file
68
ccw/frontend/src/locales/en/loops.json
Normal 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"
|
||||
}
|
||||
}
|
||||
37
ccw/frontend/src/locales/en/mcp-manager.json
Normal file
37
ccw/frontend/src/locales/en/mcp-manager.json
Normal 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."
|
||||
}
|
||||
}
|
||||
65
ccw/frontend/src/locales/en/memory.json
Normal file
65
ccw/frontend/src/locales/en/memory.json
Normal 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"
|
||||
}
|
||||
}
|
||||
38
ccw/frontend/src/locales/en/navigation.json
Normal file
38
ccw/frontend/src/locales/en/navigation.json
Normal 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"
|
||||
}
|
||||
}
|
||||
63
ccw/frontend/src/locales/en/orchestrator.json
Normal file
63
ccw/frontend/src/locales/en/orchestrator.json
Normal 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"
|
||||
}
|
||||
}
|
||||
52
ccw/frontend/src/locales/en/project-overview.json
Normal file
52
ccw/frontend/src/locales/en/project-overview.json
Normal 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"
|
||||
}
|
||||
}
|
||||
41
ccw/frontend/src/locales/en/review-session.json
Normal file
41
ccw/frontend/src/locales/en/review-session.json
Normal 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."
|
||||
}
|
||||
}
|
||||
54
ccw/frontend/src/locales/en/session-detail.json
Normal file
54
ccw/frontend/src/locales/en/session-detail.json
Normal 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"
|
||||
}
|
||||
}
|
||||
48
ccw/frontend/src/locales/en/sessions.json
Normal file
48
ccw/frontend/src/locales/en/sessions.json
Normal 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"
|
||||
}
|
||||
}
|
||||
42
ccw/frontend/src/locales/en/skills.json
Normal file
42
ccw/frontend/src/locales/en/skills.json
Normal 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."
|
||||
}
|
||||
}
|
||||
127
ccw/frontend/src/locales/zh/cli-manager.json
Normal file
127
ccw/frontend/src/locales/zh/cli-manager.json
Normal 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": "添加规则以强制执行代码质量标准。"
|
||||
}
|
||||
}
|
||||
}
|
||||
43
ccw/frontend/src/locales/zh/commands.json
Normal file
43
ccw/frontend/src/locales/zh/commands.json
Normal 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": "状态"
|
||||
}
|
||||
}
|
||||
180
ccw/frontend/src/locales/zh/common.json
Normal file
180
ccw/frontend/src/locales/zh/common.json
Normal 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": "教程"
|
||||
}
|
||||
}
|
||||
}
|
||||
44
ccw/frontend/src/locales/zh/fix-session.json
Normal file
44
ccw/frontend/src/locales/zh/fix-session.json
Normal 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": "没有匹配当前筛选条件的修复任务。"
|
||||
}
|
||||
}
|
||||
29
ccw/frontend/src/locales/zh/history.json
Normal file
29
ccw/frontend/src/locales/zh/history.json
Normal 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": "没有匹配当前筛选条件的执行记录。请尝试调整搜索或筛选条件。"
|
||||
}
|
||||
}
|
||||
37
ccw/frontend/src/locales/zh/home.json
Normal file
37
ccw/frontend/src/locales/zh/home.json
Normal 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": "重试"
|
||||
}
|
||||
}
|
||||
64
ccw/frontend/src/locales/zh/index.ts
Normal file
64
ccw/frontend/src/locales/zh/index.ts
Normal 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>;
|
||||
64
ccw/frontend/src/locales/zh/issues.json
Normal file
64
ccw/frontend/src/locales/zh/issues.json
Normal 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 {解决方案}}"
|
||||
}
|
||||
}
|
||||
26
ccw/frontend/src/locales/zh/lite-tasks.json
Normal file
26
ccw/frontend/src/locales/zh/lite-tasks.json
Normal 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": "无法找到请求的轻量任务会话。"
|
||||
}
|
||||
}
|
||||
68
ccw/frontend/src/locales/zh/loops.json
Normal file
68
ccw/frontend/src/locales/zh/loops.json
Normal 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": "失败"
|
||||
}
|
||||
}
|
||||
37
ccw/frontend/src/locales/zh/mcp-manager.json
Normal file
37
ccw/frontend/src/locales/zh/mcp-manager.json
Normal 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 集成。"
|
||||
}
|
||||
}
|
||||
65
ccw/frontend/src/locales/zh/memory.json
Normal file
65
ccw/frontend/src/locales/zh/memory.json
Normal 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 历史"
|
||||
}
|
||||
}
|
||||
38
ccw/frontend/src/locales/zh/navigation.json
Normal file
38
ccw/frontend/src/locales/zh/navigation.json
Normal 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": "设置"
|
||||
}
|
||||
}
|
||||
63
ccw/frontend/src/locales/zh/orchestrator.json
Normal file
63
ccw/frontend/src/locales/zh/orchestrator.json
Normal 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": "实时更新"
|
||||
}
|
||||
}
|
||||
52
ccw/frontend/src/locales/zh/project-overview.json
Normal file
52
ccw/frontend/src/locales/zh/project-overview.json
Normal 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 初始化项目分析"
|
||||
}
|
||||
}
|
||||
41
ccw/frontend/src/locales/zh/review-session.json
Normal file
41
ccw/frontend/src/locales/zh/review-session.json
Normal 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": "无法找到请求的审查会话。"
|
||||
}
|
||||
}
|
||||
54
ccw/frontend/src/locales/zh/session-detail.json
Normal file
54
ccw/frontend/src/locales/zh/session-detail.json
Normal 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": "描述"
|
||||
}
|
||||
}
|
||||
48
ccw/frontend/src/locales/zh/sessions.json
Normal file
48
ccw/frontend/src/locales/zh/sessions.json
Normal 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": "时间线"
|
||||
}
|
||||
}
|
||||
42
ccw/frontend/src/locales/zh/skills.json
Normal file
42
ccw/frontend/src/locales/zh/skills.json
Normal 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": "没有符合当前筛选条件的技能。"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
334
ccw/frontend/src/pages/EndpointsPage.tsx
Normal file
334
ccw/frontend/src/pages/EndpointsPage.tsx
Normal 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;
|
||||
364
ccw/frontend/src/pages/FixSessionPage.tsx
Normal file
364
ccw/frontend/src/pages/FixSessionPage.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
314
ccw/frontend/src/pages/HistoryPage.tsx
Normal file
314
ccw/frontend/src/pages/HistoryPage.tsx
Normal 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;
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
310
ccw/frontend/src/pages/InstallationsPage.tsx
Normal file
310
ccw/frontend/src/pages/InstallationsPage.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
|
||||
318
ccw/frontend/src/pages/LiteTaskDetailPage.tsx
Normal file
318
ccw/frontend/src/pages/LiteTaskDetailPage.tsx
Normal 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;
|
||||
302
ccw/frontend/src/pages/LiteTasksPage.tsx
Normal file
302
ccw/frontend/src/pages/LiteTasksPage.tsx
Normal 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;
|
||||
@@ -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
Reference in New Issue
Block a user