#
tokens: 47752/50000 45/45 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .cursor
│   └── rules
│       ├── solution-exploration-guide.mdc
│       ├── task-plannning.mdc
│       └── task-status-update-rule.mdc
├── .gitignore
├── .npmignore
├── coverage
│   ├── clover.xml
│   ├── coverage-final.json
│   ├── lcov-report
│   │   ├── base.css
│   │   ├── block-navigation.js
│   │   ├── favicon.png
│   │   ├── index.html
│   │   ├── prettify.css
│   │   ├── prettify.js
│   │   ├── sort-arrow-sprite.png
│   │   └── sorter.js
│   └── lcov.info
├── jest.config.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── aggregates
│   │   └── workplan.ts
│   ├── index.ts
│   ├── prompts.ts
│   ├── tools
│   │   ├── common.ts
│   │   ├── index.ts
│   │   ├── plan
│   │   │   └── toolDefs.ts
│   │   ├── track
│   │   │   └── toolDefs.ts
│   │   └── update
│   │       └── toolDefs.ts
│   ├── utils
│   │   ├── fileStorage.ts
│   │   └── logger.ts
│   └── values
│       ├── commit.ts
│       ├── common.ts
│       ├── pullRequest.ts
│       ├── status.ts
│       └── ticket.ts
├── tsconfig.json
└── visualization
    ├── .gitignore
    ├── eslint.config.js
    ├── index.html
    ├── package-lock.json
    ├── package.json
    ├── postcss.config.cjs
    ├── public
    │   └── vite.svg
    ├── README.md
    ├── src
    │   ├── App.css
    │   ├── App.tsx
    │   ├── assets
    │   │   └── react.svg
    │   ├── components
    │   │   ├── CommitNode.tsx
    │   │   ├── ExportPanel.tsx
    │   │   ├── FilterPanel.tsx
    │   │   ├── nodes
    │   │   │   └── nodes.css
    │   │   ├── OrientationWarning.tsx
    │   │   └── WorkplanFlow.tsx
    │   ├── index.css
    │   ├── main.tsx
    │   ├── types.ts
    │   ├── utils
    │   │   ├── exportUtils.ts
    │   │   ├── responsiveUtils.ts
    │   │   ├── storageUtils.ts
    │   │   └── workplanConverter.ts
    │   └── vite-env.d.ts
    ├── tailwind.config.cjs
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts
```

# Files

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
 1 | # Source files
 2 | src/
 3 | 
 4 | # Tests
 5 | test/
 6 | coverage/
 7 | *.test.js
 8 | *.spec.js
 9 | 
10 | # Build tools
11 | tsconfig.json
12 | .github/
13 | .vscode/
14 | .cursor/
15 | 
16 | # Misc
17 | .git/
18 | .gitignore
19 | node_modules/
20 | npm-debug.log
21 | visualization/
22 | 
23 | # Environment
24 | .env
25 | .env.*
26 | 
27 | # Documentation (except README)
28 | docs/ 
```

--------------------------------------------------------------------------------
/visualization/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | 
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | 
26 | # Project specific files
27 | public/data/workplan.json
28 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependency directories
 2 | node_modules/
 3 | dist/
 4 | 
 5 | # Logs
 6 | logs
 7 | *.log
 8 | npm-debug.log*
 9 | yarn-debug.log*
10 | yarn-error.log*
11 | 
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 | 
18 | # Environment variables
19 | .env
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 | 
25 | # IDE files
26 | .idea/
27 | .vscode/
28 | *.swp
29 | *.swo
30 | 
31 | # OS files
32 | .DS_Store
33 | Thumbs.db
34 | 
35 | # Data files
36 | visualization/public/data/workplan.json 
37 | visualization/public/data/workplan.json
38 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | [![npm](https://img.shields.io/npm/v/@yodakeisuke/mcp-micromanage)](https://www.npmjs.com/package/@yodakeisuke/mcp-micromanage)
  2 | 
  3 | # mcp-micromanage
  4 | 
  5 | Control your coding agent colleague who tends to go off track.
  6 | 
  7 | If [sequentialthinking](https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking) is a dynamic formulation and externalization of thought workflows, this tool is a dynamic formulation and externalization of development task flows.
  8 | 
  9 | 
 10 | ![image](https://github.com/user-attachments/assets/d3e060a1-77a1-4a86-bd91-e0917cf405ba)
 11 | 
 12 | ## Motivation
 13 | 
 14 | ### Challenges with Coding Agents
 15 | - Coding agents often make modifications beyond what they're asked to do
 16 |     - Assuming cursor+claude
 17 | - They struggle to request user feedback at key decision points during implementation
 18 | - Work plans and progress tracking can be challenging to visualize and monitor
 19 |   
 20 | ### Solution
 21 | - **Commit and PR-based Work Plans**: Force implementation plans that break down tickets into PRs and commits as the minimum units of work
 22 | - **Forced Frequent Feedback**: Enforce user reviews at the commit level, creating natural checkpoints for feedback
 23 | - **Visualization**: Instantly understand the current work plan and implementation status through a local React app
 24 | 
 25 | ## tool
 26 | 
 27 | 1. **plan**: Define your implementation plan with PRs and commits
 28 | 2. **track**: Monitor progress and current status of all work items
 29 | 3. **update**: Change status as work progresses, with mandatory user reviews
 30 | 
 31 | ## Visualization Dashboard
 32 | 
 33 | The project includes a React-based visualization tool that provides:
 34 | 
 35 | - Hierarchical view of PRs and commits
 36 | - Real-time updates with auto-refresh
 37 | - Status-based color coding
 38 | - Zoom and pan capabilities
 39 | 
 40 | ## Getting Started
 41 | 
 42 | ### Headless(mcp tool only)
 43 | 
 44 | 1. Add to your mcp json
 45 | ```json
 46 | {
 47 |   "mcpServers": {
 48 |     "micromanage": {
 49 |       "command": "npx",
 50 |       "args": [
 51 |         "-y",
 52 |         "@yodakeisuke/mcp-micromanage"
 53 |       ]
 54 |     }
 55 |   }
 56 | }
 57 | ```
 58 | 
 59 | 2. (Highly recommended) Add the following `.mdc`s to your project
 60 | 
 61 | [recommended-rules](https://github.com/yodakeisuke/mcp-micromanage-your-agent/tree/main/.cursor/rules)
 62 | 
 63 | (Can be adjusted to your preference)
 64 | 
 65 | ### With Visualisation
 66 | 
 67 | 1. clone this repository
 68 | 
 69 | 2. Add to your mcp json
 70 | ```json
 71 | {
 72 |   "mcpServers": {
 73 |     "micromanage": {
 74 |       "command": "node",
 75 |       "args": [
 76 |         "[CLONE_DESTINATION_PATH]/sequentialdeveloping/dist/index.js"
 77 |       ]
 78 |     }
 79 |   }
 80 | }
 81 | ```
 82 | 
 83 | 3. build server
 84 | ```bash
 85 | npm install
 86 | npm run build
 87 | ```
 88 | 
 89 | 4. run frontend
 90 | ```bash
 91 | cd visualization/ 
 92 | npm install
 93 | npm run dev
 94 | ```
 95 | 
 96 | ## License
 97 | 
 98 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
 99 | 
100 | ### Third-Party Software
101 | 
102 | This project uses the following third-party software:
103 | 
104 | - **MCP TypeScript SDK**: Licensed under the MIT License. Copyright © 2023-2025 Anthropic, PBC.
105 | 
106 | ## Acknowledgments
107 | 
108 | - Built with [MCP (Model Context Protocol)](https://github.com/modelcontextprotocol/typescript-sdk)
109 | - Maintained by [yodakeisuke](https://github.com/yodakeisuke)
110 | 
```

--------------------------------------------------------------------------------
/visualization/src/vite-env.d.ts:
--------------------------------------------------------------------------------

```typescript
1 | /// <reference types="vite/client" />
2 | 
```

--------------------------------------------------------------------------------
/visualization/postcss.config.cjs:
--------------------------------------------------------------------------------

```
1 | module.exports = {
2 |   plugins: {
3 |     '@tailwindcss/postcss': {},
4 |     autoprefixer: {},
5 |   },
6 | } 
```

--------------------------------------------------------------------------------
/visualization/tsconfig.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "files": [],
3 |   "references": [
4 |     { "path": "./tsconfig.app.json" },
5 |     { "path": "./tsconfig.node.json" }
6 |   ]
7 | }
8 | 
```

--------------------------------------------------------------------------------
/src/values/common.ts:
--------------------------------------------------------------------------------

```typescript
1 | export type Status = 
2 |   | "not_started" 
3 |   | "in_progress" 
4 |   | "user_review"
5 |   | "completed" 
6 |   | "cancelled"
7 |   | "needsRefinment"; 
```

--------------------------------------------------------------------------------
/visualization/tailwind.config.cjs:
--------------------------------------------------------------------------------

```
 1 | /** @type {import('tailwindcss').Config} */
 2 | module.exports = {
 3 |   content: [
 4 |     "./index.html",
 5 |     "./src/**/*.{js,ts,jsx,tsx}",
 6 |   ],
 7 |   theme: {
 8 |     extend: {},
 9 |   },
10 |   plugins: [],
11 | } 
```

--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | export { PLAN_TOOL } from './plan/toolDefs.js';
2 | export { TRACK_TOOL } from './track/toolDefs.js';
3 | export { UPDATE_STATUS_TOOL } from './update/toolDefs.js';
4 | 
5 | 
6 | export * from './common.js';
```

--------------------------------------------------------------------------------
/visualization/src/main.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import React, { StrictMode } from 'react'
 2 | import { createRoot } from 'react-dom/client'
 3 | import App from './App.tsx'
 4 | import './index.css'
 5 | 
 6 | createRoot(document.getElementById('root')!).render(
 7 |   <StrictMode>
 8 |     <App />
 9 |   </StrictMode>,
10 | )
11 | 
```

--------------------------------------------------------------------------------
/src/values/commit.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Status } from './status.js';
 2 | 
 3 | export type CommitId = string;
 4 | 
 5 | export type Commit = {
 6 |   commitId?: CommitId;
 7 |   goal: string;
 8 |   status: Status;
 9 |   needsMoreThoughts?: boolean;
10 |   needsRevision?: boolean;
11 |   revisesTargetCommit?: CommitId;
12 |   developerNote?: string;
13 | }; 
```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | export default {
 2 |   transform: {},
 3 |   extensionsToTreatAsEsm: ['.ts'],
 4 |   moduleNameMapper: {
 5 |     '^(\\.{1,2}/.*)\\.js$': '$1',
 6 |   },
 7 |   testEnvironment: 'node',
 8 |   testMatch: ['**/*.test.js'],
 9 |   verbose: true,
10 |   collectCoverage: true,
11 |   collectCoverageFrom: ['src/**/*.js', '!src/tests/**'],
12 |   transformIgnorePatterns: [],
13 | }; 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "esModuleInterop": true,
 7 |     "outDir": "./dist",
 8 |     "strict": true,
 9 |     "skipLibCheck": true,
10 |     "forceConsistentCasingInFileNames": true
11 |   },
12 |   "include": ["src/**/*"],
13 |   "exclude": ["node_modules", "dist", "requirements"]
14 | }
15 | 
```

--------------------------------------------------------------------------------
/visualization/index.html:
--------------------------------------------------------------------------------

```html
 1 | <!doctype html>
 2 | <html lang="en">
 3 |   <head>
 4 |     <meta charset="UTF-8" />
 5 |     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
 6 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 7 |     <title>Vite + React + TS</title>
 8 |   </head>
 9 |   <body>
10 |     <div id="root"></div>
11 |     <script type="module" src="/src/main.tsx"></script>
12 |   </body>
13 | </html>
14 | 
```

--------------------------------------------------------------------------------
/visualization/tsconfig.node.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
 4 |     "target": "ES2022",
 5 |     "lib": ["ES2023"],
 6 |     "module": "ESNext",
 7 |     "skipLibCheck": true,
 8 | 
 9 |     /* Bundler mode */
10 |     "moduleResolution": "bundler",
11 |     "allowImportingTsExtensions": true,
12 |     "isolatedModules": true,
13 |     "moduleDetection": "force",
14 |     "noEmit": true,
15 | 
16 |     /* Linting */
17 |     "strict": true,
18 |     "noUnusedLocals": true,
19 |     "noUnusedParameters": true,
20 |     "noFallthroughCasesInSwitch": true,
21 |     "noUncheckedSideEffectImports": true
22 |   },
23 |   "include": ["vite.config.ts"]
24 | }
25 | 
```

--------------------------------------------------------------------------------
/visualization/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | import js from '@eslint/js'
 2 | import globals from 'globals'
 3 | import reactHooks from 'eslint-plugin-react-hooks'
 4 | import reactRefresh from 'eslint-plugin-react-refresh'
 5 | import tseslint from 'typescript-eslint'
 6 | 
 7 | export default tseslint.config(
 8 |   { ignores: ['dist'] },
 9 |   {
10 |     extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 |     files: ['**/*.{ts,tsx}'],
12 |     languageOptions: {
13 |       ecmaVersion: 2020,
14 |       globals: globals.browser,
15 |     },
16 |     plugins: {
17 |       'react-hooks': reactHooks,
18 |       'react-refresh': reactRefresh,
19 |     },
20 |     rules: {
21 |       ...reactHooks.configs.recommended.rules,
22 |       'react-refresh/only-export-components': [
23 |         'warn',
24 |         { allowConstantExport: true },
25 |       ],
26 |     },
27 |   },
28 | )
29 | 
```

--------------------------------------------------------------------------------
/visualization/tsconfig.app.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 4 |     "target": "ES2020",
 5 |     "useDefineForClassFields": true,
 6 |     "lib": ["ES2020", "DOM", "DOM.Iterable"],
 7 |     "module": "ESNext",
 8 |     "skipLibCheck": true,
 9 | 
10 |     /* Bundler mode */
11 |     "moduleResolution": "bundler",
12 |     "allowImportingTsExtensions": true,
13 |     "isolatedModules": true,
14 |     "moduleDetection": "force",
15 |     "noEmit": true,
16 |     "jsx": "react-jsx",
17 | 
18 |     /* Linting */
19 |     "strict": true,
20 |     "noUnusedLocals": true,
21 |     "noUnusedParameters": true,
22 |     "noFallthroughCasesInSwitch": true,
23 |     "noUncheckedSideEffectImports": true,
24 | 
25 |     /* Path Aliases */
26 |     "baseUrl": ".",
27 |     "paths": {
28 |       "@/*": ["src/*"],
29 |       "@data/*": ["../data/*"]
30 |     },
31 |     "resolveJsonModule": true
32 |   },
33 |   "include": ["src", "../data/*.json"]
34 | }
35 | 
```

--------------------------------------------------------------------------------
/visualization/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "visualization",
 3 |   "private": true,
 4 |   "version": "0.0.0",
 5 |   "type": "module",
 6 |   "scripts": {
 7 |     "dev": "vite",
 8 |     "build": "tsc -b && vite build",
 9 |     "lint": "eslint .",
10 |     "preview": "vite preview",
11 |     "test:responsive": "node test/responsive-tests.js"
12 |   },
13 |   "dependencies": {
14 |     "@tailwindcss/postcss": "^4.0.15",
15 |     "autoprefixer": "^10.4.21",
16 |     "postcss": "^8.5.3",
17 |     "react": "^19.0.0",
18 |     "react-dom": "^19.0.0",
19 |     "reactflow": "^11.11.4",
20 |     "tailwindcss": "^4.0.15"
21 |   },
22 |   "devDependencies": {
23 |     "@eslint/js": "^9.21.0",
24 |     "@types/react": "^19.0.10",
25 |     "@types/react-dom": "^19.0.4",
26 |     "@vitejs/plugin-react": "^4.3.4",
27 |     "eslint": "^9.21.0",
28 |     "eslint-plugin-react-hooks": "^5.1.0",
29 |     "eslint-plugin-react-refresh": "^0.4.19",
30 |     "globals": "^15.15.0",
31 |     "puppeteer": "^21.7.0",
32 |     "typescript": "~5.7.2",
33 |     "typescript-eslint": "^8.24.1",
34 |     "vite": "^6.2.0"
35 |   }
36 | }
37 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@yodakeisuke/mcp-micromanage",
 3 |   "version": "0.1.3",
 4 |   "description": "MCP server for sequential development task management",
 5 |   "license": "MIT",
 6 |   "author": "yoda keisuke",
 7 |   "type": "module",
 8 |   "bin": {
 9 |     "mcp-micromanage": "dist/index.js"
10 |   },
11 |   "files": [
12 |     "dist"
13 |   ],
14 |   "scripts": {
15 |     "build": "tsc && shx chmod +x dist/*.js",
16 |     "prepare": "npm run build",
17 |     "watch": "tsc --watch",
18 |     "test": "NODE_OPTIONS=--experimental-vm-modules jest"
19 |   },
20 |   "dependencies": {
21 |     "@modelcontextprotocol/sdk": "1.7.0",
22 |     "chalk": "^5.3.0",
23 |     "cors": "^2.8.5",
24 |     "express": "^5.0.1"
25 |   },
26 |   "devDependencies": {
27 |     "@types/cors": "^2.8.17",
28 |     "@types/express": "^5.0.1",
29 |     "@types/jest": "^29.5.14",
30 |     "@types/node": "^22",
31 |     "jest": "^29.7.0",
32 |     "shx": "^0.3.4",
33 |     "typescript": "^5.6.3"
34 |   },
35 |   "keywords": [
36 |     "mcp",
37 |     "agent",
38 |     "micromanage",
39 |     "ai",
40 |     "claude",
41 |     "cursor"
42 |   ],
43 |   "publishConfig": {
44 |     "access": "public"
45 |   }
46 | }
47 | 
```

--------------------------------------------------------------------------------
/visualization/src/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ReactNode } from 'react';
 2 | import { Node, Edge as ReactFlowEdge } from 'reactflow';
 3 | 
 4 | // Possible commit statuses
 5 | export type CommitStatus = 'not_started' | 'in_progress' | 'completed' | 'cancelled' | 'needsRefinment' | 'user_review';
 6 | 
 7 | // Node data type definition
 8 | export interface NodeData {
 9 |   label: ReactNode; // Node label
10 |   description?: string; // Node description
11 |   status?: CommitStatus; // Node status
12 |   statusLabel?: string; // Status label
13 |   statusIcon?: string; // Status icon
14 |   onEdit?: (data: NodeData) => void; // Callback when edit button is clicked
15 |   onStatusChange?: (data: NodeData) => void; // Callback when status change button is clicked
16 | }
17 | 
18 | // Extended node type
19 | export type ExtendedNode<T = any> = Node<T>;
20 | 
21 | // Edge type
22 | export type Edge = ReactFlowEdge;
23 | 
24 | // Commit plan type
25 | export interface CommitPlan {
26 |   goal: string;
27 |   status?: CommitStatus;
28 |   developerNote?: string; // Developer implementation notes captured during refinement
29 | }
30 | 
31 | // PR plan type
32 | export interface PRPlan {
33 |   goal: string;
34 |   commitPlans: CommitPlan[];
35 |   status?: CommitStatus;
36 |   developerNote?: string; // Developer implementation notes captured during refinement
37 | }
38 | 
39 | // Work plan type
40 | export interface WorkPlan {
41 |   goal: string;
42 |   prPlans: PRPlan[];
43 | } 
```

--------------------------------------------------------------------------------
/visualization/public/vite.svg:
--------------------------------------------------------------------------------

```
1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
```

--------------------------------------------------------------------------------
/src/values/ticket.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Status } from './status.js';
 2 | import { PullRequest } from './pullRequest.js';
 3 | import { PlanTaskInput } from '../aggregates/workplan.js';
 4 | 
 5 | export type Ticket = {
 6 |   goal: string;
 7 |   pullRequests: PullRequest[];
 8 |   needsMoreThoughts?: boolean;
 9 | };
10 | 
11 | // Helper function to check if ticket exists
12 | export const ensureTicketExists = (
13 |   ticketState: Ticket | "noTicket", 
14 |   isError = false
15 | ): { result: true; ticket: Ticket } | { result: false; response: { content: Array<{ type: string; text: string }>; isError?: boolean } } => {
16 |   if (ticketState === "noTicket") {
17 |     return {
18 |       result: false,
19 |       response: {
20 |         content: [{
21 |           type: "text",
22 |           text: JSON.stringify({
23 |             message: "No implementation plan found. Create an implementation plan first using the 'plan' tool.",
24 |             ...(isError && { status: 'failed' })
25 |           }, null, 2)
26 |         }],
27 |         isError
28 |       }
29 |     };
30 |   }
31 |   
32 |   return {
33 |     result: true,
34 |     ticket: ticketState
35 |   };
36 | };
37 | 
38 | // Create a plan for a ticket from input data
39 | export const planTicket = (data: PlanTaskInput): Ticket => {
40 |   // PRプランからPullRequestオブジェクトを作成
41 |   const pullRequests = data.prPlans.map(prPlan => {
42 |     // コミットプランからCommitオブジェクトを作成
43 |     const commits = prPlan.commitPlans.map(commitPlan => ({
44 |       goal: commitPlan.goal,
45 |       status: "not_started" as Status,
46 |       developerNote: commitPlan.developerNote
47 |     }));
48 |     
49 |     return {
50 |       goal: prPlan.goal,
51 |       status: "not_started" as Status,
52 |       commits,
53 |       developerNote: prPlan.developerNote
54 |     };
55 |   });
56 | 
57 |   // チケットを作成して返す
58 |   return {
59 |     goal: data.goal,
60 |     pullRequests,
61 |     needsMoreThoughts: data.needsMoreThoughts
62 |   };
63 | }; 
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import chalk from 'chalk';
 2 | 
 3 | 
 4 | export enum LogLevel {
 5 |   DEBUG = 0,
 6 |   INFO = 1,
 7 |   WARN = 2,
 8 |   ERROR = 3,
 9 |   NONE = 4
10 | }
11 | 
12 | // 現在のログレベル(デフォルトはINFO)
13 | let currentLogLevel = LogLevel.INFO;
14 | 
15 | export function setLogLevel(level: LogLevel): void {
16 |   currentLogLevel = level;
17 | }
18 | 
19 | export function getLogLevel(): LogLevel {
20 |   return currentLogLevel;
21 | }
22 | 
23 | function getTimestamp(): string {
24 |   const now = new Date();
25 |   return `[${now.toISOString().replace('T', ' ').substring(0, 19)}]`;
26 | }
27 | 
28 | export function debug(message: string, ...args: any[]): void {
29 |   if (currentLogLevel <= LogLevel.DEBUG) {
30 |     console.error(`${getTimestamp()} ${chalk.blue('DEBUG')} ${message}`, ...args);
31 |   }
32 | }
33 | 
34 | export function info(message: string, ...args: any[]): void {
35 |   if (currentLogLevel <= LogLevel.INFO) {
36 |     console.error(`${getTimestamp()} ${chalk.green('INFO')} ${message}`, ...args);
37 |   }
38 | }
39 | 
40 | export function warn(message: string, ...args: any[]): void {
41 |   if (currentLogLevel <= LogLevel.WARN) {
42 |     console.warn(`${getTimestamp()} ${chalk.yellow('WARN')} ${message}`, ...args);
43 |   }
44 | }
45 | 
46 | export function error(message: string, ...args: any[]): void {
47 |   if (currentLogLevel <= LogLevel.ERROR) {
48 |     console.error(`${getTimestamp()} ${chalk.red('ERROR')} ${message}`, ...args);
49 |   }
50 | }
51 | 
52 | export function logError(message: string, error: unknown): void {
53 |   if (currentLogLevel <= LogLevel.ERROR) {
54 |     console.error(`${getTimestamp()} ${chalk.red('ERROR')} ${message}: ${error instanceof Error ? error.message : String(error)}`);
55 |     if (error instanceof Error && error.stack) {
56 |       console.error(`${getTimestamp()} ${chalk.red('STACK')} ${error.stack}`);
57 |     }
58 |   }
59 | }
60 | 
61 | // デフォルトエクスポート
62 | export default {
63 |   setLogLevel,
64 |   getLogLevel,
65 |   debug,
66 |   info,
67 |   warn,
68 |   error,
69 |   logError,
70 |   LogLevel
71 | }; 
```

--------------------------------------------------------------------------------
/visualization/src/components/OrientationWarning.tsx:
--------------------------------------------------------------------------------

```typescript
 1 | import React, { useState, useEffect } from 'react';
 2 | import { useBreakpoint, Breakpoint } from '../utils/responsiveUtils';
 3 | 
 4 | /**
 5 |  * Component that detects mobile device orientation and displays a warning
 6 |  * Shows a recommendation to use landscape orientation
 7 |  */
 8 | const OrientationWarning: React.FC = () => {
 9 |   const breakpoint = useBreakpoint();
10 |   const [isPortrait, setIsPortrait] = useState(false);
11 |   const [isDismissed, setIsDismissed] = useState(false);
12 | 
13 |   // Detect screen orientation
14 |   useEffect(() => {
15 |     const checkOrientation = () => {
16 |       if (window.matchMedia("(orientation: portrait)").matches) {
17 |         setIsPortrait(true);
18 |       } else {
19 |         setIsPortrait(false);
20 |       }
21 |     };
22 | 
23 |     // Initial check
24 |     checkOrientation();
25 | 
26 |     // Orientation change listener
27 |     window.addEventListener('resize', checkOrientation);
28 |     
29 |     // Cleanup
30 |     return () => {
31 |       window.removeEventListener('resize', checkOrientation);
32 |     };
33 |   }, []);
34 | 
35 |   // Determine whether to show the warning
36 |   const shouldShowWarning = isPortrait && ['xs', 'sm'].includes(breakpoint) && !isDismissed;
37 |   
38 |   if (!shouldShowWarning) {
39 |     return null;
40 |   }
41 | 
42 |   return (
43 |     <div className="fixed bottom-0 left-0 right-0 bg-yellow-100 border-t border-yellow-200 p-3 z-50 shadow-lg">
44 |       <div className="flex justify-between items-center">
45 |         <div className="flex items-center">
46 |           <span className="text-xl mr-2" aria-hidden="true">📱</span>
47 |           <div>
48 |             <p className="text-yellow-800 font-medium">Landscape orientation recommended</p>
49 |             <p className="text-yellow-700 text-xs">Rotate your device to landscape for a better experience.</p>
50 |           </div>
51 |         </div>
52 |         <button 
53 |           onClick={() => setIsDismissed(true)}
54 |           className="text-yellow-700 hover:text-yellow-900"
55 |           aria-label="Close warning"
56 |         >
57 |           ✕
58 |         </button>
59 |       </div>
60 |     </div>
61 |   );
62 | };
63 | 
64 | export default OrientationWarning; 
```

--------------------------------------------------------------------------------
/src/tools/track/toolDefs.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { Tool, createErrorResponse } from '../common.js';
 2 | import { workPlan } from '../../index.js';
 3 | import logger from '../../utils/logger.js';
 4 | import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
 5 | 
 6 | export const TRACK_TOOL: Tool<{}> = {
 7 |   name: "track",
 8 |   description: `
 9 |     This tool helps you monitor the current state of the implementation plan, view progress, and identify possible next steps.
10 |     There is always exactly one task in either the in_progress or user_review state.
11 |     **IMPORTANT**: There is always exactly one task in either the in_progress or user_review or needsRefinment state.
12 |     
13 |     **MANDATORY STATUS TRANSITION RULES:**
14 |     needsRefinment → in_progress:
15 |       - Requirements, implementation plan, and impact MUST be clearly explained.
16 |     user_review → completed:
17 |       - A commit may not transition to "completed" without explicit user approval. No exceptions.
18 |     
19 |     **After receiving the tool response, you MUST**::
20 |     - Monitor PRs, commits, and overall progress to spot blockers or items needing refinement.
21 |     - Review recommended actions to decide your next steps.
22 |     - **Absolutely follow the content of "agentInstruction" in the response JSON**!!.
23 |   `,
24 |   schema: {},
25 |   handler: async (_, extra: RequestHandlerExtra) => {
26 |     try {
27 |       logger.info('Track tool called');
28 |       
29 |       if (!workPlan) {
30 |         logger.error('WorkPlan instance is not available');
31 |         return createErrorResponse('WorkPlan instance is not initialized');
32 |       }
33 |       
34 |       if (!workPlan.isInitialized()) {
35 |         logger.error('WorkPlan is not initialized');
36 |         return createErrorResponse('WorkPlan is not ready. Server initialization incomplete.');
37 |       }
38 |       
39 |       const result = workPlan.trackProgress();
40 |       
41 |       return {
42 |         content: result.content.map(item => ({
43 |           type: 'text' as const,
44 |           text: item.text
45 |         })),
46 |         isError: result.isError
47 |       };
48 |     } catch (error) {
49 |       logger.logError('Error in TRACK tool', error);
50 |       return createErrorResponse(error);
51 |     }
52 |   }
53 | }; 
```

--------------------------------------------------------------------------------
/src/tools/common.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
 2 | import { z } from 'zod';
 3 | import { type PlanTaskInput, type UpdateStatusInput, type WorkPlanInitOptions } from '../aggregates/workplan.js';
 4 | import logger from '../utils/logger.js';
 5 | 
 6 | 
 7 | export type Tool<Args extends z.ZodRawShape> = {
 8 |   name: string;
 9 |   description: string;
10 |   schema: Args;
11 |   handler: (
12 |     args: z.infer<z.ZodObject<Args>>,
13 |     extra: RequestHandlerExtra,
14 |   ) =>
15 |     | Promise<{
16 |       content: Array<{
17 |         type: 'text';
18 |         text: string;
19 |       }>;
20 |       isError?: boolean;
21 |     }>
22 |     | {
23 |       content: Array<{
24 |         type: 'text';
25 |         text: string;
26 |       }>;
27 |       isError?: boolean;
28 |     };
29 | };
30 | 
31 | /**
32 |  * Enhanced error response generator with improved formatting and guidance.
33 |  * 
34 |  * Formats error messages to be more user-friendly and provides specific guidance
35 |  * for validation errors and other common issues.
36 |  * 
37 |  * @param error - The error to format
38 |  * @param context - Optional context about the error source (e.g., 'plan', 'update')
39 |  * @returns Formatted error response
40 |  */
41 | export function createErrorResponse(
42 |   error: unknown, 
43 |   context?: string
44 | ): {
45 |   content: Array<{ type: 'text'; text: string }>;
46 |   isError: boolean;
47 | } {
48 |   // Extract base error message
49 |   const errorMessage = error instanceof Error ? error.message : String(error);
50 |   logger.error(`Creating error response: ${errorMessage}`);
51 |   
52 |   // Format error message with guidance based on error type
53 |   let formattedError = errorMessage;
54 |   
55 |   // Check if it's a validation error
56 |   if (errorMessage.includes('Invalid arguments') || errorMessage.includes('Validation')) {
57 |     formattedError = `Validation Error: The provided data does not meet requirements.\n\n${errorMessage}`;
58 |     
59 |     if (context === 'plan') {
60 |       formattedError += `\n\nSuggestions for fixing plan validation errors:
61 | - Ensure goal fields are concise (max 60 characters) and focus on WHAT will be done
62 | - Use developer notes for detailed implementation information (max 300 characters)
63 | - Make sure at least one PR plan and one commit plan are provided
64 | - Check that all required fields are present and properly formatted`;
65 |     }
66 |   }
67 |   
68 |   return {
69 |     content: [{
70 |       type: 'text',
71 |       text: JSON.stringify({
72 |         error: formattedError,
73 |         status: 'failed'
74 |       }, null, 2)
75 |     }],
76 |     isError: true
77 |   };
78 | }
79 | 
80 | // エクスポートする型
81 | export type { PlanTaskInput, UpdateStatusInput, WorkPlanInitOptions }; 
```

--------------------------------------------------------------------------------
/visualization/src/components/nodes/nodes.css:
--------------------------------------------------------------------------------

```css
  1 | /* コミットノードのスタイル */
  2 | .commit-node {
  3 |   border: 2px solid #ccc;
  4 |   border-radius: 5px;
  5 |   padding: 10px;
  6 |   width: 220px;
  7 |   background-color: white;
  8 |   transition: all 0.2s ease;
  9 |   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
 10 |   position: relative;
 11 | }
 12 | 
 13 | .commit-node:hover {
 14 |   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
 15 |   transform: translateY(-2px);
 16 | }
 17 | 
 18 | .commit-node .header {
 19 |   display: flex;
 20 |   align-items: center;
 21 |   justify-content: space-between;
 22 |   margin-bottom: 5px;
 23 | }
 24 | 
 25 | .commit-node .title {
 26 |   margin-top: 5px;
 27 |   font-weight: bold;
 28 |   font-size: 14px;
 29 |   color: #333;
 30 |   word-break: break-word;
 31 | }
 32 | 
 33 | .commit-node .handle {
 34 |   width: 10px;
 35 |   height: 10px;
 36 |   border-radius: 50%;
 37 |   background-color: #60a5fa;
 38 |   border: 2px solid white;
 39 |   transition: transform 0.2s ease, background-color 0.2s ease;
 40 | }
 41 | 
 42 | .commit-node .handle-left {
 43 |   left: -6px;
 44 | }
 45 | 
 46 | .commit-node .handle-right {
 47 |   right: -6px;
 48 | }
 49 | 
 50 | /* ホバー時にハンドルを大きく */
 51 | .commit-node:hover .handle {
 52 |   transform: scale(1.2);
 53 |   background-color: #3b82f6;
 54 | }
 55 | 
 56 | .commit-node .status-label {
 57 |   display: inline-block;
 58 |   padding: 3px 6px;
 59 |   border-radius: 12px;
 60 |   font-size: 11px;
 61 |   font-weight: 500;
 62 |   color: white;
 63 | }
 64 | 
 65 | .commit-node .status-icon {
 66 |   margin-right: 4px;
 67 | }
 68 | 
 69 | /* ノードアクションボタン */
 70 | .node-actions {
 71 |   position: absolute;
 72 |   top: -10px;
 73 |   right: -10px;
 74 |   display: none;
 75 |   flex-direction: row;
 76 |   gap: 4px;
 77 |   background-color: white;
 78 |   border-radius: 4px;
 79 |   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
 80 |   padding: 4px;
 81 |   z-index: 10;
 82 | }
 83 | 
 84 | .commit-node:hover .node-actions {
 85 |   display: flex;
 86 | }
 87 | 
 88 | .node-action-button {
 89 |   width: 24px;
 90 |   height: 24px;
 91 |   display: flex;
 92 |   align-items: center;
 93 |   justify-content: center;
 94 |   border-radius: 4px;
 95 |   background-color: #f9fafb;
 96 |   border: 1px solid #e5e7eb;
 97 |   cursor: pointer;
 98 |   font-size: 12px;
 99 |   transition: background-color 0.2s ease;
100 | }
101 | 
102 | .node-action-button:hover {
103 |   background-color: #f3f4f6;
104 | }
105 | 
106 | .node-action-edit {
107 |   color: #3b82f6;
108 | }
109 | 
110 | .node-action-delete {
111 |   color: #ef4444;
112 | }
113 | 
114 | /* エッジスタイル */
115 | .react-flow__edge-path {
116 |   stroke-width: 2;
117 |   stroke: #aaa;
118 |   transition: stroke 0.3s, stroke-width 0.3s;
119 | }
120 | 
121 | /* エッジアニメーション */
122 | .react-flow__edge-path.animated {
123 |   stroke-dasharray: 5;
124 |   animation: dashdraw 0.5s linear infinite;
125 | }
126 | 
127 | @keyframes dashdraw {
128 |   from {
129 |     stroke-dashoffset: 10;
130 |   }
131 | }
132 | 
133 | /* ホバー時のエッジスタイル */
134 | .react-flow__edge:hover .react-flow__edge-path {
135 |   stroke-width: 3;
136 |   stroke: #3b82f6;
137 | }
138 | 
139 | /* エッジラベルスタイル */
140 | .react-flow__edge-text {
141 |   font-size: 10px;
142 |   fill: #666;
143 |   pointer-events: none;
144 | }
145 | 
146 | /* PRノードのスタイル */
147 | .pr-node {
148 |   padding: 15px;
149 |   background-color: white;
150 |   border-radius: 8px;
151 |   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
152 |   transition: all 0.3s;
153 | }
154 | 
155 | .pr-node:hover {
156 |   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
157 |   transform: translateY(-2px);
158 | }
159 | 
160 | /* ミニマップ強調 */
161 | .react-flow__minimap-mask {
162 |   fill: rgba(240, 240, 240, 0.6);
163 | }
164 | 
165 | .react-flow__minimap-node {
166 |   transition: fill 0.2s;
167 | }
168 | 
169 | /* ノード選択時のスタイル */
170 | .react-flow__node.selected {
171 |   box-shadow: 0 0 0 2px #3b82f6 !important;
172 | } 
```

--------------------------------------------------------------------------------
/src/values/pullRequest.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Status } from './status.js';
  2 | import { Commit } from './commit.js';
  3 | 
  4 | export type PullRequest = {
  5 |   goal: string;
  6 |   status: Status;
  7 |   commits: Commit[];
  8 |   needsMoreThoughts?: boolean;
  9 |   developerNote?: string;
 10 | };
 11 | 
 12 | /**
 13 |  * Generates summary information for a list of pull requests
 14 |  */
 15 | export const generatePRSummaries = (pullRequests: PullRequest[]): Array<{
 16 |   prIndex: number;
 17 |   goal: string;
 18 |   status: Status;
 19 |   developerNote?: string;
 20 |   commits: {
 21 |     completed: number;
 22 |     total: number;
 23 |     percentComplete: number;
 24 |   };
 25 | }> => {
 26 |   return pullRequests.map((pr: PullRequest, prIndex: number) => {
 27 |     const completedCommits = pr.commits.filter((c: { status: Status }) => c.status === "completed").length;
 28 |     const totalCommits = pr.commits.length;
 29 |     
 30 |     return {
 31 |       prIndex: prIndex,
 32 |       goal: pr.goal,
 33 |       status: pr.status,
 34 |       developerNote: pr.developerNote,
 35 |       commits: {
 36 |         completed: completedCommits,
 37 |         total: totalCommits,
 38 |         percentComplete: totalCommits ? Math.round((completedCommits / totalCommits) * 100) : 0
 39 |       }
 40 |     };
 41 |   });
 42 | };
 43 | 
 44 | // Helper functions for status updates
 45 | export const updatePRStatusBasedOnCommits = (pr: PullRequest): PullRequest => {
 46 |   const commits = pr.commits;
 47 |   
 48 |   if (commits.length === 0) return pr;
 49 |   
 50 |   const updatedPr = { ...pr };
 51 |   
 52 |   // If all commits are completed, mark PR as completed
 53 |   if (commits.every(commit => commit.status === "completed")) {
 54 |     updatedPr.status = "completed";
 55 |   }
 56 |   // If any commit is in user_review status, mark PR as user_review (unless there are in_progress commits)
 57 |   else if (commits.some(commit => commit.status === "user_review") &&
 58 |            !commits.some(commit => commit.status === "in_progress")) {
 59 |     updatedPr.status = "user_review";
 60 |   }
 61 |   // If any commit is in progress, mark PR as in progress
 62 |   else if (commits.some(commit => commit.status === "in_progress")) {
 63 |     updatedPr.status = "in_progress";
 64 |   }
 65 |   // "blocked" status has been removed as per requirements
 66 |   
 67 |   // If any commit needs refinement, mark PR as needs refinement
 68 |   else if (commits.some(commit => commit.status === "needsRefinment") &&
 69 |            !commits.some(commit => commit.status === "in_progress")) {
 70 |     updatedPr.status = "needsRefinment";
 71 |   }
 72 |   
 73 |   return updatedPr;
 74 | };
 75 | 
 76 | /**
 77 |  * Updates the developer note for a pull request
 78 |  */
 79 | export const updatePRDeveloperNote = (pr: PullRequest, note: string): PullRequest => {
 80 |   return {
 81 |     ...pr,
 82 |     developerNote: note
 83 |   };
 84 | };
 85 | 
 86 | /**
 87 |  * Updates the developer note for a commit in a pull request
 88 |  */
 89 | export const updateCommitDeveloperNote = (pr: PullRequest, commitIndex: number, note: string): PullRequest => {
 90 |   if (commitIndex < 0 || commitIndex >= pr.commits.length) {
 91 |     return pr; // Invalid commit index
 92 |   }
 93 | 
 94 |   const updatedCommits = [...pr.commits];
 95 |   updatedCommits[commitIndex] = {
 96 |     ...updatedCommits[commitIndex],
 97 |     developerNote: note
 98 |   };
 99 | 
100 |   return {
101 |     ...pr,
102 |     commits: updatedCommits
103 |   };
104 | }; 
```

--------------------------------------------------------------------------------
/visualization/src/utils/storageUtils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WorkPlan, CommitStatus, PRPlan, CommitPlan } from '../types';
  2 | 
  3 | // Local storage key
  4 | const STORAGE_KEY = 'workplan-visualization-data';
  5 | 
  6 | // List of valid status values
  7 | const VALID_STATUSES: CommitStatus[] = [
  8 |   'not_started', 
  9 |   'in_progress', 
 10 |   'completed', 
 11 |   'cancelled', 
 12 |   'needsRefinment',
 13 |   'user_review'
 14 | ];
 15 | 
 16 | /**
 17 |  * Check if an object is a valid WorkPlan and fix it if necessary
 18 |  */
 19 | const validateWorkPlan = (data: any): WorkPlan => {
 20 |   // Basic structure check
 21 |   if (!data || typeof data !== 'object' || !data.goal || !Array.isArray(data.prPlans)) {
 22 |     throw new Error('Invalid workplan data structure');
 23 |   }
 24 | 
 25 |   // Validate and normalize PRPlans
 26 |   const validatedPrPlans: PRPlan[] = data.prPlans.map((pr: any): PRPlan => {
 27 |     if (!pr || typeof pr !== 'object' || !pr.goal || !Array.isArray(pr.commitPlans)) {
 28 |       throw new Error('Invalid PR data structure');
 29 |     }
 30 |     
 31 |     // Validate status
 32 |     const status = pr.status && VALID_STATUSES.includes(pr.status as CommitStatus) 
 33 |       ? pr.status as CommitStatus 
 34 |       : undefined;
 35 |     
 36 |     // Validate and normalize commit plans
 37 |     const commitPlans: CommitPlan[] = pr.commitPlans.map((commit: any): CommitPlan => {
 38 |       if (!commit || typeof commit !== 'object' || !commit.goal) {
 39 |         throw new Error('Invalid commit data structure');
 40 |       }
 41 |       
 42 |       // Validate commit status
 43 |       const commitStatus = commit.status && VALID_STATUSES.includes(commit.status as CommitStatus) 
 44 |         ? commit.status as CommitStatus 
 45 |         : undefined;
 46 |       
 47 |       return {
 48 |         goal: commit.goal,
 49 |         status: commitStatus,
 50 |         developerNote: commit.developerNote ? String(commit.developerNote) : undefined
 51 |       };
 52 |     });
 53 |     
 54 |     return {
 55 |       goal: pr.goal,
 56 |       status,
 57 |       commitPlans,
 58 |       developerNote: pr.developerNote ? String(pr.developerNote) : undefined
 59 |     };
 60 |   });
 61 |   
 62 |   return {
 63 |     goal: data.goal,
 64 |     prPlans: validatedPrPlans
 65 |   };
 66 | };
 67 | 
 68 | /**
 69 |  * Save workplan to local storage
 70 |  * @param workplan The workplan to save
 71 |  */
 72 | export const saveWorkPlanToStorage = (workplan: WorkPlan): void => {
 73 |   try {
 74 |     const serializedData = JSON.stringify(workplan);
 75 |     localStorage.setItem(STORAGE_KEY, serializedData);
 76 |   } catch (error) {
 77 |     console.error('Failed to save workplan:', error);
 78 |   }
 79 | };
 80 | 
 81 | /**
 82 |  * Get workplan from local storage
 83 |  * @returns The stored workplan, or null if not found
 84 |  */
 85 | export const getWorkPlanFromStorage = (): WorkPlan | null => {
 86 |   try {
 87 |     const serializedData = localStorage.getItem(STORAGE_KEY);
 88 |     if (!serializedData) return null;
 89 |     
 90 |     const parsedData = JSON.parse(serializedData);
 91 |     return validateWorkPlan(parsedData);
 92 |   } catch (error) {
 93 |     console.error('Failed to get workplan:', error);
 94 |     return null;
 95 |   }
 96 | };
 97 | 
 98 | /**
 99 |  * Clear workplan from local storage
100 |  */
101 | export const clearWorkPlanStorage = (): void => {
102 |   try {
103 |     localStorage.removeItem(STORAGE_KEY);
104 |   } catch (error) {
105 |     console.error('Failed to delete workplan:', error);
106 |   }
107 | }; 
```

--------------------------------------------------------------------------------
/visualization/src/App.css:
--------------------------------------------------------------------------------

```css
  1 | #root {
  2 |   max-width: 1280px;
  3 |   margin: 0 auto;
  4 |   padding: 2rem;
  5 |   text-align: center;
  6 | }
  7 | 
  8 | .logo {
  9 |   height: 6em;
 10 |   padding: 1.5em;
 11 |   will-change: filter;
 12 |   transition: filter 300ms;
 13 | }
 14 | .logo:hover {
 15 |   filter: drop-shadow(0 0 2em #646cffaa);
 16 | }
 17 | .logo.react:hover {
 18 |   filter: drop-shadow(0 0 2em #61dafbaa);
 19 | }
 20 | 
 21 | @keyframes logo-spin {
 22 |   from {
 23 |     transform: rotate(0deg);
 24 |   }
 25 |   to {
 26 |     transform: rotate(360deg);
 27 |   }
 28 | }
 29 | 
 30 | @media (prefers-reduced-motion: no-preference) {
 31 |   a:nth-of-type(2) .logo {
 32 |     animation: logo-spin infinite 20s linear;
 33 |   }
 34 | }
 35 | 
 36 | .card {
 37 |   padding: 2em;
 38 | }
 39 | 
 40 | .read-the-docs {
 41 |   color: #888;
 42 | }
 43 | 
 44 | .app {
 45 |   display: flex;
 46 |   flex-direction: column;
 47 |   height: 100vh;
 48 |   overflow: hidden;
 49 |   font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
 50 | }
 51 | 
 52 | header {
 53 |   flex-shrink: 0;
 54 |   z-index: 10;
 55 | }
 56 | 
 57 | .app-header {
 58 |   height: 60px;
 59 |   padding: 0 20px;
 60 |   display: flex;
 61 |   align-items: center;
 62 |   background-color: #1a1a1a;
 63 |   color: white;
 64 |   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
 65 | }
 66 | 
 67 | .app-header h1 {
 68 |   font-size: 1.5rem;
 69 |   margin: 0;
 70 | }
 71 | 
 72 | .app-main {
 73 |   flex-grow: 1;
 74 |   overflow: hidden;
 75 |   position: relative;
 76 | }
 77 | 
 78 | /* フィルターパネルのスタイル */
 79 | .filter-panel-container {
 80 |   position: absolute;
 81 |   top: 80px;
 82 |   left: 20px;
 83 |   z-index: 20;
 84 |   max-width: 90%;
 85 |   width: 320px;
 86 | }
 87 | 
 88 | /* ノードとエッジのスタイル */
 89 | .pr-node {
 90 |   border-radius: 8px;
 91 |   background-color: #f3f4f6;
 92 |   border: 2px solid #9ca3af;
 93 |   padding: 10px;
 94 |   box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
 95 | }
 96 | 
 97 | .commit-node {
 98 |   min-height: 80px;
 99 |   width: 100%;
100 |   word-break: break-word;
101 | }
102 | 
103 | /* ReactFlowのカスタマイズ */
104 | .react-flow__node {
105 |   transition: all 0.2s ease-in-out;
106 | }
107 | 
108 | .react-flow__node:hover {
109 |   transform: translateY(-2px);
110 |   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
111 | }
112 | 
113 | .react-flow__node.selected {
114 |   box-shadow: 0 0 0 2px #3b82f6;
115 | }
116 | 
117 | .react-flow__edge-path.animated {
118 |   stroke-dasharray: 5;
119 |   animation: dashdraw 3s linear infinite;
120 | }
121 | 
122 | @keyframes dashdraw {
123 |   from {
124 |     stroke-dashoffset: 10;
125 |   }
126 | }
127 | 
128 | /* レスポンシブデザイン */
129 | @media (max-width: 768px) {
130 |   header {
131 |     flex-direction: column;
132 |     gap: 10px;
133 |     align-items: flex-start;
134 |     padding: 10px;
135 |   }
136 |   
137 |   header h1 {
138 |     font-size: 1.5rem;
139 |   }
140 |   
141 |   header .flex {
142 |     flex-wrap: wrap;
143 |     gap: 8px;
144 |   }
145 |   
146 |   .filter-panel-container {
147 |     top: 120px;
148 |     left: 10px;
149 |     width: calc(100% - 20px);
150 |     max-width: none;
151 |   }
152 | }
153 | 
154 | /* アニメーション */
155 | @keyframes fadeIn {
156 |   from {
157 |     opacity: 0;
158 |     transform: translateY(10px);
159 |   }
160 |   to {
161 |     opacity: 1;
162 |     transform: translateY(0);
163 |   }
164 | }
165 | 
166 | .filter-panel-container > div,
167 | .export-panel-container > div {
168 |   animation: fadeIn 0.3s ease-out;
169 | }
170 | 
171 | /* ダークモードサポート */
172 | @media (prefers-color-scheme: dark) {
173 |   .react-flow__node {
174 |     color: #f1f5f9;
175 |   }
176 |   
177 |   .react-flow__node.start-node {
178 |     background-color: #1e3a8a;
179 |     border-color: #3b82f6;
180 |   }
181 |   
182 |   .react-flow__node.pr-node {
183 |     background-color: #1f2937;
184 |     border-color: #4b5563;
185 |   }
186 |   
187 |   .commit-node {
188 |     background-color: #374151;
189 |     border-color: #6b7280;
190 |   }
191 | }
192 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import path from 'path';
  6 | import { fileURLToPath } from 'url';
  7 | 
  8 | import { PLAN_TOOL, TRACK_TOOL, UPDATE_STATUS_TOOL } from "./tools/index.js";
  9 | import { taskPlanningGuide } from "./prompts.js";
 10 | import logger, { LogLevel } from './utils/logger.js';
 11 | import { WorkPlan, WorkPlanInitOptions } from './aggregates/workplan.js';
 12 | 
 13 | const __filename = fileURLToPath(import.meta.url);
 14 | const __dirname = path.dirname(__filename);
 15 | 
 16 | const logLevel = LogLevel.INFO;
 17 | logger.setLogLevel(logLevel);
 18 | logger.info(`Logger initialized with level: ${LogLevel[logLevel]}`);
 19 | 
 20 | const dataDir = path.resolve(__dirname, '../visualization/public/data');
 21 | const dataFileName = 'workplan.json';
 22 | 
 23 | const workPlanOptions: WorkPlanInitOptions = {
 24 |   dataDir,
 25 |   dataFileName
 26 | };
 27 | 
 28 | export const workPlan = new WorkPlan();
 29 | const initSuccess = workPlan.initialize(workPlanOptions);
 30 | 
 31 | if (!initSuccess) {
 32 |   logger.error(`Failed to initialize WorkPlan with options: ${JSON.stringify(workPlanOptions)}`);
 33 |   process.exit(1);
 34 | }
 35 | 
 36 | const server = new McpServer({
 37 |   name: "micromanage",
 38 |   version: "0.1.0",
 39 |   description: `
 40 |   Externalizing developers' working memory and procedures related to work plans for resolving tickets.
 41 |   Because it would involve unnecessary overhead, we **do not apply it to tiny tasks**.
 42 |   
 43 |   This server helps organize and track development tasks through a structured approach with the following key features:
 44 | - Breaking down the tasks of the current ticket into minimal PRs and commits
 45 | - Tracking progress of implementation work
 46 | - Updating status of development items as work progresses
 47 | 
 48 | ## When to Use This
 49 | - When users request ticket assignment
 50 | - When users ask for detailed task management
 51 | - For complex tasks requiring structured breakdown and tracking
 52 | 
 53 | Best practices:
 54 | 1. Start with the task-planning-guide prompt before creating a plan
 55 | 2. Keep PRs minimal and focused on single logical changes
 56 | 3. Make commits small and atomic
 57 | 4. Update status promptly as work begins or completes
 58 | 5. Check progress regularly to maintain workflow efficiency
 59 | 6. Always check the agentInstruction field in the response JSON from the 'track' tool
 60 | `,
 61 |   capabilities: {
 62 |     tools: {
 63 |       listChanged: true
 64 |     },
 65 |     prompts: {
 66 |       listChanged: true
 67 |     }
 68 |   }
 69 | });
 70 | 
 71 | server.prompt(
 72 |   taskPlanningGuide.name,
 73 |   taskPlanningGuide.description,
 74 |   {}, 
 75 |   taskPlanningGuide.handler
 76 | );
 77 | 
 78 | [PLAN_TOOL, TRACK_TOOL, UPDATE_STATUS_TOOL].forEach(tool => {
 79 |   server.tool(tool.name, tool.description ?? "", tool.schema, tool.handler);
 80 | });
 81 | 
 82 | async function runServer() {
 83 |   const transport = new StdioServerTransport();
 84 |   await server.connect(transport);
 85 | }
 86 | 
 87 | process.on('SIGINT', () => {
 88 |   workPlan.forceSave();
 89 |   process.exit(0);
 90 | });
 91 | 
 92 | process.on('SIGTERM', () => {
 93 |   workPlan.forceSave();
 94 |   process.exit(0);
 95 | });
 96 | 
 97 | runServer().catch((error) => {
 98 |   logger.logError("Fatal error running server", error);
 99 |   workPlan.forceSave();
100 |   process.exit(1);
101 | }); 
```

--------------------------------------------------------------------------------
/visualization/src/utils/responsiveUtils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useEffect } from 'react';
  2 | 
  3 | // Breakpoint definitions
  4 | export enum Breakpoint {
  5 |   xs = 0,    // Extra small devices (phones)
  6 |   sm = 640,  // Small devices (tablets)
  7 |   md = 768,  // Medium devices (landscape tablets)
  8 |   lg = 1024, // Large devices (desktops)
  9 |   xl = 1280, // Extra large devices (large desktops)
 10 |   xxl = 1536 // Double extra large devices
 11 | }
 12 | 
 13 | // Custom hook to determine current breakpoint
 14 | export function useBreakpoint() {
 15 |   const [breakpoint, setBreakpoint] = useState<keyof typeof Breakpoint>(() => {
 16 |     // Set initial value
 17 |     return getBreakpoint(window.innerWidth);
 18 |   });
 19 | 
 20 |   useEffect(() => {
 21 |     // Handler for window resize events
 22 |     const handleResize = () => {
 23 |       setBreakpoint(getBreakpoint(window.innerWidth));
 24 |     };
 25 | 
 26 |     // Register listener
 27 |     window.addEventListener('resize', handleResize);
 28 |     
 29 |     // Cleanup function
 30 |     return () => {
 31 |       window.removeEventListener('resize', handleResize);
 32 |     };
 33 |   }, []);
 34 | 
 35 |   return breakpoint;
 36 | }
 37 | 
 38 | // Function to determine breakpoint from width
 39 | function getBreakpoint(width: number): keyof typeof Breakpoint {
 40 |   if (width >= Breakpoint.xxl) return 'xxl';
 41 |   if (width >= Breakpoint.xl) return 'xl';
 42 |   if (width >= Breakpoint.lg) return 'lg';
 43 |   if (width >= Breakpoint.md) return 'md';
 44 |   if (width >= Breakpoint.sm) return 'sm';
 45 |   return 'xs';
 46 | }
 47 | 
 48 | // Utility function to return responsive values
 49 | type ResponsiveValues<T> = {
 50 |   [key in keyof typeof Breakpoint]?: T;
 51 | } & {
 52 |   default: T;
 53 | };
 54 | 
 55 | export function getResponsiveValue<T>(
 56 |   values: ResponsiveValues<T>,
 57 |   currentBreakpoint: keyof typeof Breakpoint
 58 | ): T {
 59 |   // Return the value of the largest breakpoint less than or equal to the current breakpoint
 60 |   const breakpoints: (keyof typeof Breakpoint)[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
 61 |   const currentIndex = breakpoints.indexOf(currentBreakpoint);
 62 |   
 63 |   // Search in descending order from current breakpoint
 64 |   for (let i = currentIndex; i >= 0; i--) {
 65 |     const bp = breakpoints[i];
 66 |     if (values[bp] !== undefined) {
 67 |       return values[bp] as T;
 68 |     }
 69 |   }
 70 |   
 71 |   // Return default value if not found
 72 |   return values.default;
 73 | }
 74 | 
 75 | // Hook for adjusting react-flow canvas size
 76 | export function useResponsiveFlowDimensions() {
 77 |   const breakpoint = useBreakpoint();
 78 |   
 79 |   // Adjust flow canvas size based on breakpoint
 80 |   const nodePadding = getResponsiveValue<number>(
 81 |     {
 82 |       xs: 5,
 83 |       sm: 10,
 84 |       md: 15,
 85 |       lg: 20,
 86 |       default: 20
 87 |     },
 88 |     breakpoint
 89 |   );
 90 | 
 91 |   const nodeSpacing = getResponsiveValue<number>(
 92 |     {
 93 |       xs: 30,
 94 |       sm: 40,
 95 |       md: 50,
 96 |       lg: 60,
 97 |       default: 60
 98 |     },
 99 |     breakpoint
100 |   );
101 |   
102 |   const miniMapVisible = getResponsiveValue<boolean>(
103 |     {
104 |       xs: false,
105 |       sm: false,
106 |       md: true,
107 |       default: true
108 |     },
109 |     breakpoint
110 |   );
111 |   
112 |   const controlsStyle = getResponsiveValue<React.CSSProperties>(
113 |     {
114 |       xs: { right: '5px', bottom: '5px', transform: 'scale(0.8)' },
115 |       sm: { right: '10px', bottom: '10px', transform: 'scale(0.9)' },
116 |       md: { right: '10px', bottom: '10px' },
117 |       default: { right: '10px', bottom: '10px' }
118 |     },
119 |     breakpoint
120 |   );
121 |   
122 |   const miniMapStyle = getResponsiveValue<React.CSSProperties>(
123 |     {
124 |       xs: { display: 'none' },
125 |       sm: { display: 'none' },
126 |       md: { right: 10, top: 10, width: 120, height: 80 },
127 |       lg: { right: 10, top: 10, width: 150, height: 100 },
128 |       default: { right: 10, top: 10, width: 200, height: 120 }
129 |     },
130 |     breakpoint
131 |   );
132 | 
133 |   return {
134 |     nodePadding,
135 |     nodeSpacing,
136 |     miniMapVisible,
137 |     controlsStyle,
138 |     miniMapStyle,
139 |     currentBreakpoint: breakpoint
140 |   };
141 | } 
```

--------------------------------------------------------------------------------
/src/tools/update/toolDefs.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { Tool, createErrorResponse, UpdateStatusInput } from '../common.js';
 3 | import { workPlan } from '../../index.js';
 4 | import logger from '../../utils/logger.js';
 5 | import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
 6 | 
 7 | export const UPDATE_STATUS_TOOL: Tool<{
 8 |   prIndex: z.ZodNumber;
 9 |   commitIndex: z.ZodNumber;
10 |   status: z.ZodEnum<["not_started", "in_progress", "user_review", "completed", "cancelled", "needsRefinment"]>;
11 |   goal?: z.ZodOptional<z.ZodString>;
12 |   developerNote?: z.ZodOptional<z.ZodString>;
13 | }> = {
14 |   name: "update",
15 |   description: `
16 |     A tool for updating the status and goals of development tasks.
17 |     **IMPORTANT**: 
18 |       - There is always exactly one task in either the in_progress or user_review or needsRefinment state.
19 |       - When setting status to "user_review", you MUST generate a review request message to the user.
20 |       - Always check and follow **task-status-update-rule.mdc** when updating task status.
21 |     
22 |     **After receiving the tool response, you MUST**::
23 |     - 1. track the current whole status of the workplan.
24 |     - 2. check the detailed information including developer notes in the next task.
25 |   `,
26 |   schema: {
27 |     prIndex: z.number().int().min(0, "PR index must be a non-negative integer").describe("Index of the PR containing the commit to update. Zero-based index in the PR array."),
28 |     commitIndex: z.number().int().describe("Index of the commit to update within the specified PR. Zero-based index in the commit array. Use -1 to update the PR directly."),
29 |     status: z.enum(["not_started", "in_progress", "user_review", "completed", "cancelled", "needsRefinment"])
30 |       .describe("New status value for the commit. Valid values: 'not_started' (work has not begun), 'in_progress' (currently being worked on), 'user_review' (waiting for user to review), 'completed' (work is finished), 'cancelled' (work has been abandoned), or 'needsRefinment' (needs further refinement)."),
31 |     goal: z.string().min(1, "Goal must be a non-empty string").optional()
32 |       .describe("New goal description for the commit. Optional - only provide if you want to change the commit goal."),
33 |     developerNote: z.string().optional()
34 |       .describe("Developer implementation notes. Can be added to both PRs and commits to document important implementation details.")
35 |   },
36 |   handler: async (params, extra: RequestHandlerExtra) => {
37 |     try {
38 |       logger.info(`Update tool called for PR #${params.prIndex}, commit #${params.commitIndex}, status: ${params.status}`);
39 |       
40 |       if (!workPlan) {
41 |         logger.error('WorkPlan instance is not available');
42 |         return createErrorResponse('WorkPlan instance is not initialized');
43 |       }
44 |       
45 |       if (!workPlan.isInitialized()) {
46 |         logger.error('WorkPlan is not initialized');
47 |         return createErrorResponse('WorkPlan is not ready. Server initialization incomplete.');
48 |       }
49 |       
50 |       const updateParams: UpdateStatusInput = {
51 |         prIndex: params.prIndex,
52 |         commitIndex: params.commitIndex,
53 |         status: params.status,
54 |         goal: params.goal ? String(params.goal) : undefined,
55 |         developerNote: params.developerNote ? String(params.developerNote) : undefined
56 |       };
57 |       
58 |       const result = workPlan.updateStatus(updateParams);
59 |       
60 |       return {
61 |         content: result.content.map(item => ({
62 |           type: "text" as const,
63 |           text: item.text
64 |         })),
65 |         isError: result.isError
66 |       };
67 |     } catch (error) {
68 |       logger.logError('Error in UPDATE_STATUS tool', error);
69 |       return createErrorResponse(error);
70 |     }
71 |   }
72 | }; 
```

--------------------------------------------------------------------------------
/visualization/src/components/CommitNode.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useRef, useState } from 'react';
  2 | import { Handle, Position } from 'reactflow';
  3 | import { CommitStatus } from '../types';
  4 | 
  5 | export interface CommitNodeData {
  6 |   title: string;
  7 |   label?: string;
  8 |   status: CommitStatus;
  9 |   prIndex: number;
 10 |   commitIndex: number;
 11 |   developerNote?: string; // Developer implementation notes captured during refinement
 12 | }
 13 | 
 14 | const statusLabels: Record<CommitStatus, string> = {
 15 |   'not_started': 'Not Started',
 16 |   'in_progress': 'In Progress',
 17 |   'completed': 'Completed',
 18 |   'cancelled': 'Cancelled',
 19 |   'needsRefinment': 'Needs Refinement',
 20 |   'user_review': 'Awaiting User Review'
 21 | };
 22 | 
 23 | const statusIcons: Record<CommitStatus, string> = {
 24 |   'not_started': '⚪',
 25 |   'in_progress': '🔵',
 26 |   'completed': '✅',
 27 |   'cancelled': '❌',
 28 |   'needsRefinment': '🔄',
 29 |   'user_review': '👀'
 30 | };
 31 | 
 32 | const statusColors: Record<CommitStatus, string> = {
 33 |   'not_started': 'bg-gray-100 text-gray-600',
 34 |   'in_progress': 'bg-blue-100 text-blue-600',
 35 |   'completed': 'bg-green-100 text-green-600',
 36 |   'cancelled': 'bg-red-100 text-red-600',
 37 |   'needsRefinment': 'bg-purple-100 text-purple-600',
 38 |   'user_review': 'bg-violet-100 text-violet-600'
 39 | };
 40 | 
 41 | function CommitNode({ data }: { data: CommitNodeData }) {
 42 |   const nodeRef = useRef<HTMLDivElement>(null);
 43 |   const [showTooltip, setShowTooltip] = useState(false);
 44 | 
 45 |   const getStatusClass = (status: CommitStatus): string => {
 46 |     const baseClass = `status-badge ${status}`;
 47 |     
 48 |     // Additional style for completed commits
 49 |     if (status === 'completed') {
 50 |       return `${baseClass} completed-commit`;
 51 |     }
 52 |     
 53 |     return baseClass;
 54 |   };
 55 | 
 56 |   return (
 57 |     <div 
 58 |       ref={nodeRef}
 59 |       className={`commit-node transition-all relative py-3 px-4 rounded-md border shadow-sm ${
 60 |         data.status === 'completed' ? 'completed-node' : ''
 61 |       }`}
 62 |       aria-label={`Commit: ${data.title}, Status: ${statusLabels[data.status]}`}
 63 |       onMouseEnter={() => data.developerNote && setShowTooltip(true)}
 64 |       onMouseLeave={() => setShowTooltip(false)}
 65 |     >
 66 |       <Handle
 67 |         type="target"
 68 |         position={Position.Left}
 69 |         id="left"
 70 |         className="w-3 h-3 !bg-blue-500"
 71 |         aria-label="Left input point"
 72 |       />
 73 |       
 74 |       <Handle
 75 |         type="target"
 76 |         position={Position.Left}
 77 |         id="top"
 78 |         className="w-3 h-3 !bg-blue-500"
 79 |         aria-label="Top input point"
 80 |       />
 81 |       
 82 |       {/* Status Badge */}
 83 |       <div 
 84 |         className={`${getStatusClass(data.status)} inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mx-auto`}
 85 |         aria-label={`Status: ${statusLabels[data.status]}`}
 86 |       >
 87 |         <span className="mr-1" aria-hidden="true">{statusIcons[data.status]}</span>
 88 |         <span>{statusLabels[data.status]}</span>
 89 |       </div>
 90 |               {/* Goal (Title) */}
 91 |               <div 
 92 |         className="text-base font-medium break-words text-center mb-2"
 93 |         aria-label="Commit goal"
 94 |       >
 95 |         {data.title}
 96 |       </div>
 97 |       
 98 |       {/* Developer Note Tooltip */}
 99 |       {showTooltip && data.developerNote && (
100 |         <div className="absolute z-10 bg-white dark:bg-gray-800 p-3 rounded-md shadow-lg border border-gray-200 dark:border-gray-700 max-w-xs -translate-x-1/2 left-1/2 bottom-full mb-2 text-sm">
101 |           <div className="font-semibold mb-1 text-gray-700 dark:text-gray-300">Developer Note:</div>
102 |           <div className="text-gray-600 dark:text-gray-400">{data.developerNote}</div>
103 |           <div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-0 h-0 border-l-8 border-l-transparent border-r-8 border-r-transparent border-t-8 border-t-gray-200 dark:border-t-gray-700"></div>
104 |         </div>
105 |       )}
106 |       
107 |       <Handle
108 |         type="source"
109 |         position={Position.Right}
110 |         id="right"
111 |         className="w-3 h-3 !bg-blue-500"
112 |         aria-label="Right output point"
113 |       />
114 |       
115 |       <Handle
116 |         type="source"
117 |         position={Position.Bottom}
118 |         id="bottom"
119 |         className="w-3 h-3 !bg-blue-500"
120 |         aria-label="Bottom output point"
121 |       />
122 |     </div>
123 |   );
124 | }
125 | 
126 | export default CommitNode; 
```

--------------------------------------------------------------------------------
/visualization/src/assets/react.svg:
--------------------------------------------------------------------------------

```
1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
```

--------------------------------------------------------------------------------
/visualization/src/utils/exportUtils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WorkPlan, CommitStatus } from '../types';
  2 | 
  3 | // Status display name mapping
  4 | const STATUS_LABELS: Record<CommitStatus, string> = {
  5 |   'not_started': 'Not Started',
  6 |   'in_progress': 'In Progress',
  7 |   'completed': 'Completed',
  8 |   'cancelled': 'Cancelled',
  9 |   'needsRefinment': 'Needs Refinement',
 10 |   'user_review': 'Waiting for User Review'
 11 | };
 12 | 
 13 | // Status icon mapping
 14 | const STATUS_ICONS: Record<CommitStatus, string> = {
 15 |   'not_started': '⚪',
 16 |   'in_progress': '🔄',
 17 |   'completed': '✅',
 18 |   'cancelled': '❌',
 19 |   'needsRefinment': '⚠️',
 20 |   'user_review': '👀'
 21 | };
 22 | 
 23 | /**
 24 |  * Export WorkPlan as JSON
 25 |  * @param workplan The workplan to export
 26 |  * @returns Formatted JSON string
 27 |  */
 28 | export const exportToJSON = (workplan: WorkPlan): string => {
 29 |   return JSON.stringify(workplan, null, 2);
 30 | };
 31 | 
 32 | /**
 33 |  * Export WorkPlan as Markdown
 34 |  * @param workplan The workplan to export
 35 |  * @returns Markdown formatted string
 36 |  */
 37 | export const exportToMarkdown = (workplan: WorkPlan): string => {
 38 |   let markdown = `# ${workplan.goal}\n\n`;
 39 |   
 40 |   markdown += `Created: ${new Date().toLocaleString()}\n\n`;
 41 |   
 42 |   const totalPRs = workplan.prPlans.length;
 43 |   const completedPRs = workplan.prPlans.filter(pr => 
 44 |     pr.status === 'completed' || 
 45 |     pr.commitPlans.every(commit => commit.status === 'completed')
 46 |   ).length;
 47 |   
 48 |   const totalCommits = workplan.prPlans.reduce((total, pr) => total + pr.commitPlans.length, 0);
 49 |   const completedCommits = workplan.prPlans.reduce((total, pr) => 
 50 |     total + pr.commitPlans.filter(commit => commit.status === 'completed').length, 
 51 |   0);
 52 |   
 53 |   markdown += `## Progress Summary\n\n`;
 54 |   markdown += `- PR Progress: ${completedPRs}/${totalPRs} (${Math.round(completedPRs / totalPRs * 100)}%)\n`;
 55 |   markdown += `- Commit Progress: ${completedCommits}/${totalCommits} (${Math.round(completedCommits / totalCommits * 100)}%)\n\n`;
 56 |   
 57 |   workplan.prPlans.forEach((pr, prIndex) => {
 58 |     const prStatus = pr.status || 
 59 |       (pr.commitPlans.every(commit => commit.status === 'completed') ? 'completed' : 
 60 |       pr.commitPlans.some(commit => commit.status === 'in_progress') ? 'in_progress' : 'not_started');
 61 |     
 62 |     const prIcon = STATUS_ICONS[prStatus as CommitStatus] || '';
 63 |     
 64 |     markdown += `## PR ${prIndex + 1}: ${pr.goal} ${prIcon}\n\n`;
 65 |     
 66 |     pr.commitPlans.forEach((commit, commitIndex) => {
 67 |       const status = commit.status || 'not_started';
 68 |       const statusIcon = STATUS_ICONS[status];
 69 |       const statusLabel = STATUS_LABELS[status];
 70 |       
 71 |       markdown += `### Commit ${commitIndex + 1}: ${statusIcon} ${commit.goal}\n`;
 72 |       markdown += `Status: ${statusLabel}\n\n`;
 73 |     });
 74 |     
 75 |     markdown += '\n';
 76 |   });
 77 |   
 78 |   return markdown;
 79 | };
 80 | 
 81 | /**
 82 |  * Export WorkPlan as CSV
 83 |  * @param workplan The workplan to export
 84 |  * @returns CSV formatted string
 85 |  */
 86 | export const exportToCSV = (workplan: WorkPlan): string => {
 87 |   let csv = 'PR Number,PR Goal,PR Status,Commit Number,Commit Goal,Commit Status\n';
 88 |   
 89 |   workplan.prPlans.forEach((pr, prIndex) => {
 90 |     const prStatus = pr.status || 
 91 |       (pr.commitPlans.every(commit => commit.status === 'completed') ? 'completed' : 
 92 |       pr.commitPlans.some(commit => commit.status === 'in_progress') ? 'in_progress' : 'not_started');
 93 |     
 94 |     pr.commitPlans.forEach((commit, commitIndex) => {
 95 |       const status = commit.status || 'not_started';
 96 |       const statusLabel = STATUS_LABELS[status];
 97 |       
 98 |       const escapeCsv = (text: string) => `"${text.replace(/"/g, '""')}"`;
 99 |       
100 |       csv += `${prIndex + 1},${escapeCsv(pr.goal)},${STATUS_LABELS[prStatus as CommitStatus] || ''},`;
101 |       csv += `${commitIndex + 1},${escapeCsv(commit.goal)},${statusLabel}\n`;
102 |     });
103 |   });
104 |   
105 |   return csv;
106 | };
107 | 
108 | /**
109 |  * Download file to the user's device
110 |  * @param content File content
111 |  * @param fileName File name
112 |  * @param contentType Content type
113 |  */
114 | export const downloadFile = (content: string, fileName: string, contentType: string): void => {
115 |   const a = document.createElement('a');
116 |   const file = new Blob([content], { type: contentType });
117 |   
118 |   a.href = URL.createObjectURL(file);
119 |   a.download = fileName;
120 |   a.click();
121 |   
122 |   URL.revokeObjectURL(a.href);
123 | };
124 | 
125 | /**
126 |  * Download WorkPlan as JSON
127 |  * @param workplan The workplan to export
128 |  * @param fileName File name (default: workplan.json)
129 |  */
130 | export const downloadAsJSON = (workplan: WorkPlan, fileName: string = 'workplan.json'): void => {
131 |   const json = exportToJSON(workplan);
132 |   downloadFile(json, fileName, 'application/json');
133 | };
134 | 
135 | /**
136 |  * Download WorkPlan as Markdown
137 |  * @param workplan The workplan to export
138 |  * @param fileName File name (default: workplan.md)
139 |  */
140 | export const downloadAsMarkdown = (workplan: WorkPlan, fileName: string = 'workplan.md'): void => {
141 |   const markdown = exportToMarkdown(workplan);
142 |   downloadFile(markdown, fileName, 'text/markdown');
143 | };
144 | 
145 | /**
146 |  * Download WorkPlan as CSV
147 |  * @param workplan The workplan to export
148 |  * @param fileName File name (default: workplan.csv)
149 |  */
150 | export const downloadAsCSV = (workplan: WorkPlan, fileName: string = 'workplan.csv'): void => {
151 |   const csv = exportToCSV(workplan);
152 |   downloadFile(csv, fileName, 'text/csv');
153 | }; 
```

--------------------------------------------------------------------------------
/src/prompts.ts:
--------------------------------------------------------------------------------

```typescript
  1 | interface RequestExtra {
  2 |   readonly context?: {
  3 |     [key: string]: unknown;
  4 |   };
  5 |   readonly [key: string]: unknown;
  6 | }
  7 | 
  8 | // hope that the cursor supports the prompt as an mcp client
  9 | // alternative, you can include this in your .mdc
 10 | export const taskPlanningGuide = {
 11 |   name: "task-planning-guide",
 12 |   description: "A comprehensive guide for planning development tasks with minimal PRs and commits. Helps structure work before using the plan tool.",
 13 |   handler: () => ({
 14 |     description: "Task planning guide for minimal PRs and commits",
 15 |     messages: [
 16 |       {
 17 |         role: "user" as const,
 18 |         content: {
 19 |           type: "text" as const,
 20 |           text: `
 21 | # Task Planning Guide
 22 | 
 23 | ## Purpose
 24 | - To design a structured implementation approach before any coding begins
 25 | - To ensure clear understanding of requirements and dependencies
 26 | - To divide tickets into small PRs and commits for easier review and integration
 27 | 
 28 | ## Output
 29 | - A comprehensive work plan organized by PRs and commits
 30 | - Each PR should be small, functional, and independently reviewable
 31 | - Each commit should represent an atomic change within its PR
 32 | - To minimize dependencies between PRs, ideally making them independently implementable
 33 | 
 34 | ## Method
 35 | ### Analysis Phase
 36 | - Analyze the task's architecture requirements thoroughly
 37 | - Explore and read all potentially relevant existing code and similar reference code
 38 | - Develop a meta architecture plan for the solution
 39 | 
 40 | ### Planning Approach
 41 | - Break down the task into smallest possible PRs
 42 | - Order PRs logically (e.g., setup → core logic → tests)
 43 | - Define 2-4 minimal atomic commits for each PR
 44 | - Prefer splitting tasks into PRs rather than large commits
 45 | 
 46 | ### Implementation Criteria
 47 | - Only proceed after achieving 100% clarity and confidence
 48 | - Ensure all affected parts of the codebase have been fully identified through dependency tracing
 49 | - **The work plan must be approved by the user before implementation**
 50 | 
 51 | ## Prohibited Actions
 52 | - **Any implementation or code writing, even "example code"**
 53 |           `
 54 |         }
 55 |       }
 56 |     ]
 57 |   })
 58 | };
 59 | 
 60 | 
 61 | // internal prompt
 62 | export const progressInstructionGuide = {
 63 |   name: "progressInstructionGuide",
 64 |   description: "Based on this progress report, analyze the current state and suggest the next commit to work on.",
 65 |   text: `
 66 |   Based on this progress report, analyze the current state and suggest the next commit to work on.
 67 |   **Next, you must strictly follow these procedures without exception**:
 68 | 1. Before starting your next commit, obtain the user's approval.  
 69 | 2. Once approved, implement the code and test scripts strictly within this commit's scope.  
 70 |    - Make sure there are no build errors or test failures.  
 71 |    - If a UI is involved, verify its behavior using mcp playwright.  
 72 | 3. After completing the commit:  
 73 |    - Review and evaluate the implementation, and conduct a self-feedback cycle.  
 74 |    - Request feedback from the user.
 75 | 
 76 |   **User Review State Procedures**:
 77 |   When setting a task status to "user_review":
 78 |   1. Create a comprehensive review request message that includes:
 79 |      - A clear summary of the implementation derived from PR/commit goals and developer notes
 80 |      - Specific changes made and their intended functionality
 81 |      - Areas that particularly need user verification
 82 |      - Clear instructions for the user to approve (change status to "completed") or request changes
 83 |   2. Format the review request message in a structured way with clear section headings
 84 |   3. Remember that tasks cannot transition directly from other states to "completed" - they must go through "user_review" first
 85 |   4. Only users can transition tasks from "user_review" to "completed" after their review
 86 | 
 87 |   **Always secure the user's agreement before starting the next task.**
 88 | `};
 89 | 
 90 | // alternative, you can include this in your .mdc
 91 | export const updateTaskStatusRule= {
 92 |   name: "update-task-status-rule",
 93 |   description: "Always When updating the status of a task",
 94 |   text: `
 95 |     **STRICT RULES - MUST BE FOLLOWED WITHOUT EXCEPTION:**
 96 | 
 97 |     in_progress → user_review conditions:
 98 |     ✅ No compilation errors exist
 99 |     ✅ Necessary tests have been added and all pass
100 |     ✅ Required documentation updates are completed
101 | 
102 |     needsRefinment → in_progress conditions:
103 |     ✅ Requirements have been sufficiently clarified and ready for implementation
104 |     ✅ All necessary information for implementation is available
105 |     ✅ The scope of the task is clearly defined
106 | 
107 |     user_review → in_progress conditions:
108 |     ✅ From feedback, the content to be modified is completely clear
109 | 
110 |     * → needsRefinment conditions:
111 |     ✅ It becomes apparent that the task requirements are unclear or incomplete
112 | 
113 |     * → cancelled conditions:
114 |     ✅ There is a clear reason why the task is no longer needed, or alternative methods or solutions to meet the requirements are clear
115 |     ✅ The impact of cancellation on other related tasks has been evaluated
116 | `};
117 | 
118 | // alternative, you can include this in your .mdc
119 | export const solutionExplorationGuide= {
120 |   name: "solution-exploration-guide",
121 |   description: "When examining how to implement an issue",
122 |   text: `
123 |     ## Purpose
124 |     - nformation gathering and Brainstorming potential approaches
125 | 
126 |     ## Forbidden: 
127 |     - Concrete planning, implementation details, or any code writing
128 | 
129 |     ##output-format
130 |     - file: ticket_[issue name].md
131 |     - chapters:
132 |         - Summary of the issue that will be resolved with the ticket
133 |         - Overview of the architecture, domain model and data model to be changed or added this time
134 |         - Work plan based on PR and commitment units(This is only for creating chapters, do not write content)
135 | 
136 |     ## Method
137 |     - Understand the issue from the given information and existing code
138 |     - research and interviews with users to ensure that we have all the information we need to complete the task
139 |     - Analyze potential impacts to the existing codebase
140 | `};
```

--------------------------------------------------------------------------------
/src/tools/plan/toolDefs.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from 'zod';
  2 | import { Tool, createErrorResponse, PlanTaskInput } from '../common.js';
  3 | import { workPlan } from '../../index.js';
  4 | import logger from '../../utils/logger.js';
  5 | import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
  6 | 
  7 | // Improved error messages with guidance
  8 | const goalLengthErrorMessage = "Goal must be at most 60 characters. Consider moving detailed information to the developerNote field.";
  9 | const developerNoteLengthErrorMessage = "Developer note must be at most 300 characters. Consider breaking it into smaller, focused notes.";
 10 | 
 11 | export const PLAN_TOOL: Tool<{
 12 |   goal: z.ZodString;
 13 |   prPlans: z.ZodArray<z.ZodObject<{
 14 |     goal: z.ZodString;
 15 |     commitPlans: z.ZodArray<z.ZodObject<{
 16 |       goal: z.ZodString;
 17 |       developerNote?: z.ZodOptional<z.ZodString>;
 18 |     }>>;
 19 |     developerNote?: z.ZodOptional<z.ZodString>;
 20 |   }>>;
 21 |   needsMoreThoughts: z.ZodOptional<z.ZodBoolean>;
 22 | }> = {
 23 |   name: "plan",
 24 |   description: `
 25 |     A tool for managing a development work plan for a ticket, organized by PRs and commits.
 26 |     Register or update the whole work plan for the current ticket you assigned to.
 27 | 
 28 |     Before using this tool, you **MUST**:
 29 |     - Understand requirements, goals, and specifications.
 30 |     - Clarify the task scope and break it down into a hierarchy of PRs and commit plans.
 31 |     - Analyze existing codebase and impact area.
 32 |     - Include developer notes to document implementation considerations discovered during refinement.
 33 |     
 34 |     Make sure PR and commit goals are clear enough to stand alone without their developer notes
 35 |   `,
 36 |   schema: {
 37 |     goal: z.string().min(1, "Goal must be a non-empty string").max(60, goalLengthErrorMessage)
 38 |       .describe("High-level description of what this ticket aims to achieve. Should be concise and focus on the overall outcome or deliverable of the entire task."),
 39 |     prPlans: z.array(z.object({
 40 |       goal: z.string().min(1, "PR goal must be a non-empty string").max(60, goalLengthErrorMessage)
 41 |         .describe("Description of what this PR aims to accomplish. Should be written as a PR title would be - concise and focused on WHAT will be delivered."),
 42 |       commitPlans: z.array(z.object({
 43 |         goal: z.string().min(1, "Commit goal must be a non-empty string").max(60, goalLengthErrorMessage)
 44 |           .describe("Description of what this commit aims to accomplish. Should match the format of an actual commit message - concise and specific to a single change."),
 45 |         developerNote: z.string().max(300, developerNoteLengthErrorMessage).optional()
 46 |           .describe("Developer implementation notes for this commit. Use this field for detailed HOW information and implementation considerations discovered during refinement.")
 47 |       })).min(1, "At least one commit plan is required").describe("Array of commit plans for this PR. Each commit should be small and atomic."),
 48 |       developerNote: z.string().max(300, developerNoteLengthErrorMessage).optional()
 49 |         .describe("Developer implementation notes for this PR. Use this field for detailed implementation considerations and technical context discovered during refinement.")
 50 |     })).min(1, "At least one PR plan is required").describe("Array of PR plans. Each PR represents a logical unit of work in the implementation plan."),
 51 |     needsMoreThoughts: z.boolean().optional().describe("Whether this plan might need further refinement. Set to true if you think the plan may need changes.")
 52 |   },
 53 |   handler: async (params, extra: RequestHandlerExtra) => {
 54 |     try {
 55 |       logger.info(`Plan tool called with goal: ${params.goal}, PRs: ${params.prPlans.length}`);
 56 |       
 57 |       if (!workPlan) {
 58 |         logger.error('WorkPlan instance is not available');
 59 |         return createErrorResponse('WorkPlan instance is not initialized', 'plan');
 60 |       }
 61 |       
 62 |       if (!workPlan.isInitialized()) {
 63 |         logger.error('WorkPlan is not initialized');
 64 |         return createErrorResponse('WorkPlan is not ready. Server initialization incomplete.', 'plan');
 65 |       }
 66 |       
 67 |       const planParams: PlanTaskInput = {
 68 |         goal: params.goal,
 69 |         prPlans: params.prPlans.map(pr => ({
 70 |           goal: pr.goal,
 71 |           commitPlans: pr.commitPlans.map(commit => ({
 72 |             goal: commit.goal,
 73 |             developerNote: commit.developerNote ? String(commit.developerNote) : undefined
 74 |           })),
 75 |           developerNote: pr.developerNote ? String(pr.developerNote) : undefined
 76 |         })),
 77 |         needsMoreThoughts: params.needsMoreThoughts
 78 |       };
 79 |       
 80 |       const result = workPlan.plan(planParams);
 81 |       
 82 |       return {
 83 |         content: result.content.map(item => ({
 84 |           type: "text" as const,
 85 |           text: item.text
 86 |         })),
 87 |         isError: result.isError
 88 |       };
 89 |     } catch (error) {
 90 |       // Enhance error handling with guidance
 91 |       const errorMessage = error instanceof Error ? error.message : String(error);
 92 |       
 93 |       // Detect specific validation errors and enhance with guidance
 94 |       let enhancedError = errorMessage;
 95 |       if (errorMessage.includes("too_big") && errorMessage.includes("goal")) {
 96 |         enhancedError = `Validation Error: Goal field exceeds maximum length of 60 characters. 
 97 | Please move implementation details to the developerNote field and keep goal fields concise.
 98 | - Goal fields should describe WHAT will be done (the end result)
 99 | - Developer notes should explain HOW it will be done (implementation details)`;
100 |       } else if (errorMessage.includes("too_big") && errorMessage.includes("developerNote")) {
101 |         enhancedError = `Validation Error: Developer note exceeds maximum length of 300 characters.
102 | Please break down the information into smaller, more focused notes or consider creating separate commits for different aspects.`;
103 |       }
104 |       
105 |       logger.logError(`Error in PLAN tool`, error);
106 |       return createErrorResponse(enhancedError, 'plan');
107 |     }
108 |   }
109 | }; 
```

--------------------------------------------------------------------------------
/visualization/src/index.css:
--------------------------------------------------------------------------------

```css
  1 | @tailwind base;
  2 | @tailwind components;
  3 | @tailwind utilities;
  4 | 
  5 | :root {
  6 |   font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  7 |   line-height: 1.5;
  8 |   font-weight: 400;
  9 | 
 10 |   /* ライトモード(デフォルト)の変数 */
 11 |   --background-color: #f8f8f8;
 12 |   --text-color: #1a1a1a;
 13 |   --node-bg: #ffffff;
 14 |   --node-border: #e2e8f0;
 15 |   --node-selected: #d4e6ff;
 16 |   --node-hover: #f0f7ff;
 17 |   --pr-node-bg: #ebf4ff;
 18 |   --commit-node-bg: #fff;
 19 |   --start-node-bg: #e6fffa;
 20 |   --edge-color: #64748b;
 21 |   --panel-bg: #ffffff;
 22 |   --panel-shadow: rgba(0, 0, 0, 0.15);
 23 |   --filter-button-bg: #f1f5f9;
 24 |   --filter-button-hover: #e2e8f0;
 25 |   --status-not-started-bg: #f1f5f9;
 26 |   --status-in-progress-bg: #eff6ff;
 27 |   --status-completed-bg: #f0fdf4;
 28 |   --status-cancelled-bg: #fef2f2;
 29 |   --status-blocked-bg: #fff7ed;
 30 |   --status-needs-refinement-bg: #fdf4ff;
 31 |   --status-user-review-bg: #f3e8ff;
 32 |   --input-bg: #ffffff;
 33 |   --input-border: #e2e8f0;
 34 |   --input-focus-border: #3b82f6;
 35 |   --button-primary-bg: #3b82f6;
 36 |   --button-primary-hover: #2563eb;
 37 |   --button-secondary-bg: #64748b;
 38 |   --button-secondary-hover: #475569;
 39 |   --button-danger-bg: #ef4444;
 40 |   --button-danger-hover: #dc2626;
 41 | }
 42 | 
 43 | /* ダークモードの変数 */
 44 | .dark-mode {
 45 |   --background-color: #1a1a1a;
 46 |   --text-color: #f0f0f0;
 47 |   --node-bg: #2d3748;
 48 |   --node-border: #4a5568;
 49 |   --node-selected: #3b4858;
 50 |   --node-hover: #4a5568;
 51 |   --pr-node-bg: #2c3e50;
 52 |   --commit-node-bg: #2d3748;
 53 |   --edge-color: #a0aec0;
 54 |   --panel-bg: #2d3748;
 55 |   --panel-shadow: rgba(0, 0, 0, 0.3);
 56 |   --filter-button-bg: #4a5568;
 57 |   --filter-button-hover: #2d3748;
 58 |   --status-not-started-bg: #374151;
 59 |   --status-in-progress-bg: #1e3a8a;
 60 |   --status-completed-bg: #065f46;
 61 |   --status-cancelled-bg: #7f1d1d;
 62 |   --status-blocked-bg: #783c00;
 63 |   --status-needs-refinement-bg: #701a75;
 64 |   --status-user-review-bg: #701a75;
 65 |   --input-bg: #4a5568;
 66 |   --input-border: #64748b;
 67 |   --input-focus-border: #60a5fa;
 68 |   --button-primary-bg: #3b82f6;
 69 |   --button-primary-hover: #1d4ed8;
 70 |   --button-secondary-bg: #64748b;
 71 |   --button-secondary-hover: #475569;
 72 |   --button-danger-bg: #ef4444;
 73 |   --button-danger-hover: #b91c1c;
 74 | }
 75 | 
 76 | body {
 77 |   margin: 0;
 78 |   display: flex;
 79 |   place-items: center;
 80 |   min-width: 320px;
 81 |   min-height: 100vh;
 82 |   background-color: var(--background-color);
 83 |   color: var(--text-color);
 84 |   transition: color 0.2s, background-color 0.2s;
 85 | }
 86 | 
 87 | h1 {
 88 |   font-size: 3.2em;
 89 |   line-height: 1.1;
 90 | }
 91 | 
 92 | button {
 93 |   border-radius: 8px;
 94 |   border: 1px solid transparent;
 95 |   padding: 0.6em 1.2em;
 96 |   font-size: 1em;
 97 |   font-weight: 500;
 98 |   font-family: inherit;
 99 |   background-color: #1a1a1a;
100 |   cursor: pointer;
101 |   transition: border-color 0.25s;
102 | }
103 | button:hover {
104 |   border-color: #646cff;
105 | }
106 | button:focus,
107 | button:focus-visible {
108 |   outline: 4px auto -webkit-focus-ring-color;
109 | }
110 | 
111 | @media (prefers-color-scheme: light) {
112 |   :root {
113 |     color: #213547;
114 |     background-color: #ffffff;
115 |   }
116 |   a:hover {
117 |     color: #747bff;
118 |   }
119 |   button {
120 |     background-color: #f9f9f9;
121 |   }
122 | }
123 | 
124 | /* 全幅表示用のスタイル */
125 | #root {
126 |   width: 100%;
127 |   height: 100vh;
128 |   margin: 0;
129 |   padding: 0;
130 |   display: flex;
131 |   flex-direction: column;
132 | }
133 | 
134 | /* ReactFlowのカスタマイズ */
135 | 
136 | 
137 | 
138 | .animate-pulse {
139 |   animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
140 | }
141 | 
142 | /* ReactFlow ダークモード対応 */
143 | .dark-mode .react-flow__node {
144 |   background-color: var(--node-bg);
145 |   color: var(--text-color);
146 |   border-color: var(--node-border);
147 | }
148 | 
149 | .dark-mode .react-flow__edge-path {
150 |   stroke: var(--edge-color);
151 | }
152 | 
153 | .dark-mode .react-flow__controls {
154 |   background-color: var(--panel-bg);
155 |   box-shadow: 0 0 2px 1px var(--panel-shadow);
156 | }
157 | 
158 | .dark-mode .react-flow__controls-button {
159 |   background-color: var(--filter-button-bg);
160 |   color: var(--text-color);
161 |   border-color: var(--node-border);
162 | }
163 | 
164 | .dark-mode .react-flow__controls-button:hover {
165 |   background-color: var(--filter-button-hover);
166 | }
167 | 
168 | .dark-mode .react-flow__background {
169 |   background-color: var(--background-color);
170 | }
171 | 
172 | .dark-mode .react-flow__attribution {
173 |   background-color: var(--panel-bg);
174 |   color: var(--text-color);
175 | }
176 | 
177 | .dark-mode .react-flow__handle {
178 |   background-color: var(--button-primary-bg);
179 |   border-color: var(--node-border);
180 | }
181 | 
182 | .dark-mode .react-flow__edge-text {
183 |   fill: var(--text-color);
184 | }
185 | 
186 | .dark-mode .react-flow__minimap {
187 |   background-color: var(--panel-bg);
188 | }
189 | 
190 | .dark-mode .react-flow__minimap-mask {
191 |   fill: var(--panel-bg);
192 | }
193 | 
194 | .dark-mode .react-flow__minimap-node {
195 |   fill: var(--node-bg);
196 |   stroke: var(--node-border);
197 | }
198 | 
199 | /* コンポーネントのダークモード対応 */
200 | .dark-mode button {
201 |   background-color: var(--button-secondary-bg);
202 |   color: white;
203 | }
204 | 
205 | .dark-mode button:hover {
206 |   background-color: var(--button-secondary-hover);
207 | }
208 | 
209 | .dark-mode input, .dark-mode select, .dark-mode textarea {
210 |   background-color: var(--input-bg);
211 |   color: var(--text-color);
212 |   border-color: var(--input-border);
213 | }
214 | 
215 | .dark-mode input::placeholder, .dark-mode textarea::placeholder {
216 |   color: #9ca3af;
217 | }
218 | 
219 | .dark-mode input:focus, .dark-mode select:focus, .dark-mode textarea:focus {
220 |   border-color: var(--input-focus-border);
221 |   outline: none;
222 |   box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
223 | }
224 | 
225 | /* 特定のコンポーネントスタイル */
226 | .dark-mode .filter-panel {
227 |   background-color: var(--panel-bg);
228 |   box-shadow: 0 4px 6px var(--panel-shadow);
229 | }
230 | 
231 | .dark-mode .export-panel {
232 |   background-color: var(--panel-bg);
233 |   box-shadow: 0 4px 6px var(--panel-shadow);
234 | }
235 | 
236 | /* Node 特定のスタイル */
237 | .dark-mode .start-node {
238 |   background-color: var(--start-node-bg);
239 | }
240 | 
241 | .dark-mode .pr-node {
242 |   background-color: var(--pr-node-bg);
243 | }
244 | 
245 | .dark-mode .commit-node {
246 |   background-color: var(--commit-node-bg);
247 | }
248 | 
249 | /* ステータスラベルのダークモード対応 */
250 | .dark-mode .status-badge.not_started {
251 |   background-color: var(--status-not-started-bg);
252 | }
253 | 
254 | .dark-mode .status-badge.in_progress {
255 |   background-color: var(--status-in-progress-bg);
256 | }
257 | 
258 | .dark-mode .status-badge.completed {
259 |   background-color: var(--status-completed-bg);
260 | }
261 | 
262 | .dark-mode .status-badge.cancelled {
263 |   background-color: var(--status-cancelled-bg);
264 | }
265 | 
266 | .dark-mode .status-badge.blocked {
267 |   background-color: var(--status-blocked-bg);
268 | }
269 | 
270 | .dark-mode .status-badge.needsRefinment {
271 |   background-color: var(--status-needs-refinement-bg);
272 | }
273 | 
274 | .dark-mode .status-badge.user_review {
275 |   background-color: var(--status-user-review-bg);
276 | }
277 | 
278 | /* 一般的なトランジション */
279 | .transition-all {
280 |   transition: all 0.2s ease;
281 | }
282 | 
283 | /* アクセシビリティ */
284 | .visually-hidden {
285 |   position: absolute;
286 |   width: 1px;
287 |   height: 1px;
288 |   padding: 0;
289 |   margin: -1px;
290 |   overflow: hidden;
291 |   clip: rect(0, 0, 0, 0);
292 |   border: 0;
293 | }
294 | 
295 | /* フォーカス可視性 */
296 | :focus-visible {
297 |   outline: 2px solid var(--input-focus-border);
298 |   outline-offset: 2px;
299 | }
300 | 
301 | /* ダークモードでのスクロールバーカスタマイズ */
302 | .dark-mode::-webkit-scrollbar {
303 |   width: 8px;
304 |   height: 8px;
305 | }
306 | 
307 | .dark-mode::-webkit-scrollbar-track {
308 |   background: #2d3748;
309 | }
310 | 
311 | .dark-mode::-webkit-scrollbar-thumb {
312 |   background-color: #4a5568;
313 |   border-radius: 4px;
314 | }
315 | 
316 | .dark-mode::-webkit-scrollbar-thumb:hover {
317 |   background-color: #a0aec0;
318 | }
319 | 
320 | /* レスポンシブメディアクエリ */
321 | @media (max-width: 768px) {
322 |   button, input, select {
323 |     font-size: 14px;
324 |   }
325 |   
326 |   .dark-mode .react-flow__controls {
327 |     transform: scale(0.8);
328 |     transform-origin: bottom right;
329 |   }
330 | }
331 | 
332 | /* ステータスバッジのデフォルトスタイル - ライトモード */
333 | .status-badge.not_started {
334 |   background-color: var(--status-not-started-bg);
335 | }
336 | 
337 | .status-badge.in_progress {
338 |   background-color: var(--status-in-progress-bg);
339 | }
340 | 
341 | .status-badge.completed {
342 |   background-color: var(--status-completed-bg);
343 | }
344 | 
345 | .status-badge.cancelled {
346 |   background-color: var(--status-cancelled-bg);
347 | }
348 | 
349 | .status-badge.blocked {
350 |   background-color: var(--status-blocked-bg);
351 | }
352 | 
353 | .status-badge.needsRefinment {
354 |   background-color: var(--status-needs-refinement-bg);
355 | }
356 | 
357 | .status-badge.user_review {
358 |   background-color: var(--status-user-review-bg);
359 | }
360 | 
361 | /* ステータスバッジのダークモードスタイル */
362 | .dark-mode .status-badge.not_started {
363 |   background-color: var(--status-not-started-bg);
364 | }
365 | 
```

--------------------------------------------------------------------------------
/visualization/src/components/ExportPanel.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useRef, useEffect } from 'react';
  2 | import { WorkPlan } from '../types';
  3 | import { 
  4 |   downloadAsJSON, 
  5 |   downloadAsMarkdown, 
  6 |   downloadAsCSV 
  7 | } from '../utils/exportUtils';
  8 | 
  9 | interface ExportPanelProps {
 10 |   workplan: WorkPlan;
 11 |   isOpen: boolean;
 12 |   onClose: () => void;
 13 | }
 14 | 
 15 | const ExportPanel: React.FC<ExportPanelProps> = ({ 
 16 |   workplan, 
 17 |   isOpen,
 18 |   onClose
 19 | }) => {
 20 |   const [fileName, setFileName] = useState('workplan');
 21 |   const [isDownloading, setIsDownloading] = useState<string | null>(null);
 22 |   const panelRef = useRef<HTMLDivElement>(null);
 23 |   
 24 |   // Close when clicking outside the panel
 25 |   useEffect(() => {
 26 |     const handleOutsideClick = (event: MouseEvent) => {
 27 |       if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
 28 |         onClose();
 29 |       }
 30 |     };
 31 |     
 32 |     if (isOpen) {
 33 |       document.addEventListener('mousedown', handleOutsideClick);
 34 |     }
 35 |     
 36 |     return () => {
 37 |       document.removeEventListener('mousedown', handleOutsideClick);
 38 |     };
 39 |   }, [isOpen, onClose]);
 40 |   
 41 |   // Close panel with Esc key
 42 |   useEffect(() => {
 43 |     const handleKeyDown = (event: KeyboardEvent) => {
 44 |       if (event.key === 'Escape') {
 45 |         onClose();
 46 |       }
 47 |     };
 48 |     
 49 |     if (isOpen) {
 50 |       document.addEventListener('keydown', handleKeyDown);
 51 |     }
 52 |     
 53 |     return () => {
 54 |       document.removeEventListener('keydown', handleKeyDown);
 55 |     };
 56 |   }, [isOpen, onClose]);
 57 |   
 58 |   if (!isOpen) return null;
 59 |   
 60 |   // Common download handler
 61 |   const handleDownload = async (format: string, downloadFn: () => void) => {
 62 |     setIsDownloading(format);
 63 |     try {
 64 |       // Add a slight delay to provide user feedback
 65 |       await new Promise(resolve => setTimeout(resolve, 300));
 66 |       downloadFn();
 67 |     } finally {
 68 |       setTimeout(() => {
 69 |         setIsDownloading(null);
 70 |       }, 500);
 71 |     }
 72 |   };
 73 |   
 74 |   // Export as JSON format
 75 |   const handleJSONExport = () => {
 76 |     handleDownload('json', () => downloadAsJSON(workplan, `${fileName}.json`));
 77 |   };
 78 |   
 79 |   // Export as Markdown format
 80 |   const handleMarkdownExport = () => {
 81 |     handleDownload('markdown', () => downloadAsMarkdown(workplan, `${fileName}.md`));
 82 |   };
 83 |   
 84 |   // Export as CSV format
 85 |   const handleCSVExport = () => {
 86 |     handleDownload('csv', () => downloadAsCSV(workplan, `${fileName}.csv`));
 87 |   };
 88 |   
 89 |   return (
 90 |     <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 export-panel-container p-4">
 91 |       <div 
 92 |         ref={panelRef}
 93 |         className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md transform transition-all duration-300"
 94 |         role="dialog"
 95 |         aria-labelledby="export-panel-title"
 96 |         aria-modal="true"
 97 |       >
 98 |         <div className="flex justify-between items-center mb-4 border-b pb-3">
 99 |           <h2 
100 |             id="export-panel-title" 
101 |             className="text-xl font-bold text-gray-800 flex items-center"
102 |           >
103 |             <span className="mr-2">📊</span>
104 |             Export
105 |           </h2>
106 |           <button 
107 |             onClick={onClose}
108 |             className="text-gray-500 hover:text-gray-700 text-xl transition-colors p-1 rounded-full hover:bg-gray-100"
109 |             aria-label="Close"
110 |           >
111 |             &times;
112 |           </button>
113 |         </div>
114 |         
115 |         <div className="mb-4">
116 |           <label 
117 |             htmlFor="fileName" 
118 |             className="block text-sm font-medium text-gray-700 mb-1"
119 |           >
120 |             Filename
121 |           </label>
122 |           <div className="relative">
123 |             <input
124 |               id="fileName"
125 |               type="text"
126 |               value={fileName}
127 |               onChange={(e) => setFileName(e.target.value)}
128 |               className="w-full p-2 pl-8 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
129 |               placeholder="Enter filename"
130 |               aria-describedby="filename-desc"
131 |             />
132 |             <div className="absolute inset-y-0 left-0 flex items-center pl-2 pointer-events-none">
133 |               <svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
134 |                 <path fillRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clipRule="evenodd" />
135 |               </svg>
136 |             </div>
137 |           </div>
138 |           <p id="filename-desc" className="text-xs text-gray-500 mt-1">
139 |             *File extension will be added automatically
140 |           </p>
141 |         </div>
142 |         
143 |         <div className="grid grid-cols-1 gap-3 mb-5">
144 |           <button
145 |             onClick={handleJSONExport}
146 |             className={`flex items-center justify-center px-4 py-3 bg-blue-50 hover:bg-blue-100 text-blue-700 font-medium rounded-md border border-blue-200 transition-all ${isDownloading === 'json' ? 'opacity-75 pointer-events-none' : ''}`}
147 |             disabled={isDownloading !== null}
148 |           >
149 |             {isDownloading === 'json' ? (
150 |               <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-blue-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
151 |                 <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
152 |                 <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
153 |               </svg>
154 |             ) : (
155 |               <span className="mr-2">📄</span>
156 |             )}
157 |             Export as JSON
158 |           </button>
159 |           
160 |           <button
161 |             onClick={handleMarkdownExport}
162 |             className={`flex items-center justify-center px-4 py-3 bg-purple-50 hover:bg-purple-100 text-purple-700 font-medium rounded-md border border-purple-200 transition-all ${isDownloading === 'markdown' ? 'opacity-75 pointer-events-none' : ''}`}
163 |             disabled={isDownloading !== null}
164 |           >
165 |             {isDownloading === 'markdown' ? (
166 |               <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-purple-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
167 |                 <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
168 |                 <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
169 |               </svg>
170 |             ) : (
171 |               <span className="mr-2">📝</span>
172 |             )}
173 |             Export as Markdown
174 |           </button>
175 |           
176 |           <button
177 |             onClick={handleCSVExport}
178 |             className={`flex items-center justify-center px-4 py-3 bg-green-50 hover:bg-green-100 text-green-700 font-medium rounded-md border border-green-200 transition-all ${isDownloading === 'csv' ? 'opacity-75 pointer-events-none' : ''}`}
179 |             disabled={isDownloading !== null}
180 |           >
181 |             {isDownloading === 'csv' ? (
182 |               <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-green-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
183 |                 <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
184 |                 <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
185 |               </svg>
186 |             ) : (
187 |               <span className="mr-2">📊</span>
188 |             )}
189 |             Export as CSV
190 |           </button>
191 |         </div>
192 |         
193 |         <div className="bg-gray-50 p-3 rounded-md border border-gray-100">
194 |           <h3 className="text-sm font-medium text-gray-700 mb-2">Export Format Explanation:</h3>
195 |           <ul className="list-disc pl-5 space-y-1.5 text-xs text-gray-600">
196 |             <li>
197 |               <span className="font-medium text-blue-600">JSON:</span> Original data format. Ideal for reuse or import in other applications.
198 |             </li>
199 |             <li>
200 |               <span className="font-medium text-purple-600">Markdown:</span> Readable document format. Can be displayed in GitHub and similar platforms.
201 |             </li>
202 |             <li>
203 |               <span className="font-medium text-green-600">CSV:</span> Format for spreadsheet software. Suitable for analysis in Excel and similar tools.
204 |             </li>
205 |           </ul>
206 |         </div>
207 |         
208 |         <div className="mt-4 flex justify-end">
209 |           <button
210 |             onClick={onClose}
211 |             className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded text-gray-700 text-sm transition-colors"
212 |           >
213 |             Close
214 |           </button>
215 |         </div>
216 |       </div>
217 |     </div>
218 |   );
219 | };
220 | 
221 | export default ExportPanel; 
```

--------------------------------------------------------------------------------
/visualization/src/components/WorkplanFlow.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useCallback, useMemo } from 'react';
  2 | import ReactFlow, {
  3 |   MiniMap,
  4 |   Controls,
  5 |   Background,
  6 |   useNodesState,
  7 |   useEdgesState,
  8 |   NodeTypes,
  9 |   EdgeTypes,
 10 |   Panel,
 11 |   BackgroundVariant,
 12 |   getBezierPath,
 13 |   EdgeProps,
 14 |   MarkerType,
 15 |   Position,
 16 | } from 'reactflow';
 17 | import 'reactflow/dist/style.css';
 18 | import { WorkPlan, NodeData, ExtendedNode, CommitStatus } from '../types';
 19 | import { 
 20 |   convertWorkPlanToFlow, 
 21 |   filterWorkplan,
 22 |   filterNodesAndEdges
 23 | } from '../utils/workplanConverter';
 24 | import { useResponsiveFlowDimensions } from '../utils/responsiveUtils';
 25 | import CommitNode from './CommitNode';
 26 | import { FilterOptions } from '../components/FilterPanel';
 27 | import './nodes/nodes.css';
 28 | 
 29 | export interface WorkplanFlowProps {
 30 |   workplan: WorkPlan;
 31 |   filterOptions: FilterOptions;
 32 | }
 33 | 
 34 | // Custom edge component
 35 | const CustomEdge = ({
 36 |   id,
 37 |   sourceX,
 38 |   sourceY,
 39 |   targetX,
 40 |   targetY,
 41 |   sourcePosition,
 42 |   targetPosition,
 43 |   style = {},
 44 |   data,
 45 |   markerEnd,
 46 |   animated
 47 | }: EdgeProps) => {
 48 |   // Adjust connection point calculation
 49 |   // Ensure targetPosition is set to Position.Left
 50 |   const targetPos = targetPosition || Position.Left;
 51 |   const sourcePos = sourcePosition || Position.Right;
 52 |   
 53 |   // Adjust arguments for getBezierPath
 54 |   const [edgePath, labelX, labelY] = getBezierPath({
 55 |     sourceX,
 56 |     sourceY,
 57 |     sourcePosition: sourcePos,
 58 |     targetX,
 59 |     targetY,
 60 |     targetPosition: targetPos,
 61 |     curvature: 0.4
 62 |   });
 63 | 
 64 |   return (
 65 |     <>
 66 |       <path
 67 |         id={id}
 68 |         style={{
 69 |           ...style,
 70 |           strokeWidth: style.strokeWidth || 2,
 71 |           stroke: style.stroke || '#555',
 72 |           transition: 'stroke 0.3s, stroke-width 0.3s',
 73 |         }}
 74 |         className={`react-flow__edge-path ${animated ? 'animated' : ''}`}
 75 |         d={edgePath}
 76 |         markerEnd={markerEnd}
 77 |       />
 78 |       {data?.label && (
 79 |         <text
 80 |           x={labelX}
 81 |           y={labelY}
 82 |           style={{
 83 |             fontSize: '10px',
 84 |             fill: '#666',
 85 |             textAnchor: 'middle',
 86 |             dominantBaseline: 'middle',
 87 |             pointerEvents: 'none',
 88 |             fontWeight: 'normal',
 89 |           }}
 90 |           className="react-flow__edge-text"
 91 |         >
 92 |           {data.label}
 93 |         </text>
 94 |       )}
 95 |     </>
 96 |   );
 97 | };
 98 | 
 99 | // Register custom node types
100 | const nodeTypes: NodeTypes = {
101 |   commitNode: CommitNode,
102 | };
103 | 
104 | // Register custom edge types
105 | const edgeTypes: EdgeTypes = {
106 |   custom: CustomEdge,
107 | };
108 | 
109 | // Status label definitions
110 | const statusLabels: Record<CommitStatus, string> = {
111 |   'not_started': 'Not Started',
112 |   'in_progress': 'In Progress',
113 |   'completed': 'Completed',
114 |   'cancelled': 'Cancelled',
115 |   'needsRefinment': 'Needs Refinement',
116 |   'user_review': 'Awaiting User Review'
117 | };
118 | 
119 | 
120 | const WorkplanFlow = ({ 
121 |   workplan, 
122 |   filterOptions
123 | }: WorkplanFlowProps) => {
124 |   // Default filter options
125 |   const defaultFilterOptions: FilterOptions = {
126 |     statusFilter: 'all',
127 |     searchQuery: '',
128 |     onlyShowActive: false
129 |   };
130 |   
131 |   // Get responsive settings
132 |   const {
133 |     miniMapVisible,
134 |     controlsStyle,
135 |     miniMapStyle,
136 |     currentBreakpoint
137 |   } = useResponsiveFlowDimensions();
138 |   
139 |   // Use provided filter options or default
140 |   const activeFilterOptions = filterOptions || defaultFilterOptions;
141 |   
142 |   // Apply filtering
143 |   const filteredWorkplan = useMemo(() => {
144 |     return filterWorkplan(workplan, activeFilterOptions);
145 |   }, [workplan, activeFilterOptions]);
146 | 
147 |   // Set initial nodes and edges (generated from filtered workplan)
148 |   const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
149 |     return convertWorkPlanToFlow(filteredWorkplan);
150 |   }, [filteredWorkplan]);
151 |   
152 |   // Add callbacks to commit nodes
153 |   const nodesWithCallbacks = useMemo(() => {
154 |     return initialNodes.map(node => {
155 |       if (node.type === 'commitNode') {
156 |         const nodeData = node.data as any; // Temporarily handle as any
157 |         return {
158 |           ...node,
159 |           data: {
160 |             ...nodeData,
161 |             // Fallback if data doesn't have necessary properties
162 |             title: nodeData.title || nodeData.label || '',
163 |             status: nodeData.status || 'not_started'
164 |           }
165 |         };
166 |       }
167 |       return node;
168 |     });
169 |   }, [initialNodes]);
170 |   
171 |   // Change edge type to custom
172 |   const customEdges = useMemo(() => {
173 |     return initialEdges.map(edge => ({
174 |       ...edge,
175 |       type: 'custom',
176 |       data: { label: edge.label },
177 |       markerEnd: {
178 |         type: MarkerType.ArrowClosed,
179 |         color: edge.style?.stroke || '#555',
180 |       },
181 |     }));
182 |   }, [initialEdges]);
183 |   
184 |   // Apply filtering directly to nodes and edges
185 |   const { nodes: filteredNodes, edges: filteredEdges } = useMemo(() => {
186 |     return filterNodesAndEdges(nodesWithCallbacks, customEdges, activeFilterOptions);
187 |   }, [nodesWithCallbacks, customEdges, activeFilterOptions]);
188 |   
189 |   // Use handlers for interaction only
190 |   const [, , onNodesChange] = useNodesState([]);
191 |   const [, , onEdgesChange] = useEdgesState([]);
192 |   
193 |   // Selected node information
194 |   const [selectedNode, setSelectedNode] = useState<ExtendedNode<NodeData> | null>(null);
195 | 
196 |   // Handler for node selection
197 |   const onNodeClick = useCallback((event: React.MouseEvent, node: ExtendedNode) => {
198 |     setSelectedNode(node as ExtendedNode<NodeData>);
199 |   }, []);
200 |   
201 |   // Handler for canvas click (deselection)
202 |   const onPaneClick = useCallback(() => {
203 |     setSelectedNode(null);
204 |   }, []);
205 |   
206 |   // Flow component style
207 |   const proOptions = { hideAttribution: true };
208 | 
209 |   // FitView options based on current breakpoint
210 |   const fitViewOptions = useMemo(() => ({
211 |     padding: currentBreakpoint === 'xs' ? 0.1 : 
212 |              currentBreakpoint === 'sm' ? 0.15 : 0.2,
213 |     maxZoom: 1.5,
214 |     includeHiddenNodes: false,
215 |     minZoom: 0.2,
216 |     alignmentX: 0.5,  // Horizontal center
217 |     alignmentY: 0,    // Top alignment
218 |   }), [currentBreakpoint]);
219 | 
220 |   // Initial viewport settings
221 |   const defaultViewport = { x: 0, y: 0, zoom: 1 };
222 | 
223 |   return (
224 |     <div style={{ width: '100%', height: '100vh', position: 'relative' }}>
225 |       <ReactFlow
226 |         nodes={filteredNodes}
227 |         edges={filteredEdges}
228 |         onNodesChange={onNodesChange}
229 |         onEdgesChange={onEdgesChange}
230 |         onNodeClick={onNodeClick}
231 |         onPaneClick={onPaneClick}
232 |         nodeTypes={nodeTypes}
233 |         edgeTypes={edgeTypes}
234 |         fitView
235 |         fitViewOptions={fitViewOptions}
236 |         defaultViewport={defaultViewport}
237 |         proOptions={proOptions}
238 |         minZoom={0.1}
239 |         maxZoom={2}
240 |       >
241 |         {/* Display active filters - responsive */}
242 |         {(activeFilterOptions.statusFilter !== 'all' || 
243 |           activeFilterOptions.searchQuery.trim() || 
244 |           activeFilterOptions.onlyShowActive) && (
245 |           <Panel 
246 |             position="top-left" 
247 |             className={`bg-white p-2 rounded-lg shadow-md border border-gray-200 ${
248 |               currentBreakpoint === 'xs' ? 'text-xs max-w-[80vw]' : ''
249 |             }`}
250 |           >
251 |             <div className={`text-gray-700 ${currentBreakpoint === 'xs' ? 'text-xs' : 'text-sm'}`}>
252 |               <span className="font-bold">Filters Applied:</span>
253 |               <div className={`flex flex-wrap gap-1 mt-1 ${currentBreakpoint === 'xs' ? 'max-w-full' : ''}`}>
254 |                 {activeFilterOptions.statusFilter !== 'all' && (
255 |                   <span className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-full text-xs">
256 |                     Status: {activeFilterOptions.statusFilter}
257 |                   </span>
258 |                 )}
259 |                 {activeFilterOptions.searchQuery.trim() && (
260 |                   <span className="px-2 py-0.5 bg-green-100 text-green-800 rounded-full text-xs">
261 |                     Search: {activeFilterOptions.searchQuery}
262 |                   </span>
263 |                 )}
264 |                 {activeFilterOptions.onlyShowActive && (
265 |                   <span className="px-2 py-0.5 bg-yellow-100 text-yellow-800 rounded-full text-xs">
266 |                     Active Only
267 |                   </span>
268 |                 )}
269 |               </div>
270 |             </div>
271 |           </Panel>
272 |         )}
273 |         
274 |         {/* Mini map - responsive */}
275 |         {miniMapVisible && (
276 |           <MiniMap 
277 |             style={miniMapStyle}
278 |             nodeStrokeWidth={3}
279 |             zoomable
280 |             pannable
281 |           />
282 |         )}
283 |         
284 |         {/* Controls - responsive */}
285 |         <Controls style={controlsStyle} />
286 |         
287 |         <Background
288 |           variant={BackgroundVariant.Dots}
289 |           gap={12}
290 |           size={1}
291 |         />
292 |         
293 |         {/* Help information for mobile display */}
294 |         {currentBreakpoint === 'xs' && (
295 |           <Panel position="bottom-center" className="p-2 bg-white bg-opacity-80 rounded text-xs text-center">
296 |             Pinch to zoom, swipe to move
297 |           </Panel>
298 |         )}
299 |       </ReactFlow>
300 |       
301 |       {/* Detailed information for selected node - responsive */}
302 |       {selectedNode && (
303 |         <div 
304 |           className={`absolute p-3 bg-white dark:bg-gray-800 border rounded-md shadow-lg 
305 |             ${currentBreakpoint === 'xs' ? 'left-2 right-2 bottom-2' : 'right-4 top-4 w-64'}`}
306 |         >
307 |           <div className="flex justify-between mb-2">
308 |             <h3 className="font-bold">Details</h3>
309 |             <button 
310 |               onClick={onPaneClick}
311 |               className="text-gray-500 hover:text-gray-700"
312 |             >
313 |               ✕
314 |             </button>
315 |           </div>
316 |           <div className="text-sm">
317 |             <p><strong>Title:</strong> {selectedNode.data.label}</p>
318 |             <p><strong>Status:</strong> {selectedNode.data.status && statusLabels[selectedNode.data.status]}</p>
319 |             {selectedNode.data.description && (
320 |               <p><strong>Description:</strong> {selectedNode.data.description}</p>
321 |             )}
322 |           </div>
323 |         </div>
324 |       )}
325 |     </div>
326 |   );
327 | };
328 | 
329 | export default WorkplanFlow; 
```

--------------------------------------------------------------------------------
/visualization/src/components/FilterPanel.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useEffect, useRef, useCallback } from 'react';
  2 | import { CommitStatus } from '../types';
  3 | 
  4 | // Filter settings type
  5 | export interface FilterOptions {
  6 |   statusFilter: CommitStatus | 'all';
  7 |   searchQuery: string;
  8 |   onlyShowActive: boolean; // Show only active PRs/commits
  9 | }
 10 | 
 11 | interface FilterPanelProps {
 12 |   options: FilterOptions;
 13 |   onChange: (newOptions: FilterOptions) => void;
 14 |   isOpen: boolean;
 15 |   onClose: () => void;
 16 | }
 17 | 
 18 | // Status display name mapping
 19 | const STATUS_LABELS: Record<CommitStatus | 'all', string> = {
 20 |   'all': 'All',
 21 |   'not_started': 'Not Started',
 22 |   'in_progress': 'In Progress',
 23 |   'blocked': 'Blocked',
 24 |   'completed': 'Completed',
 25 |   'cancelled': 'Cancelled',
 26 |   'needsRefinment': 'Needs Refinement',
 27 |   'user_review': 'Awaiting User Review'
 28 | };
 29 | 
 30 | // Status icon mapping
 31 | const STATUS_ICONS: Record<CommitStatus | 'all', string> = {
 32 |   'all': '🔍',
 33 |   'not_started': '⚪',
 34 |   'in_progress': '🔄',
 35 |   'blocked': '⛔',
 36 |   'completed': '✅',
 37 |   'cancelled': '❌',
 38 |   'needsRefinment': '⚠️',
 39 |   'user_review': '👀'
 40 | };
 41 | 
 42 | // Pre-generate status options
 43 | const STATUS_OPTIONS = Object.entries(STATUS_LABELS).map(([status, label]) => (
 44 |   <option key={status} value={status}>
 45 |     {STATUS_ICONS[status as CommitStatus | 'all']} {label}
 46 |   </option>
 47 | ));
 48 | 
 49 | const FilterPanel: React.FC<FilterPanelProps> = ({ 
 50 |   options, 
 51 |   onChange,
 52 |   isOpen,
 53 |   onClose
 54 | }) => {
 55 |   const [localOptions, setLocalOptions] = useState<FilterOptions>(options);
 56 |   const panelRef = useRef<HTMLDivElement>(null);
 57 |   
 58 |   // Update internal state when options from props change
 59 |   useEffect(() => {
 60 |     setLocalOptions(options);
 61 |   }, [options]);
 62 |   
 63 |   // Change handler (memoized)
 64 |   const handleChange = useCallback((key: keyof FilterOptions, value: any) => {
 65 |     const newOptions = { ...localOptions, [key]: value };
 66 |     setLocalOptions(newOptions);
 67 |     onChange(newOptions);
 68 |   }, [localOptions, onChange]);
 69 |   
 70 |   // Reset handler (memoized)
 71 |   const handleReset = useCallback(() => {
 72 |     const defaultOptions: FilterOptions = {
 73 |       statusFilter: 'all',
 74 |       searchQuery: '',
 75 |       onlyShowActive: false
 76 |     };
 77 |     setLocalOptions(defaultOptions);
 78 |     onChange(defaultOptions);
 79 |   }, [onChange]);
 80 |   
 81 |   // Close when clicking outside the panel
 82 |   useEffect(() => {
 83 |     if (!isOpen) return;
 84 |     
 85 |     const handleOutsideClick = (event: MouseEvent) => {
 86 |       if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
 87 |         onClose();
 88 |       }
 89 |     };
 90 |     
 91 |     document.addEventListener('mousedown', handleOutsideClick);
 92 |     return () => document.removeEventListener('mousedown', handleOutsideClick);
 93 |   }, [isOpen, onClose]);
 94 |   
 95 |   // Close panel with Esc key
 96 |   useEffect(() => {
 97 |     if (!isOpen) return;
 98 |     
 99 |     const handleKeyDown = (event: KeyboardEvent) => {
100 |       if (event.key === 'Escape') {
101 |         onClose();
102 |       }
103 |     };
104 |     
105 |     document.addEventListener('keydown', handleKeyDown);
106 |     return () => document.removeEventListener('keydown', handleKeyDown);
107 |   }, [isOpen, onClose]);
108 |   
109 |   if (!isOpen) return null;
110 | 
111 |   return (
112 |     <div 
113 |       ref={panelRef}
114 |       className="bg-white rounded-lg shadow-lg p-4 border border-gray-200 transition-all duration-300"
115 |     >
116 |       <div className="flex justify-between items-center mb-4">
117 |         <h3 className="text-lg font-semibold text-gray-800 flex items-center">
118 |           <span className="mr-2">🔍</span>
119 |           Filter Settings
120 |         </h3>
121 |         <button 
122 |           onClick={onClose}
123 |           className="text-gray-500 hover:text-gray-700 text-xl transition-colors p-1 rounded-full hover:bg-gray-100"
124 |           aria-label="Close"
125 |         >
126 |           &times;
127 |         </button>
128 |       </div>
129 |       
130 |       <div className="space-y-4">
131 |         {/* Status filter */}
132 |         <div className="transition-all duration-200 hover:shadow-sm">
133 |           <label className="block text-sm font-medium text-gray-700 mb-1">
134 |             Status
135 |           </label>
136 |           <div className="relative">
137 |             <select
138 |               value={localOptions.statusFilter}
139 |               onChange={(e) => handleChange('statusFilter', e.target.value)}
140 |               className="w-full p-2 pl-8 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 appearance-none"
141 |             >
142 |               {STATUS_OPTIONS}
143 |             </select>
144 |             <div className="absolute inset-y-0 left-0 flex items-center pl-2 pointer-events-none">
145 |               {STATUS_ICONS[localOptions.statusFilter as CommitStatus | 'all']}
146 |             </div>
147 |             <div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
148 |               <svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
149 |                 <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
150 |               </svg>
151 |             </div>
152 |           </div>
153 |         </div>
154 |         
155 |         {/* Search filter */}
156 |         <div className="transition-all duration-200 hover:shadow-sm">
157 |           <label className="block text-sm font-medium text-gray-700 mb-1">
158 |             Search
159 |           </label>
160 |           <div className="relative">
161 |             <input
162 |               type="text"
163 |               value={localOptions.searchQuery}
164 |               onChange={(e) => handleChange('searchQuery', e.target.value)}
165 |               placeholder="Search in commit content..."
166 |               className="w-full p-2 pl-8 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
167 |             />
168 |             <div className="absolute inset-y-0 left-0 flex items-center pl-2 pointer-events-none">
169 |               <svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
170 |                 <path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd" />
171 |               </svg>
172 |             </div>
173 |             {localOptions.searchQuery && (
174 |               <button
175 |                 onClick={() => handleChange('searchQuery', '')}
176 |                 className="absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400 hover:text-gray-600"
177 |                 aria-label="Clear search"
178 |               >
179 |                 <svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
180 |                   <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
181 |                 </svg>
182 |               </button>
183 |             )}
184 |           </div>
185 |         </div>
186 |         
187 |         {/* Show only active items */}
188 |         <div className="flex items-center p-2 hover:bg-gray-50 rounded-md transition-colors">
189 |           <input
190 |             type="checkbox"
191 |             id="onlyActive"
192 |             checked={localOptions.onlyShowActive}
193 |             onChange={(e) => handleChange('onlyShowActive', e.target.checked)}
194 |             className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded transition-colors"
195 |           />
196 |           <label htmlFor="onlyActive" className="ml-2 block text-sm text-gray-700">
197 |             Show only active tasks
198 |           </label>
199 |         </div>
200 |         
201 |         {/* Filter badge display */}
202 |         {(localOptions.statusFilter !== 'all' || 
203 |          localOptions.searchQuery.trim() || 
204 |          localOptions.onlyShowActive) && (
205 |           <div className="flex flex-wrap gap-2 pt-2 pb-3">
206 |             <p className="w-full text-xs text-gray-500 mb-1">Active filters:</p>
207 |             
208 |             {localOptions.statusFilter !== 'all' && (
209 |               <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
210 |                 {STATUS_ICONS[localOptions.statusFilter as CommitStatus]}
211 |                 <span className="ml-1">{STATUS_LABELS[localOptions.statusFilter as CommitStatus]}</span>
212 |                 <button
213 |                   onClick={() => handleChange('statusFilter', 'all')}
214 |                   className="ml-1 text-blue-500 hover:text-blue-700"
215 |                 >
216 |                   ×
217 |                 </button>
218 |               </span>
219 |             )}
220 |             
221 |             {localOptions.searchQuery.trim() && (
222 |               <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
223 |                 🔍 {localOptions.searchQuery}
224 |                 <button
225 |                   onClick={() => handleChange('searchQuery', '')}
226 |                   className="ml-1 text-green-500 hover:text-green-700"
227 |                 >
228 |                   ×
229 |                 </button>
230 |               </span>
231 |             )}
232 |             
233 |             {localOptions.onlyShowActive && (
234 |               <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
235 |                 🔄 Active Only
236 |                 <button
237 |                   onClick={() => handleChange('onlyShowActive', false)}
238 |                   className="ml-1 text-yellow-500 hover:text-yellow-700"
239 |                 >
240 |                   ×
241 |                 </button>
242 |               </span>
243 |             )}
244 |           </div>
245 |         )}
246 |         
247 |         {/* Reset button */}
248 |         <div className="pt-2 flex justify-end">
249 |           <button
250 |             onClick={handleReset}
251 |             className="px-3 py-1.5 bg-gray-200 hover:bg-gray-300 rounded text-gray-700 text-sm transition-colors flex items-center"
252 |           >
253 |             <svg className="w-4 h-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
254 |               <path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
255 |             </svg>
256 |             Reset
257 |           </button>
258 |         </div>
259 |       </div>
260 |     </div>
261 |   );
262 | };
263 | 
264 | export default FilterPanel; 
```

--------------------------------------------------------------------------------
/visualization/src/utils/workplanConverter.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Edge, Position } from 'reactflow';
  2 | import { WorkPlan, CommitStatus, NodeData, ExtendedNode } from '../types';
  3 | import { CommitNodeData } from '../components/CommitNode';
  4 | import { FilterOptions } from '../components/FilterPanel';
  5 | 
  6 | // Function to return color based on status
  7 | export const getStatusColor = (status: CommitStatus): string => {
  8 |   switch (status) {
  9 |     case 'completed':
 10 |       return '#10b981'; // green
 11 |     case 'in_progress':
 12 |       return '#3b82f6'; // blue
 13 |     case 'cancelled':
 14 |       return '#6b7280'; // gray
 15 |     case 'needsRefinment':
 16 |       return '#f59e0b'; // orange
 17 |     case 'user_review':
 18 |       return '#9333ea'; // purple
 19 |     case 'not_started':
 20 |     default:
 21 |       return '#94a3b8'; // light gray
 22 |   }
 23 | };
 24 | 
 25 | // Function to return background color based on status
 26 | const getStatusBackgroundColor = (status: CommitStatus): string => {
 27 |   switch (status) {
 28 |     case 'completed':
 29 |       return '#d1fae5'; // light green
 30 |     case 'in_progress':
 31 |       return '#dbeafe'; // light blue
 32 |     case 'cancelled':
 33 |       return '#f3f4f6'; // light gray
 34 |     case 'needsRefinment':
 35 |       return '#fef3c7'; // light orange
 36 |     case 'user_review':
 37 |       return '#f3e8ff'; // light purple
 38 |     case 'not_started':
 39 |     default:
 40 |       return '#f1f5f9'; // light gray
 41 |   }
 42 | };
 43 | 
 44 | // Function to return label based on status
 45 | const getStatusLabel = (status: CommitStatus): string => {
 46 |   switch (status) {
 47 |     case 'completed':
 48 |       return 'Completed';
 49 |     case 'in_progress':
 50 |       return 'In Progress';
 51 |     case 'cancelled':
 52 |       return 'Cancelled';
 53 |     case 'needsRefinment':
 54 |       return 'Needs Refinement';
 55 |     case 'user_review':
 56 |       return 'Waiting for User Review';
 57 |     case 'not_started':
 58 |     default:
 59 |       return 'Not Started';
 60 |   }
 61 | };
 62 | 
 63 | // Function to return icon based on status
 64 | const getStatusIcon = (status: CommitStatus): string => {
 65 |   switch (status) {
 66 |     case 'completed':
 67 |       return '✅';
 68 |     case 'in_progress':
 69 |       return '🔄';
 70 |     case 'cancelled':
 71 |       return '❌';
 72 |     case 'needsRefinment':
 73 |       return '🔍';
 74 |     case 'user_review':
 75 |       return '👀';
 76 |     case 'not_started':
 77 |     default:
 78 |       return '⏳';
 79 |   }
 80 | };
 81 | 
 82 | // Layout constants
 83 | const DEFAULT_LAYOUT = {
 84 |   HORIZONTAL_SPACING: 550, // Horizontal spacing between PR nodes
 85 |   PR_WIDTH: 200, // Width of PR nodes
 86 |   COMMIT_WIDTH: 240, // Width of commit nodes
 87 |   COMMIT_HEIGHT: 100, // Height of commit nodes
 88 |   VERTICAL_CENTER: 100, // Vertical center position (top alignment)
 89 |   COMMIT_HORIZONTAL_OFFSET: 250, // Horizontal offset for commit nodes to the right of PR
 90 |   INITIAL_X: 100, // Initial X coordinate for the first node
 91 | };
 92 | 
 93 | // Interface for responsive layout options
 94 | export interface LayoutOptions {
 95 |   nodePadding?: number;
 96 |   nodeSpacing?: number;
 97 | }
 98 | 
 99 | /**
100 |  * Function to convert workplan to react-flow nodes and edges
101 |  * @param workplan Workplan
102 |  * @param options Responsive layout options
103 |  * @returns Object containing nodes and edges
104 |  */
105 | export const convertWorkPlanToFlow = (workplan: WorkPlan, options?: LayoutOptions) => {
106 |   if (!workplan) return { nodes: [], edges: [] };
107 |   
108 |   // Initialize arrays for nodes and edges
109 |   const nodes: ExtendedNode<NodeData | CommitNodeData>[] = [];
110 |   const edges: Edge[] = [];
111 |   
112 |   // Apply spacing and padding factors
113 |   const spacing = options?.nodeSpacing ?? 1;
114 |   const padding = options?.nodePadding ?? 1;
115 |   
116 |   // Calculate responsive layout values
117 |   const layout = {
118 |     HORIZONTAL_SPACING: DEFAULT_LAYOUT.HORIZONTAL_SPACING * spacing,
119 |     PR_WIDTH: DEFAULT_LAYOUT.PR_WIDTH * padding,
120 |     COMMIT_WIDTH: DEFAULT_LAYOUT.COMMIT_WIDTH * padding,
121 |     COMMIT_HEIGHT: DEFAULT_LAYOUT.COMMIT_HEIGHT,
122 |     VERTICAL_CENTER: DEFAULT_LAYOUT.VERTICAL_CENTER,
123 |     COMMIT_HORIZONTAL_OFFSET: DEFAULT_LAYOUT.COMMIT_HORIZONTAL_OFFSET * spacing,
124 |     INITIAL_X: DEFAULT_LAYOUT.INITIAL_X * spacing,
125 |   };
126 |   
127 |   // Generate PR nodes and commit nodes
128 |   workplan.prPlans.forEach((pr, prIndex) => {
129 |     // PR node
130 |     const prId = `pr-${prIndex}`;
131 |     const prStatus = getOverallStatus(pr.commitPlans.map(commit => commit.status));
132 |     
133 |     const prNode: ExtendedNode<NodeData | CommitNodeData> = {
134 |       id: prId,
135 |       type: 'default',
136 |       className: 'pr-node',
137 |       position: { 
138 |         x: layout.INITIAL_X + layout.HORIZONTAL_SPACING * prIndex, 
139 |         y: layout.VERTICAL_CENTER 
140 |       },
141 |       data: {
142 |         label: pr.goal,
143 |         description: `PR ${prIndex + 1}`,
144 |         status: prStatus,
145 |         statusLabel: getStatusLabel(prStatus),
146 |         statusIcon: getStatusIcon(prStatus),
147 |         developerNote: pr.developerNote
148 |       },
149 |       style: {
150 |         width: layout.PR_WIDTH,
151 |         backgroundColor: getStatusBackgroundColor(prStatus),
152 |         borderColor: getStatusColor(prStatus),
153 |         borderWidth: 2,
154 |       },
155 |       sourcePosition: Position.Right,
156 |       targetPosition: Position.Left,
157 |     };
158 |     
159 |     nodes.push(prNode);
160 |     
161 |     // Connect to previous PR (for second and subsequent PRs)
162 |     if (prIndex > 0) {
163 |       edges.push({
164 |         id: `pr-${prIndex - 1}-to-${prId}`,
165 |         source: `pr-${prIndex - 1}`,
166 |         target: prId,
167 |         animated: false,
168 |         sourceHandle: 'right',
169 |         targetHandle: 'left',
170 |         style: {
171 |           stroke: '#aaa',
172 |           strokeWidth: 2,
173 |         },
174 |       });
175 |     }
176 |     
177 |     // Commit node
178 |     pr.commitPlans.forEach((commit, commitIndex) => {
179 |       const commitId = `commit-${prIndex}-${commitIndex}`;
180 |       const commitStatus = commit.status || 'not_started';
181 |       
182 |       // Calculate commit vertical position (spread out from PR center)
183 |       const verticalOffset = (commitIndex - (pr.commitPlans.length - 1) / 2) * (layout.COMMIT_HEIGHT * 1.5);
184 |       
185 |       // Commit node
186 |       const commitNode: ExtendedNode<CommitNodeData> = {
187 |         id: commitId,
188 |         type: 'commitNode',
189 |         position: { 
190 |           x: layout.INITIAL_X + layout.HORIZONTAL_SPACING * prIndex + layout.COMMIT_HORIZONTAL_OFFSET, 
191 |           y: layout.VERTICAL_CENTER + verticalOffset 
192 |         },
193 |         data: {
194 |           title: commit.goal,
195 |           label: commit.goal,
196 |           status: commitStatus,
197 |           prIndex: prIndex,
198 |           commitIndex: commitIndex,
199 |           developerNote: commit.developerNote
200 |         },
201 |         style: {
202 |           borderColor: getStatusColor(commitStatus),
203 |           width: layout.COMMIT_WIDTH,
204 |         },
205 |         sourcePosition: Position.Right,
206 |         targetPosition: Position.Left,
207 |       };
208 |       
209 |       nodes.push(commitNode);
210 |       
211 |       // Connect commit to PR
212 |       edges.push({
213 |         id: `${prId}-to-${commitId}`,
214 |         source: prId,
215 |         target: commitId,
216 |         animated: false,
217 |         sourceHandle: 'right',
218 |         targetHandle: 'left',
219 |         label: `Commit ${commitIndex + 1}`,
220 |         style: {
221 |           stroke: getStatusColor(commitStatus),
222 |           strokeWidth: 2,
223 |         },
224 |       });
225 |       
226 |       // If there's a next PR, add edge from final commit to next PR
227 |       if (prIndex < workplan.prPlans.length - 1 && commitIndex === pr.commitPlans.length - 1) {
228 |         edges.push({
229 |           id: `${commitId}-to-pr-${prIndex + 1}`,
230 |           source: commitId,
231 |           target: `pr-${prIndex + 1}`,
232 |           animated: false,
233 |           sourceHandle: 'right',
234 |           targetHandle: 'left',
235 |           style: {
236 |             stroke: '#aaa',
237 |             strokeWidth: 2,
238 |             strokeDasharray: '5, 5',
239 |           },
240 |         });
241 |       }
242 |     });
243 |   });
244 |   
245 |   return { nodes, edges };
246 | };
247 | 
248 | // Function to determine overall status of PR based on commit statuses
249 | const getOverallStatus = (statuses: (CommitStatus | undefined)[]): CommitStatus => {
250 |   // Treat undefined status as 'not_started'
251 |   const definedStatuses = statuses.map(s => s || 'not_started');
252 |   
253 |   if (definedStatuses.every(status => status === 'completed')) {
254 |     return 'completed';
255 |   }
256 |   
257 |   if (definedStatuses.some(status => status === 'in_progress')) {
258 |     return 'in_progress';
259 |   }
260 |   
261 |   if (definedStatuses.some(status => status === 'needsRefinment')) {
262 |     return 'needsRefinment';
263 |   }
264 |   
265 |   if (definedStatuses.some(status => status === 'cancelled')) {
266 |     return 'cancelled';
267 |   }
268 |   
269 |   return 'not_started';
270 | };
271 | 
272 | // Function to filter workplan
273 | export const filterWorkplan = (workplan: WorkPlan, options: FilterOptions): WorkPlan => {
274 |   if (!workplan) return { goal: '', prPlans: [] };
275 |   
276 |   // Return unchanged if filtering is not needed
277 |   if (options.statusFilter === 'all' && !options.searchQuery && !options.onlyShowActive) {
278 |     return workplan;
279 |   }
280 |   
281 |   // Convert search query to lowercase
282 |   const searchQueryLower = options.searchQuery.toLowerCase();
283 |   
284 |   const filteredPrPlans = workplan.prPlans
285 |     .map(pr => {
286 |       // Filter commits in PR
287 |       const filteredCommits = pr.commitPlans.filter(commit => {
288 |         // Status filter
289 |         const statusMatch = options.statusFilter === 'all' || commit.status === options.statusFilter;
290 |         
291 |         // Search filter
292 |         const searchMatch = !searchQueryLower || 
293 |           commit.goal.toLowerCase().includes(searchQueryLower);
294 |         
295 |         // Active filter
296 |         const activeMatch = !options.onlyShowActive || 
297 |           commit.status === 'in_progress' || 
298 |           commit.status === 'needsRefinment';
299 |         
300 |         return statusMatch && searchMatch && activeMatch;
301 |       });
302 |       
303 |       // Return PR with filtered commits
304 |       return {
305 |         ...pr,
306 |         commitPlans: filteredCommits
307 |       };
308 |     })
309 |     .filter(pr => {
310 |       // Keep PRs with non-empty commits
311 |       return pr.commitPlans.length > 0;
312 |     });
313 |   
314 |   return {
315 |     goal: workplan.goal,
316 |     prPlans: filteredPrPlans
317 |   };
318 | };
319 | 
320 | // Function to apply direct filtering to nodes and edges
321 | export const filterNodesAndEdges = (
322 |   nodes: ExtendedNode<NodeData | CommitNodeData>[], 
323 |   edges: Edge[],
324 |   options: FilterOptions
325 | ): { nodes: ExtendedNode<NodeData | CommitNodeData>[], edges: Edge[] } => {
326 |   // Return unchanged if filtering is not needed
327 |   if (options.statusFilter === 'all' && !options.searchQuery && !options.onlyShowActive) {
328 |     return { nodes, edges };
329 |   }
330 |   
331 |   // Convert search query to lowercase
332 |   const searchQueryLower = options.searchQuery.toLowerCase();
333 |   
334 |   // Apply filtering to commit nodes
335 |   // PR nodes are always displayed
336 |   const filteredNodeIds = new Set();
337 |   
338 |   // Collect displayed node IDs
339 |   const filteredNodes = nodes.filter(node => {
340 |     // Always display PR nodes
341 |     if (node.type !== 'commitNode') {
342 |       filteredNodeIds.add(node.id);
343 |       return true;
344 |     }
345 |     
346 |     // Apply filtering to commit nodes
347 |     const nodeData = node.data as CommitNodeData;
348 |     
349 |     // Status filter
350 |     const statusMatch = options.statusFilter === 'all' || nodeData.status === options.statusFilter;
351 |     
352 |     // Search filter
353 |     const searchMatch = !searchQueryLower || 
354 |       nodeData.title.toLowerCase().includes(searchQueryLower);
355 |     
356 |     // Active filter
357 |     const activeMatch = !options.onlyShowActive || 
358 |       nodeData.status === 'in_progress' || 
359 |       nodeData.status === 'user_review';
360 |     
361 |     // If all conditions match, include this node
362 |     const shouldInclude = statusMatch && searchMatch && activeMatch;
363 |     
364 |     if (shouldInclude) {
365 |       filteredNodeIds.add(node.id);
366 |     }
367 |     
368 |     return shouldInclude;
369 |   });
370 |   
371 |   // Display only edges connected to filtered nodes
372 |   const filteredEdges = edges.filter(edge => 
373 |     filteredNodeIds.has(edge.source) && filteredNodeIds.has(edge.target)
374 |   );
375 |   
376 |   return { nodes: filteredNodes, edges: filteredEdges };
377 | }; 
```

--------------------------------------------------------------------------------
/visualization/src/App.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import { useState, useEffect, useCallback, useRef } from 'react'
  2 | import { ReactFlowProvider } from 'reactflow'
  3 | import WorkplanFlow from './components/WorkplanFlow'
  4 | import FilterPanel, { FilterOptions } from './components/FilterPanel'
  5 | import OrientationWarning from './components/OrientationWarning'
  6 | import { WorkPlan, CommitStatus } from './types'
  7 | import { useBreakpoint } from './utils/responsiveUtils'
  8 | import './App.css'
  9 | 
 10 | function App() {
 11 |   // Get breakpoint
 12 |   const breakpoint = useBreakpoint();
 13 |   
 14 |   // Initialize with normalized sample data
 15 |   const [workplan, setWorkplan] = useState<WorkPlan | null>(null);
 16 |   const [lastLoadedTime, setLastLoadedTime] = useState<Date | null>(null);
 17 |   const [loadError, setLoadError] = useState<string | null>(null);
 18 |   // Polling interval (milliseconds)
 19 |   const [pollingInterval, setPollingInterval] = useState<number>(5000); // Default: 1 second
 20 |   // Whether polling is enabled
 21 |   const [pollingEnabled, setPollingEnabled] = useState<boolean>(true);
 22 |   // Loading flag
 23 |   const [isLoading, setIsLoading] = useState<boolean>(false);
 24 |   // Track last polling attempt
 25 |   const pollingTimeoutRef = useRef<number | null>(null);
 26 |   
 27 |   const [showFilterPanel, setShowFilterPanel] = useState<boolean>(false);
 28 |   const [filterOptions, setFilterOptions] = useState<FilterOptions>({
 29 |     statusFilter: 'all',
 30 |     searchQuery: '',
 31 |     onlyShowActive: false
 32 |   });
 33 |   
 34 |   // Dark mode state
 35 |   const [isDarkMode, setIsDarkMode] = useState<boolean>(() => {
 36 |     // Load settings from localStorage
 37 |     const savedMode = localStorage.getItem('darkMode');
 38 |     // Check system color scheme settings
 39 |     const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
 40 |     
 41 |     // Use saved settings if available, otherwise follow system settings
 42 |     return savedMode !== null ? savedMode === 'true' : prefersDark;
 43 |   });
 44 | 
 45 |   // Separate data loading function for reusability
 46 |   const loadData = useCallback(async () => {
 47 |     if (isLoading) return; // Do nothing if already loading
 48 |     
 49 |     setIsLoading(true);
 50 |     try {
 51 |       // Get JSON file directly (add timestamp parameter to avoid cache)
 52 |       // Options to completely disable caching
 53 |       const fetchOptions = {
 54 |         method: 'GET',
 55 |         headers: {
 56 |           'Cache-Control': 'no-cache, no-store, must-revalidate',
 57 |           'Pragma': 'no-cache',
 58 |           'Expires': '0'
 59 |         }
 60 |       };
 61 |       
 62 |       const timestamp = new Date().getTime();
 63 |       const response = await fetch(`/data/workplan.json?t=${timestamp}`, fetchOptions);
 64 |       if (!response.ok) {
 65 |         throw new Error(`Failed to fetch data. Status: ${response.status}`);
 66 |       }
 67 |       
 68 |       const actualWorkPlan = await response.json();
 69 |       
 70 |       // Data structure conversion
 71 |       const convertedWorkPlan: WorkPlan = {
 72 |         goal: actualWorkPlan.currentTicket.goal,
 73 |         prPlans: actualWorkPlan.currentTicket.pullRequests.map((pr: {
 74 |           goal: string;
 75 |           status: string;
 76 |           developerNote?: string;
 77 |           commits: Array<{
 78 |             goal: string;
 79 |             status: string;
 80 |             developerNote?: string;
 81 |           }>;
 82 |         }) => ({
 83 |           goal: pr.goal,
 84 |           status: pr.status as CommitStatus,
 85 |           developerNote: pr.developerNote,
 86 |           commitPlans: pr.commits.map((commit: {
 87 |             goal: string;
 88 |             status: string;
 89 |             developerNote?: string;
 90 |           }) => ({
 91 |             goal: commit.goal,
 92 |             status: commit.status as CommitStatus,
 93 |             developerNote: commit.developerNote
 94 |           }))
 95 |         }))
 96 |       };
 97 |       
 98 |       // Apply updates
 99 |       setWorkplan(convertedWorkPlan);
100 |       setLastLoadedTime(new Date());
101 |       setLoadError(null);
102 |     } catch (error) {
103 |       console.error('Error occurred while loading data:', error);
104 |       setLoadError('Failed to load data.');
105 |     } finally {
106 |       setIsLoading(false);
107 |     }
108 |   }, []); // Empty dependency array
109 | 
110 |   // Get data from JSON file on initial load
111 |   useEffect(() => {
112 |     // Initial load
113 |     loadData();
114 |     
115 |     // Set up polling
116 |     const setupPolling = () => {
117 |       if (pollingTimeoutRef.current) {
118 |         clearTimeout(pollingTimeoutRef.current);
119 |         pollingTimeoutRef.current = null;
120 |       }
121 |       
122 |       if (pollingEnabled && pollingInterval > 0) {
123 |         pollingTimeoutRef.current = window.setTimeout(() => {
124 |           // Load data, then schedule next polling
125 |           loadData().finally(() => {
126 |             if (pollingEnabled) {
127 |               setupPolling();
128 |             }
129 |           });
130 |         }, pollingInterval);
131 |       }
132 |     };
133 |     
134 |     // Start polling
135 |     setupPolling();
136 |     
137 |     // Clean up on component unmount
138 |     return () => {
139 |       if (pollingTimeoutRef.current) {
140 |         clearTimeout(pollingTimeoutRef.current);
141 |         pollingTimeoutRef.current = null;
142 |       }
143 |     };
144 |   }, [loadData, pollingEnabled, pollingInterval]);
145 |   
146 |   // Add/remove class from html tag when dark mode setting changes
147 |   useEffect(() => {
148 |     if (isDarkMode) {
149 |       document.documentElement.classList.add('dark-mode');
150 |     } else {
151 |       document.documentElement.classList.remove('dark-mode');
152 |     }
153 |     
154 |     // Save settings to localStorage
155 |     localStorage.setItem('darkMode', isDarkMode.toString());
156 |   }, [isDarkMode]);
157 | 
158 |   // Filter options change handler
159 |   const handleFilterChange = useCallback((newOptions: FilterOptions) => {
160 |     setFilterOptions(newOptions);
161 |   }, []);
162 | 
163 |   // Filter button click handler
164 |   const handleFilterClick = useCallback(() => {
165 |     setShowFilterPanel(true);
166 |   }, []);
167 |   
168 |   // Dark mode toggle handler
169 |   const toggleDarkMode = useCallback(() => {
170 |     setIsDarkMode(prev => !prev);
171 |   }, []);
172 | 
173 |   // Polling settings toggle handler
174 |   const togglePolling = useCallback(() => {
175 |     setPollingEnabled(prev => !prev);
176 |   }, []);
177 | 
178 |   // Fallback display for errors
179 |   if (loadError || !workplan) {
180 |     return (
181 |       <div className="fixed inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-50">
182 |         <div className="text-center p-8 max-w-md">
183 |           <div className="animate-pulse text-5xl mb-6">⚠️</div>
184 |           <h2 className="text-xl font-bold text-red-600 dark:text-red-400 mb-4">
185 |             An error occurred
186 |           </h2>
187 |           <p className="text-gray-700 dark:text-gray-300 mb-6">
188 |             {loadError || "Workplan data not found"}
189 |           </p>
190 |           <button 
191 |             onClick={() => window.location.reload()}
192 |             className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
193 |           >
194 |             Reload
195 |           </button>
196 |         </div>
197 |       </div>
198 |     );
199 |   }
200 | 
201 |   return (
202 |     <div className={`app ${isDarkMode ? 'dark-theme' : 'light-theme'}`}>
203 |       <header className={`bg-gray-800 text-white shadow-md flex justify-between items-center ${
204 |         breakpoint === 'xs' ? 'p-2' : 'p-4'
205 |       }`}>
206 |         <div>
207 |           {/* Currently unused area - available for future use */}
208 |         </div>
209 |         
210 |         <div className="flex items-center gap-2 sm:gap-4">
211 |           {lastLoadedTime && (
212 |             <span className={`text-gray-300 ${breakpoint === 'xs' ? 'text-xs' : 'text-sm'} hidden sm:inline`}>
213 |               Last updated: {lastLoadedTime.toLocaleTimeString()}
214 |             </span>
215 |           )}
216 |           
217 |           {/* Polling toggle button */}
218 |           <button 
219 |             onClick={togglePolling}
220 |             className={`px-1 py-0.5 ${pollingEnabled ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-600 hover:bg-gray-500'} rounded-sm text-white transition flex items-center text-xs`}
221 |             title={pollingEnabled ? "Stop auto-refresh" : "Start auto-refresh"}
222 |             aria-label={pollingEnabled ? "Stop auto-refresh" : "Start auto-refresh"}
223 |           >
224 |             <svg className={`h-2.5 w-2.5 ${pollingEnabled ? 'animate-spin' : ''}`} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
225 |               <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
226 |               <path d="M21 3v5h-5"></path>
227 |             </svg>
228 |             <span className="hidden sm:inline text-xs ml-0.5">{pollingEnabled ? 'Auto ON' : 'Auto OFF'}</span>
229 |           </button>
230 |           
231 |           {/* Manual refresh button */}
232 |           <button 
233 |             onClick={() => loadData()}
234 |             disabled={isLoading}
235 |             className={`px-2 sm:px-3 py-1 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 rounded text-white transition flex items-center ${
236 |               breakpoint === 'xs' ? 'text-xs' : 'text-sm'
237 |             }`}
238 |             title="Refresh now"
239 |             aria-label="Refresh now"
240 |           >
241 |             <svg className="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
242 |               <polyline points="1 4 1 10 7 10"></polyline>
243 |               <polyline points="23 20 23 14 17 14"></polyline>
244 |               <path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
245 |             </svg>
246 |             <span className="hidden sm:inline">Refresh</span>
247 |           </button>
248 |           
249 |           {/* Dark mode toggle button */}
250 |           <button 
251 |             onClick={toggleDarkMode}
252 |             className={`px-2 sm:px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded text-white transition flex items-center ${
253 |               breakpoint === 'xs' ? 'text-xs' : 'text-sm'
254 |             }`}
255 |             title={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
256 |             aria-label={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
257 |           >
258 |             {isDarkMode ? (
259 |               <>
260 |                 <svg className="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
261 |                   <path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
262 |                 </svg>
263 |                 <span className="hidden sm:inline">Light</span>
264 |               </>
265 |             ) : (
266 |               <>
267 |                 <svg className="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
268 |                   <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
269 |                 </svg>
270 |                 <span className="hidden sm:inline">Dark</span>
271 |               </>
272 |             )}
273 |           </button>
274 |           
275 |           <button 
276 |             onClick={handleFilterClick}
277 |             className={`px-2 sm:px-3 py-1 bg-indigo-600 hover:bg-indigo-700 rounded text-white transition ${
278 |               breakpoint === 'xs' ? 'text-xs' : 'text-sm'
279 |             }`}
280 |             aria-label="Open filter"
281 |           >
282 |             <span className="mr-1">🔍</span>
283 |             {breakpoint !== 'xs' && "Filter"}
284 |           </button>
285 |         </div>
286 |       </header>
287 |       
288 |       <main className="app-main">
289 |         <ReactFlowProvider>
290 |           <WorkplanFlow 
291 |             workplan={workplan}
292 |             filterOptions={filterOptions}
293 |           />
294 |         </ReactFlowProvider>
295 |       </main>
296 |       
297 |       {/* Filter panel */}
298 |       <div className="filter-panel-container">
299 |         {showFilterPanel && (
300 |           <FilterPanel
301 |             options={filterOptions}
302 |             onChange={handleFilterChange}
303 |             isOpen={showFilterPanel}
304 |             onClose={() => setShowFilterPanel(false)}
305 |           />
306 |         )}
307 |       </div>
308 |       
309 |       {/* Device orientation warning (mobile only) */}
310 |       <OrientationWarning />
311 |     </div>
312 |   );
313 | }
314 | 
315 | export default App
316 | 
```