#
tokens: 36903/50000 45/45 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
# Source files
src/

# Tests
test/
coverage/
*.test.js
*.spec.js

# Build tools
tsconfig.json
.github/
.vscode/
.cursor/

# Misc
.git/
.gitignore
node_modules/
npm-debug.log
visualization/

# Environment
.env
.env.*

# Documentation (except README)
docs/ 
```

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

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Project specific files
public/data/workplan.json

```

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

```
# Dependency directories
node_modules/
dist/

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# IDE files
.idea/
.vscode/
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# Data files
visualization/public/data/workplan.json 
visualization/public/data/workplan.json

```

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

```markdown
[![npm](https://img.shields.io/npm/v/@yodakeisuke/mcp-micromanage)](https://www.npmjs.com/package/@yodakeisuke/mcp-micromanage)

# mcp-micromanage

Control your coding agent colleague who tends to go off track.

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.


![image](https://github.com/user-attachments/assets/d3e060a1-77a1-4a86-bd91-e0917cf405ba)

## Motivation

### Challenges with Coding Agents
- Coding agents often make modifications beyond what they're asked to do
    - Assuming cursor+claude
- They struggle to request user feedback at key decision points during implementation
- Work plans and progress tracking can be challenging to visualize and monitor
  
### Solution
- **Commit and PR-based Work Plans**: Force implementation plans that break down tickets into PRs and commits as the minimum units of work
- **Forced Frequent Feedback**: Enforce user reviews at the commit level, creating natural checkpoints for feedback
- **Visualization**: Instantly understand the current work plan and implementation status through a local React app

## tool

1. **plan**: Define your implementation plan with PRs and commits
2. **track**: Monitor progress and current status of all work items
3. **update**: Change status as work progresses, with mandatory user reviews

## Visualization Dashboard

The project includes a React-based visualization tool that provides:

- Hierarchical view of PRs and commits
- Real-time updates with auto-refresh
- Status-based color coding
- Zoom and pan capabilities

## Getting Started

### Headless(mcp tool only)

1. Add to your mcp json
```json
{
  "mcpServers": {
    "micromanage": {
      "command": "npx",
      "args": [
        "-y",
        "@yodakeisuke/mcp-micromanage"
      ]
    }
  }
}
```

2. (Highly recommended) Add the following `.mdc`s to your project

[recommended-rules](https://github.com/yodakeisuke/mcp-micromanage-your-agent/tree/main/.cursor/rules)

(Can be adjusted to your preference)

### With Visualisation

1. clone this repository

2. Add to your mcp json
```json
{
  "mcpServers": {
    "micromanage": {
      "command": "node",
      "args": [
        "[CLONE_DESTINATION_PATH]/sequentialdeveloping/dist/index.js"
      ]
    }
  }
}
```

3. build server
```bash
npm install
npm run build
```

4. run frontend
```bash
cd visualization/ 
npm install
npm run dev
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

### Third-Party Software

This project uses the following third-party software:

- **MCP TypeScript SDK**: Licensed under the MIT License. Copyright © 2023-2025 Anthropic, PBC.

## Acknowledgments

- Built with [MCP (Model Context Protocol)](https://github.com/modelcontextprotocol/typescript-sdk)
- Maintained by [yodakeisuke](https://github.com/yodakeisuke)

```

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

```typescript
/// <reference types="vite/client" />

```

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

```
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {},
    autoprefixer: {},
  },
} 
```

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

```json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}

```

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

```typescript
export type Status = 
  | "not_started" 
  | "in_progress" 
  | "user_review"
  | "completed" 
  | "cancelled"
  | "needsRefinment"; 
```

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

```
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
} 
```

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

```typescript
export { PLAN_TOOL } from './plan/toolDefs.js';
export { TRACK_TOOL } from './track/toolDefs.js';
export { UPDATE_STATUS_TOOL } from './update/toolDefs.js';


export * from './common.js';
```

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

```typescript
import React, { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

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

```typescript
import { Status } from './status.js';

export type CommitId = string;

export type Commit = {
  commitId?: CommitId;
  goal: string;
  status: Status;
  needsMoreThoughts?: boolean;
  needsRevision?: boolean;
  revisesTargetCommit?: CommitId;
  developerNote?: string;
}; 
```

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

```javascript
export default {
  transform: {},
  extensionsToTreatAsEsm: ['.ts'],
  moduleNameMapper: {
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
  testEnvironment: 'node',
  testMatch: ['**/*.test.js'],
  verbose: true,
  collectCoverage: true,
  collectCoverageFrom: ['src/**/*.js', '!src/tests/**'],
  transformIgnorePatterns: [],
}; 
```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "outDir": "./dist",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "requirements"]
}

```

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

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

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

```json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["vite.config.ts"]
}

```

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

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
    },
  },
)

```

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

```json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,

    /* Path Aliases */
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@data/*": ["../data/*"]
    },
    "resolveJsonModule": true
  },
  "include": ["src", "../data/*.json"]
}

```

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

```json
{
  "name": "visualization",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview",
    "test:responsive": "node test/responsive-tests.js"
  },
  "dependencies": {
    "@tailwindcss/postcss": "^4.0.15",
    "autoprefixer": "^10.4.21",
    "postcss": "^8.5.3",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "reactflow": "^11.11.4",
    "tailwindcss": "^4.0.15"
  },
  "devDependencies": {
    "@eslint/js": "^9.21.0",
    "@types/react": "^19.0.10",
    "@types/react-dom": "^19.0.4",
    "@vitejs/plugin-react": "^4.3.4",
    "eslint": "^9.21.0",
    "eslint-plugin-react-hooks": "^5.1.0",
    "eslint-plugin-react-refresh": "^0.4.19",
    "globals": "^15.15.0",
    "puppeteer": "^21.7.0",
    "typescript": "~5.7.2",
    "typescript-eslint": "^8.24.1",
    "vite": "^6.2.0"
  }
}

```

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

```json
{
  "name": "@yodakeisuke/mcp-micromanage",
  "version": "0.1.3",
  "description": "MCP server for sequential development task management",
  "license": "MIT",
  "author": "yoda keisuke",
  "type": "module",
  "bin": {
    "mcp-micromanage": "dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "prepare": "npm run build",
    "watch": "tsc --watch",
    "test": "NODE_OPTIONS=--experimental-vm-modules jest"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.7.0",
    "chalk": "^5.3.0",
    "cors": "^2.8.5",
    "express": "^5.0.1"
  },
  "devDependencies": {
    "@types/cors": "^2.8.17",
    "@types/express": "^5.0.1",
    "@types/jest": "^29.5.14",
    "@types/node": "^22",
    "jest": "^29.7.0",
    "shx": "^0.3.4",
    "typescript": "^5.6.3"
  },
  "keywords": [
    "mcp",
    "agent",
    "micromanage",
    "ai",
    "claude",
    "cursor"
  ],
  "publishConfig": {
    "access": "public"
  }
}

```

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

```typescript
import { ReactNode } from 'react';
import { Node, Edge as ReactFlowEdge } from 'reactflow';

// Possible commit statuses
export type CommitStatus = 'not_started' | 'in_progress' | 'completed' | 'cancelled' | 'needsRefinment' | 'user_review';

// Node data type definition
export interface NodeData {
  label: ReactNode; // Node label
  description?: string; // Node description
  status?: CommitStatus; // Node status
  statusLabel?: string; // Status label
  statusIcon?: string; // Status icon
  onEdit?: (data: NodeData) => void; // Callback when edit button is clicked
  onStatusChange?: (data: NodeData) => void; // Callback when status change button is clicked
}

// Extended node type
export type ExtendedNode<T = any> = Node<T>;

// Edge type
export type Edge = ReactFlowEdge;

// Commit plan type
export interface CommitPlan {
  goal: string;
  status?: CommitStatus;
  developerNote?: string; // Developer implementation notes captured during refinement
}

// PR plan type
export interface PRPlan {
  goal: string;
  commitPlans: CommitPlan[];
  status?: CommitStatus;
  developerNote?: string; // Developer implementation notes captured during refinement
}

// Work plan type
export interface WorkPlan {
  goal: string;
  prPlans: PRPlan[];
} 
```

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

```
<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
import { Status } from './status.js';
import { PullRequest } from './pullRequest.js';
import { PlanTaskInput } from '../aggregates/workplan.js';

export type Ticket = {
  goal: string;
  pullRequests: PullRequest[];
  needsMoreThoughts?: boolean;
};

// Helper function to check if ticket exists
export const ensureTicketExists = (
  ticketState: Ticket | "noTicket", 
  isError = false
): { result: true; ticket: Ticket } | { result: false; response: { content: Array<{ type: string; text: string }>; isError?: boolean } } => {
  if (ticketState === "noTicket") {
    return {
      result: false,
      response: {
        content: [{
          type: "text",
          text: JSON.stringify({
            message: "No implementation plan found. Create an implementation plan first using the 'plan' tool.",
            ...(isError && { status: 'failed' })
          }, null, 2)
        }],
        isError
      }
    };
  }
  
  return {
    result: true,
    ticket: ticketState
  };
};

// Create a plan for a ticket from input data
export const planTicket = (data: PlanTaskInput): Ticket => {
  // PRプランからPullRequestオブジェクトを作成
  const pullRequests = data.prPlans.map(prPlan => {
    // コミットプランからCommitオブジェクトを作成
    const commits = prPlan.commitPlans.map(commitPlan => ({
      goal: commitPlan.goal,
      status: "not_started" as Status,
      developerNote: commitPlan.developerNote
    }));
    
    return {
      goal: prPlan.goal,
      status: "not_started" as Status,
      commits,
      developerNote: prPlan.developerNote
    };
  });

  // チケットを作成して返す
  return {
    goal: data.goal,
    pullRequests,
    needsMoreThoughts: data.needsMoreThoughts
  };
}; 
```

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

```typescript
import chalk from 'chalk';


export enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
  NONE = 4
}

// 現在のログレベル(デフォルトはINFO)
let currentLogLevel = LogLevel.INFO;

export function setLogLevel(level: LogLevel): void {
  currentLogLevel = level;
}

export function getLogLevel(): LogLevel {
  return currentLogLevel;
}

function getTimestamp(): string {
  const now = new Date();
  return `[${now.toISOString().replace('T', ' ').substring(0, 19)}]`;
}

export function debug(message: string, ...args: any[]): void {
  if (currentLogLevel <= LogLevel.DEBUG) {
    console.error(`${getTimestamp()} ${chalk.blue('DEBUG')} ${message}`, ...args);
  }
}

export function info(message: string, ...args: any[]): void {
  if (currentLogLevel <= LogLevel.INFO) {
    console.error(`${getTimestamp()} ${chalk.green('INFO')} ${message}`, ...args);
  }
}

export function warn(message: string, ...args: any[]): void {
  if (currentLogLevel <= LogLevel.WARN) {
    console.warn(`${getTimestamp()} ${chalk.yellow('WARN')} ${message}`, ...args);
  }
}

export function error(message: string, ...args: any[]): void {
  if (currentLogLevel <= LogLevel.ERROR) {
    console.error(`${getTimestamp()} ${chalk.red('ERROR')} ${message}`, ...args);
  }
}

export function logError(message: string, error: unknown): void {
  if (currentLogLevel <= LogLevel.ERROR) {
    console.error(`${getTimestamp()} ${chalk.red('ERROR')} ${message}: ${error instanceof Error ? error.message : String(error)}`);
    if (error instanceof Error && error.stack) {
      console.error(`${getTimestamp()} ${chalk.red('STACK')} ${error.stack}`);
    }
  }
}

// デフォルトエクスポート
export default {
  setLogLevel,
  getLogLevel,
  debug,
  info,
  warn,
  error,
  logError,
  LogLevel
}; 
```

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

```typescript
import React, { useState, useEffect } from 'react';
import { useBreakpoint, Breakpoint } from '../utils/responsiveUtils';

/**
 * Component that detects mobile device orientation and displays a warning
 * Shows a recommendation to use landscape orientation
 */
const OrientationWarning: React.FC = () => {
  const breakpoint = useBreakpoint();
  const [isPortrait, setIsPortrait] = useState(false);
  const [isDismissed, setIsDismissed] = useState(false);

  // Detect screen orientation
  useEffect(() => {
    const checkOrientation = () => {
      if (window.matchMedia("(orientation: portrait)").matches) {
        setIsPortrait(true);
      } else {
        setIsPortrait(false);
      }
    };

    // Initial check
    checkOrientation();

    // Orientation change listener
    window.addEventListener('resize', checkOrientation);
    
    // Cleanup
    return () => {
      window.removeEventListener('resize', checkOrientation);
    };
  }, []);

  // Determine whether to show the warning
  const shouldShowWarning = isPortrait && ['xs', 'sm'].includes(breakpoint) && !isDismissed;
  
  if (!shouldShowWarning) {
    return null;
  }

  return (
    <div className="fixed bottom-0 left-0 right-0 bg-yellow-100 border-t border-yellow-200 p-3 z-50 shadow-lg">
      <div className="flex justify-between items-center">
        <div className="flex items-center">
          <span className="text-xl mr-2" aria-hidden="true">📱</span>
          <div>
            <p className="text-yellow-800 font-medium">Landscape orientation recommended</p>
            <p className="text-yellow-700 text-xs">Rotate your device to landscape for a better experience.</p>
          </div>
        </div>
        <button 
          onClick={() => setIsDismissed(true)}
          className="text-yellow-700 hover:text-yellow-900"
          aria-label="Close warning"
        >
          ✕
        </button>
      </div>
    </div>
  );
};

export default OrientationWarning; 
```

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

```typescript
import { Tool, createErrorResponse } from '../common.js';
import { workPlan } from '../../index.js';
import logger from '../../utils/logger.js';
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';

export const TRACK_TOOL: Tool<{}> = {
  name: "track",
  description: `
    This tool helps you monitor the current state of the implementation plan, view progress, and identify possible next steps.
    There is always exactly one task in either the in_progress or user_review state.
    **IMPORTANT**: There is always exactly one task in either the in_progress or user_review or needsRefinment state.
    
    **MANDATORY STATUS TRANSITION RULES:**
    needsRefinment → in_progress:
      - Requirements, implementation plan, and impact MUST be clearly explained.
    user_review → completed:
      - A commit may not transition to "completed" without explicit user approval. No exceptions.
    
    **After receiving the tool response, you MUST**::
    - Monitor PRs, commits, and overall progress to spot blockers or items needing refinement.
    - Review recommended actions to decide your next steps.
    - **Absolutely follow the content of "agentInstruction" in the response JSON**!!.
  `,
  schema: {},
  handler: async (_, extra: RequestHandlerExtra) => {
    try {
      logger.info('Track tool called');
      
      if (!workPlan) {
        logger.error('WorkPlan instance is not available');
        return createErrorResponse('WorkPlan instance is not initialized');
      }
      
      if (!workPlan.isInitialized()) {
        logger.error('WorkPlan is not initialized');
        return createErrorResponse('WorkPlan is not ready. Server initialization incomplete.');
      }
      
      const result = workPlan.trackProgress();
      
      return {
        content: result.content.map(item => ({
          type: 'text' as const,
          text: item.text
        })),
        isError: result.isError
      };
    } catch (error) {
      logger.logError('Error in TRACK tool', error);
      return createErrorResponse(error);
    }
  }
}; 
```

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

```typescript
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import { z } from 'zod';
import { type PlanTaskInput, type UpdateStatusInput, type WorkPlanInitOptions } from '../aggregates/workplan.js';
import logger from '../utils/logger.js';


export type Tool<Args extends z.ZodRawShape> = {
  name: string;
  description: string;
  schema: Args;
  handler: (
    args: z.infer<z.ZodObject<Args>>,
    extra: RequestHandlerExtra,
  ) =>
    | Promise<{
      content: Array<{
        type: 'text';
        text: string;
      }>;
      isError?: boolean;
    }>
    | {
      content: Array<{
        type: 'text';
        text: string;
      }>;
      isError?: boolean;
    };
};

/**
 * Enhanced error response generator with improved formatting and guidance.
 * 
 * Formats error messages to be more user-friendly and provides specific guidance
 * for validation errors and other common issues.
 * 
 * @param error - The error to format
 * @param context - Optional context about the error source (e.g., 'plan', 'update')
 * @returns Formatted error response
 */
export function createErrorResponse(
  error: unknown, 
  context?: string
): {
  content: Array<{ type: 'text'; text: string }>;
  isError: boolean;
} {
  // Extract base error message
  const errorMessage = error instanceof Error ? error.message : String(error);
  logger.error(`Creating error response: ${errorMessage}`);
  
  // Format error message with guidance based on error type
  let formattedError = errorMessage;
  
  // Check if it's a validation error
  if (errorMessage.includes('Invalid arguments') || errorMessage.includes('Validation')) {
    formattedError = `Validation Error: The provided data does not meet requirements.\n\n${errorMessage}`;
    
    if (context === 'plan') {
      formattedError += `\n\nSuggestions for fixing plan validation errors:
- Ensure goal fields are concise (max 60 characters) and focus on WHAT will be done
- Use developer notes for detailed implementation information (max 300 characters)
- Make sure at least one PR plan and one commit plan are provided
- Check that all required fields are present and properly formatted`;
    }
  }
  
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        error: formattedError,
        status: 'failed'
      }, null, 2)
    }],
    isError: true
  };
}

// エクスポートする型
export type { PlanTaskInput, UpdateStatusInput, WorkPlanInitOptions }; 
```

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

```css
/* コミットノードのスタイル */
.commit-node {
  border: 2px solid #ccc;
  border-radius: 5px;
  padding: 10px;
  width: 220px;
  background-color: white;
  transition: all 0.2s ease;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
  position: relative;
}

.commit-node:hover {
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  transform: translateY(-2px);
}

.commit-node .header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 5px;
}

.commit-node .title {
  margin-top: 5px;
  font-weight: bold;
  font-size: 14px;
  color: #333;
  word-break: break-word;
}

.commit-node .handle {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background-color: #60a5fa;
  border: 2px solid white;
  transition: transform 0.2s ease, background-color 0.2s ease;
}

.commit-node .handle-left {
  left: -6px;
}

.commit-node .handle-right {
  right: -6px;
}

/* ホバー時にハンドルを大きく */
.commit-node:hover .handle {
  transform: scale(1.2);
  background-color: #3b82f6;
}

.commit-node .status-label {
  display: inline-block;
  padding: 3px 6px;
  border-radius: 12px;
  font-size: 11px;
  font-weight: 500;
  color: white;
}

.commit-node .status-icon {
  margin-right: 4px;
}

/* ノードアクションボタン */
.node-actions {
  position: absolute;
  top: -10px;
  right: -10px;
  display: none;
  flex-direction: row;
  gap: 4px;
  background-color: white;
  border-radius: 4px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
  padding: 4px;
  z-index: 10;
}

.commit-node:hover .node-actions {
  display: flex;
}

.node-action-button {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  background-color: #f9fafb;
  border: 1px solid #e5e7eb;
  cursor: pointer;
  font-size: 12px;
  transition: background-color 0.2s ease;
}

.node-action-button:hover {
  background-color: #f3f4f6;
}

.node-action-edit {
  color: #3b82f6;
}

.node-action-delete {
  color: #ef4444;
}

/* エッジスタイル */
.react-flow__edge-path {
  stroke-width: 2;
  stroke: #aaa;
  transition: stroke 0.3s, stroke-width 0.3s;
}

/* エッジアニメーション */
.react-flow__edge-path.animated {
  stroke-dasharray: 5;
  animation: dashdraw 0.5s linear infinite;
}

@keyframes dashdraw {
  from {
    stroke-dashoffset: 10;
  }
}

/* ホバー時のエッジスタイル */
.react-flow__edge:hover .react-flow__edge-path {
  stroke-width: 3;
  stroke: #3b82f6;
}

/* エッジラベルスタイル */
.react-flow__edge-text {
  font-size: 10px;
  fill: #666;
  pointer-events: none;
}

/* PRノードのスタイル */
.pr-node {
  padding: 15px;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: all 0.3s;
}

.pr-node:hover {
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
  transform: translateY(-2px);
}

/* ミニマップ強調 */
.react-flow__minimap-mask {
  fill: rgba(240, 240, 240, 0.6);
}

.react-flow__minimap-node {
  transition: fill 0.2s;
}

/* ノード選択時のスタイル */
.react-flow__node.selected {
  box-shadow: 0 0 0 2px #3b82f6 !important;
} 
```

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

```typescript
import { Status } from './status.js';
import { Commit } from './commit.js';

export type PullRequest = {
  goal: string;
  status: Status;
  commits: Commit[];
  needsMoreThoughts?: boolean;
  developerNote?: string;
};

/**
 * Generates summary information for a list of pull requests
 */
export const generatePRSummaries = (pullRequests: PullRequest[]): Array<{
  prIndex: number;
  goal: string;
  status: Status;
  developerNote?: string;
  commits: {
    completed: number;
    total: number;
    percentComplete: number;
  };
}> => {
  return pullRequests.map((pr: PullRequest, prIndex: number) => {
    const completedCommits = pr.commits.filter((c: { status: Status }) => c.status === "completed").length;
    const totalCommits = pr.commits.length;
    
    return {
      prIndex: prIndex,
      goal: pr.goal,
      status: pr.status,
      developerNote: pr.developerNote,
      commits: {
        completed: completedCommits,
        total: totalCommits,
        percentComplete: totalCommits ? Math.round((completedCommits / totalCommits) * 100) : 0
      }
    };
  });
};

// Helper functions for status updates
export const updatePRStatusBasedOnCommits = (pr: PullRequest): PullRequest => {
  const commits = pr.commits;
  
  if (commits.length === 0) return pr;
  
  const updatedPr = { ...pr };
  
  // If all commits are completed, mark PR as completed
  if (commits.every(commit => commit.status === "completed")) {
    updatedPr.status = "completed";
  }
  // If any commit is in user_review status, mark PR as user_review (unless there are in_progress commits)
  else if (commits.some(commit => commit.status === "user_review") &&
           !commits.some(commit => commit.status === "in_progress")) {
    updatedPr.status = "user_review";
  }
  // If any commit is in progress, mark PR as in progress
  else if (commits.some(commit => commit.status === "in_progress")) {
    updatedPr.status = "in_progress";
  }
  // "blocked" status has been removed as per requirements
  
  // If any commit needs refinement, mark PR as needs refinement
  else if (commits.some(commit => commit.status === "needsRefinment") &&
           !commits.some(commit => commit.status === "in_progress")) {
    updatedPr.status = "needsRefinment";
  }
  
  return updatedPr;
};

/**
 * Updates the developer note for a pull request
 */
export const updatePRDeveloperNote = (pr: PullRequest, note: string): PullRequest => {
  return {
    ...pr,
    developerNote: note
  };
};

/**
 * Updates the developer note for a commit in a pull request
 */
export const updateCommitDeveloperNote = (pr: PullRequest, commitIndex: number, note: string): PullRequest => {
  if (commitIndex < 0 || commitIndex >= pr.commits.length) {
    return pr; // Invalid commit index
  }

  const updatedCommits = [...pr.commits];
  updatedCommits[commitIndex] = {
    ...updatedCommits[commitIndex],
    developerNote: note
  };

  return {
    ...pr,
    commits: updatedCommits
  };
}; 
```

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

```typescript
import { WorkPlan, CommitStatus, PRPlan, CommitPlan } from '../types';

// Local storage key
const STORAGE_KEY = 'workplan-visualization-data';

// List of valid status values
const VALID_STATUSES: CommitStatus[] = [
  'not_started', 
  'in_progress', 
  'completed', 
  'cancelled', 
  'needsRefinment',
  'user_review'
];

/**
 * Check if an object is a valid WorkPlan and fix it if necessary
 */
const validateWorkPlan = (data: any): WorkPlan => {
  // Basic structure check
  if (!data || typeof data !== 'object' || !data.goal || !Array.isArray(data.prPlans)) {
    throw new Error('Invalid workplan data structure');
  }

  // Validate and normalize PRPlans
  const validatedPrPlans: PRPlan[] = data.prPlans.map((pr: any): PRPlan => {
    if (!pr || typeof pr !== 'object' || !pr.goal || !Array.isArray(pr.commitPlans)) {
      throw new Error('Invalid PR data structure');
    }
    
    // Validate status
    const status = pr.status && VALID_STATUSES.includes(pr.status as CommitStatus) 
      ? pr.status as CommitStatus 
      : undefined;
    
    // Validate and normalize commit plans
    const commitPlans: CommitPlan[] = pr.commitPlans.map((commit: any): CommitPlan => {
      if (!commit || typeof commit !== 'object' || !commit.goal) {
        throw new Error('Invalid commit data structure');
      }
      
      // Validate commit status
      const commitStatus = commit.status && VALID_STATUSES.includes(commit.status as CommitStatus) 
        ? commit.status as CommitStatus 
        : undefined;
      
      return {
        goal: commit.goal,
        status: commitStatus,
        developerNote: commit.developerNote ? String(commit.developerNote) : undefined
      };
    });
    
    return {
      goal: pr.goal,
      status,
      commitPlans,
      developerNote: pr.developerNote ? String(pr.developerNote) : undefined
    };
  });
  
  return {
    goal: data.goal,
    prPlans: validatedPrPlans
  };
};

/**
 * Save workplan to local storage
 * @param workplan The workplan to save
 */
export const saveWorkPlanToStorage = (workplan: WorkPlan): void => {
  try {
    const serializedData = JSON.stringify(workplan);
    localStorage.setItem(STORAGE_KEY, serializedData);
  } catch (error) {
    console.error('Failed to save workplan:', error);
  }
};

/**
 * Get workplan from local storage
 * @returns The stored workplan, or null if not found
 */
export const getWorkPlanFromStorage = (): WorkPlan | null => {
  try {
    const serializedData = localStorage.getItem(STORAGE_KEY);
    if (!serializedData) return null;
    
    const parsedData = JSON.parse(serializedData);
    return validateWorkPlan(parsedData);
  } catch (error) {
    console.error('Failed to get workplan:', error);
    return null;
  }
};

/**
 * Clear workplan from local storage
 */
export const clearWorkPlanStorage = (): void => {
  try {
    localStorage.removeItem(STORAGE_KEY);
  } catch (error) {
    console.error('Failed to delete workplan:', error);
  }
}; 
```

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

```css
#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
  filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: no-preference) {
  a:nth-of-type(2) .logo {
    animation: logo-spin infinite 20s linear;
  }
}

.card {
  padding: 2em;
}

.read-the-docs {
  color: #888;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

header {
  flex-shrink: 0;
  z-index: 10;
}

.app-header {
  height: 60px;
  padding: 0 20px;
  display: flex;
  align-items: center;
  background-color: #1a1a1a;
  color: white;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.app-header h1 {
  font-size: 1.5rem;
  margin: 0;
}

.app-main {
  flex-grow: 1;
  overflow: hidden;
  position: relative;
}

/* フィルターパネルのスタイル */
.filter-panel-container {
  position: absolute;
  top: 80px;
  left: 20px;
  z-index: 20;
  max-width: 90%;
  width: 320px;
}

/* ノードとエッジのスタイル */
.pr-node {
  border-radius: 8px;
  background-color: #f3f4f6;
  border: 2px solid #9ca3af;
  padding: 10px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}

.commit-node {
  min-height: 80px;
  width: 100%;
  word-break: break-word;
}

/* ReactFlowのカスタマイズ */
.react-flow__node {
  transition: all 0.2s ease-in-out;
}

.react-flow__node:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.react-flow__node.selected {
  box-shadow: 0 0 0 2px #3b82f6;
}

.react-flow__edge-path.animated {
  stroke-dasharray: 5;
  animation: dashdraw 3s linear infinite;
}

@keyframes dashdraw {
  from {
    stroke-dashoffset: 10;
  }
}

/* レスポンシブデザイン */
@media (max-width: 768px) {
  header {
    flex-direction: column;
    gap: 10px;
    align-items: flex-start;
    padding: 10px;
  }
  
  header h1 {
    font-size: 1.5rem;
  }
  
  header .flex {
    flex-wrap: wrap;
    gap: 8px;
  }
  
  .filter-panel-container {
    top: 120px;
    left: 10px;
    width: calc(100% - 20px);
    max-width: none;
  }
}

/* アニメーション */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.filter-panel-container > div,
.export-panel-container > div {
  animation: fadeIn 0.3s ease-out;
}

/* ダークモードサポート */
@media (prefers-color-scheme: dark) {
  .react-flow__node {
    color: #f1f5f9;
  }
  
  .react-flow__node.start-node {
    background-color: #1e3a8a;
    border-color: #3b82f6;
  }
  
  .react-flow__node.pr-node {
    background-color: #1f2937;
    border-color: #4b5563;
  }
  
  .commit-node {
    background-color: #374151;
    border-color: #6b7280;
  }
}

```

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

```typescript
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import path from 'path';
import { fileURLToPath } from 'url';

import { PLAN_TOOL, TRACK_TOOL, UPDATE_STATUS_TOOL } from "./tools/index.js";
import { taskPlanningGuide } from "./prompts.js";
import logger, { LogLevel } from './utils/logger.js';
import { WorkPlan, WorkPlanInitOptions } from './aggregates/workplan.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const logLevel = LogLevel.INFO;
logger.setLogLevel(logLevel);
logger.info(`Logger initialized with level: ${LogLevel[logLevel]}`);

const dataDir = path.resolve(__dirname, '../visualization/public/data');
const dataFileName = 'workplan.json';

const workPlanOptions: WorkPlanInitOptions = {
  dataDir,
  dataFileName
};

export const workPlan = new WorkPlan();
const initSuccess = workPlan.initialize(workPlanOptions);

if (!initSuccess) {
  logger.error(`Failed to initialize WorkPlan with options: ${JSON.stringify(workPlanOptions)}`);
  process.exit(1);
}

const server = new McpServer({
  name: "micromanage",
  version: "0.1.0",
  description: `
  Externalizing developers' working memory and procedures related to work plans for resolving tickets.
  Because it would involve unnecessary overhead, we **do not apply it to tiny tasks**.
  
  This server helps organize and track development tasks through a structured approach with the following key features:
- Breaking down the tasks of the current ticket into minimal PRs and commits
- Tracking progress of implementation work
- Updating status of development items as work progresses

## When to Use This
- When users request ticket assignment
- When users ask for detailed task management
- For complex tasks requiring structured breakdown and tracking

Best practices:
1. Start with the task-planning-guide prompt before creating a plan
2. Keep PRs minimal and focused on single logical changes
3. Make commits small and atomic
4. Update status promptly as work begins or completes
5. Check progress regularly to maintain workflow efficiency
6. Always check the agentInstruction field in the response JSON from the 'track' tool
`,
  capabilities: {
    tools: {
      listChanged: true
    },
    prompts: {
      listChanged: true
    }
  }
});

server.prompt(
  taskPlanningGuide.name,
  taskPlanningGuide.description,
  {}, 
  taskPlanningGuide.handler
);

[PLAN_TOOL, TRACK_TOOL, UPDATE_STATUS_TOOL].forEach(tool => {
  server.tool(tool.name, tool.description ?? "", tool.schema, tool.handler);
});

async function runServer() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

process.on('SIGINT', () => {
  workPlan.forceSave();
  process.exit(0);
});

process.on('SIGTERM', () => {
  workPlan.forceSave();
  process.exit(0);
});

runServer().catch((error) => {
  logger.logError("Fatal error running server", error);
  workPlan.forceSave();
  process.exit(1);
}); 
```

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

```typescript
import { useState, useEffect } from 'react';

// Breakpoint definitions
export enum Breakpoint {
  xs = 0,    // Extra small devices (phones)
  sm = 640,  // Small devices (tablets)
  md = 768,  // Medium devices (landscape tablets)
  lg = 1024, // Large devices (desktops)
  xl = 1280, // Extra large devices (large desktops)
  xxl = 1536 // Double extra large devices
}

// Custom hook to determine current breakpoint
export function useBreakpoint() {
  const [breakpoint, setBreakpoint] = useState<keyof typeof Breakpoint>(() => {
    // Set initial value
    return getBreakpoint(window.innerWidth);
  });

  useEffect(() => {
    // Handler for window resize events
    const handleResize = () => {
      setBreakpoint(getBreakpoint(window.innerWidth));
    };

    // Register listener
    window.addEventListener('resize', handleResize);
    
    // Cleanup function
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return breakpoint;
}

// Function to determine breakpoint from width
function getBreakpoint(width: number): keyof typeof Breakpoint {
  if (width >= Breakpoint.xxl) return 'xxl';
  if (width >= Breakpoint.xl) return 'xl';
  if (width >= Breakpoint.lg) return 'lg';
  if (width >= Breakpoint.md) return 'md';
  if (width >= Breakpoint.sm) return 'sm';
  return 'xs';
}

// Utility function to return responsive values
type ResponsiveValues<T> = {
  [key in keyof typeof Breakpoint]?: T;
} & {
  default: T;
};

export function getResponsiveValue<T>(
  values: ResponsiveValues<T>,
  currentBreakpoint: keyof typeof Breakpoint
): T {
  // Return the value of the largest breakpoint less than or equal to the current breakpoint
  const breakpoints: (keyof typeof Breakpoint)[] = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
  const currentIndex = breakpoints.indexOf(currentBreakpoint);
  
  // Search in descending order from current breakpoint
  for (let i = currentIndex; i >= 0; i--) {
    const bp = breakpoints[i];
    if (values[bp] !== undefined) {
      return values[bp] as T;
    }
  }
  
  // Return default value if not found
  return values.default;
}

// Hook for adjusting react-flow canvas size
export function useResponsiveFlowDimensions() {
  const breakpoint = useBreakpoint();
  
  // Adjust flow canvas size based on breakpoint
  const nodePadding = getResponsiveValue<number>(
    {
      xs: 5,
      sm: 10,
      md: 15,
      lg: 20,
      default: 20
    },
    breakpoint
  );

  const nodeSpacing = getResponsiveValue<number>(
    {
      xs: 30,
      sm: 40,
      md: 50,
      lg: 60,
      default: 60
    },
    breakpoint
  );
  
  const miniMapVisible = getResponsiveValue<boolean>(
    {
      xs: false,
      sm: false,
      md: true,
      default: true
    },
    breakpoint
  );
  
  const controlsStyle = getResponsiveValue<React.CSSProperties>(
    {
      xs: { right: '5px', bottom: '5px', transform: 'scale(0.8)' },
      sm: { right: '10px', bottom: '10px', transform: 'scale(0.9)' },
      md: { right: '10px', bottom: '10px' },
      default: { right: '10px', bottom: '10px' }
    },
    breakpoint
  );
  
  const miniMapStyle = getResponsiveValue<React.CSSProperties>(
    {
      xs: { display: 'none' },
      sm: { display: 'none' },
      md: { right: 10, top: 10, width: 120, height: 80 },
      lg: { right: 10, top: 10, width: 150, height: 100 },
      default: { right: 10, top: 10, width: 200, height: 120 }
    },
    breakpoint
  );

  return {
    nodePadding,
    nodeSpacing,
    miniMapVisible,
    controlsStyle,
    miniMapStyle,
    currentBreakpoint: breakpoint
  };
} 
```

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

```typescript
import { z } from 'zod';
import { Tool, createErrorResponse, UpdateStatusInput } from '../common.js';
import { workPlan } from '../../index.js';
import logger from '../../utils/logger.js';
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';

export const UPDATE_STATUS_TOOL: Tool<{
  prIndex: z.ZodNumber;
  commitIndex: z.ZodNumber;
  status: z.ZodEnum<["not_started", "in_progress", "user_review", "completed", "cancelled", "needsRefinment"]>;
  goal?: z.ZodOptional<z.ZodString>;
  developerNote?: z.ZodOptional<z.ZodString>;
}> = {
  name: "update",
  description: `
    A tool for updating the status and goals of development tasks.
    **IMPORTANT**: 
      - There is always exactly one task in either the in_progress or user_review or needsRefinment state.
      - When setting status to "user_review", you MUST generate a review request message to the user.
      - Always check and follow **task-status-update-rule.mdc** when updating task status.
    
    **After receiving the tool response, you MUST**::
    - 1. track the current whole status of the workplan.
    - 2. check the detailed information including developer notes in the next task.
  `,
  schema: {
    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."),
    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."),
    status: z.enum(["not_started", "in_progress", "user_review", "completed", "cancelled", "needsRefinment"])
      .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)."),
    goal: z.string().min(1, "Goal must be a non-empty string").optional()
      .describe("New goal description for the commit. Optional - only provide if you want to change the commit goal."),
    developerNote: z.string().optional()
      .describe("Developer implementation notes. Can be added to both PRs and commits to document important implementation details.")
  },
  handler: async (params, extra: RequestHandlerExtra) => {
    try {
      logger.info(`Update tool called for PR #${params.prIndex}, commit #${params.commitIndex}, status: ${params.status}`);
      
      if (!workPlan) {
        logger.error('WorkPlan instance is not available');
        return createErrorResponse('WorkPlan instance is not initialized');
      }
      
      if (!workPlan.isInitialized()) {
        logger.error('WorkPlan is not initialized');
        return createErrorResponse('WorkPlan is not ready. Server initialization incomplete.');
      }
      
      const updateParams: UpdateStatusInput = {
        prIndex: params.prIndex,
        commitIndex: params.commitIndex,
        status: params.status,
        goal: params.goal ? String(params.goal) : undefined,
        developerNote: params.developerNote ? String(params.developerNote) : undefined
      };
      
      const result = workPlan.updateStatus(updateParams);
      
      return {
        content: result.content.map(item => ({
          type: "text" as const,
          text: item.text
        })),
        isError: result.isError
      };
    } catch (error) {
      logger.logError('Error in UPDATE_STATUS tool', error);
      return createErrorResponse(error);
    }
  }
}; 
```

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

```typescript
import { useRef, useState } from 'react';
import { Handle, Position } from 'reactflow';
import { CommitStatus } from '../types';

export interface CommitNodeData {
  title: string;
  label?: string;
  status: CommitStatus;
  prIndex: number;
  commitIndex: number;
  developerNote?: string; // Developer implementation notes captured during refinement
}

const statusLabels: Record<CommitStatus, string> = {
  'not_started': 'Not Started',
  'in_progress': 'In Progress',
  'completed': 'Completed',
  'cancelled': 'Cancelled',
  'needsRefinment': 'Needs Refinement',
  'user_review': 'Awaiting User Review'
};

const statusIcons: Record<CommitStatus, string> = {
  'not_started': '⚪',
  'in_progress': '🔵',
  'completed': '✅',
  'cancelled': '❌',
  'needsRefinment': '🔄',
  'user_review': '👀'
};

const statusColors: Record<CommitStatus, string> = {
  'not_started': 'bg-gray-100 text-gray-600',
  'in_progress': 'bg-blue-100 text-blue-600',
  'completed': 'bg-green-100 text-green-600',
  'cancelled': 'bg-red-100 text-red-600',
  'needsRefinment': 'bg-purple-100 text-purple-600',
  'user_review': 'bg-violet-100 text-violet-600'
};

function CommitNode({ data }: { data: CommitNodeData }) {
  const nodeRef = useRef<HTMLDivElement>(null);
  const [showTooltip, setShowTooltip] = useState(false);

  const getStatusClass = (status: CommitStatus): string => {
    const baseClass = `status-badge ${status}`;
    
    // Additional style for completed commits
    if (status === 'completed') {
      return `${baseClass} completed-commit`;
    }
    
    return baseClass;
  };

  return (
    <div 
      ref={nodeRef}
      className={`commit-node transition-all relative py-3 px-4 rounded-md border shadow-sm ${
        data.status === 'completed' ? 'completed-node' : ''
      }`}
      aria-label={`Commit: ${data.title}, Status: ${statusLabels[data.status]}`}
      onMouseEnter={() => data.developerNote && setShowTooltip(true)}
      onMouseLeave={() => setShowTooltip(false)}
    >
      <Handle
        type="target"
        position={Position.Left}
        id="left"
        className="w-3 h-3 !bg-blue-500"
        aria-label="Left input point"
      />
      
      <Handle
        type="target"
        position={Position.Left}
        id="top"
        className="w-3 h-3 !bg-blue-500"
        aria-label="Top input point"
      />
      
      {/* Status Badge */}
      <div 
        className={`${getStatusClass(data.status)} inline-flex items-center px-2 py-1 rounded-full text-xs font-medium mx-auto`}
        aria-label={`Status: ${statusLabels[data.status]}`}
      >
        <span className="mr-1" aria-hidden="true">{statusIcons[data.status]}</span>
        <span>{statusLabels[data.status]}</span>
      </div>
              {/* Goal (Title) */}
              <div 
        className="text-base font-medium break-words text-center mb-2"
        aria-label="Commit goal"
      >
        {data.title}
      </div>
      
      {/* Developer Note Tooltip */}
      {showTooltip && data.developerNote && (
        <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">
          <div className="font-semibold mb-1 text-gray-700 dark:text-gray-300">Developer Note:</div>
          <div className="text-gray-600 dark:text-gray-400">{data.developerNote}</div>
          <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>
        </div>
      )}
      
      <Handle
        type="source"
        position={Position.Right}
        id="right"
        className="w-3 h-3 !bg-blue-500"
        aria-label="Right output point"
      />
      
      <Handle
        type="source"
        position={Position.Bottom}
        id="bottom"
        className="w-3 h-3 !bg-blue-500"
        aria-label="Bottom output point"
      />
    </div>
  );
}

export default CommitNode; 
```

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

```
<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
import { WorkPlan, CommitStatus } from '../types';

// Status display name mapping
const STATUS_LABELS: Record<CommitStatus, string> = {
  'not_started': 'Not Started',
  'in_progress': 'In Progress',
  'completed': 'Completed',
  'cancelled': 'Cancelled',
  'needsRefinment': 'Needs Refinement',
  'user_review': 'Waiting for User Review'
};

// Status icon mapping
const STATUS_ICONS: Record<CommitStatus, string> = {
  'not_started': '⚪',
  'in_progress': '🔄',
  'completed': '✅',
  'cancelled': '❌',
  'needsRefinment': '⚠️',
  'user_review': '👀'
};

/**
 * Export WorkPlan as JSON
 * @param workplan The workplan to export
 * @returns Formatted JSON string
 */
export const exportToJSON = (workplan: WorkPlan): string => {
  return JSON.stringify(workplan, null, 2);
};

/**
 * Export WorkPlan as Markdown
 * @param workplan The workplan to export
 * @returns Markdown formatted string
 */
export const exportToMarkdown = (workplan: WorkPlan): string => {
  let markdown = `# ${workplan.goal}\n\n`;
  
  markdown += `Created: ${new Date().toLocaleString()}\n\n`;
  
  const totalPRs = workplan.prPlans.length;
  const completedPRs = workplan.prPlans.filter(pr => 
    pr.status === 'completed' || 
    pr.commitPlans.every(commit => commit.status === 'completed')
  ).length;
  
  const totalCommits = workplan.prPlans.reduce((total, pr) => total + pr.commitPlans.length, 0);
  const completedCommits = workplan.prPlans.reduce((total, pr) => 
    total + pr.commitPlans.filter(commit => commit.status === 'completed').length, 
  0);
  
  markdown += `## Progress Summary\n\n`;
  markdown += `- PR Progress: ${completedPRs}/${totalPRs} (${Math.round(completedPRs / totalPRs * 100)}%)\n`;
  markdown += `- Commit Progress: ${completedCommits}/${totalCommits} (${Math.round(completedCommits / totalCommits * 100)}%)\n\n`;
  
  workplan.prPlans.forEach((pr, prIndex) => {
    const prStatus = pr.status || 
      (pr.commitPlans.every(commit => commit.status === 'completed') ? 'completed' : 
      pr.commitPlans.some(commit => commit.status === 'in_progress') ? 'in_progress' : 'not_started');
    
    const prIcon = STATUS_ICONS[prStatus as CommitStatus] || '';
    
    markdown += `## PR ${prIndex + 1}: ${pr.goal} ${prIcon}\n\n`;
    
    pr.commitPlans.forEach((commit, commitIndex) => {
      const status = commit.status || 'not_started';
      const statusIcon = STATUS_ICONS[status];
      const statusLabel = STATUS_LABELS[status];
      
      markdown += `### Commit ${commitIndex + 1}: ${statusIcon} ${commit.goal}\n`;
      markdown += `Status: ${statusLabel}\n\n`;
    });
    
    markdown += '\n';
  });
  
  return markdown;
};

/**
 * Export WorkPlan as CSV
 * @param workplan The workplan to export
 * @returns CSV formatted string
 */
export const exportToCSV = (workplan: WorkPlan): string => {
  let csv = 'PR Number,PR Goal,PR Status,Commit Number,Commit Goal,Commit Status\n';
  
  workplan.prPlans.forEach((pr, prIndex) => {
    const prStatus = pr.status || 
      (pr.commitPlans.every(commit => commit.status === 'completed') ? 'completed' : 
      pr.commitPlans.some(commit => commit.status === 'in_progress') ? 'in_progress' : 'not_started');
    
    pr.commitPlans.forEach((commit, commitIndex) => {
      const status = commit.status || 'not_started';
      const statusLabel = STATUS_LABELS[status];
      
      const escapeCsv = (text: string) => `"${text.replace(/"/g, '""')}"`;
      
      csv += `${prIndex + 1},${escapeCsv(pr.goal)},${STATUS_LABELS[prStatus as CommitStatus] || ''},`;
      csv += `${commitIndex + 1},${escapeCsv(commit.goal)},${statusLabel}\n`;
    });
  });
  
  return csv;
};

/**
 * Download file to the user's device
 * @param content File content
 * @param fileName File name
 * @param contentType Content type
 */
export const downloadFile = (content: string, fileName: string, contentType: string): void => {
  const a = document.createElement('a');
  const file = new Blob([content], { type: contentType });
  
  a.href = URL.createObjectURL(file);
  a.download = fileName;
  a.click();
  
  URL.revokeObjectURL(a.href);
};

/**
 * Download WorkPlan as JSON
 * @param workplan The workplan to export
 * @param fileName File name (default: workplan.json)
 */
export const downloadAsJSON = (workplan: WorkPlan, fileName: string = 'workplan.json'): void => {
  const json = exportToJSON(workplan);
  downloadFile(json, fileName, 'application/json');
};

/**
 * Download WorkPlan as Markdown
 * @param workplan The workplan to export
 * @param fileName File name (default: workplan.md)
 */
export const downloadAsMarkdown = (workplan: WorkPlan, fileName: string = 'workplan.md'): void => {
  const markdown = exportToMarkdown(workplan);
  downloadFile(markdown, fileName, 'text/markdown');
};

/**
 * Download WorkPlan as CSV
 * @param workplan The workplan to export
 * @param fileName File name (default: workplan.csv)
 */
export const downloadAsCSV = (workplan: WorkPlan, fileName: string = 'workplan.csv'): void => {
  const csv = exportToCSV(workplan);
  downloadFile(csv, fileName, 'text/csv');
}; 
```

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

```typescript
interface RequestExtra {
  readonly context?: {
    [key: string]: unknown;
  };
  readonly [key: string]: unknown;
}

// hope that the cursor supports the prompt as an mcp client
// alternative, you can include this in your .mdc
export const taskPlanningGuide = {
  name: "task-planning-guide",
  description: "A comprehensive guide for planning development tasks with minimal PRs and commits. Helps structure work before using the plan tool.",
  handler: () => ({
    description: "Task planning guide for minimal PRs and commits",
    messages: [
      {
        role: "user" as const,
        content: {
          type: "text" as const,
          text: `
# Task Planning Guide

## Purpose
- To design a structured implementation approach before any coding begins
- To ensure clear understanding of requirements and dependencies
- To divide tickets into small PRs and commits for easier review and integration

## Output
- A comprehensive work plan organized by PRs and commits
- Each PR should be small, functional, and independently reviewable
- Each commit should represent an atomic change within its PR
- To minimize dependencies between PRs, ideally making them independently implementable

## Method
### Analysis Phase
- Analyze the task's architecture requirements thoroughly
- Explore and read all potentially relevant existing code and similar reference code
- Develop a meta architecture plan for the solution

### Planning Approach
- Break down the task into smallest possible PRs
- Order PRs logically (e.g., setup → core logic → tests)
- Define 2-4 minimal atomic commits for each PR
- Prefer splitting tasks into PRs rather than large commits

### Implementation Criteria
- Only proceed after achieving 100% clarity and confidence
- Ensure all affected parts of the codebase have been fully identified through dependency tracing
- **The work plan must be approved by the user before implementation**

## Prohibited Actions
- **Any implementation or code writing, even "example code"**
          `
        }
      }
    ]
  })
};


// internal prompt
export const progressInstructionGuide = {
  name: "progressInstructionGuide",
  description: "Based on this progress report, analyze the current state and suggest the next commit to work on.",
  text: `
  Based on this progress report, analyze the current state and suggest the next commit to work on.
  **Next, you must strictly follow these procedures without exception**:
1. Before starting your next commit, obtain the user's approval.  
2. Once approved, implement the code and test scripts strictly within this commit's scope.  
   - Make sure there are no build errors or test failures.  
   - If a UI is involved, verify its behavior using mcp playwright.  
3. After completing the commit:  
   - Review and evaluate the implementation, and conduct a self-feedback cycle.  
   - Request feedback from the user.

  **User Review State Procedures**:
  When setting a task status to "user_review":
  1. Create a comprehensive review request message that includes:
     - A clear summary of the implementation derived from PR/commit goals and developer notes
     - Specific changes made and their intended functionality
     - Areas that particularly need user verification
     - Clear instructions for the user to approve (change status to "completed") or request changes
  2. Format the review request message in a structured way with clear section headings
  3. Remember that tasks cannot transition directly from other states to "completed" - they must go through "user_review" first
  4. Only users can transition tasks from "user_review" to "completed" after their review

  **Always secure the user's agreement before starting the next task.**
`};

// alternative, you can include this in your .mdc
export const updateTaskStatusRule= {
  name: "update-task-status-rule",
  description: "Always When updating the status of a task",
  text: `
    **STRICT RULES - MUST BE FOLLOWED WITHOUT EXCEPTION:**

    in_progress → user_review conditions:
    ✅ No compilation errors exist
    ✅ Necessary tests have been added and all pass
    ✅ Required documentation updates are completed

    needsRefinment → in_progress conditions:
    ✅ Requirements have been sufficiently clarified and ready for implementation
    ✅ All necessary information for implementation is available
    ✅ The scope of the task is clearly defined

    user_review → in_progress conditions:
    ✅ From feedback, the content to be modified is completely clear

    * → needsRefinment conditions:
    ✅ It becomes apparent that the task requirements are unclear or incomplete

    * → cancelled conditions:
    ✅ There is a clear reason why the task is no longer needed, or alternative methods or solutions to meet the requirements are clear
    ✅ The impact of cancellation on other related tasks has been evaluated
`};

// alternative, you can include this in your .mdc
export const solutionExplorationGuide= {
  name: "solution-exploration-guide",
  description: "When examining how to implement an issue",
  text: `
    ## Purpose
    - nformation gathering and Brainstorming potential approaches

    ## Forbidden: 
    - Concrete planning, implementation details, or any code writing

    ##output-format
    - file: ticket_[issue name].md
    - chapters:
        - Summary of the issue that will be resolved with the ticket
        - Overview of the architecture, domain model and data model to be changed or added this time
        - Work plan based on PR and commitment units(This is only for creating chapters, do not write content)

    ## Method
    - Understand the issue from the given information and existing code
    - research and interviews with users to ensure that we have all the information we need to complete the task
    - Analyze potential impacts to the existing codebase
`};
```

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

```typescript
import { z } from 'zod';
import { Tool, createErrorResponse, PlanTaskInput } from '../common.js';
import { workPlan } from '../../index.js';
import logger from '../../utils/logger.js';
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';

// Improved error messages with guidance
const goalLengthErrorMessage = "Goal must be at most 60 characters. Consider moving detailed information to the developerNote field.";
const developerNoteLengthErrorMessage = "Developer note must be at most 300 characters. Consider breaking it into smaller, focused notes.";

export const PLAN_TOOL: Tool<{
  goal: z.ZodString;
  prPlans: z.ZodArray<z.ZodObject<{
    goal: z.ZodString;
    commitPlans: z.ZodArray<z.ZodObject<{
      goal: z.ZodString;
      developerNote?: z.ZodOptional<z.ZodString>;
    }>>;
    developerNote?: z.ZodOptional<z.ZodString>;
  }>>;
  needsMoreThoughts: z.ZodOptional<z.ZodBoolean>;
}> = {
  name: "plan",
  description: `
    A tool for managing a development work plan for a ticket, organized by PRs and commits.
    Register or update the whole work plan for the current ticket you assigned to.

    Before using this tool, you **MUST**:
    - Understand requirements, goals, and specifications.
    - Clarify the task scope and break it down into a hierarchy of PRs and commit plans.
    - Analyze existing codebase and impact area.
    - Include developer notes to document implementation considerations discovered during refinement.
    
    Make sure PR and commit goals are clear enough to stand alone without their developer notes
  `,
  schema: {
    goal: z.string().min(1, "Goal must be a non-empty string").max(60, goalLengthErrorMessage)
      .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."),
    prPlans: z.array(z.object({
      goal: z.string().min(1, "PR goal must be a non-empty string").max(60, goalLengthErrorMessage)
        .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."),
      commitPlans: z.array(z.object({
        goal: z.string().min(1, "Commit goal must be a non-empty string").max(60, goalLengthErrorMessage)
          .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."),
        developerNote: z.string().max(300, developerNoteLengthErrorMessage).optional()
          .describe("Developer implementation notes for this commit. Use this field for detailed HOW information and implementation considerations discovered during refinement.")
      })).min(1, "At least one commit plan is required").describe("Array of commit plans for this PR. Each commit should be small and atomic."),
      developerNote: z.string().max(300, developerNoteLengthErrorMessage).optional()
        .describe("Developer implementation notes for this PR. Use this field for detailed implementation considerations and technical context discovered during refinement.")
    })).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."),
    needsMoreThoughts: z.boolean().optional().describe("Whether this plan might need further refinement. Set to true if you think the plan may need changes.")
  },
  handler: async (params, extra: RequestHandlerExtra) => {
    try {
      logger.info(`Plan tool called with goal: ${params.goal}, PRs: ${params.prPlans.length}`);
      
      if (!workPlan) {
        logger.error('WorkPlan instance is not available');
        return createErrorResponse('WorkPlan instance is not initialized', 'plan');
      }
      
      if (!workPlan.isInitialized()) {
        logger.error('WorkPlan is not initialized');
        return createErrorResponse('WorkPlan is not ready. Server initialization incomplete.', 'plan');
      }
      
      const planParams: PlanTaskInput = {
        goal: params.goal,
        prPlans: params.prPlans.map(pr => ({
          goal: pr.goal,
          commitPlans: pr.commitPlans.map(commit => ({
            goal: commit.goal,
            developerNote: commit.developerNote ? String(commit.developerNote) : undefined
          })),
          developerNote: pr.developerNote ? String(pr.developerNote) : undefined
        })),
        needsMoreThoughts: params.needsMoreThoughts
      };
      
      const result = workPlan.plan(planParams);
      
      return {
        content: result.content.map(item => ({
          type: "text" as const,
          text: item.text
        })),
        isError: result.isError
      };
    } catch (error) {
      // Enhance error handling with guidance
      const errorMessage = error instanceof Error ? error.message : String(error);
      
      // Detect specific validation errors and enhance with guidance
      let enhancedError = errorMessage;
      if (errorMessage.includes("too_big") && errorMessage.includes("goal")) {
        enhancedError = `Validation Error: Goal field exceeds maximum length of 60 characters. 
Please move implementation details to the developerNote field and keep goal fields concise.
- Goal fields should describe WHAT will be done (the end result)
- Developer notes should explain HOW it will be done (implementation details)`;
      } else if (errorMessage.includes("too_big") && errorMessage.includes("developerNote")) {
        enhancedError = `Validation Error: Developer note exceeds maximum length of 300 characters.
Please break down the information into smaller, more focused notes or consider creating separate commits for different aspects.`;
      }
      
      logger.logError(`Error in PLAN tool`, error);
      return createErrorResponse(enhancedError, 'plan');
    }
  }
}; 
```

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

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  /* ライトモード(デフォルト)の変数 */
  --background-color: #f8f8f8;
  --text-color: #1a1a1a;
  --node-bg: #ffffff;
  --node-border: #e2e8f0;
  --node-selected: #d4e6ff;
  --node-hover: #f0f7ff;
  --pr-node-bg: #ebf4ff;
  --commit-node-bg: #fff;
  --start-node-bg: #e6fffa;
  --edge-color: #64748b;
  --panel-bg: #ffffff;
  --panel-shadow: rgba(0, 0, 0, 0.15);
  --filter-button-bg: #f1f5f9;
  --filter-button-hover: #e2e8f0;
  --status-not-started-bg: #f1f5f9;
  --status-in-progress-bg: #eff6ff;
  --status-completed-bg: #f0fdf4;
  --status-cancelled-bg: #fef2f2;
  --status-blocked-bg: #fff7ed;
  --status-needs-refinement-bg: #fdf4ff;
  --status-user-review-bg: #f3e8ff;
  --input-bg: #ffffff;
  --input-border: #e2e8f0;
  --input-focus-border: #3b82f6;
  --button-primary-bg: #3b82f6;
  --button-primary-hover: #2563eb;
  --button-secondary-bg: #64748b;
  --button-secondary-hover: #475569;
  --button-danger-bg: #ef4444;
  --button-danger-hover: #dc2626;
}

/* ダークモードの変数 */
.dark-mode {
  --background-color: #1a1a1a;
  --text-color: #f0f0f0;
  --node-bg: #2d3748;
  --node-border: #4a5568;
  --node-selected: #3b4858;
  --node-hover: #4a5568;
  --pr-node-bg: #2c3e50;
  --commit-node-bg: #2d3748;
  --edge-color: #a0aec0;
  --panel-bg: #2d3748;
  --panel-shadow: rgba(0, 0, 0, 0.3);
  --filter-button-bg: #4a5568;
  --filter-button-hover: #2d3748;
  --status-not-started-bg: #374151;
  --status-in-progress-bg: #1e3a8a;
  --status-completed-bg: #065f46;
  --status-cancelled-bg: #7f1d1d;
  --status-blocked-bg: #783c00;
  --status-needs-refinement-bg: #701a75;
  --status-user-review-bg: #701a75;
  --input-bg: #4a5568;
  --input-border: #64748b;
  --input-focus-border: #60a5fa;
  --button-primary-bg: #3b82f6;
  --button-primary-hover: #1d4ed8;
  --button-secondary-bg: #64748b;
  --button-secondary-hover: #475569;
  --button-danger-bg: #ef4444;
  --button-danger-hover: #b91c1c;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
  background-color: var(--background-color);
  color: var(--text-color);
  transition: color 0.2s, background-color 0.2s;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}

/* 全幅表示用のスタイル */
#root {
  width: 100%;
  height: 100vh;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

/* ReactFlowのカスタマイズ */



.animate-pulse {
  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

/* ReactFlow ダークモード対応 */
.dark-mode .react-flow__node {
  background-color: var(--node-bg);
  color: var(--text-color);
  border-color: var(--node-border);
}

.dark-mode .react-flow__edge-path {
  stroke: var(--edge-color);
}

.dark-mode .react-flow__controls {
  background-color: var(--panel-bg);
  box-shadow: 0 0 2px 1px var(--panel-shadow);
}

.dark-mode .react-flow__controls-button {
  background-color: var(--filter-button-bg);
  color: var(--text-color);
  border-color: var(--node-border);
}

.dark-mode .react-flow__controls-button:hover {
  background-color: var(--filter-button-hover);
}

.dark-mode .react-flow__background {
  background-color: var(--background-color);
}

.dark-mode .react-flow__attribution {
  background-color: var(--panel-bg);
  color: var(--text-color);
}

.dark-mode .react-flow__handle {
  background-color: var(--button-primary-bg);
  border-color: var(--node-border);
}

.dark-mode .react-flow__edge-text {
  fill: var(--text-color);
}

.dark-mode .react-flow__minimap {
  background-color: var(--panel-bg);
}

.dark-mode .react-flow__minimap-mask {
  fill: var(--panel-bg);
}

.dark-mode .react-flow__minimap-node {
  fill: var(--node-bg);
  stroke: var(--node-border);
}

/* コンポーネントのダークモード対応 */
.dark-mode button {
  background-color: var(--button-secondary-bg);
  color: white;
}

.dark-mode button:hover {
  background-color: var(--button-secondary-hover);
}

.dark-mode input, .dark-mode select, .dark-mode textarea {
  background-color: var(--input-bg);
  color: var(--text-color);
  border-color: var(--input-border);
}

.dark-mode input::placeholder, .dark-mode textarea::placeholder {
  color: #9ca3af;
}

.dark-mode input:focus, .dark-mode select:focus, .dark-mode textarea:focus {
  border-color: var(--input-focus-border);
  outline: none;
  box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
}

/* 特定のコンポーネントスタイル */
.dark-mode .filter-panel {
  background-color: var(--panel-bg);
  box-shadow: 0 4px 6px var(--panel-shadow);
}

.dark-mode .export-panel {
  background-color: var(--panel-bg);
  box-shadow: 0 4px 6px var(--panel-shadow);
}

/* Node 特定のスタイル */
.dark-mode .start-node {
  background-color: var(--start-node-bg);
}

.dark-mode .pr-node {
  background-color: var(--pr-node-bg);
}

.dark-mode .commit-node {
  background-color: var(--commit-node-bg);
}

/* ステータスラベルのダークモード対応 */
.dark-mode .status-badge.not_started {
  background-color: var(--status-not-started-bg);
}

.dark-mode .status-badge.in_progress {
  background-color: var(--status-in-progress-bg);
}

.dark-mode .status-badge.completed {
  background-color: var(--status-completed-bg);
}

.dark-mode .status-badge.cancelled {
  background-color: var(--status-cancelled-bg);
}

.dark-mode .status-badge.blocked {
  background-color: var(--status-blocked-bg);
}

.dark-mode .status-badge.needsRefinment {
  background-color: var(--status-needs-refinement-bg);
}

.dark-mode .status-badge.user_review {
  background-color: var(--status-user-review-bg);
}

/* 一般的なトランジション */
.transition-all {
  transition: all 0.2s ease;
}

/* アクセシビリティ */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}

/* フォーカス可視性 */
:focus-visible {
  outline: 2px solid var(--input-focus-border);
  outline-offset: 2px;
}

/* ダークモードでのスクロールバーカスタマイズ */
.dark-mode::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

.dark-mode::-webkit-scrollbar-track {
  background: #2d3748;
}

.dark-mode::-webkit-scrollbar-thumb {
  background-color: #4a5568;
  border-radius: 4px;
}

.dark-mode::-webkit-scrollbar-thumb:hover {
  background-color: #a0aec0;
}

/* レスポンシブメディアクエリ */
@media (max-width: 768px) {
  button, input, select {
    font-size: 14px;
  }
  
  .dark-mode .react-flow__controls {
    transform: scale(0.8);
    transform-origin: bottom right;
  }
}

/* ステータスバッジのデフォルトスタイル - ライトモード */
.status-badge.not_started {
  background-color: var(--status-not-started-bg);
}

.status-badge.in_progress {
  background-color: var(--status-in-progress-bg);
}

.status-badge.completed {
  background-color: var(--status-completed-bg);
}

.status-badge.cancelled {
  background-color: var(--status-cancelled-bg);
}

.status-badge.blocked {
  background-color: var(--status-blocked-bg);
}

.status-badge.needsRefinment {
  background-color: var(--status-needs-refinement-bg);
}

.status-badge.user_review {
  background-color: var(--status-user-review-bg);
}

/* ステータスバッジのダークモードスタイル */
.dark-mode .status-badge.not_started {
  background-color: var(--status-not-started-bg);
}

```

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

```typescript
import { useState, useRef, useEffect } from 'react';
import { WorkPlan } from '../types';
import { 
  downloadAsJSON, 
  downloadAsMarkdown, 
  downloadAsCSV 
} from '../utils/exportUtils';

interface ExportPanelProps {
  workplan: WorkPlan;
  isOpen: boolean;
  onClose: () => void;
}

const ExportPanel: React.FC<ExportPanelProps> = ({ 
  workplan, 
  isOpen,
  onClose
}) => {
  const [fileName, setFileName] = useState('workplan');
  const [isDownloading, setIsDownloading] = useState<string | null>(null);
  const panelRef = useRef<HTMLDivElement>(null);
  
  // Close when clicking outside the panel
  useEffect(() => {
    const handleOutsideClick = (event: MouseEvent) => {
      if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
        onClose();
      }
    };
    
    if (isOpen) {
      document.addEventListener('mousedown', handleOutsideClick);
    }
    
    return () => {
      document.removeEventListener('mousedown', handleOutsideClick);
    };
  }, [isOpen, onClose]);
  
  // Close panel with Esc key
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        onClose();
      }
    };
    
    if (isOpen) {
      document.addEventListener('keydown', handleKeyDown);
    }
    
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [isOpen, onClose]);
  
  if (!isOpen) return null;
  
  // Common download handler
  const handleDownload = async (format: string, downloadFn: () => void) => {
    setIsDownloading(format);
    try {
      // Add a slight delay to provide user feedback
      await new Promise(resolve => setTimeout(resolve, 300));
      downloadFn();
    } finally {
      setTimeout(() => {
        setIsDownloading(null);
      }, 500);
    }
  };
  
  // Export as JSON format
  const handleJSONExport = () => {
    handleDownload('json', () => downloadAsJSON(workplan, `${fileName}.json`));
  };
  
  // Export as Markdown format
  const handleMarkdownExport = () => {
    handleDownload('markdown', () => downloadAsMarkdown(workplan, `${fileName}.md`));
  };
  
  // Export as CSV format
  const handleCSVExport = () => {
    handleDownload('csv', () => downloadAsCSV(workplan, `${fileName}.csv`));
  };
  
  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 export-panel-container p-4">
      <div 
        ref={panelRef}
        className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md transform transition-all duration-300"
        role="dialog"
        aria-labelledby="export-panel-title"
        aria-modal="true"
      >
        <div className="flex justify-between items-center mb-4 border-b pb-3">
          <h2 
            id="export-panel-title" 
            className="text-xl font-bold text-gray-800 flex items-center"
          >
            <span className="mr-2">📊</span>
            Export
          </h2>
          <button 
            onClick={onClose}
            className="text-gray-500 hover:text-gray-700 text-xl transition-colors p-1 rounded-full hover:bg-gray-100"
            aria-label="Close"
          >
            &times;
          </button>
        </div>
        
        <div className="mb-4">
          <label 
            htmlFor="fileName" 
            className="block text-sm font-medium text-gray-700 mb-1"
          >
            Filename
          </label>
          <div className="relative">
            <input
              id="fileName"
              type="text"
              value={fileName}
              onChange={(e) => setFileName(e.target.value)}
              className="w-full p-2 pl-8 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
              placeholder="Enter filename"
              aria-describedby="filename-desc"
            />
            <div className="absolute inset-y-0 left-0 flex items-center pl-2 pointer-events-none">
              <svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                <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" />
              </svg>
            </div>
          </div>
          <p id="filename-desc" className="text-xs text-gray-500 mt-1">
            *File extension will be added automatically
          </p>
        </div>
        
        <div className="grid grid-cols-1 gap-3 mb-5">
          <button
            onClick={handleJSONExport}
            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' : ''}`}
            disabled={isDownloading !== null}
          >
            {isDownloading === 'json' ? (
              <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">
                <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                <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>
              </svg>
            ) : (
              <span className="mr-2">📄</span>
            )}
            Export as JSON
          </button>
          
          <button
            onClick={handleMarkdownExport}
            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' : ''}`}
            disabled={isDownloading !== null}
          >
            {isDownloading === 'markdown' ? (
              <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">
                <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                <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>
              </svg>
            ) : (
              <span className="mr-2">📝</span>
            )}
            Export as Markdown
          </button>
          
          <button
            onClick={handleCSVExport}
            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' : ''}`}
            disabled={isDownloading !== null}
          >
            {isDownloading === 'csv' ? (
              <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">
                <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                <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>
              </svg>
            ) : (
              <span className="mr-2">📊</span>
            )}
            Export as CSV
          </button>
        </div>
        
        <div className="bg-gray-50 p-3 rounded-md border border-gray-100">
          <h3 className="text-sm font-medium text-gray-700 mb-2">Export Format Explanation:</h3>
          <ul className="list-disc pl-5 space-y-1.5 text-xs text-gray-600">
            <li>
              <span className="font-medium text-blue-600">JSON:</span> Original data format. Ideal for reuse or import in other applications.
            </li>
            <li>
              <span className="font-medium text-purple-600">Markdown:</span> Readable document format. Can be displayed in GitHub and similar platforms.
            </li>
            <li>
              <span className="font-medium text-green-600">CSV:</span> Format for spreadsheet software. Suitable for analysis in Excel and similar tools.
            </li>
          </ul>
        </div>
        
        <div className="mt-4 flex justify-end">
          <button
            onClick={onClose}
            className="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded text-gray-700 text-sm transition-colors"
          >
            Close
          </button>
        </div>
      </div>
    </div>
  );
};

export default ExportPanel; 
```

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

```typescript
import { useState, useCallback, useMemo } from 'react';
import ReactFlow, {
  MiniMap,
  Controls,
  Background,
  useNodesState,
  useEdgesState,
  NodeTypes,
  EdgeTypes,
  Panel,
  BackgroundVariant,
  getBezierPath,
  EdgeProps,
  MarkerType,
  Position,
} from 'reactflow';
import 'reactflow/dist/style.css';
import { WorkPlan, NodeData, ExtendedNode, CommitStatus } from '../types';
import { 
  convertWorkPlanToFlow, 
  filterWorkplan,
  filterNodesAndEdges
} from '../utils/workplanConverter';
import { useResponsiveFlowDimensions } from '../utils/responsiveUtils';
import CommitNode from './CommitNode';
import { FilterOptions } from '../components/FilterPanel';
import './nodes/nodes.css';

export interface WorkplanFlowProps {
  workplan: WorkPlan;
  filterOptions: FilterOptions;
}

// Custom edge component
const CustomEdge = ({
  id,
  sourceX,
  sourceY,
  targetX,
  targetY,
  sourcePosition,
  targetPosition,
  style = {},
  data,
  markerEnd,
  animated
}: EdgeProps) => {
  // Adjust connection point calculation
  // Ensure targetPosition is set to Position.Left
  const targetPos = targetPosition || Position.Left;
  const sourcePos = sourcePosition || Position.Right;
  
  // Adjust arguments for getBezierPath
  const [edgePath, labelX, labelY] = getBezierPath({
    sourceX,
    sourceY,
    sourcePosition: sourcePos,
    targetX,
    targetY,
    targetPosition: targetPos,
    curvature: 0.4
  });

  return (
    <>
      <path
        id={id}
        style={{
          ...style,
          strokeWidth: style.strokeWidth || 2,
          stroke: style.stroke || '#555',
          transition: 'stroke 0.3s, stroke-width 0.3s',
        }}
        className={`react-flow__edge-path ${animated ? 'animated' : ''}`}
        d={edgePath}
        markerEnd={markerEnd}
      />
      {data?.label && (
        <text
          x={labelX}
          y={labelY}
          style={{
            fontSize: '10px',
            fill: '#666',
            textAnchor: 'middle',
            dominantBaseline: 'middle',
            pointerEvents: 'none',
            fontWeight: 'normal',
          }}
          className="react-flow__edge-text"
        >
          {data.label}
        </text>
      )}
    </>
  );
};

// Register custom node types
const nodeTypes: NodeTypes = {
  commitNode: CommitNode,
};

// Register custom edge types
const edgeTypes: EdgeTypes = {
  custom: CustomEdge,
};

// Status label definitions
const statusLabels: Record<CommitStatus, string> = {
  'not_started': 'Not Started',
  'in_progress': 'In Progress',
  'completed': 'Completed',
  'cancelled': 'Cancelled',
  'needsRefinment': 'Needs Refinement',
  'user_review': 'Awaiting User Review'
};


const WorkplanFlow = ({ 
  workplan, 
  filterOptions
}: WorkplanFlowProps) => {
  // Default filter options
  const defaultFilterOptions: FilterOptions = {
    statusFilter: 'all',
    searchQuery: '',
    onlyShowActive: false
  };
  
  // Get responsive settings
  const {
    miniMapVisible,
    controlsStyle,
    miniMapStyle,
    currentBreakpoint
  } = useResponsiveFlowDimensions();
  
  // Use provided filter options or default
  const activeFilterOptions = filterOptions || defaultFilterOptions;
  
  // Apply filtering
  const filteredWorkplan = useMemo(() => {
    return filterWorkplan(workplan, activeFilterOptions);
  }, [workplan, activeFilterOptions]);

  // Set initial nodes and edges (generated from filtered workplan)
  const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
    return convertWorkPlanToFlow(filteredWorkplan);
  }, [filteredWorkplan]);
  
  // Add callbacks to commit nodes
  const nodesWithCallbacks = useMemo(() => {
    return initialNodes.map(node => {
      if (node.type === 'commitNode') {
        const nodeData = node.data as any; // Temporarily handle as any
        return {
          ...node,
          data: {
            ...nodeData,
            // Fallback if data doesn't have necessary properties
            title: nodeData.title || nodeData.label || '',
            status: nodeData.status || 'not_started'
          }
        };
      }
      return node;
    });
  }, [initialNodes]);
  
  // Change edge type to custom
  const customEdges = useMemo(() => {
    return initialEdges.map(edge => ({
      ...edge,
      type: 'custom',
      data: { label: edge.label },
      markerEnd: {
        type: MarkerType.ArrowClosed,
        color: edge.style?.stroke || '#555',
      },
    }));
  }, [initialEdges]);
  
  // Apply filtering directly to nodes and edges
  const { nodes: filteredNodes, edges: filteredEdges } = useMemo(() => {
    return filterNodesAndEdges(nodesWithCallbacks, customEdges, activeFilterOptions);
  }, [nodesWithCallbacks, customEdges, activeFilterOptions]);
  
  // Use handlers for interaction only
  const [, , onNodesChange] = useNodesState([]);
  const [, , onEdgesChange] = useEdgesState([]);
  
  // Selected node information
  const [selectedNode, setSelectedNode] = useState<ExtendedNode<NodeData> | null>(null);

  // Handler for node selection
  const onNodeClick = useCallback((event: React.MouseEvent, node: ExtendedNode) => {
    setSelectedNode(node as ExtendedNode<NodeData>);
  }, []);
  
  // Handler for canvas click (deselection)
  const onPaneClick = useCallback(() => {
    setSelectedNode(null);
  }, []);
  
  // Flow component style
  const proOptions = { hideAttribution: true };

  // FitView options based on current breakpoint
  const fitViewOptions = useMemo(() => ({
    padding: currentBreakpoint === 'xs' ? 0.1 : 
             currentBreakpoint === 'sm' ? 0.15 : 0.2,
    maxZoom: 1.5,
    includeHiddenNodes: false,
    minZoom: 0.2,
    alignmentX: 0.5,  // Horizontal center
    alignmentY: 0,    // Top alignment
  }), [currentBreakpoint]);

  // Initial viewport settings
  const defaultViewport = { x: 0, y: 0, zoom: 1 };

  return (
    <div style={{ width: '100%', height: '100vh', position: 'relative' }}>
      <ReactFlow
        nodes={filteredNodes}
        edges={filteredEdges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onNodeClick={onNodeClick}
        onPaneClick={onPaneClick}
        nodeTypes={nodeTypes}
        edgeTypes={edgeTypes}
        fitView
        fitViewOptions={fitViewOptions}
        defaultViewport={defaultViewport}
        proOptions={proOptions}
        minZoom={0.1}
        maxZoom={2}
      >
        {/* Display active filters - responsive */}
        {(activeFilterOptions.statusFilter !== 'all' || 
          activeFilterOptions.searchQuery.trim() || 
          activeFilterOptions.onlyShowActive) && (
          <Panel 
            position="top-left" 
            className={`bg-white p-2 rounded-lg shadow-md border border-gray-200 ${
              currentBreakpoint === 'xs' ? 'text-xs max-w-[80vw]' : ''
            }`}
          >
            <div className={`text-gray-700 ${currentBreakpoint === 'xs' ? 'text-xs' : 'text-sm'}`}>
              <span className="font-bold">Filters Applied:</span>
              <div className={`flex flex-wrap gap-1 mt-1 ${currentBreakpoint === 'xs' ? 'max-w-full' : ''}`}>
                {activeFilterOptions.statusFilter !== 'all' && (
                  <span className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-full text-xs">
                    Status: {activeFilterOptions.statusFilter}
                  </span>
                )}
                {activeFilterOptions.searchQuery.trim() && (
                  <span className="px-2 py-0.5 bg-green-100 text-green-800 rounded-full text-xs">
                    Search: {activeFilterOptions.searchQuery}
                  </span>
                )}
                {activeFilterOptions.onlyShowActive && (
                  <span className="px-2 py-0.5 bg-yellow-100 text-yellow-800 rounded-full text-xs">
                    Active Only
                  </span>
                )}
              </div>
            </div>
          </Panel>
        )}
        
        {/* Mini map - responsive */}
        {miniMapVisible && (
          <MiniMap 
            style={miniMapStyle}
            nodeStrokeWidth={3}
            zoomable
            pannable
          />
        )}
        
        {/* Controls - responsive */}
        <Controls style={controlsStyle} />
        
        <Background
          variant={BackgroundVariant.Dots}
          gap={12}
          size={1}
        />
        
        {/* Help information for mobile display */}
        {currentBreakpoint === 'xs' && (
          <Panel position="bottom-center" className="p-2 bg-white bg-opacity-80 rounded text-xs text-center">
            Pinch to zoom, swipe to move
          </Panel>
        )}
      </ReactFlow>
      
      {/* Detailed information for selected node - responsive */}
      {selectedNode && (
        <div 
          className={`absolute p-3 bg-white dark:bg-gray-800 border rounded-md shadow-lg 
            ${currentBreakpoint === 'xs' ? 'left-2 right-2 bottom-2' : 'right-4 top-4 w-64'}`}
        >
          <div className="flex justify-between mb-2">
            <h3 className="font-bold">Details</h3>
            <button 
              onClick={onPaneClick}
              className="text-gray-500 hover:text-gray-700"
            >
              ✕
            </button>
          </div>
          <div className="text-sm">
            <p><strong>Title:</strong> {selectedNode.data.label}</p>
            <p><strong>Status:</strong> {selectedNode.data.status && statusLabels[selectedNode.data.status]}</p>
            {selectedNode.data.description && (
              <p><strong>Description:</strong> {selectedNode.data.description}</p>
            )}
          </div>
        </div>
      )}
    </div>
  );
};

export default WorkplanFlow; 
```

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

```typescript
import { useState, useEffect, useRef, useCallback } from 'react';
import { CommitStatus } from '../types';

// Filter settings type
export interface FilterOptions {
  statusFilter: CommitStatus | 'all';
  searchQuery: string;
  onlyShowActive: boolean; // Show only active PRs/commits
}

interface FilterPanelProps {
  options: FilterOptions;
  onChange: (newOptions: FilterOptions) => void;
  isOpen: boolean;
  onClose: () => void;
}

// Status display name mapping
const STATUS_LABELS: Record<CommitStatus | 'all', string> = {
  'all': 'All',
  'not_started': 'Not Started',
  'in_progress': 'In Progress',
  'blocked': 'Blocked',
  'completed': 'Completed',
  'cancelled': 'Cancelled',
  'needsRefinment': 'Needs Refinement',
  'user_review': 'Awaiting User Review'
};

// Status icon mapping
const STATUS_ICONS: Record<CommitStatus | 'all', string> = {
  'all': '🔍',
  'not_started': '⚪',
  'in_progress': '🔄',
  'blocked': '⛔',
  'completed': '✅',
  'cancelled': '❌',
  'needsRefinment': '⚠️',
  'user_review': '👀'
};

// Pre-generate status options
const STATUS_OPTIONS = Object.entries(STATUS_LABELS).map(([status, label]) => (
  <option key={status} value={status}>
    {STATUS_ICONS[status as CommitStatus | 'all']} {label}
  </option>
));

const FilterPanel: React.FC<FilterPanelProps> = ({ 
  options, 
  onChange,
  isOpen,
  onClose
}) => {
  const [localOptions, setLocalOptions] = useState<FilterOptions>(options);
  const panelRef = useRef<HTMLDivElement>(null);
  
  // Update internal state when options from props change
  useEffect(() => {
    setLocalOptions(options);
  }, [options]);
  
  // Change handler (memoized)
  const handleChange = useCallback((key: keyof FilterOptions, value: any) => {
    const newOptions = { ...localOptions, [key]: value };
    setLocalOptions(newOptions);
    onChange(newOptions);
  }, [localOptions, onChange]);
  
  // Reset handler (memoized)
  const handleReset = useCallback(() => {
    const defaultOptions: FilterOptions = {
      statusFilter: 'all',
      searchQuery: '',
      onlyShowActive: false
    };
    setLocalOptions(defaultOptions);
    onChange(defaultOptions);
  }, [onChange]);
  
  // Close when clicking outside the panel
  useEffect(() => {
    if (!isOpen) return;
    
    const handleOutsideClick = (event: MouseEvent) => {
      if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
        onClose();
      }
    };
    
    document.addEventListener('mousedown', handleOutsideClick);
    return () => document.removeEventListener('mousedown', handleOutsideClick);
  }, [isOpen, onClose]);
  
  // Close panel with Esc key
  useEffect(() => {
    if (!isOpen) return;
    
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        onClose();
      }
    };
    
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [isOpen, onClose]);
  
  if (!isOpen) return null;

  return (
    <div 
      ref={panelRef}
      className="bg-white rounded-lg shadow-lg p-4 border border-gray-200 transition-all duration-300"
    >
      <div className="flex justify-between items-center mb-4">
        <h3 className="text-lg font-semibold text-gray-800 flex items-center">
          <span className="mr-2">🔍</span>
          Filter Settings
        </h3>
        <button 
          onClick={onClose}
          className="text-gray-500 hover:text-gray-700 text-xl transition-colors p-1 rounded-full hover:bg-gray-100"
          aria-label="Close"
        >
          &times;
        </button>
      </div>
      
      <div className="space-y-4">
        {/* Status filter */}
        <div className="transition-all duration-200 hover:shadow-sm">
          <label className="block text-sm font-medium text-gray-700 mb-1">
            Status
          </label>
          <div className="relative">
            <select
              value={localOptions.statusFilter}
              onChange={(e) => handleChange('statusFilter', e.target.value)}
              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"
            >
              {STATUS_OPTIONS}
            </select>
            <div className="absolute inset-y-0 left-0 flex items-center pl-2 pointer-events-none">
              {STATUS_ICONS[localOptions.statusFilter as CommitStatus | 'all']}
            </div>
            <div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
              <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">
                <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" />
              </svg>
            </div>
          </div>
        </div>
        
        {/* Search filter */}
        <div className="transition-all duration-200 hover:shadow-sm">
          <label className="block text-sm font-medium text-gray-700 mb-1">
            Search
          </label>
          <div className="relative">
            <input
              type="text"
              value={localOptions.searchQuery}
              onChange={(e) => handleChange('searchQuery', e.target.value)}
              placeholder="Search in commit content..."
              className="w-full p-2 pl-8 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
            <div className="absolute inset-y-0 left-0 flex items-center pl-2 pointer-events-none">
              <svg className="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                <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" />
              </svg>
            </div>
            {localOptions.searchQuery && (
              <button
                onClick={() => handleChange('searchQuery', '')}
                className="absolute inset-y-0 right-0 flex items-center pr-2 text-gray-400 hover:text-gray-600"
                aria-label="Clear search"
              >
                <svg className="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                  <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" />
                </svg>
              </button>
            )}
          </div>
        </div>
        
        {/* Show only active items */}
        <div className="flex items-center p-2 hover:bg-gray-50 rounded-md transition-colors">
          <input
            type="checkbox"
            id="onlyActive"
            checked={localOptions.onlyShowActive}
            onChange={(e) => handleChange('onlyShowActive', e.target.checked)}
            className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded transition-colors"
          />
          <label htmlFor="onlyActive" className="ml-2 block text-sm text-gray-700">
            Show only active tasks
          </label>
        </div>
        
        {/* Filter badge display */}
        {(localOptions.statusFilter !== 'all' || 
         localOptions.searchQuery.trim() || 
         localOptions.onlyShowActive) && (
          <div className="flex flex-wrap gap-2 pt-2 pb-3">
            <p className="w-full text-xs text-gray-500 mb-1">Active filters:</p>
            
            {localOptions.statusFilter !== 'all' && (
              <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
                {STATUS_ICONS[localOptions.statusFilter as CommitStatus]}
                <span className="ml-1">{STATUS_LABELS[localOptions.statusFilter as CommitStatus]}</span>
                <button
                  onClick={() => handleChange('statusFilter', 'all')}
                  className="ml-1 text-blue-500 hover:text-blue-700"
                >
                  ×
                </button>
              </span>
            )}
            
            {localOptions.searchQuery.trim() && (
              <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
                🔍 {localOptions.searchQuery}
                <button
                  onClick={() => handleChange('searchQuery', '')}
                  className="ml-1 text-green-500 hover:text-green-700"
                >
                  ×
                </button>
              </span>
            )}
            
            {localOptions.onlyShowActive && (
              <span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
                🔄 Active Only
                <button
                  onClick={() => handleChange('onlyShowActive', false)}
                  className="ml-1 text-yellow-500 hover:text-yellow-700"
                >
                  ×
                </button>
              </span>
            )}
          </div>
        )}
        
        {/* Reset button */}
        <div className="pt-2 flex justify-end">
          <button
            onClick={handleReset}
            className="px-3 py-1.5 bg-gray-200 hover:bg-gray-300 rounded text-gray-700 text-sm transition-colors flex items-center"
          >
            <svg className="w-4 h-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
              <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" />
            </svg>
            Reset
          </button>
        </div>
      </div>
    </div>
  );
};

export default FilterPanel; 
```

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

```typescript
import { Edge, Position } from 'reactflow';
import { WorkPlan, CommitStatus, NodeData, ExtendedNode } from '../types';
import { CommitNodeData } from '../components/CommitNode';
import { FilterOptions } from '../components/FilterPanel';

// Function to return color based on status
export const getStatusColor = (status: CommitStatus): string => {
  switch (status) {
    case 'completed':
      return '#10b981'; // green
    case 'in_progress':
      return '#3b82f6'; // blue
    case 'cancelled':
      return '#6b7280'; // gray
    case 'needsRefinment':
      return '#f59e0b'; // orange
    case 'user_review':
      return '#9333ea'; // purple
    case 'not_started':
    default:
      return '#94a3b8'; // light gray
  }
};

// Function to return background color based on status
const getStatusBackgroundColor = (status: CommitStatus): string => {
  switch (status) {
    case 'completed':
      return '#d1fae5'; // light green
    case 'in_progress':
      return '#dbeafe'; // light blue
    case 'cancelled':
      return '#f3f4f6'; // light gray
    case 'needsRefinment':
      return '#fef3c7'; // light orange
    case 'user_review':
      return '#f3e8ff'; // light purple
    case 'not_started':
    default:
      return '#f1f5f9'; // light gray
  }
};

// Function to return label based on status
const getStatusLabel = (status: CommitStatus): string => {
  switch (status) {
    case 'completed':
      return 'Completed';
    case 'in_progress':
      return 'In Progress';
    case 'cancelled':
      return 'Cancelled';
    case 'needsRefinment':
      return 'Needs Refinement';
    case 'user_review':
      return 'Waiting for User Review';
    case 'not_started':
    default:
      return 'Not Started';
  }
};

// Function to return icon based on status
const getStatusIcon = (status: CommitStatus): string => {
  switch (status) {
    case 'completed':
      return '✅';
    case 'in_progress':
      return '🔄';
    case 'cancelled':
      return '❌';
    case 'needsRefinment':
      return '🔍';
    case 'user_review':
      return '👀';
    case 'not_started':
    default:
      return '⏳';
  }
};

// Layout constants
const DEFAULT_LAYOUT = {
  HORIZONTAL_SPACING: 550, // Horizontal spacing between PR nodes
  PR_WIDTH: 200, // Width of PR nodes
  COMMIT_WIDTH: 240, // Width of commit nodes
  COMMIT_HEIGHT: 100, // Height of commit nodes
  VERTICAL_CENTER: 100, // Vertical center position (top alignment)
  COMMIT_HORIZONTAL_OFFSET: 250, // Horizontal offset for commit nodes to the right of PR
  INITIAL_X: 100, // Initial X coordinate for the first node
};

// Interface for responsive layout options
export interface LayoutOptions {
  nodePadding?: number;
  nodeSpacing?: number;
}

/**
 * Function to convert workplan to react-flow nodes and edges
 * @param workplan Workplan
 * @param options Responsive layout options
 * @returns Object containing nodes and edges
 */
export const convertWorkPlanToFlow = (workplan: WorkPlan, options?: LayoutOptions) => {
  if (!workplan) return { nodes: [], edges: [] };
  
  // Initialize arrays for nodes and edges
  const nodes: ExtendedNode<NodeData | CommitNodeData>[] = [];
  const edges: Edge[] = [];
  
  // Apply spacing and padding factors
  const spacing = options?.nodeSpacing ?? 1;
  const padding = options?.nodePadding ?? 1;
  
  // Calculate responsive layout values
  const layout = {
    HORIZONTAL_SPACING: DEFAULT_LAYOUT.HORIZONTAL_SPACING * spacing,
    PR_WIDTH: DEFAULT_LAYOUT.PR_WIDTH * padding,
    COMMIT_WIDTH: DEFAULT_LAYOUT.COMMIT_WIDTH * padding,
    COMMIT_HEIGHT: DEFAULT_LAYOUT.COMMIT_HEIGHT,
    VERTICAL_CENTER: DEFAULT_LAYOUT.VERTICAL_CENTER,
    COMMIT_HORIZONTAL_OFFSET: DEFAULT_LAYOUT.COMMIT_HORIZONTAL_OFFSET * spacing,
    INITIAL_X: DEFAULT_LAYOUT.INITIAL_X * spacing,
  };
  
  // Generate PR nodes and commit nodes
  workplan.prPlans.forEach((pr, prIndex) => {
    // PR node
    const prId = `pr-${prIndex}`;
    const prStatus = getOverallStatus(pr.commitPlans.map(commit => commit.status));
    
    const prNode: ExtendedNode<NodeData | CommitNodeData> = {
      id: prId,
      type: 'default',
      className: 'pr-node',
      position: { 
        x: layout.INITIAL_X + layout.HORIZONTAL_SPACING * prIndex, 
        y: layout.VERTICAL_CENTER 
      },
      data: {
        label: pr.goal,
        description: `PR ${prIndex + 1}`,
        status: prStatus,
        statusLabel: getStatusLabel(prStatus),
        statusIcon: getStatusIcon(prStatus),
        developerNote: pr.developerNote
      },
      style: {
        width: layout.PR_WIDTH,
        backgroundColor: getStatusBackgroundColor(prStatus),
        borderColor: getStatusColor(prStatus),
        borderWidth: 2,
      },
      sourcePosition: Position.Right,
      targetPosition: Position.Left,
    };
    
    nodes.push(prNode);
    
    // Connect to previous PR (for second and subsequent PRs)
    if (prIndex > 0) {
      edges.push({
        id: `pr-${prIndex - 1}-to-${prId}`,
        source: `pr-${prIndex - 1}`,
        target: prId,
        animated: false,
        sourceHandle: 'right',
        targetHandle: 'left',
        style: {
          stroke: '#aaa',
          strokeWidth: 2,
        },
      });
    }
    
    // Commit node
    pr.commitPlans.forEach((commit, commitIndex) => {
      const commitId = `commit-${prIndex}-${commitIndex}`;
      const commitStatus = commit.status || 'not_started';
      
      // Calculate commit vertical position (spread out from PR center)
      const verticalOffset = (commitIndex - (pr.commitPlans.length - 1) / 2) * (layout.COMMIT_HEIGHT * 1.5);
      
      // Commit node
      const commitNode: ExtendedNode<CommitNodeData> = {
        id: commitId,
        type: 'commitNode',
        position: { 
          x: layout.INITIAL_X + layout.HORIZONTAL_SPACING * prIndex + layout.COMMIT_HORIZONTAL_OFFSET, 
          y: layout.VERTICAL_CENTER + verticalOffset 
        },
        data: {
          title: commit.goal,
          label: commit.goal,
          status: commitStatus,
          prIndex: prIndex,
          commitIndex: commitIndex,
          developerNote: commit.developerNote
        },
        style: {
          borderColor: getStatusColor(commitStatus),
          width: layout.COMMIT_WIDTH,
        },
        sourcePosition: Position.Right,
        targetPosition: Position.Left,
      };
      
      nodes.push(commitNode);
      
      // Connect commit to PR
      edges.push({
        id: `${prId}-to-${commitId}`,
        source: prId,
        target: commitId,
        animated: false,
        sourceHandle: 'right',
        targetHandle: 'left',
        label: `Commit ${commitIndex + 1}`,
        style: {
          stroke: getStatusColor(commitStatus),
          strokeWidth: 2,
        },
      });
      
      // If there's a next PR, add edge from final commit to next PR
      if (prIndex < workplan.prPlans.length - 1 && commitIndex === pr.commitPlans.length - 1) {
        edges.push({
          id: `${commitId}-to-pr-${prIndex + 1}`,
          source: commitId,
          target: `pr-${prIndex + 1}`,
          animated: false,
          sourceHandle: 'right',
          targetHandle: 'left',
          style: {
            stroke: '#aaa',
            strokeWidth: 2,
            strokeDasharray: '5, 5',
          },
        });
      }
    });
  });
  
  return { nodes, edges };
};

// Function to determine overall status of PR based on commit statuses
const getOverallStatus = (statuses: (CommitStatus | undefined)[]): CommitStatus => {
  // Treat undefined status as 'not_started'
  const definedStatuses = statuses.map(s => s || 'not_started');
  
  if (definedStatuses.every(status => status === 'completed')) {
    return 'completed';
  }
  
  if (definedStatuses.some(status => status === 'in_progress')) {
    return 'in_progress';
  }
  
  if (definedStatuses.some(status => status === 'needsRefinment')) {
    return 'needsRefinment';
  }
  
  if (definedStatuses.some(status => status === 'cancelled')) {
    return 'cancelled';
  }
  
  return 'not_started';
};

// Function to filter workplan
export const filterWorkplan = (workplan: WorkPlan, options: FilterOptions): WorkPlan => {
  if (!workplan) return { goal: '', prPlans: [] };
  
  // Return unchanged if filtering is not needed
  if (options.statusFilter === 'all' && !options.searchQuery && !options.onlyShowActive) {
    return workplan;
  }
  
  // Convert search query to lowercase
  const searchQueryLower = options.searchQuery.toLowerCase();
  
  const filteredPrPlans = workplan.prPlans
    .map(pr => {
      // Filter commits in PR
      const filteredCommits = pr.commitPlans.filter(commit => {
        // Status filter
        const statusMatch = options.statusFilter === 'all' || commit.status === options.statusFilter;
        
        // Search filter
        const searchMatch = !searchQueryLower || 
          commit.goal.toLowerCase().includes(searchQueryLower);
        
        // Active filter
        const activeMatch = !options.onlyShowActive || 
          commit.status === 'in_progress' || 
          commit.status === 'needsRefinment';
        
        return statusMatch && searchMatch && activeMatch;
      });
      
      // Return PR with filtered commits
      return {
        ...pr,
        commitPlans: filteredCommits
      };
    })
    .filter(pr => {
      // Keep PRs with non-empty commits
      return pr.commitPlans.length > 0;
    });
  
  return {
    goal: workplan.goal,
    prPlans: filteredPrPlans
  };
};

// Function to apply direct filtering to nodes and edges
export const filterNodesAndEdges = (
  nodes: ExtendedNode<NodeData | CommitNodeData>[], 
  edges: Edge[],
  options: FilterOptions
): { nodes: ExtendedNode<NodeData | CommitNodeData>[], edges: Edge[] } => {
  // Return unchanged if filtering is not needed
  if (options.statusFilter === 'all' && !options.searchQuery && !options.onlyShowActive) {
    return { nodes, edges };
  }
  
  // Convert search query to lowercase
  const searchQueryLower = options.searchQuery.toLowerCase();
  
  // Apply filtering to commit nodes
  // PR nodes are always displayed
  const filteredNodeIds = new Set();
  
  // Collect displayed node IDs
  const filteredNodes = nodes.filter(node => {
    // Always display PR nodes
    if (node.type !== 'commitNode') {
      filteredNodeIds.add(node.id);
      return true;
    }
    
    // Apply filtering to commit nodes
    const nodeData = node.data as CommitNodeData;
    
    // Status filter
    const statusMatch = options.statusFilter === 'all' || nodeData.status === options.statusFilter;
    
    // Search filter
    const searchMatch = !searchQueryLower || 
      nodeData.title.toLowerCase().includes(searchQueryLower);
    
    // Active filter
    const activeMatch = !options.onlyShowActive || 
      nodeData.status === 'in_progress' || 
      nodeData.status === 'user_review';
    
    // If all conditions match, include this node
    const shouldInclude = statusMatch && searchMatch && activeMatch;
    
    if (shouldInclude) {
      filteredNodeIds.add(node.id);
    }
    
    return shouldInclude;
  });
  
  // Display only edges connected to filtered nodes
  const filteredEdges = edges.filter(edge => 
    filteredNodeIds.has(edge.source) && filteredNodeIds.has(edge.target)
  );
  
  return { nodes: filteredNodes, edges: filteredEdges };
}; 
```

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

```typescript
import { useState, useEffect, useCallback, useRef } from 'react'
import { ReactFlowProvider } from 'reactflow'
import WorkplanFlow from './components/WorkplanFlow'
import FilterPanel, { FilterOptions } from './components/FilterPanel'
import OrientationWarning from './components/OrientationWarning'
import { WorkPlan, CommitStatus } from './types'
import { useBreakpoint } from './utils/responsiveUtils'
import './App.css'

function App() {
  // Get breakpoint
  const breakpoint = useBreakpoint();
  
  // Initialize with normalized sample data
  const [workplan, setWorkplan] = useState<WorkPlan | null>(null);
  const [lastLoadedTime, setLastLoadedTime] = useState<Date | null>(null);
  const [loadError, setLoadError] = useState<string | null>(null);
  // Polling interval (milliseconds)
  const [pollingInterval, setPollingInterval] = useState<number>(5000); // Default: 1 second
  // Whether polling is enabled
  const [pollingEnabled, setPollingEnabled] = useState<boolean>(true);
  // Loading flag
  const [isLoading, setIsLoading] = useState<boolean>(false);
  // Track last polling attempt
  const pollingTimeoutRef = useRef<number | null>(null);
  
  const [showFilterPanel, setShowFilterPanel] = useState<boolean>(false);
  const [filterOptions, setFilterOptions] = useState<FilterOptions>({
    statusFilter: 'all',
    searchQuery: '',
    onlyShowActive: false
  });
  
  // Dark mode state
  const [isDarkMode, setIsDarkMode] = useState<boolean>(() => {
    // Load settings from localStorage
    const savedMode = localStorage.getItem('darkMode');
    // Check system color scheme settings
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    
    // Use saved settings if available, otherwise follow system settings
    return savedMode !== null ? savedMode === 'true' : prefersDark;
  });

  // Separate data loading function for reusability
  const loadData = useCallback(async () => {
    if (isLoading) return; // Do nothing if already loading
    
    setIsLoading(true);
    try {
      // Get JSON file directly (add timestamp parameter to avoid cache)
      // Options to completely disable caching
      const fetchOptions = {
        method: 'GET',
        headers: {
          'Cache-Control': 'no-cache, no-store, must-revalidate',
          'Pragma': 'no-cache',
          'Expires': '0'
        }
      };
      
      const timestamp = new Date().getTime();
      const response = await fetch(`/data/workplan.json?t=${timestamp}`, fetchOptions);
      if (!response.ok) {
        throw new Error(`Failed to fetch data. Status: ${response.status}`);
      }
      
      const actualWorkPlan = await response.json();
      
      // Data structure conversion
      const convertedWorkPlan: WorkPlan = {
        goal: actualWorkPlan.currentTicket.goal,
        prPlans: actualWorkPlan.currentTicket.pullRequests.map((pr: {
          goal: string;
          status: string;
          developerNote?: string;
          commits: Array<{
            goal: string;
            status: string;
            developerNote?: string;
          }>;
        }) => ({
          goal: pr.goal,
          status: pr.status as CommitStatus,
          developerNote: pr.developerNote,
          commitPlans: pr.commits.map((commit: {
            goal: string;
            status: string;
            developerNote?: string;
          }) => ({
            goal: commit.goal,
            status: commit.status as CommitStatus,
            developerNote: commit.developerNote
          }))
        }))
      };
      
      // Apply updates
      setWorkplan(convertedWorkPlan);
      setLastLoadedTime(new Date());
      setLoadError(null);
    } catch (error) {
      console.error('Error occurred while loading data:', error);
      setLoadError('Failed to load data.');
    } finally {
      setIsLoading(false);
    }
  }, []); // Empty dependency array

  // Get data from JSON file on initial load
  useEffect(() => {
    // Initial load
    loadData();
    
    // Set up polling
    const setupPolling = () => {
      if (pollingTimeoutRef.current) {
        clearTimeout(pollingTimeoutRef.current);
        pollingTimeoutRef.current = null;
      }
      
      if (pollingEnabled && pollingInterval > 0) {
        pollingTimeoutRef.current = window.setTimeout(() => {
          // Load data, then schedule next polling
          loadData().finally(() => {
            if (pollingEnabled) {
              setupPolling();
            }
          });
        }, pollingInterval);
      }
    };
    
    // Start polling
    setupPolling();
    
    // Clean up on component unmount
    return () => {
      if (pollingTimeoutRef.current) {
        clearTimeout(pollingTimeoutRef.current);
        pollingTimeoutRef.current = null;
      }
    };
  }, [loadData, pollingEnabled, pollingInterval]);
  
  // Add/remove class from html tag when dark mode setting changes
  useEffect(() => {
    if (isDarkMode) {
      document.documentElement.classList.add('dark-mode');
    } else {
      document.documentElement.classList.remove('dark-mode');
    }
    
    // Save settings to localStorage
    localStorage.setItem('darkMode', isDarkMode.toString());
  }, [isDarkMode]);

  // Filter options change handler
  const handleFilterChange = useCallback((newOptions: FilterOptions) => {
    setFilterOptions(newOptions);
  }, []);

  // Filter button click handler
  const handleFilterClick = useCallback(() => {
    setShowFilterPanel(true);
  }, []);
  
  // Dark mode toggle handler
  const toggleDarkMode = useCallback(() => {
    setIsDarkMode(prev => !prev);
  }, []);

  // Polling settings toggle handler
  const togglePolling = useCallback(() => {
    setPollingEnabled(prev => !prev);
  }, []);

  // Fallback display for errors
  if (loadError || !workplan) {
    return (
      <div className="fixed inset-0 flex items-center justify-center bg-white dark:bg-gray-900 z-50">
        <div className="text-center p-8 max-w-md">
          <div className="animate-pulse text-5xl mb-6">⚠️</div>
          <h2 className="text-xl font-bold text-red-600 dark:text-red-400 mb-4">
            An error occurred
          </h2>
          <p className="text-gray-700 dark:text-gray-300 mb-6">
            {loadError || "Workplan data not found"}
          </p>
          <button 
            onClick={() => window.location.reload()}
            className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
          >
            Reload
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className={`app ${isDarkMode ? 'dark-theme' : 'light-theme'}`}>
      <header className={`bg-gray-800 text-white shadow-md flex justify-between items-center ${
        breakpoint === 'xs' ? 'p-2' : 'p-4'
      }`}>
        <div>
          {/* Currently unused area - available for future use */}
        </div>
        
        <div className="flex items-center gap-2 sm:gap-4">
          {lastLoadedTime && (
            <span className={`text-gray-300 ${breakpoint === 'xs' ? 'text-xs' : 'text-sm'} hidden sm:inline`}>
              Last updated: {lastLoadedTime.toLocaleTimeString()}
            </span>
          )}
          
          {/* Polling toggle button */}
          <button 
            onClick={togglePolling}
            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`}
            title={pollingEnabled ? "Stop auto-refresh" : "Start auto-refresh"}
            aria-label={pollingEnabled ? "Stop auto-refresh" : "Start auto-refresh"}
          >
            <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">
              <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
              <path d="M21 3v5h-5"></path>
            </svg>
            <span className="hidden sm:inline text-xs ml-0.5">{pollingEnabled ? 'Auto ON' : 'Auto OFF'}</span>
          </button>
          
          {/* Manual refresh button */}
          <button 
            onClick={() => loadData()}
            disabled={isLoading}
            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 ${
              breakpoint === 'xs' ? 'text-xs' : 'text-sm'
            }`}
            title="Refresh now"
            aria-label="Refresh now"
          >
            <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">
              <polyline points="1 4 1 10 7 10"></polyline>
              <polyline points="23 20 23 14 17 14"></polyline>
              <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>
            </svg>
            <span className="hidden sm:inline">Refresh</span>
          </button>
          
          {/* Dark mode toggle button */}
          <button 
            onClick={toggleDarkMode}
            className={`px-2 sm:px-3 py-1 bg-gray-700 hover:bg-gray-600 rounded text-white transition flex items-center ${
              breakpoint === 'xs' ? 'text-xs' : 'text-sm'
            }`}
            title={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
            aria-label={isDarkMode ? "Switch to light mode" : "Switch to dark mode"}
          >
            {isDarkMode ? (
              <>
                <svg className="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                  <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" />
                </svg>
                <span className="hidden sm:inline">Light</span>
              </>
            ) : (
              <>
                <svg className="h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                  <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
                </svg>
                <span className="hidden sm:inline">Dark</span>
              </>
            )}
          </button>
          
          <button 
            onClick={handleFilterClick}
            className={`px-2 sm:px-3 py-1 bg-indigo-600 hover:bg-indigo-700 rounded text-white transition ${
              breakpoint === 'xs' ? 'text-xs' : 'text-sm'
            }`}
            aria-label="Open filter"
          >
            <span className="mr-1">🔍</span>
            {breakpoint !== 'xs' && "Filter"}
          </button>
        </div>
      </header>
      
      <main className="app-main">
        <ReactFlowProvider>
          <WorkplanFlow 
            workplan={workplan}
            filterOptions={filterOptions}
          />
        </ReactFlowProvider>
      </main>
      
      {/* Filter panel */}
      <div className="filter-panel-container">
        {showFilterPanel && (
          <FilterPanel
            options={filterOptions}
            onChange={handleFilterChange}
            isOpen={showFilterPanel}
            onClose={() => setShowFilterPanel(false)}
          />
        )}
      </div>
      
      {/* Device orientation warning (mobile only) */}
      <OrientationWarning />
    </div>
  );
}

export default App

```