diff --git a/.antigravity/INSTALL.md b/.antigravity/INSTALL.md
new file mode 100644
index 0000000..2c7681a
--- /dev/null
+++ b/.antigravity/INSTALL.md
@@ -0,0 +1,61 @@
+# Installing Understand-Anything for Antigravity
+
+## Prerequisites
+
+- Git
+
+## Installation
+
+1. **Clone the repository:**
+ ```bash
+ git clone https://github.com/Lum1104/Understand-Anything.git ~/.antigravity/understand-anything
+ ```
+
+2. **Create the skills symlinks:**
+ ```bash
+ mkdir -p ~/.gemini/antigravity/skills
+ ln -s ~/.antigravity/understand-anything/understand-anything-plugin/skills ~/.gemini/antigravity/skills/understand-anything
+ # Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
+ # Skip if already exists (e.g. another platform was installed first)
+ [ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.antigravity/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
+ ```
+
+ **Windows (PowerShell):**
+ ```powershell
+ New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.gemini\antigravity\skills"
+ cmd /c mklink /J "$env:USERPROFILE\.gemini\antigravity\skills\understand-anything" "$env:USERPROFILE\.antigravity\understand-anything\understand-anything-plugin\skills"
+ cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.antigravity\understand-anything\understand-anything-plugin"
+ ```
+
+3. **Restart the chat or IDE** so Antigravity can discover the skills.
+
+## Verify
+
+```bash
+ls -la ~/.gemini/antigravity/skills/understand-anything
+```
+
+You should see a symlink pointing to the skills directory in the cloned repo.
+
+## Usage
+
+Skills activate automatically when relevant. You can also invoke directly by saying:
+- "Run the understand skill to analyze this codebase"
+- "Use the understand-dashboard skill to view the architecture map"
+- "Use understand-chat to answer a question about the graph"
+
+## Updating
+
+```bash
+cd ~/.antigravity/understand-anything && git pull
+```
+
+Skills update instantly through the symlink.
+
+## Uninstalling
+
+```bash
+rm ~/.gemini/antigravity/skills/understand-anything
+rm ~/.understand-anything-plugin
+rm -rf ~/.antigravity/understand-anything
+```
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 9397ad4..6372323 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -9,7 +9,7 @@
{
"name": "understand-anything",
"description": "Multi-agent codebase analysis with interactive dashboard, guided tours, and skill commands",
- "version": "1.1.0",
+ "version": "1.2.0",
"source": "./understand-anything-plugin"
}
]
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 7fc838c..b496896 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "understand-anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "1.1.0",
+ "version": "1.2.0",
"author": {
"name": "Lum1104"
},
diff --git a/.codex/INSTALL.md b/.codex/INSTALL.md
index 155ade1..bc4e9a2 100644
--- a/.codex/INSTALL.md
+++ b/.codex/INSTALL.md
@@ -11,16 +11,29 @@
git clone https://github.com/Lum1104/Understand-Anything.git ~/.codex/understand-anything
```
-2. **Create the skills symlink:**
+2. **Create the skills symlinks:**
```bash
mkdir -p ~/.agents/skills
- ln -s ~/.codex/understand-anything/understand-anything-plugin/skills ~/.agents/skills/understand-anything
+ # Note: if OpenCode's Understand-Anything is already installed, these symlinks
+ # already exist and the ln commands will safely fail — that is fine, the
+ # existing symlinks work for Codex too.
+ for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
+ ln -sf ~/.codex/understand-anything/understand-anything-plugin/skills/$skill ~/.agents/skills/$skill
+ done
+ # Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
+ # Skip if already exists (e.g. another platform was installed first)
+ [ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.codex/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
```
**Windows (PowerShell):**
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills"
- cmd /c mklink /J "$env:USERPROFILE\.agents\skills\understand-anything" "$env:USERPROFILE\.codex\understand-anything\understand-anything-plugin\skills"
+ $skills = @("understand","understand-chat","understand-dashboard","understand-diff","understand-explain","understand-onboard")
+ foreach ($skill in $skills) {
+ cmd /c mklink /J "$env:USERPROFILE\.agents\skills\$skill" "$env:USERPROFILE\.codex\understand-anything\understand-anything-plugin\skills\$skill"
+ }
+ # Universal plugin root symlink
+ cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.codex\understand-anything\understand-anything-plugin"
```
3. **Restart Codex** to discover the skills.
@@ -28,10 +41,10 @@
## Verify
```bash
-ls -la ~/.agents/skills/understand-anything
+ls -la ~/.agents/skills/ | grep understand
```
-You should see a symlink pointing to the skills directory.
+You should see symlinks for each skill pointing into the cloned repository.
## Usage
@@ -45,11 +58,14 @@ Skills activate automatically when relevant. You can also invoke directly:
cd ~/.codex/understand-anything && git pull
```
-Skills update instantly through the symlink.
+Skills update instantly through the symlinks.
## Uninstalling
```bash
-rm ~/.agents/skills/understand-anything
+for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
+ rm -f ~/.agents/skills/$skill
+done
+rm ~/.understand-anything-plugin
rm -rf ~/.codex/understand-anything
```
diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json
index 73fcfbe..006ddb2 100644
--- a/.cursor-plugin/plugin.json
+++ b/.cursor-plugin/plugin.json
@@ -2,7 +2,7 @@
"name": "understand-anything",
"displayName": "Understand Anything",
"description": "AI-powered codebase understanding — analyze, visualize, and explain any project",
- "version": "1.0.5",
+ "version": "1.2.0",
"author": {
"name": "Lum1104"
},
diff --git a/.gemini/INSTALL.md b/.gemini/INSTALL.md
new file mode 100644
index 0000000..fda5bf3
--- /dev/null
+++ b/.gemini/INSTALL.md
@@ -0,0 +1,72 @@
+# Installing Understand-Anything for Gemini CLI
+
+## Prerequisites
+
+- Git
+- [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed
+
+## Installation
+
+1. **Clone the repository:**
+ ```bash
+ git clone https://github.com/Lum1104/Understand-Anything.git ~/.gemini/understand-anything
+ ```
+
+2. **Create the skills symlinks:**
+ ```bash
+ mkdir -p ~/.agents/skills
+ # Note: if another platform's Understand-Anything is already installed, these symlinks
+ # already exist and the ln commands will safely fail — that is fine, the
+ # existing symlinks work for Gemini CLI too.
+ for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
+ ln -sf ~/.gemini/understand-anything/understand-anything-plugin/skills/$skill ~/.agents/skills/$skill
+ done
+ # Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
+ # Skip if already exists (e.g. another platform was installed first)
+ [ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.gemini/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
+ ```
+
+ **Windows (PowerShell):**
+ ```powershell
+ New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills"
+ $skills = @("understand","understand-chat","understand-dashboard","understand-diff","understand-explain","understand-onboard")
+ foreach ($skill in $skills) {
+ cmd /c mklink /J "$env:USERPROFILE\.agents\skills\$skill" "$env:USERPROFILE\.gemini\understand-anything\understand-anything-plugin\skills\$skill"
+ }
+ # Universal plugin root symlink
+ cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.gemini\understand-anything\understand-anything-plugin"
+ ```
+
+3. **Restart Gemini CLI** to discover the skills.
+
+## Verify
+
+```bash
+ls -la ~/.agents/skills/ | grep understand
+```
+
+You should see symlinks for each skill pointing into the cloned repository.
+
+## Usage
+
+Skills activate automatically when relevant. You can also invoke directly:
+- "Analyze this codebase and build a knowledge graph"
+- "Help me understand this project's architecture"
+
+## Updating
+
+```bash
+cd ~/.gemini/understand-anything && git pull
+```
+
+Skills update instantly through the symlinks.
+
+## Uninstalling
+
+```bash
+for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
+ rm -f ~/.agents/skills/$skill
+done
+rm ~/.understand-anything-plugin
+rm -rf ~/.gemini/understand-anything
+```
diff --git a/.openclaw/INSTALL.md b/.openclaw/INSTALL.md
index f06d566..d3150a6 100644
--- a/.openclaw/INSTALL.md
+++ b/.openclaw/INSTALL.md
@@ -11,16 +11,20 @@
git clone https://github.com/Lum1104/Understand-Anything.git ~/.openclaw/understand-anything
```
-2. **Create the skills symlink:**
+2. **Create the skills symlinks:**
```bash
mkdir -p ~/.openclaw/skills
ln -s ~/.openclaw/understand-anything/understand-anything-plugin/skills ~/.openclaw/skills/understand-anything
+ # Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
+ # Skip if already exists (e.g. another platform was installed first)
+ [ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.openclaw/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
```
**Windows (PowerShell):**
```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.openclaw\skills"
cmd /c mklink /J "$env:USERPROFILE\.openclaw\skills\understand-anything" "$env:USERPROFILE\.openclaw\understand-anything\understand-anything-plugin\skills"
+ cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.openclaw\understand-anything\understand-anything-plugin"
```
3. **Restart OpenClaw** to discover the skills.
@@ -41,5 +45,6 @@ cd ~/.openclaw/understand-anything && git pull
```bash
rm ~/.openclaw/skills/understand-anything
+rm ~/.understand-anything-plugin
rm -rf ~/.openclaw/understand-anything
```
diff --git a/.opencode/INSTALL.md b/.opencode/INSTALL.md
index 6cac25a..e462957 100644
--- a/.opencode/INSTALL.md
+++ b/.opencode/INSTALL.md
@@ -2,36 +2,91 @@
## Prerequisites
-- [OpenCode.ai](https://opencode.ai) installed
+- Git
+- [OpenCode](https://opencode.ai) installed
## Installation
-Add understand-anything to the `plugin` array in your `opencode.json` (global or project-level):
+1. **Clone the repository:**
+ ```bash
+ git clone https://github.com/Lum1104/Understand-Anything.git ~/.opencode/understand-anything
+ ```
-```json
-{
- "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
-}
-```
+2. **Create the skills symlinks:**
+ ```bash
+ mkdir -p ~/.agents/skills
+ # Note: if Codex's Understand-Anything is already installed, these symlinks
+ # already exist and the ln commands will safely fail — that is fine, the
+ # existing symlinks work for OpenCode too.
+ for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
+ ln -sf ~/.opencode/understand-anything/understand-anything-plugin/skills/$skill ~/.agents/skills/$skill
+ done
+ # Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
+ # Skip if already exists (e.g. another platform was installed first)
+ [ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.opencode/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
+ ```
-Restart OpenCode. The plugin auto-installs and registers all skills.
+ **Windows (PowerShell):**
+ ```powershell
+ New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills"
+ $skills = @("understand","understand-chat","understand-dashboard","understand-diff","understand-explain","understand-onboard")
+ foreach ($skill in $skills) {
+ cmd /c mklink /J "$env:USERPROFILE\.agents\skills\$skill" "$env:USERPROFILE\.opencode\understand-anything\understand-anything-plugin\skills\$skill"
+ }
+ # Universal plugin root symlink
+ cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.opencode\understand-anything\understand-anything-plugin"
+ ```
+
+3. **Restart OpenCode** to discover the skills.
## Verify
-Ask: "List available skills" — you should see understand, understand-chat, understand-dashboard, etc.
+```bash
+ls -la ~/.agents/skills/ | grep understand
+```
+
+You should see symlinks for each skill pointing into the cloned repository.
## Usage
+Skills activate automatically when relevant. You can also invoke directly:
+
```
-use skill tool to load understand-anything/understand
+use skill tool to load understand
```
Or just ask: "Analyze this codebase and build a knowledge graph"
## Updating
-Restart OpenCode — the plugin re-installs from git automatically.
+```bash
+cd ~/.opencode/understand-anything && git pull
+```
+
+Skills update instantly through the symlinks.
## Uninstalling
-Remove the plugin line from `opencode.json` and restart.
+```bash
+for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
+ rm -f ~/.agents/skills/$skill
+done
+rm ~/.understand-anything-plugin
+rm -rf ~/.opencode/understand-anything
+```
+
+## Troubleshooting
+
+### Skills not found
+
+1. Check that the symlinks exist: `ls -la ~/.agents/skills/ | grep understand`
+2. Verify the clone succeeded: `ls ~/.opencode/understand-anything/understand-anything-plugin/skills/`
+3. Restart OpenCode
+
+### Tool mapping
+
+When skills reference Claude Code tools:
+- `TodoWrite` → `todowrite`
+- `Task` with subagents → `@mention` syntax
+- `Skill` tool → OpenCode's native `skill` tool
+- File operations → your native tools
diff --git a/.pi/INSTALL.md b/.pi/INSTALL.md
new file mode 100644
index 0000000..02a2012
--- /dev/null
+++ b/.pi/INSTALL.md
@@ -0,0 +1,72 @@
+# Installing Understand-Anything for Pi Agent
+
+## Prerequisites
+
+- Git
+- [Pi Agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) installed
+
+## Installation
+
+1. **Clone the repository:**
+ ```bash
+ git clone https://github.com/Lum1104/Understand-Anything.git ~/.pi/understand-anything
+ ```
+
+2. **Create the skills symlinks:**
+ ```bash
+ mkdir -p ~/.agents/skills
+ # Note: if another platform's Understand-Anything is already installed, these symlinks
+ # already exist and the ln commands will safely fail — that is fine, the
+ # existing symlinks work for Pi Agent too.
+ for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
+ ln -sf ~/.pi/understand-anything/understand-anything-plugin/skills/$skill ~/.agents/skills/$skill
+ done
+ # Universal plugin root symlink — lets the dashboard skill find packages/dashboard/
+ # Skip if already exists (e.g. another platform was installed first)
+ [ -e ~/.understand-anything-plugin ] || [ -L ~/.understand-anything-plugin ] || ln -s ~/.pi/understand-anything/understand-anything-plugin ~/.understand-anything-plugin
+ ```
+
+ **Windows (PowerShell):**
+ ```powershell
+ New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.agents\skills"
+ $skills = @("understand","understand-chat","understand-dashboard","understand-diff","understand-explain","understand-onboard")
+ foreach ($skill in $skills) {
+ cmd /c mklink /J "$env:USERPROFILE\.agents\skills\$skill" "$env:USERPROFILE\.pi\understand-anything\understand-anything-plugin\skills\$skill"
+ }
+ # Universal plugin root symlink
+ cmd /c mklink /J "$env:USERPROFILE\.understand-anything-plugin" "$env:USERPROFILE\.pi\understand-anything\understand-anything-plugin"
+ ```
+
+3. **Restart Pi Agent** to discover the skills.
+
+## Verify
+
+```bash
+ls -la ~/.agents/skills/ | grep understand
+```
+
+You should see symlinks for each skill pointing into the cloned repository.
+
+## Usage
+
+Skills activate automatically when relevant. You can also invoke directly:
+- "Analyze this codebase and build a knowledge graph"
+- "Help me understand this project's architecture"
+
+## Updating
+
+```bash
+cd ~/.pi/understand-anything && git pull
+```
+
+Skills update instantly through the symlinks.
+
+## Uninstalling
+
+```bash
+for skill in understand understand-chat understand-dashboard understand-diff understand-explain understand-onboard; do
+ rm -f ~/.agents/skills/$skill
+done
+rm ~/.understand-anything-plugin
+rm -rf ~/.pi/understand-anything
+```
diff --git a/CLAUDE.md b/CLAUDE.md
index 443042e..84abe60 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -3,6 +3,10 @@
## Project Overview
An open-source tool combining LLM intelligence + static analysis to produce interactive dashboards for understanding codebases.
+## Prerequisites
+- Node.js >= 22 (developed on v24)
+- pnpm >= 10 (pinned via `packageManager` field in root `package.json`)
+
## Architecture
- **Monorepo** with pnpm workspaces
- **understand-anything-plugin/** — Claude Code plugin containing all source code:
@@ -34,6 +38,7 @@ An open-source tool combining LLM intelligence + static analysis to produce inte
- `pnpm --filter @understand-anything/skill test` — Run plugin tests
- `pnpm --filter @understand-anything/dashboard build` — Build the dashboard
- `pnpm dev:dashboard` — Start dashboard dev server
+- `pnpm lint` — Run ESLint across the project
## Conventions
- TypeScript strict mode everywhere
@@ -42,10 +47,19 @@ An open-source tool combining LLM intelligence + static analysis to produce inte
- Knowledge graph JSON lives in `.understand-anything/` directory of analyzed projects
- Core uses subpath exports (`./search`, `./types`, `./schema`) to avoid pulling Node.js modules into browser
+## Gotchas
+- **tree-sitter**: Uses `web-tree-sitter` (WASM) instead of native `tree-sitter` — native bindings fail on darwin/arm64 + Node 24
+- **Dashboard imports**: Dashboard must only import from core's browser-safe subpath exports (`./search`, `./types`, `./schema`), never the main entry point which pulls in Node.js modules
+
+## Scripts
+- `scripts/generate-large-graph.mjs` — Generates a fake knowledge graph for performance testing (e.g. large-graph layout). Writes to `.understand-anything/knowledge-graph.json`. Usage: `node scripts/generate-large-graph.mjs [nodeCount]` (default: 3000 nodes). Not part of the production pipeline.
+
## Versioning
-When pushing to remote, bump the version in **both** of these files (keep them in sync):
+When pushing to remote, bump the version in **all four** of these files (keep them in sync):
- `understand-anything-plugin/package.json` → `"version"` field
- `.claude-plugin/marketplace.json` → `plugins[0].version` field
+- `.claude-plugin/plugin.json` → `"version"` field
+- `.cursor-plugin/plugin.json` → `"version"` field
## Testing Local Plugin Changes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..64a8154
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,270 @@
+# Contributing to Understand Anything
+
+Thank you for your interest in contributing to Understand Anything! This document provides guidelines and instructions for contributing to the project.
+
+## 🌟 Ways to Contribute
+
+- **Bug Reports**: Found a bug? Open an issue with detailed reproduction steps
+- **Feature Requests**: Have an idea? Share it in the issues section
+- **Documentation**: Improve or translate documentation
+- **Code**: Fix bugs, add features, or improve performance
+- **Testing**: Write tests to improve code coverage
+
+## 🚀 Getting Started
+
+### Prerequisites
+
+- Node.js >= 22 (developed on v24)
+- pnpm >= 10 (pinned via `packageManager` field in root `package.json`)
+- Git for version control
+
+### Setup
+
+1. **Fork and Clone**
+ ```bash
+ git clone https://github.com/YOUR_USERNAME/Understand-Anything.git
+ cd Understand-Anything
+ ```
+
+2. **Install Dependencies**
+ ```bash
+ pnpm install
+ ```
+
+3. **Build Core Package**
+ ```bash
+ pnpm --filter @understand-anything/core build
+ ```
+
+4. **Run Tests**
+ ```bash
+ pnpm --filter @understand-anything/core test
+ pnpm --filter @understand-anything/skill test
+ ```
+
+5. **Start Dashboard (Optional)**
+ ```bash
+ pnpm dev:dashboard
+ ```
+
+## 📝 Development Workflow
+
+### 1. Create a Branch
+
+Create a descriptive branch name:
+```bash
+git checkout -b feat/my-feature # For new features
+git checkout -b fix/bug-description # For bug fixes
+git checkout -b docs/update-readme # For documentation
+```
+
+### 2. Make Changes
+
+- Write clean, readable code
+- Follow existing code style and conventions
+- Add tests for new functionality
+- Update documentation as needed
+
+### 3. Test Your Changes
+
+```bash
+# Run all tests
+pnpm --filter @understand-anything/core test
+pnpm --filter @understand-anything/skill test
+
+# Run linter
+pnpm lint
+
+# Build packages
+pnpm build
+```
+
+### 4. Commit Your Changes
+
+Write clear, descriptive commit messages:
+```bash
+git add .
+git commit -m "feat: add keyboard shortcuts to dashboard"
+```
+
+**Commit Message Convention:**
+- `feat:` - New feature
+- `fix:` - Bug fix
+- `docs:` - Documentation changes
+- `style:` - Code style changes (formatting, etc.)
+- `refactor:` - Code refactoring
+- `test:` - Adding or updating tests
+- `chore:` - Maintenance tasks
+
+### 5. Push and Create Pull Request
+
+```bash
+git push origin your-branch-name
+```
+
+Then open a Pull Request on GitHub with:
+- Clear title describing the change
+- Detailed description of what changed and why
+- Link to related issues (if any)
+- Screenshots (for UI changes)
+
+## 🧪 Testing Guidelines
+
+### Writing Tests
+
+- Use Vitest for testing
+- Place tests in `__tests__` directories or `*.test.ts` files
+- Aim for high test coverage for new features
+- Test edge cases and error conditions
+
+Example test structure:
+```typescript
+import { describe, it, expect } from 'vitest';
+
+describe('MyFeature', () => {
+ it('should do something', () => {
+ // Arrange
+ const input = 'test';
+
+ // Act
+ const result = myFunction(input);
+
+ // Assert
+ expect(result).toBe('expected');
+ });
+});
+```
+
+### Running Tests
+
+```bash
+# Run all tests
+pnpm test
+
+# Run tests for specific package
+pnpm --filter @understand-anything/core test
+
+# Run tests in watch mode
+pnpm --filter @understand-anything/core test --watch
+```
+
+## 📚 Code Style Guidelines
+
+### TypeScript
+
+- Use TypeScript strict mode
+- Define explicit types for function parameters and return values
+- Avoid `any` type - use `unknown` if type is truly unknown
+- Use interfaces for object shapes
+- Use type aliases for unions and complex types
+
+### Formatting
+
+- The project uses ESLint for code quality
+- Consistent indentation (2 spaces)
+- Use meaningful variable and function names
+- Keep functions small and focused
+
+### React/Dashboard
+
+- Use functional components with hooks
+- Keep components focused and single-purpose
+- Use Zustand for state management
+- Follow the existing component structure
+
+### File Organization
+
+```
+understand-anything-plugin/
+├── packages/
+│ ├── core/ # Core analysis engine
+│ │ ├── src/
+│ │ └── package.json
+│ └── dashboard/ # React dashboard
+│ ├── src/
+│ │ ├── components/
+│ │ ├── utils/
+│ │ └── store.ts
+│ └── package.json
+├── src/ # Plugin skills implementation
+├── agents/ # AI agent prompts
+└── skills/ # Skill definitions
+```
+
+## 🌍 Translation Guidelines
+
+### Adding a New Language
+
+1. Create `README.{language-code}.md` (e.g., `README.fr-FR.md`)
+2. Translate all sections while maintaining formatting
+3. Update main `README.md` to include language link
+4. Keep technical terms in English where appropriate
+5. Ensure all links still work
+
+Example:
+```markdown
+English | Français
+```
+
+## 🐛 Bug Reports
+
+When reporting bugs, include:
+
+- **Description**: Clear description of the issue
+- **Steps to Reproduce**: Detailed steps to reproduce the bug
+- **Expected Behavior**: What you expected to happen
+- **Actual Behavior**: What actually happened
+- **Environment**: OS, Node version, pnpm version
+- **Screenshots**: If applicable
+- **Error Messages**: Full error output
+
+## 💡 Feature Requests
+
+When requesting features:
+
+- **Use Case**: Describe the problem you're trying to solve
+- **Proposed Solution**: How you envision the feature working
+- **Alternatives**: Other solutions you've considered
+- **Additional Context**: Any other relevant information
+
+## 📋 Pull Request Checklist
+
+Before submitting a PR, ensure:
+
+- [ ] Code follows the project's style guidelines
+- [ ] All tests pass (`pnpm test`)
+- [ ] New code has test coverage
+- [ ] Documentation is updated (if needed)
+- [ ] Commit messages follow convention
+- [ ] PR description clearly explains changes
+- [ ] No console.log or debug code left behind
+- [ ] Branch is up to date with main
+
+## 🤝 Code Review Process
+
+1. **Automated Checks**: CI runs tests and linting
+2. **Maintainer Review**: Project maintainers review the code
+3. **Feedback**: Address any requested changes
+4. **Approval**: Once approved, PR will be merged
+5. **Cleanup**: Delete your branch after merge
+
+## 📞 Getting Help
+
+- **Issues**: For bugs and feature requests
+- **Discussions**: For questions and general discussion
+- **Documentation**: Check existing docs first
+
+## 📄 License
+
+By contributing, you agree that your contributions will be licensed under the MIT License.
+
+## 🙏 Recognition
+
+Contributors will be recognized in:
+- GitHub contributors list
+- Release notes (for significant contributions)
+- Special mentions for exceptional contributions
+
+---
+
+**Thank you for contributing to Understand Anything! Your contributions help make code understanding accessible to everyone.** 🚀
diff --git a/README.ja-JP.md b/README.ja-JP.md
new file mode 100644
index 0000000..bad5000
--- /dev/null
+++ b/README.ja-JP.md
@@ -0,0 +1,304 @@
+
Understand Anything
+
+
+ あらゆるコードベースを、探索・検索・質問ができるインタラクティブなナレッジグラフに変換します。
+
+
+
+ English | 中文 | 日本語 | Türkçe
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+> [!TIP]
+> **コミュニティの皆さんに感謝!** Understand-Anythingへのサポートは本当に素晴らしいものです。このツールが複雑なコードを理解する時間を少しでも短縮できたなら、それが私の望みです。🚀
+
+**新しいチームに参加したばかり。コードベースは20万行。どこから手をつければいいのか?**
+
+Understand Anything は [Claude Code](https://docs.anthropic.com/en/docs/claude-code) プラグインです。マルチエージェントパイプラインでプロジェクトを分析し、すべてのファイル・関数・クラス・依存関係のナレッジグラフを構築して、インタラクティブなダッシュボードで視覚的に探索できるようにします。コードを闇雲に読むのはやめて、全体像を把握しましょう。
+
+---
+
+## 🤔 なぜ必要なのか?
+
+コードを読むのは大変です。コードベース全体を理解するのはさらに大変です。ドキュメントは常に古く、オンボーディングには数週間かかり、新機能の開発はまるで考古学のようです。
+
+Understand Anything は、**LLMの知能**と**静的解析**を組み合わせることでこの問題を解決します。プロジェクトの生きた探索可能なマップを生成し、すべてに平易な日本語の説明が付きます。
+
+---
+
+## 🎯 誰のためのツール?
+
+
+
+
+ 👩💻 ジュニア開発者
+ 不慣れなコードに溺れるのはもう終わり。アーキテクチャをステップバイステップで案内するガイドツアーで、すべての関数やクラスが平易な言葉で説明されます。
+
+
+ 📋 プロダクトマネージャー&デザイナー
+ コードを読まなくても、システムが実際にどう動くかを理解できます。「認証はどう動いているの?」のような質問をすれば、実際のコードベースに基づいた明確な回答が得られます。
+
+
+ 🤖 AI活用開発者
+ AIツールにプロジェクトの深いコンテキストを与えましょう。コードレビュー前に /understand-diff、モジュールの詳細調査に /understand-explain、アーキテクチャの推論に /understand-chat を使えます。
+
+
+
+
+---
+
+## 🚀 クイックスタート
+
+### 1. プラグインをインストール
+
+```bash
+/plugin marketplace add Lum1104/Understand-Anything
+/plugin install understand-anything
+```
+
+### 2. コードベースを分析
+
+```bash
+/understand
+```
+
+マルチエージェントパイプラインがプロジェクトをスキャンし、すべてのファイル・関数・クラス・依存関係を抽出して、`.understand-anything/knowledge-graph.json` にナレッジグラフを保存します。
+
+### 3. ダッシュボードで探索
+
+```bash
+/understand-dashboard
+```
+
+インタラクティブなWebダッシュボードが開き、コードベースがグラフとして可視化されます。アーキテクチャ層ごとに色分けされ、検索やクリックが可能です。ノードを選択すると、コード・関連関係・平易な説明が表示されます。
+
+### 4. さらに学ぶ
+
+```bash
+# コードベースについて何でも質問
+/understand-chat 支払いフローはどう動いているの?
+
+# 現在の変更の影響を分析
+/understand-diff
+
+# 特定のファイルや関数を詳しく調べる
+/understand-explain src/auth/login.ts
+
+# 新メンバー向けのオンボーディングガイドを生成
+/understand-onboard
+```
+
+---
+
+## 🌐 マルチプラットフォームインストール
+
+Understand-Anythingは複数のAIコーディングプラットフォームで動作します。
+
+### Claude Code(ネイティブ)
+
+```bash
+/plugin marketplace add Lum1104/Understand-Anything
+/plugin install understand-anything
+```
+
+### Codex
+
+Codexに以下を伝えてください:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.codex/INSTALL.md
+```
+
+### OpenCode
+
+OpenCodeに以下を伝えてください:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
+```
+
+### OpenClaw
+
+OpenClawに以下を伝えてください:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.openclaw/INSTALL.md
+```
+
+### Cursor
+
+Cursorはこのリポジトリをクローンすると `.cursor-plugin/plugin.json` 経由でプラグインを自動検出します。手動インストールは不要です — クローンしてCursorで開くだけです。
+
+### Antigravity
+
+Antigravityに以下を伝えてください:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.antigravity/INSTALL.md
+```
+
+### Gemini CLI
+
+Gemini CLIに以下を伝えてください:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.gemini/INSTALL.md
+```
+
+### Pi Agent
+
+Pi Agentに以下を伝えてください:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.pi/INSTALL.md
+```
+
+### プラットフォーム互換性
+
+| プラットフォーム | ステータス | インストール方法 |
+|----------|--------|----------------|
+| Claude Code | ✅ ネイティブ | プラグインマーケットプレイス |
+| Codex | ✅ サポート | AI駆動インストール |
+| OpenCode | ✅ サポート | AI駆動インストール |
+| OpenClaw | ✅ サポート | AI駆動インストール |
+| Cursor | ✅ サポート | 自動検出 |
+| Antigravity | ✅ サポート | AI駆動インストール |
+| Gemini CLI | ✅ サポート | AI駆動インストール |
+| Pi Agent | ✅ サポート | AI駆動インストール |
+
+---
+
+## ✨ 機能
+
+
+
+
+
+
+
+
+ 🗺️ インタラクティブナレッジグラフ
+ ファイル・関数・クラスとそれらの関係をReact Flowで可視化。ノードをクリックするとコードと接続関係が表示されます。
+
+
+ 💬 平易な言葉での説明
+ すべてのノードがLLMによって説明されるため、技術者でなくても、それが何をしているのか、なぜ存在するのかを理解できます。
+
+
+
+
+ 🧭 ガイドツアー
+ 依存関係順に並べられた、自動生成のアーキテクチャウォークスルー。正しい順序でコードベースを学べます。
+
+
+ 🔍 ファジー&セマンティック検索
+ 名前や意味で何でも検索できます。「認証を処理する部分は?」と検索すれば、グラフ全体から関連する結果が得られます。
+
+
+
+
+ 📊 差分影響分析
+ コミット前に、変更がシステムのどの部分に影響するかを確認。コードベース全体への波及効果を把握できます。
+
+
+ 🎭 ペルソナ適応型UI
+ ダッシュボードは、ジュニア開発者・PM・パワーユーザーなど、ユーザーに応じて詳細レベルを調整します。
+
+
+
+
+ 🏗️ レイヤー可視化
+ API・Service・Data・UI・Utilityなどのアーキテクチャ層ごとに自動グループ化。色分けされた凡例付き。
+
+
+ 📚 言語コンセプト
+ ジェネリクス・クロージャ・デコレータなど12のプログラミングパターンが、出現箇所のコンテキストで説明されます。
+
+
+
+
+---
+
+## 🔧 内部の仕組み
+
+### マルチエージェントパイプライン
+
+`/understand` コマンドは5つの専門エージェントをオーケストレーションします:
+
+| エージェント | 役割 |
+|-------|------|
+| `project-scanner` | ファイルの検出、言語やフレームワークの検出 |
+| `file-analyzer` | 関数・クラス・インポートの抽出、グラフノードとエッジの生成 |
+| `architecture-analyzer` | アーキテクチャ層の特定 |
+| `tour-builder` | ガイド学習ツアーの生成 |
+| `graph-reviewer` | グラフの完全性と参照整合性の検証 |
+
+ファイルアナライザーは並列実行されます(最大3つ同時)。インクリメンタル更新に対応しており、前回の実行から変更されたファイルのみを再分析します。
+
+### プロジェクト構成
+
+```
+understand-anything-plugin/
+ .claude-plugin/ — プラグインマニフェスト
+ agents/ — 専門AIエージェント
+ skills/ — スキル定義(/understand、/understand-chatなど)
+ src/ — TypeScriptソース(context-builder、diff-analyzerなど)
+ packages/
+ core/ — 分析エンジン(types、persistence、tree-sitter、search、schema、tours)
+ dashboard/ — React + TypeScript Webダッシュボード
+```
+
+### 技術スタック
+
+TypeScript、pnpm workspaces、React 18、Vite、TailwindCSS v4、React Flow、Zustand、web-tree-sitter、Fuse.js、Zod、Dagre
+
+### 開発コマンド
+
+| コマンド | 説明 |
+|---------|-------------|
+| `pnpm install` | すべての依存関係をインストール |
+| `pnpm --filter @understand-anything/core build` | coreパッケージをビルド |
+| `pnpm --filter @understand-anything/core test` | coreテストを実行 |
+| `pnpm --filter @understand-anything/skill build` | プラグインパッケージをビルド |
+| `pnpm --filter @understand-anything/skill test` | プラグインテストを実行 |
+| `pnpm --filter @understand-anything/dashboard build` | ダッシュボードをビルド |
+| `pnpm dev:dashboard` | ダッシュボード開発サーバーを起動 |
+
+---
+
+## 🤝 コントリビュート
+
+コントリビュートを歓迎します!始め方は以下の通りです:
+
+1. リポジトリをフォーク
+2. フィーチャーブランチを作成(`git checkout -b feature/my-feature`)
+3. テストを実行(`pnpm --filter @understand-anything/core test`)
+4. 変更をコミットしてプルリクエストを作成
+
+大きな変更については、まずIssueを作成してアプローチを議論してください。
+
+---
+
+
+ コードを闇雲に読むのはやめよう。すべてを理解しよう。
+
+
+## Star History
+
+
+
+
+
+
+
+
+
+
+ MIT License © Lum1104
+
diff --git a/README.md b/README.md
index ded8a3b..ea6d0ff 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
- English | 中文
+ English | 中文 | 日本語 | Türkçe
@@ -122,11 +122,9 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
### OpenCode
-Add to your `opencode.json`:
-```json
-{
- "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
-}
+Tell OpenCode:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
```
### OpenClaw
@@ -140,15 +138,39 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
Cursor auto-discovers the plugin via `.cursor-plugin/plugin.json` when this repo is cloned. No manual installation needed — just clone and open in Cursor.
+### Antigravity
+
+Tell Antigravity:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.antigravity/INSTALL.md
+```
+
+### Gemini CLI
+
+Tell Gemini CLI:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.gemini/INSTALL.md
+```
+
+### Pi Agent
+
+Tell Pi Agent:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.pi/INSTALL.md
+```
+
### Platform Compatibility
| Platform | Status | Install Method |
|----------|--------|----------------|
| Claude Code | ✅ Native | Plugin marketplace |
| Codex | ✅ Supported | AI-driven install |
-| OpenCode | ✅ Supported | Plugin config |
+| OpenCode | ✅ Supported | AI-driven install |
| OpenClaw | ✅ Supported | AI-driven install |
| Cursor | ✅ Supported | Auto-discovery |
+| Antigravity | ✅ Supported | AI-driven install |
+| Gemini CLI | ✅ Supported | AI-driven install |
+| Pi Agent | ✅ Supported | AI-driven install |
---
diff --git a/README.tr-TR.md b/README.tr-TR.md
new file mode 100644
index 0000000..b39fccb
--- /dev/null
+++ b/README.tr-TR.md
@@ -0,0 +1,304 @@
+
Understand Anything
+
+
+ Herhangi bir kod tabanını keşfedebileceğin, arayabileceğin ve hakkında sorular sorabileceğin interaktif bir bilgi grafiğine dönüştür.
+
+
+
+ English | 中文 | 日本語 | Türkçe
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+> [!TIP]
+> **Topluluğa çok teşekkürler!** Understand-Anything'e gösterilen destek inanılmaz oldu. Bu araç sana karmaşıklığı anlamak için birkaç dakika kazandırıyorsa, istediğim tek şey buydu. 🚀
+
+**Yeni bir ekibe katıldın. Kod tabanı 200.000 satır kod. Nereden başlayacaksın bile bilemiyorsun?**
+
+Understand Anything, projenizi çok-ajan hattıyla analiz eden, her dosya, fonksiyon, sınıf ve bağımlılığın bilgi grafiğini oluşturan ve hepsini görsel olarak keşfetmen için interaktif bir kontrol paneli sunan bir [Claude Code](https://docs.anthropic.com/en/docs/claude-code) eklentisidir. Kodu körü körüne okumayı bırak. Büyük resmi görmeye başla.
+
+---
+
+## 🤔 Neden?
+
+Kod okumak zor. Bütün bir kod tabanını anlamak daha da zor. Dokümantasyon her zaman güncel değil, işe alıştırma haftalar alıyor ve her yeni özellik arkeoloji gibi hissettiriyor.
+
+Understand Anything bunu **LLM zekası** ile **statik analizi** birleştirerek çözüyor ve projenin canlı, keşfedilebilir bir haritasını üretiyor — her şey için sade Türkçe açıklamalarla.
+
+---
+
+## 🎯 Kimler için?
+
+
+
+
+ 👩💻 Junior Geliştiriciler
+ Tanımadığın kodda boğulmayı bırak. Her fonksiyon ve sınıfın sade Türkçe açıklandığı, mimariyi adım adım anlatan rehberli turlar al.
+
+
+ 📋 Ürün Yöneticileri ve Tasarımcılar
+ Kod okumadan sistemin gerçekte nasıl çalıştığını nihayet anla. "Kimlik doğrulama nasıl çalışır?" gibi sorular sor ve gerçek kod tabanına dayalı net cevaplar al.
+
+
+ 🤖 AI Destekli Geliştiriciler
+ AI araçlarına projen hakkında derin bağlam ver. Kod incelemeden önce /understand-diff, herhangi bir modüle dalmak için /understand-explain veya mimari hakkında akıl yürütmek için /understand-chat kullan.
+
+
+
+
+---
+
+## 🚀 Hızlı Başlangıç
+
+### 1. Eklentiyi yükle
+
+```bash
+/plugin marketplace add Lum1104/Understand-Anything
+/plugin install understand-anything
+```
+
+### 2. Kod tabanını analiz et
+
+```bash
+/understand
+```
+
+Çok-ajan hattı projenizi tarar, her dosya, fonksiyon, sınıf ve bağımlılığı çıkarır, ardından `.understand-anything/knowledge-graph.json` dosyasına kaydedilen bir bilgi grafiği oluşturur.
+
+### 3. Kontrol panelini keşfet
+
+```bash
+/understand-dashboard
+```
+
+Kod tabanın bir grafik olarak görselleştirilmiş, mimari katmana göre renklendirilmiş, aranabilir ve tıklanabilir interaktif bir web kontrol paneli açılır. Kodunu, ilişkilerini ve sade Türkçe açıklamasını görmek için herhangi bir düğüm seç.
+
+### 4. Öğrenmeye devam et
+
+```bash
+# Kod tabanı hakkında her şeyi sor
+/understand-chat Ödeme akışı nasıl çalışır?
+
+# Mevcut değişikliklerinin etkisini analiz et
+/understand-diff
+
+# Belirli bir dosya veya fonksiyona derinlemesine dal
+/understand-explain src/auth/login.ts
+
+# Yeni ekip üyeleri için bir işe alıştırma rehberi oluştur
+/understand-onboard
+```
+
+---
+
+## 🌐 Çoklu Platform Kurulumu
+
+Understand-Anything birden fazla AI kodlama platformunda çalışır.
+
+### Claude Code (Yerli)
+
+```bash
+/plugin marketplace add Lum1104/Understand-Anything
+/plugin install understand-anything
+```
+
+### Codex
+
+Codex'e söyle:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.codex/INSTALL.md
+```
+
+### OpenCode
+
+OpenCode'a söyle:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
+```
+
+### OpenClaw
+
+OpenClaw'a söyle:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.openclaw/INSTALL.md
+```
+
+### Cursor
+
+Bu depo klonlandığında Cursor, eklentiyi `.cursor-plugin/plugin.json` aracılığıyla otomatik olarak keşfeder. Manuel kurulum gerekmez — sadece klonla ve Cursor'da aç.
+
+### Antigravity
+
+Antigravity'e söyle:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.antigravity/INSTALL.md
+```
+
+### Gemini CLI
+
+Gemini CLI'a söyle:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.gemini/INSTALL.md
+```
+
+### Pi Agent
+
+Pi Agent'a söyle:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.pi/INSTALL.md
+```
+
+### Platform Uyumluluğu
+
+| Platform | Durum | Kurulum Yöntemi |
+|----------|--------|----------------|
+| Claude Code | ✅ Yerli | Eklenti pazarı |
+| Codex | ✅ Destekleniyor | AI güdümlü kurulum |
+| OpenCode | ✅ Destekleniyor | AI güdümlü kurulum |
+| OpenClaw | ✅ Destekleniyor | AI güdümlü kurulum |
+| Cursor | ✅ Destekleniyor | Otomatik keşif |
+| Antigravity | ✅ Destekleniyor | AI güdümlü kurulum |
+| Gemini CLI | ✅ Destekleniyor | AI güdümlü kurulum |
+| Pi Agent | ✅ Destekleniyor | AI güdümlü kurulum |
+
+---
+
+## ✨ Özellikler
+
+
+
+
+
+
+
+
+ 🗺️ İnteraktif Bilgi Grafiği
+ Dosyalar, fonksiyonlar, sınıflar ve ilişkileri React Flow ile görselleştirildi. Kodunu ve bağlantılarını görmek için herhangi bir düğüme tıkla.
+
+
+ 💬 Sade Türkçe Özetler
+ Her düğüm bir LLM tarafından açıklanır, böylece herkes — teknik olsun ya da olmasın — ne yaptığını ve neden var olduğunu anlayabilir.
+
+
+
+
+ 🧭 Rehberli Turlar
+ Bağımlılığa göre sıralanmış, mimarinin otomatik oluşturulmuş gözden geçirmeleri. Kod tabanını doğru sırayla öğren.
+
+
+ 🔍 Bulanık ve Anlamsal Arama
+ İsme veya anlamına göre her şeyi bul. "Kimlik doğrulamayı hangi parçalar yönetiyor?" ara ve grafik boyunca ilgili sonuçları al.
+
+
+
+
+ 📊 Diff Etki Analizi
+ Değişikliklerinin sistemin hangi bölümlerini etkilediğini commit etmeden önce gör. Kod tabanı boyunca dalgalanma etkilerini anla.
+
+
+ 🎭 Kişiye Uyarlanabilir UI
+ Kontrol paneli, kim olduğuna göre ayrıntı seviyesini ayarlar — junior geliştirici, ürün yöneticisi veya güçlü kullanıcı.
+
+
+
+
+ 🏗️ Katman Görselleştirmesi
+ Mimari katmana göre otomatik gruplama — API, Servis, Veri, UI, Yardımcı — renk kodlu efsaneyle.
+
+
+ 📚 Dil Kavramları
+ 12 programlama deseni (generikler, kapanışlar, dekoratörler, vb.) göründükleri her yerde bağlam içinde açıklanır.
+
+
+
+
+---
+
+## 🔧 Kaputun Altında
+
+### Çok-Ajan Hattı
+
+`/understand` komutu 5 özel ajan düzenler:
+
+| Ajan | Rol |
+|-------|------|
+| `project-scanner` | Dosyaları keşfet, dilleri ve çerçeveleri tespit et |
+| `file-analyzer` | Fonksiyonları, sınıfları, içe aktarmaları çıkar; grafik düğümleri ve kenarları üret |
+| `architecture-analyzer` | Mimari katmanları tanımla |
+| `tour-builder` | Rehberli öğrenme turları oluştur |
+| `graph-reviewer` | Grafik bütünlüğünü ve referans bütünlüğünü doğrula |
+
+Dosya analizörleri paralel çalışır (en fazla 3 eşzamanlı). Artımlı güncellemeleri destekler — yalnızca son çalıştırmadan bu yana değişen dosyaları yeniden analiz eder.
+
+### Proje Yapısı
+
+```
+understand-anything-plugin/
+ .claude-plugin/ — Eklenti manifestosu
+ agents/ — Özel AI ajanları
+ skills/ — Yetenek tanımları (/understand, /understand-chat, vb.)
+ src/ — TypeScript kaynağı (context-builder, diff-analyzer, vb.)
+ packages/
+ core/ — Analiz motoru (tipler, kalıcılık, tree-sitter, arama, şema, turlar)
+ dashboard/ — React + TypeScript web kontrol paneli
+```
+
+### Teknoloji Yığını
+
+TypeScript, pnpm workspaces, React 18, Vite, TailwindCSS v4, React Flow, Zustand, web-tree-sitter, Fuse.js, Zod, Dagre
+
+### Geliştirme Komutları
+
+| Komut | Açıklama |
+|---------|-------------|
+| `pnpm install` | Tüm bağımlılıkları yükle |
+| `pnpm --filter @understand-anything/core build` | Core paketini derle |
+| `pnpm --filter @understand-anything/core test` | Core testlerini çalıştır |
+| `pnpm --filter @understand-anything/skill build` | Eklenti paketini derle |
+| `pnpm --filter @understand-anything/skill test` | Eklenti testlerini çalıştır |
+| `pnpm --filter @understand-anything/dashboard build` | Kontrol panelini derle |
+| `pnpm dev:dashboard` | Kontrol paneli geliştirme sunucusunu başlat |
+
+---
+
+## 🤝 Katkıda Bulunma
+
+Katkılar memnuniyetle karşılanır! Başlamak için:
+
+1. Depoyu fork'la
+2. Bir özellik dalı oluştur (`git checkout -b feature/benim-ozellligim`)
+3. Testleri çalıştır (`pnpm --filter @understand-anything/core test`)
+4. Değişikliklerini commit et ve bir pull request aç
+
+Büyük değişiklikler için lütfen önce bir issue aç ki yaklaşımı tartışalım.
+
+---
+
+
+ Kodu körü körüne okumayı bırak. Her şeyi anlamaya başla.
+
+
+## Star Geçmişi
+
+
+
+
+
+
+
+
+
+
+ MIT Lisansı © Lum1104
+
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 7edf497..79b4ad9 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -4,7 +4,7 @@
- English | 中文
+ English | 中文 | 日本語 | Türkçe
@@ -121,11 +121,9 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
### OpenCode
-添加到你的 `opencode.json` 文件:
-```json
-{
- "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
-}
+告诉 OpenCode:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
```
### OpenClaw
@@ -139,15 +137,39 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
克隆此仓库后,Cursor 会自动通过 `.cursor-plugin/plugin.json`文件发现插件。无需手动安装 — 只需克隆并在 Cursor 中打开即可。
+### Antigravity
+
+告诉 Antigravity:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.antigravity/INSTALL.md
+```
+
+### Gemini CLI
+
+告诉 Gemini CLI:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.gemini/INSTALL.md
+```
+
+### Pi Agent
+
+告诉 Pi Agent:
+```text
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.pi/INSTALL.md
+```
+
### 多平台兼容
| 平台 | 状态 | 安装方式 |
|----------|--------|----------------|
| Claude Code | ✅ Native | 插件市场 |
| Codex | ✅ 支持 | AI驱动安装 |
-| OpenCode | ✅ 支持 | 插件配置 |
+| OpenCode | ✅ 支持 | AI驱动安装 |
| OpenClaw | ✅ 支持 | AI驱动安装 |
| Cursor | ✅ 支持 | 自动发现 |
+| Antigravity | ✅ 支持 | AI驱动安装 |
+| Gemini CLI | ✅ 支持 | AI驱动安装 |
+| Pi Agent | ✅ 支持 | AI驱动安装 |
---
diff --git a/docs/plans/2026-03-21-language-agnostic-design.md b/docs/plans/2026-03-21-language-agnostic-design.md
new file mode 100644
index 0000000..1d0ec36
--- /dev/null
+++ b/docs/plans/2026-03-21-language-agnostic-design.md
@@ -0,0 +1,249 @@
+# Language-Agnostic Support Design
+
+**Date:** 2026-03-21
+**Status:** Approved
+**Issue:** Make Understand-Anything codebase-aware and language-agnostic instead of TypeScript-heavy
+
+## Problem
+
+The tool's agent prompts, tree-sitter plugin, and language lesson system are heavily biased toward TypeScript/JavaScript. Non-TS codebases get degraded analysis because:
+
+1. Agent prompts use TS-specific examples and concepts (e.g., "barrel files", "type guards", "generics")
+2. Tree-sitter plugin only ships TS/JS grammar support — structural analysis silently fails for other languages
+3. Language lesson detection hardcodes TS-specific concept patterns and display names
+
+The architecture (PluginRegistry, GraphBuilder, dashboard, search) is already language-neutral. The bias is in shipped content, not the framework.
+
+## Decisions
+
+- **Scope:** All three layers — prompts, tree-sitter plugins, language framework
+- **Languages (v1):** TypeScript, JavaScript, Python, Go, Java, Rust, C/C++, C#, Ruby, PHP, Swift, Kotlin
+- **Architecture:** Config-first with code escape hatch (hybrid)
+- **Prompt strategy:** Base prompt + per-language markdown snippet files in a `languages/` folder
+- **Config location:** Prompt snippets in `skills/understand/languages/`, tree-sitter configs in `packages/core/src/languages/`
+- **Multi-language projects:** Per-file language analysis + project-level multi-language summary
+- **Language detection:** Auto-detect from file extensions only (no manual override for v1)
+
+## Design
+
+### 1. LanguageConfig Type & Registry
+
+#### LanguageConfig Interface
+
+```typescript
+// packages/core/src/languages/types.ts
+interface LanguageConfig {
+ id: string; // e.g., "python"
+ displayName: string; // e.g., "Python"
+ extensions: string[]; // e.g., [".py", ".pyi"]
+ treeSitter: {
+ grammarPackage: string; // npm package name
+ nodeTypes: {
+ function: string[]; // e.g., ["function_definition"]
+ class: string[]; // e.g., ["class_definition"]
+ import: string[]; // e.g., ["import_statement", "import_from_statement"]
+ export: string[]; // e.g., ["export_statement"] or [] for languages without exports
+ typeAnnotation: string[]; // e.g., ["type"] for Python type hints
+ };
+ };
+ concepts: string[]; // e.g., ["decorators", "list comprehensions", "generators"]
+ filePatterns?: Record; // special files, e.g., {"config": "pyproject.toml"}
+ customAnalyzer?: (node: SyntaxNode) => AnalysisResult; // escape hatch for unusual AST shapes
+}
+```
+
+#### Language Registry
+
+```typescript
+// packages/core/src/languages/registry.ts
+class LanguageRegistry {
+ private configs: Map;
+
+ register(config: LanguageConfig): void;
+ getByExtension(ext: string): LanguageConfig | null;
+ getById(id: string): LanguageConfig;
+ getAll(): LanguageConfig[];
+}
+```
+
+#### File Structure
+
+```
+packages/core/src/languages/
+├── types.ts
+├── registry.ts
+├── index.ts
+├── configs/
+│ ├── typescript.ts
+│ ├── javascript.ts
+│ ├── python.ts
+│ ├── go.ts
+│ ├── java.ts
+│ ├── rust.ts
+│ ├── cpp.ts
+│ ├── csharp.ts
+│ ├── ruby.ts
+│ ├── php.ts
+│ ├── swift.ts
+│ └── kotlin.ts
+```
+
+All built-in configs auto-registered on import.
+
+### 2. GenericTreeSitterPlugin
+
+Replaces the current TS-only `TreeSitterPlugin` with a config-driven version.
+
+```typescript
+// packages/core/src/plugins/generic-tree-sitter-plugin.ts
+class GenericTreeSitterPlugin implements AnalyzerPlugin {
+ private registry: LanguageRegistry;
+
+ canAnalyze(filePath: string): boolean {
+ return this.registry.getByExtension(path.extname(filePath)) !== null;
+ }
+
+ async analyzeFile(filePath: string, content: string): Promise {
+ const config = this.registry.getByExtension(path.extname(filePath));
+
+ // Custom analyzer escape hatch
+ if (config.customAnalyzer) {
+ return config.customAnalyzer(tree.rootNode);
+ }
+
+ // Generic extraction driven by config.treeSitter.nodeTypes
+ const functions = this.extractNodes(tree, config.treeSitter.nodeTypes.function);
+ const classes = this.extractNodes(tree, config.treeSitter.nodeTypes.class);
+ const imports = this.extractNodes(tree, config.treeSitter.nodeTypes.import);
+ const exports = this.extractNodes(tree, config.treeSitter.nodeTypes.export);
+ // ...
+ }
+
+ private extractNodes(tree: Tree, nodeTypes: string[]): NodeInfo[] {
+ // Walk AST, collect all nodes matching any of the given types
+ }
+}
+```
+
+#### Migration
+
+- Current `TreeSitterPlugin` deleted, replaced by `GenericTreeSitterPlugin` + TS/JS configs
+- `PluginRegistry` unchanged
+- Existing tests updated to use new plugin
+
+#### WASM Grammar Loading
+
+- Each grammar loaded lazily on first use and cached
+- WASM files bundled in `packages/core/src/languages/grammars/` or fetched from tree-sitter's official WASM builds
+
+### 3. Language-Aware Prompts
+
+#### File Structure
+
+```
+skills/understand/
+├── file-analyzer-prompt.md # Base prompt (language-neutral)
+├── tour-builder-prompt.md
+├── project-scanner-prompt.md
+├── languages/
+│ ├── typescript.md
+│ ├── javascript.md
+│ ├── python.md
+│ ├── go.md
+│ ├── java.md
+│ ├── rust.md
+│ ├── cpp.md
+│ ├── csharp.md
+│ ├── ruby.md
+│ ├── php.md
+│ ├── swift.md
+│ └── kotlin.md
+```
+
+#### Base Prompt Changes
+
+All TS-specific examples removed from base prompts. Replaced with injection point:
+
+```markdown
+## Language-Specific Guidance
+
+{{LANGUAGE_CONTEXT}}
+```
+
+#### Language Markdown Format
+
+Each language file contains:
+
+```markdown
+# Python
+
+## Key Concepts
+- Decorators, comprehensions, generators, context managers, type hints, dunder methods
+
+## Import Patterns
+- `import module`, `from module import name`, relative imports
+
+## Notable File Patterns
+- `__init__.py` (package initializer), `conftest.py` (pytest), `pyproject.toml` (config)
+
+## Example Summary Style
+> "FastAPI route handler that accepts a Pydantic model, validates input..."
+```
+
+#### Injection Logic
+
+1. Project scanner detects languages present in the codebase
+2. File-analyzer: inject matching language `.md` for that file's language
+3. Tour-builder: inject all detected languages' `.md` files
+4. Project-scanner: inject all detected languages' key concepts for project-level summary
+
+#### Multi-Language Projects
+
+Project-scanner prompt gets a combined section listing all detected languages with their key concepts.
+
+### 4. Language Lesson Updates
+
+- Delete `LANGUAGE_DISPLAY_NAMES` — use `LanguageRegistry.getById(id).displayName`
+- Delete hardcoded concept patterns — use `LanguageConfig.concepts` from registry
+- Language lesson generation becomes config-driven
+
+### 5. Testing Strategy
+
+#### Unit Tests
+
+1. **LanguageConfig validation** — Each config has all required fields, non-empty nodeTypes
+2. **LanguageRegistry** — Registration, lookup by extension/id, duplicate handling
+3. **GenericTreeSitterPlugin per language** — Small fixture file per language verifying function/class/import extraction
+4. **Language lesson generation** — Concepts sourced from config
+
+#### Integration Tests
+
+5. **Multi-language project** — Mixed TS + Python fixture, verify graph contains nodes from both languages
+6. **Prompt injection** — Correct language `.md` injected based on detected language
+
+#### Migration Tests
+
+- Current tree-sitter-plugin tests rewritten for GenericTreeSitterPlugin with TS config
+- Must produce identical results to validate non-breaking migration
+
+### 6. Error Handling & Graceful Degradation
+
+#### Key Principle
+
+**Every file always gets analyzed.** Tree-sitter is an enhancement, not a gate. The LLM is the primary analyzer; structural analysis enriches it.
+
+#### Unknown Language
+
+- Tree-sitter skipped (returns `null`)
+- LLM analysis still runs — file gets summary, tags, graph node
+- Debug log: `"No language config for .xyz, skipping structural analysis"`
+
+#### Missing WASM Grammar
+
+- Warning logged, that language degrades to LLM-only
+- Other languages unaffected
+
+#### Malformed Language Config
+
+- Validated at registration time via Zod schema
+- Invalid config throws at startup — fail fast
diff --git a/docs/plans/2026-03-21-language-agnostic-plan.md b/docs/plans/2026-03-21-language-agnostic-plan.md
new file mode 100644
index 0000000..16e14c4
--- /dev/null
+++ b/docs/plans/2026-03-21-language-agnostic-plan.md
@@ -0,0 +1,1392 @@
+# Language-Agnostic Support Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Make Understand-Anything language-agnostic by introducing a config-driven language framework, replacing the TS-only tree-sitter plugin, and creating language-aware prompts for 12 languages.
+
+**Architecture:** Config-first hybrid approach — each language defined by a `LanguageConfig` object (tree-sitter node mappings, concepts, extensions) plus a prompt snippet markdown file. A single `GenericTreeSitterPlugin` replaces the hardcoded TS-only plugin, driven by whichever config matches the file extension.
+
+**Tech Stack:** TypeScript, web-tree-sitter (WASM), Zod v4, Vitest
+
+---
+
+### Task 1: Create LanguageConfig types and Zod schema
+
+**Files:**
+- Create: `understand-anything-plugin/packages/core/src/languages/types.ts`
+
+**Step 1: Write the failing test**
+
+Create: `understand-anything-plugin/packages/core/src/languages/__tests__/types.test.ts`
+
+```typescript
+import { describe, it, expect } from "vitest";
+import { LanguageConfigSchema } from "../types.js";
+
+describe("LanguageConfigSchema", () => {
+ it("validates a complete language config", () => {
+ const config = {
+ id: "python",
+ displayName: "Python",
+ extensions: [".py", ".pyi"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-python",
+ wasmFile: "tree-sitter-python.wasm",
+ nodeTypes: {
+ function: ["function_definition"],
+ class: ["class_definition"],
+ import: ["import_statement", "import_from_statement"],
+ export: [],
+ typeAnnotation: ["type"],
+ },
+ },
+ concepts: ["decorators", "list comprehensions", "generators"],
+ };
+ const result = LanguageConfigSchema.safeParse(config);
+ expect(result.success).toBe(true);
+ });
+
+ it("rejects config missing required fields", () => {
+ const result = LanguageConfigSchema.safeParse({ id: "python" });
+ expect(result.success).toBe(false);
+ });
+
+ it("accepts optional filePatterns", () => {
+ const config = {
+ id: "python",
+ displayName: "Python",
+ extensions: [".py"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-python",
+ wasmFile: "tree-sitter-python.wasm",
+ nodeTypes: {
+ function: ["function_definition"],
+ class: ["class_definition"],
+ import: ["import_statement"],
+ export: [],
+ typeAnnotation: [],
+ },
+ },
+ concepts: ["decorators"],
+ filePatterns: { config: "pyproject.toml" },
+ };
+ const result = LanguageConfigSchema.safeParse(config);
+ expect(result.success).toBe(true);
+ });
+});
+```
+
+**Step 2: Run test to verify it fails**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/types.test.ts`
+Expected: FAIL — module `../types.js` not found
+
+**Step 3: Write minimal implementation**
+
+Create: `understand-anything-plugin/packages/core/src/languages/types.ts`
+
+```typescript
+import { z } from "zod/v4";
+
+export const TreeSitterConfigSchema = z.object({
+ grammarPackage: z.string(),
+ wasmFile: z.string(),
+ nodeTypes: z.object({
+ function: z.array(z.string()),
+ class: z.array(z.string()),
+ import: z.array(z.string()),
+ export: z.array(z.string()),
+ typeAnnotation: z.array(z.string()),
+ }),
+});
+
+export const LanguageConfigSchema = z.object({
+ id: z.string(),
+ displayName: z.string(),
+ extensions: z.array(z.string()),
+ treeSitter: TreeSitterConfigSchema,
+ concepts: z.array(z.string()),
+ filePatterns: z.record(z.string(), z.string()).optional(),
+});
+
+export type LanguageConfig = z.infer;
+export type TreeSitterConfig = z.infer;
+```
+
+**Step 4: Run test to verify it passes**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/types.test.ts`
+Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add understand-anything-plugin/packages/core/src/languages/
+git commit -m "feat: add LanguageConfig types and Zod schema"
+```
+
+---
+
+### Task 2: Create LanguageRegistry
+
+**Files:**
+- Create: `understand-anything-plugin/packages/core/src/languages/registry.ts`
+
+**Step 1: Write the failing test**
+
+Create: `understand-anything-plugin/packages/core/src/languages/__tests__/registry.test.ts`
+
+```typescript
+import { describe, it, expect } from "vitest";
+import { LanguageRegistry } from "../registry.js";
+import type { LanguageConfig } from "../types.js";
+
+const pythonConfig: LanguageConfig = {
+ id: "python",
+ displayName: "Python",
+ extensions: [".py", ".pyi"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-python",
+ wasmFile: "tree-sitter-python.wasm",
+ nodeTypes: {
+ function: ["function_definition"],
+ class: ["class_definition"],
+ import: ["import_statement", "import_from_statement"],
+ export: [],
+ typeAnnotation: ["type"],
+ },
+ },
+ concepts: ["decorators", "generators"],
+};
+
+const tsConfig: LanguageConfig = {
+ id: "typescript",
+ displayName: "TypeScript",
+ extensions: [".ts", ".tsx"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-typescript",
+ wasmFile: "tree-sitter-typescript.wasm",
+ nodeTypes: {
+ function: ["function_declaration"],
+ class: ["class_declaration"],
+ import: ["import_statement"],
+ export: ["export_statement"],
+ typeAnnotation: ["type_annotation"],
+ },
+ },
+ concepts: ["generics", "type guards", "decorators"],
+};
+
+describe("LanguageRegistry", () => {
+ it("registers and retrieves a config by id", () => {
+ const registry = new LanguageRegistry();
+ registry.register(pythonConfig);
+ expect(registry.getById("python")).toBe(pythonConfig);
+ });
+
+ it("retrieves config by file extension", () => {
+ const registry = new LanguageRegistry();
+ registry.register(pythonConfig);
+ expect(registry.getByExtension(".py")).toBe(pythonConfig);
+ expect(registry.getByExtension(".pyi")).toBe(pythonConfig);
+ });
+
+ it("returns null for unknown extension", () => {
+ const registry = new LanguageRegistry();
+ registry.register(pythonConfig);
+ expect(registry.getByExtension(".rs")).toBeNull();
+ });
+
+ it("returns all registered configs", () => {
+ const registry = new LanguageRegistry();
+ registry.register(pythonConfig);
+ registry.register(tsConfig);
+ expect(registry.getAll()).toHaveLength(2);
+ });
+
+ it("later registration overrides same id", () => {
+ const registry = new LanguageRegistry();
+ const updated = { ...pythonConfig, displayName: "Python 3" };
+ registry.register(pythonConfig);
+ registry.register(updated);
+ expect(registry.getById("python")?.displayName).toBe("Python 3");
+ });
+
+ it("throws on invalid config", () => {
+ const registry = new LanguageRegistry();
+ expect(() => registry.register({ id: "bad" } as LanguageConfig)).toThrow();
+ });
+});
+```
+
+**Step 2: Run test to verify it fails**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/registry.test.ts`
+Expected: FAIL — module `../registry.js` not found
+
+**Step 3: Write minimal implementation**
+
+```typescript
+// understand-anything-plugin/packages/core/src/languages/registry.ts
+import { LanguageConfigSchema } from "./types.js";
+import type { LanguageConfig } from "./types.js";
+
+export class LanguageRegistry {
+ private configs = new Map();
+ private extensionMap = new Map();
+
+ register(config: LanguageConfig): void {
+ const result = LanguageConfigSchema.safeParse(config);
+ if (!result.success) {
+ throw new Error(`Invalid LanguageConfig for "${config.id}": ${result.error.message}`);
+ }
+ this.configs.set(config.id, config);
+ for (const ext of config.extensions) {
+ this.extensionMap.set(ext, config.id);
+ }
+ }
+
+ getById(id: string): LanguageConfig | null {
+ return this.configs.get(id) ?? null;
+ }
+
+ getByExtension(ext: string): LanguageConfig | null {
+ const id = this.extensionMap.get(ext);
+ if (!id) return null;
+ return this.configs.get(id) ?? null;
+ }
+
+ getAll(): LanguageConfig[] {
+ return [...this.configs.values()];
+ }
+}
+```
+
+**Step 4: Run test to verify it passes**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/registry.test.ts`
+Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add understand-anything-plugin/packages/core/src/languages/
+git commit -m "feat: add LanguageRegistry with Zod validation"
+```
+
+---
+
+### Task 3: Create all 12 language configs
+
+**Files:**
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/typescript.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/javascript.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/python.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/go.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/java.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/rust.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/cpp.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/csharp.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/ruby.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/php.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/swift.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/kotlin.ts`
+- Create: `understand-anything-plugin/packages/core/src/languages/configs/index.ts`
+
+**Step 1: Write the failing test**
+
+Create: `understand-anything-plugin/packages/core/src/languages/__tests__/configs.test.ts`
+
+```typescript
+import { describe, it, expect } from "vitest";
+import { LanguageConfigSchema } from "../types.js";
+import { builtinConfigs } from "../configs/index.js";
+
+describe("builtin language configs", () => {
+ it("has 12 language configs", () => {
+ expect(builtinConfigs).toHaveLength(12);
+ });
+
+ it("all configs pass Zod validation", () => {
+ for (const config of builtinConfigs) {
+ const result = LanguageConfigSchema.safeParse(config);
+ expect(result.success, `${config.id} failed validation: ${result.error?.message}`).toBe(true);
+ }
+ });
+
+ it("all configs have unique ids", () => {
+ const ids = builtinConfigs.map((c) => c.id);
+ expect(new Set(ids).size).toBe(ids.length);
+ });
+
+ it("no duplicate extensions across configs", () => {
+ const allExts: string[] = [];
+ for (const config of builtinConfigs) {
+ allExts.push(...config.extensions);
+ }
+ expect(new Set(allExts).size).toBe(allExts.length);
+ });
+
+ it("all configs have non-empty function and class node types", () => {
+ for (const config of builtinConfigs) {
+ expect(config.treeSitter.nodeTypes.function.length, `${config.id} missing function types`).toBeGreaterThan(0);
+ expect(config.treeSitter.nodeTypes.class.length, `${config.id} missing class types`).toBeGreaterThanOrEqual(0);
+ }
+ });
+
+ it("all configs have at least one concept", () => {
+ for (const config of builtinConfigs) {
+ expect(config.concepts.length, `${config.id} has no concepts`).toBeGreaterThan(0);
+ }
+ });
+});
+```
+
+**Step 2: Run test to verify it fails**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/configs.test.ts`
+Expected: FAIL — module not found
+
+**Step 3: Write all config files**
+
+Each config file exports a `LanguageConfig`. Here are the key ones (the rest follow the same pattern):
+
+**typescript.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const typescriptConfig: LanguageConfig = {
+ id: "typescript",
+ displayName: "TypeScript",
+ extensions: [".ts", ".tsx"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-typescript",
+ wasmFile: "tree-sitter-typescript.wasm",
+ nodeTypes: {
+ function: ["function_declaration"],
+ class: ["class_declaration"],
+ import: ["import_statement"],
+ export: ["export_statement"],
+ typeAnnotation: ["type_annotation"],
+ },
+ },
+ concepts: [
+ "generics", "type guards", "discriminated unions", "utility types",
+ "decorators", "enums", "interfaces", "type inference",
+ "mapped types", "conditional types", "template literal types",
+ ],
+ filePatterns: { config: "tsconfig.json", manifest: "package.json" },
+};
+```
+
+**python.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const pythonConfig: LanguageConfig = {
+ id: "python",
+ displayName: "Python",
+ extensions: [".py", ".pyi"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-python",
+ wasmFile: "tree-sitter-python.wasm",
+ nodeTypes: {
+ function: ["function_definition"],
+ class: ["class_definition"],
+ import: ["import_statement", "import_from_statement"],
+ export: [],
+ typeAnnotation: ["type"],
+ },
+ },
+ concepts: [
+ "decorators", "list comprehensions", "generators", "context managers",
+ "type hints", "dunder methods", "metaclasses", "dataclasses",
+ "async/await", "descriptors",
+ ],
+ filePatterns: { config: "pyproject.toml", manifest: "setup.py" },
+};
+```
+
+**go.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const goConfig: LanguageConfig = {
+ id: "go",
+ displayName: "Go",
+ extensions: [".go"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-go",
+ wasmFile: "tree-sitter-go.wasm",
+ nodeTypes: {
+ function: ["function_declaration", "method_declaration"],
+ class: ["type_declaration"],
+ import: ["import_declaration"],
+ export: [],
+ typeAnnotation: [],
+ },
+ },
+ concepts: [
+ "goroutines", "channels", "interfaces", "struct embedding",
+ "error handling patterns", "defer/panic/recover", "slices",
+ "pointers", "concurrency patterns",
+ ],
+ filePatterns: { config: "go.mod" },
+};
+```
+
+**java.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const javaConfig: LanguageConfig = {
+ id: "java",
+ displayName: "Java",
+ extensions: [".java"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-java",
+ wasmFile: "tree-sitter-java.wasm",
+ nodeTypes: {
+ function: ["method_declaration", "constructor_declaration"],
+ class: ["class_declaration", "interface_declaration", "enum_declaration"],
+ import: ["import_declaration"],
+ export: [],
+ typeAnnotation: ["type_identifier"],
+ },
+ },
+ concepts: [
+ "generics", "annotations", "interfaces", "abstract classes",
+ "streams API", "lambdas", "sealed classes", "records",
+ "dependency injection", "checked exceptions",
+ ],
+ filePatterns: { config: "pom.xml", manifest: "build.gradle" },
+};
+```
+
+**rust.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const rustConfig: LanguageConfig = {
+ id: "rust",
+ displayName: "Rust",
+ extensions: [".rs"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-rust",
+ wasmFile: "tree-sitter-rust.wasm",
+ nodeTypes: {
+ function: ["function_item"],
+ class: ["struct_item", "enum_item", "impl_item", "trait_item"],
+ import: ["use_declaration"],
+ export: [],
+ typeAnnotation: ["type_identifier"],
+ },
+ },
+ concepts: [
+ "ownership", "borrowing", "lifetimes", "traits", "pattern matching",
+ "enums with data", "error handling (Result/Option)", "macros",
+ "async/await", "unsafe blocks", "generics", "closures",
+ ],
+ filePatterns: { config: "Cargo.toml" },
+};
+```
+
+**cpp.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const cppConfig: LanguageConfig = {
+ id: "cpp",
+ displayName: "C/C++",
+ extensions: [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hxx"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-cpp",
+ wasmFile: "tree-sitter-cpp.wasm",
+ nodeTypes: {
+ function: ["function_definition"],
+ class: ["class_specifier", "struct_specifier"],
+ import: ["preproc_include"],
+ export: [],
+ typeAnnotation: [],
+ },
+ },
+ concepts: [
+ "templates", "RAII", "smart pointers", "move semantics",
+ "operator overloading", "virtual functions", "namespaces",
+ "constexpr", "lambda expressions", "STL containers",
+ ],
+ filePatterns: { config: "CMakeLists.txt", manifest: "Makefile" },
+};
+```
+
+**csharp.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const csharpConfig: LanguageConfig = {
+ id: "csharp",
+ displayName: "C#",
+ extensions: [".cs"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-c-sharp",
+ wasmFile: "tree-sitter-c_sharp.wasm",
+ nodeTypes: {
+ function: ["method_declaration", "constructor_declaration"],
+ class: ["class_declaration", "interface_declaration", "struct_declaration", "enum_declaration", "record_declaration"],
+ import: ["using_directive"],
+ export: [],
+ typeAnnotation: ["type_identifier"],
+ },
+ },
+ concepts: [
+ "LINQ", "async/await", "generics", "properties",
+ "delegates and events", "attributes", "nullable reference types",
+ "pattern matching", "records", "dependency injection",
+ ],
+ filePatterns: { config: "*.csproj" },
+};
+```
+
+**ruby.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const rubyConfig: LanguageConfig = {
+ id: "ruby",
+ displayName: "Ruby",
+ extensions: [".rb", ".rake"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-ruby",
+ wasmFile: "tree-sitter-ruby.wasm",
+ nodeTypes: {
+ function: ["method"],
+ class: ["class", "module"],
+ import: ["call"],
+ export: [],
+ typeAnnotation: [],
+ },
+ },
+ concepts: [
+ "blocks and procs", "mixins", "metaprogramming", "duck typing",
+ "DSLs", "monkey patching", "gems", "symbols",
+ "method_missing", "open classes",
+ ],
+ filePatterns: { config: "Gemfile" },
+};
+```
+
+**php.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const phpConfig: LanguageConfig = {
+ id: "php",
+ displayName: "PHP",
+ extensions: [".php"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-php",
+ wasmFile: "tree-sitter-php.wasm",
+ nodeTypes: {
+ function: ["function_definition", "method_declaration"],
+ class: ["class_declaration", "interface_declaration", "trait_declaration"],
+ import: ["namespace_use_declaration"],
+ export: [],
+ typeAnnotation: ["type_list", "named_type"],
+ },
+ },
+ concepts: [
+ "namespaces", "traits", "type declarations", "attributes",
+ "enums", "fibers", "closures", "magic methods",
+ "dependency injection", "middleware",
+ ],
+ filePatterns: { config: "composer.json" },
+};
+```
+
+**swift.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const swiftConfig: LanguageConfig = {
+ id: "swift",
+ displayName: "Swift",
+ extensions: [".swift"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-swift",
+ wasmFile: "tree-sitter-swift.wasm",
+ nodeTypes: {
+ function: ["function_declaration", "init_declaration"],
+ class: ["class_declaration", "struct_declaration", "protocol_declaration", "enum_declaration"],
+ import: ["import_declaration"],
+ export: [],
+ typeAnnotation: ["type_annotation"],
+ },
+ },
+ concepts: [
+ "optionals", "protocols", "extensions", "generics",
+ "closures", "property wrappers", "result builders",
+ "actors", "structured concurrency", "value types vs reference types",
+ ],
+ filePatterns: { config: "Package.swift" },
+};
+```
+
+**kotlin.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const kotlinConfig: LanguageConfig = {
+ id: "kotlin",
+ displayName: "Kotlin",
+ extensions: [".kt", ".kts"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-kotlin",
+ wasmFile: "tree-sitter-kotlin.wasm",
+ nodeTypes: {
+ function: ["function_declaration"],
+ class: ["class_declaration", "object_declaration", "interface_declaration"],
+ import: ["import_header"],
+ export: [],
+ typeAnnotation: ["type_identifier"],
+ },
+ },
+ concepts: [
+ "coroutines", "data classes", "sealed classes", "extension functions",
+ "null safety", "delegation", "DSL builders", "inline functions",
+ "companion objects", "flow",
+ ],
+ filePatterns: { config: "build.gradle.kts" },
+};
+```
+
+**javascript.ts:**
+```typescript
+import type { LanguageConfig } from "../types.js";
+
+export const javascriptConfig: LanguageConfig = {
+ id: "javascript",
+ displayName: "JavaScript",
+ extensions: [".js", ".mjs", ".cjs", ".jsx"],
+ treeSitter: {
+ grammarPackage: "tree-sitter-javascript",
+ wasmFile: "tree-sitter-javascript.wasm",
+ nodeTypes: {
+ function: ["function_declaration"],
+ class: ["class_declaration"],
+ import: ["import_statement"],
+ export: ["export_statement"],
+ typeAnnotation: [],
+ },
+ },
+ concepts: [
+ "closures", "prototypes", "promises", "async/await",
+ "event loop", "destructuring", "spread operator",
+ "proxies", "generators", "modules (ESM/CJS)",
+ ],
+ filePatterns: { config: "package.json" },
+};
+```
+
+**configs/index.ts:**
+```typescript
+import { typescriptConfig } from "./typescript.js";
+import { javascriptConfig } from "./javascript.js";
+import { pythonConfig } from "./python.js";
+import { goConfig } from "./go.js";
+import { javaConfig } from "./java.js";
+import { rustConfig } from "./rust.js";
+import { cppConfig } from "./cpp.js";
+import { csharpConfig } from "./csharp.js";
+import { rubyConfig } from "./ruby.js";
+import { phpConfig } from "./php.js";
+import { swiftConfig } from "./swift.js";
+import { kotlinConfig } from "./kotlin.js";
+import type { LanguageConfig } from "../types.js";
+
+export const builtinConfigs: LanguageConfig[] = [
+ typescriptConfig,
+ javascriptConfig,
+ pythonConfig,
+ goConfig,
+ javaConfig,
+ rustConfig,
+ cppConfig,
+ csharpConfig,
+ rubyConfig,
+ phpConfig,
+ swiftConfig,
+ kotlinConfig,
+];
+```
+
+**Step 4: Run test to verify it passes**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/languages/__tests__/configs.test.ts`
+Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add understand-anything-plugin/packages/core/src/languages/configs/
+git commit -m "feat: add 12 builtin language configs"
+```
+
+---
+
+### Task 4: Create languages/index.ts barrel and export from core
+
+**Files:**
+- Create: `understand-anything-plugin/packages/core/src/languages/index.ts`
+- Modify: `understand-anything-plugin/packages/core/src/index.ts`
+
+**Step 1: Create barrel export**
+
+```typescript
+// understand-anything-plugin/packages/core/src/languages/index.ts
+export { LanguageRegistry } from "./registry.js";
+export { LanguageConfigSchema } from "./types.js";
+export type { LanguageConfig, TreeSitterConfig } from "./types.js";
+export { builtinConfigs } from "./configs/index.js";
+```
+
+**Step 2: Add export to core index.ts**
+
+Add to `understand-anything-plugin/packages/core/src/index.ts`:
+
+```typescript
+// Languages
+export { LanguageRegistry, builtinConfigs, LanguageConfigSchema } from "./languages/index.js";
+export type { LanguageConfig, TreeSitterConfig } from "./languages/index.js";
+```
+
+**Step 3: Build and verify**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build`
+Expected: Build succeeds with no errors
+
+**Step 4: Commit**
+
+```bash
+git add understand-anything-plugin/packages/core/src/languages/index.ts understand-anything-plugin/packages/core/src/index.ts
+git commit -m "feat: export language types and registry from core"
+```
+
+---
+
+### Task 5: Install tree-sitter WASM grammar packages
+
+**Files:**
+- Modify: `understand-anything-plugin/packages/core/package.json`
+
+**Step 1: Install new grammar packages**
+
+Run:
+```bash
+cd understand-anything-plugin && pnpm --filter @understand-anything/core add \
+ tree-sitter-python \
+ tree-sitter-go \
+ tree-sitter-java \
+ tree-sitter-rust \
+ tree-sitter-cpp \
+ tree-sitter-c-sharp \
+ tree-sitter-ruby \
+ tree-sitter-php \
+ tree-sitter-swift \
+ tree-sitter-kotlin
+```
+
+Note: Some grammar packages may not ship `.wasm` files. For those, we need to check availability and potentially build from source or use the `tree-sitter` CLI to generate WASM. Verify each package after install:
+
+```bash
+cd understand-anything-plugin && for lang in python go java rust cpp c-sharp ruby php swift kotlin; do
+ echo "=== tree-sitter-$lang ==="
+ ls node_modules/tree-sitter-$lang/*.wasm 2>/dev/null || echo "NO WASM FOUND"
+done
+```
+
+For packages without pre-built WASM, use `tree-sitter build --wasm` to compile them, or find alternative npm packages that ship WASM builds. Document which packages needed manual WASM generation.
+
+**Step 2: Verify build still passes**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build`
+Expected: PASS
+
+**Step 3: Commit**
+
+```bash
+git add understand-anything-plugin/packages/core/package.json understand-anything-plugin/pnpm-lock.yaml
+git commit -m "feat: add tree-sitter grammar packages for 10 new languages"
+```
+
+---
+
+### Task 6: Build GenericTreeSitterPlugin
+
+**Files:**
+- Create: `understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.ts`
+
+**Step 1: Write the failing test**
+
+Create: `understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.test.ts`
+
+```typescript
+import { describe, it, expect, beforeAll } from "vitest";
+import { GenericTreeSitterPlugin } from "./generic-tree-sitter-plugin.js";
+import { LanguageRegistry } from "../languages/registry.js";
+import { typescriptConfig } from "../languages/configs/typescript.js";
+import { javascriptConfig } from "../languages/configs/javascript.js";
+import { pythonConfig } from "../languages/configs/python.js";
+
+describe("GenericTreeSitterPlugin", () => {
+ let plugin: GenericTreeSitterPlugin;
+
+ beforeAll(async () => {
+ const registry = new LanguageRegistry();
+ registry.register(typescriptConfig);
+ registry.register(javascriptConfig);
+ registry.register(pythonConfig);
+ plugin = new GenericTreeSitterPlugin(registry);
+ await plugin.init();
+ });
+
+ describe("TypeScript (migration parity)", () => {
+ it("extracts function declarations", () => {
+ const code = `
+function greet(name: string): string {
+ return "Hello " + name;
+}
+`;
+ const result = plugin.analyzeFile("test.ts", code);
+ expect(result.functions).toHaveLength(1);
+ expect(result.functions[0].name).toBe("greet");
+ });
+
+ it("extracts class declarations", () => {
+ const code = `
+class UserService {
+ getName(): string { return "test"; }
+}
+`;
+ const result = plugin.analyzeFile("test.ts", code);
+ expect(result.classes).toHaveLength(1);
+ expect(result.classes[0].name).toBe("UserService");
+ });
+
+ it("extracts imports", () => {
+ const code = `import { readFile } from "fs";`;
+ const result = plugin.analyzeFile("test.ts", code);
+ expect(result.imports).toHaveLength(1);
+ expect(result.imports[0].source).toBe("fs");
+ });
+
+ it("extracts exports", () => {
+ const code = `export function hello() {}`;
+ const result = plugin.analyzeFile("test.ts", code);
+ expect(result.exports.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("extracts arrow functions", () => {
+ const code = `const add = (a: number, b: number): number => a + b;`;
+ const result = plugin.analyzeFile("test.ts", code);
+ expect(result.functions).toHaveLength(1);
+ expect(result.functions[0].name).toBe("add");
+ });
+ });
+
+ describe("Python", () => {
+ it("extracts function definitions", () => {
+ const code = `
+def greet(name):
+ return f"Hello {name}"
+
+def add(a, b):
+ return a + b
+`;
+ const result = plugin.analyzeFile("test.py", code);
+ expect(result.functions).toHaveLength(2);
+ expect(result.functions[0].name).toBe("greet");
+ expect(result.functions[1].name).toBe("add");
+ });
+
+ it("extracts class definitions", () => {
+ const code = `
+class UserService:
+ def get_name(self):
+ return "test"
+`;
+ const result = plugin.analyzeFile("test.py", code);
+ expect(result.classes).toHaveLength(1);
+ expect(result.classes[0].name).toBe("UserService");
+ });
+
+ it("extracts import statements", () => {
+ const code = `
+import os
+from pathlib import Path
+from typing import Optional
+`;
+ const result = plugin.analyzeFile("test.py", code);
+ expect(result.imports).toHaveLength(3);
+ });
+ });
+
+ it("returns null for unsupported file extension", () => {
+ expect(plugin.canAnalyze("test.unknown")).toBe(false);
+ });
+
+ it("reports all registered languages", () => {
+ const langs = plugin.supportedLanguages();
+ expect(langs).toContain("typescript");
+ expect(langs).toContain("python");
+ });
+});
+```
+
+**Step 2: Run test to verify it fails**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/plugins/generic-tree-sitter-plugin.test.ts`
+Expected: FAIL — module not found
+
+**Step 3: Write implementation**
+
+Create `understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.ts`:
+
+This file implements a `GenericTreeSitterPlugin` that:
+- Takes a `LanguageRegistry` in the constructor
+- In `init()`, lazily loads WASM grammars per language using `require.resolve(config.treeSitter.grammarPackage + '/' + config.treeSitter.wasmFile)`
+- In `analyzeFile()`, determines language from extension via registry, then walks the AST using `config.treeSitter.nodeTypes` to extract functions/classes/imports/exports
+- Reuses the same helper patterns from the old `TreeSitterPlugin` (traverse, getStringValue, extractParams) but driven by config instead of hardcoded node types
+- Implements `resolveImports()` and `extractCallGraph()` with the same logic as before
+
+Key implementation notes:
+- The `extractNodes()` method walks the AST and matches nodes against `nodeTypes.function`, `nodeTypes.class`, etc.
+- For TS/JS, also handle `lexical_declaration`/`variable_declaration` with arrow function values (existing behavior)
+- For import extraction, use the same `getStringValue()` approach but match against language-specific import node types
+- For export extraction, same pattern matching against export node types
+- Grammar loading: try `require.resolve()` first; if WASM not found, log warning and skip that language
+
+**Step 4: Run test to verify it passes**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/plugins/generic-tree-sitter-plugin.test.ts`
+Expected: PASS
+
+**Step 5: Run old TreeSitterPlugin tests with new plugin to verify migration parity**
+
+Ensure the existing `tree-sitter-plugin.test.ts` test cases also pass with `GenericTreeSitterPlugin` + TS/JS configs.
+
+**Step 6: Commit**
+
+```bash
+git add understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.ts
+git add understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.test.ts
+git commit -m "feat: add GenericTreeSitterPlugin driven by LanguageConfig"
+```
+
+---
+
+### Task 7: Add per-language test fixtures for remaining languages
+
+**Files:**
+- Modify: `understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.test.ts`
+
+**Step 1: Add test cases for Go, Java, Rust, C++, C#, Ruby, PHP, Swift, Kotlin**
+
+For each language, add a `describe` block with a small fixture testing function/class/import extraction. Example for Go:
+
+```typescript
+describe("Go", () => {
+ it("extracts function declarations", () => {
+ const code = `
+package main
+
+func greet(name string) string {
+ return "Hello " + name
+}
+`;
+ const result = plugin.analyzeFile("test.go", code);
+ expect(result.functions).toHaveLength(1);
+ expect(result.functions[0].name).toBe("greet");
+ });
+
+ it("extracts type declarations", () => {
+ const code = `
+package main
+
+type UserService struct {
+ Name string
+}
+`;
+ const result = plugin.analyzeFile("test.go", code);
+ expect(result.classes).toHaveLength(1);
+ });
+
+ it("extracts imports", () => {
+ const code = `
+package main
+
+import (
+ "fmt"
+ "os"
+)
+`;
+ const result = plugin.analyzeFile("test.go", code);
+ expect(result.imports).toHaveLength(2);
+ });
+});
+```
+
+Follow same pattern for each language with appropriate syntax. Each test uses ~10-20 lines of idiomatic code.
+
+Note: Some WASM grammars may not be available. For languages where the grammar fails to load, register them in the `beforeAll` with a try/catch and use `it.skipIf()` to conditionally skip tests. This prevents CI failures while still testing what's available.
+
+**Step 2: Run all tests**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/plugins/generic-tree-sitter-plugin.test.ts`
+Expected: PASS for all languages with available grammars
+
+**Step 3: Commit**
+
+```bash
+git add understand-anything-plugin/packages/core/src/plugins/generic-tree-sitter-plugin.test.ts
+git commit -m "test: add per-language fixtures for GenericTreeSitterPlugin"
+```
+
+---
+
+### Task 8: Replace TreeSitterPlugin with GenericTreeSitterPlugin
+
+**Files:**
+- Modify: `understand-anything-plugin/packages/core/src/index.ts`
+- Modify: `understand-anything-plugin/packages/core/src/plugins/registry.ts`
+- Delete: `understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts` (after confirming no other imports)
+
+**Step 1: Update core exports**
+
+In `understand-anything-plugin/packages/core/src/index.ts`:
+- Replace `export { TreeSitterPlugin }` with `export { GenericTreeSitterPlugin }`
+- Also export `GenericTreeSitterPlugin` as `TreeSitterPlugin` for backward compat if needed (check consumers)
+
+**Step 2: Update PluginRegistry extension map**
+
+In `understand-anything-plugin/packages/core/src/plugins/registry.ts`:
+- The `EXTENSION_TO_LANGUAGE` map is already comprehensive (has py, go, rs, etc.)
+- No changes needed here — the registry just dispatches to whatever plugin is registered
+
+**Step 3: Update all imports in skill source**
+
+Search for all imports of `TreeSitterPlugin` across the codebase:
+
+Run: `grep -r "TreeSitterPlugin" understand-anything-plugin/`
+
+Update each import to use `GenericTreeSitterPlugin`. The main consumers are:
+- `understand-anything-plugin/packages/core/src/index.ts`
+- Any skill source files that instantiate the plugin
+
+**Step 4: Delete old TreeSitterPlugin**
+
+Once all imports are updated and tests pass:
+
+Run: `rm understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts`
+
+Keep the old test file temporarily — rename it to verify parity.
+
+**Step 5: Run full test suite**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test`
+Expected: ALL PASS
+
+**Step 6: Commit**
+
+```bash
+git add -A
+git commit -m "refactor: replace TreeSitterPlugin with GenericTreeSitterPlugin"
+```
+
+---
+
+### Task 9: Update language-lesson.ts to use LanguageRegistry
+
+**Files:**
+- Modify: `understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts`
+- Modify: `understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts`
+
+**Step 1: Update the test**
+
+Update `language-lesson.test.ts` to verify concepts come from the registry:
+
+```typescript
+it("detects concepts from language config", () => {
+ const node = {
+ ...sampleNode,
+ summary: "Uses decorators and async/await with generators",
+ tags: ["decorators"],
+ };
+ const concepts = detectLanguageConcepts(node, "python");
+ expect(concepts).toContain("decorators");
+ expect(concepts).toContain("async/await");
+});
+```
+
+**Step 2: Run test to verify it fails (or passes with old behavior)**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/language-lesson.test.ts`
+
+**Step 3: Update implementation**
+
+In `language-lesson.ts`:
+- Import `LanguageRegistry` and `builtinConfigs`
+- Create a module-level registry instance, pre-populated with builtinConfigs
+- Replace `LANGUAGE_DISPLAY_NAMES` lookups with `registry.getById(lang)?.displayName`
+- Replace hardcoded `CONCEPT_PATTERNS` with `registry.getById(lang)?.concepts` merged with generic patterns (async/await, error handling, etc. that apply to all languages)
+- Keep the detection logic (search tags/summary for concept keywords) but source keywords from the config
+
+**Step 4: Run test to verify it passes**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test -- --run src/__tests__/language-lesson.test.ts`
+Expected: PASS
+
+**Step 5: Run full test suite**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test`
+Expected: ALL PASS
+
+**Step 6: Commit**
+
+```bash
+git add understand-anything-plugin/packages/core/src/analyzer/language-lesson.ts
+git add understand-anything-plugin/packages/core/src/__tests__/language-lesson.test.ts
+git commit -m "refactor: source language concepts from LanguageRegistry"
+```
+
+---
+
+### Task 10: Create language prompt snippet files
+
+**Files:**
+- Create: `understand-anything-plugin/skills/understand/languages/typescript.md`
+- Create: `understand-anything-plugin/skills/understand/languages/javascript.md`
+- Create: `understand-anything-plugin/skills/understand/languages/python.md`
+- Create: `understand-anything-plugin/skills/understand/languages/go.md`
+- Create: `understand-anything-plugin/skills/understand/languages/java.md`
+- Create: `understand-anything-plugin/skills/understand/languages/rust.md`
+- Create: `understand-anything-plugin/skills/understand/languages/cpp.md`
+- Create: `understand-anything-plugin/skills/understand/languages/csharp.md`
+- Create: `understand-anything-plugin/skills/understand/languages/ruby.md`
+- Create: `understand-anything-plugin/skills/understand/languages/php.md`
+- Create: `understand-anything-plugin/skills/understand/languages/swift.md`
+- Create: `understand-anything-plugin/skills/understand/languages/kotlin.md`
+
+**Step 1: Create all 12 language markdown files**
+
+Each file follows this structure:
+
+```markdown
+# [Language Name]
+
+## Key Concepts
+- [5-10 language-specific concepts with brief explanations]
+
+## Import Patterns
+- [All common import syntax patterns for this language]
+
+## Notable File Patterns
+- [Special files like __init__.py, go.mod, Cargo.toml, etc.]
+
+## Common Frameworks
+- [Top 3-5 frameworks/libraries in this ecosystem]
+
+## Example Summary Style
+> "[Example of how to summarize a function/class in this language's idiom]"
+```
+
+Each file should be 30-50 lines, with content specific to that language's ecosystem and idioms. The content should help the LLM produce better analysis by understanding language-specific patterns.
+
+**Step 2: Verify files are well-formed**
+
+Manually review each file for accuracy and completeness.
+
+**Step 3: Commit**
+
+```bash
+git add understand-anything-plugin/skills/understand/languages/
+git commit -m "feat: add language-specific prompt snippet files for 12 languages"
+```
+
+---
+
+### Task 11: Make base prompts language-neutral with injection points
+
+**Files:**
+- Modify: `understand-anything-plugin/skills/understand/file-analyzer-prompt.md`
+- Modify: `understand-anything-plugin/skills/understand/tour-builder-prompt.md`
+- Modify: `understand-anything-plugin/skills/understand/project-scanner-prompt.md`
+
+**Step 1: Update file-analyzer-prompt.md**
+
+- Remove all TypeScript-specific examples (e.g., "TypeScript barrel file", type guard references)
+- Replace TS-specific concept lists with generic placeholders
+- Add injection point:
+
+```markdown
+## Language-Specific Guidance
+
+{{LANGUAGE_CONTEXT}}
+```
+
+- Make the Phase 1 script detection language-aware (not just "Node.js recommended")
+
+**Step 2: Update tour-builder-prompt.md**
+
+- Remove TS-specific language lesson examples ("generics, discriminated unions, utility types")
+- Replace with injection point for detected languages:
+
+```markdown
+## Language-Specific Concepts
+
+{{LANGUAGE_CONTEXT}}
+```
+
+**Step 3: Update project-scanner-prompt.md**
+
+- Remove `tsconfig.json` hardcoded check
+- Make framework detection generic (inject detected languages' framework lists)
+- Add multi-language section:
+
+```markdown
+## Detected Languages
+
+{{LANGUAGE_CONTEXT}}
+```
+
+**Step 4: Verify prompts are well-formed**
+
+Read each modified prompt to ensure it's coherent with injection points and no residual TS bias.
+
+**Step 5: Commit**
+
+```bash
+git add understand-anything-plugin/skills/understand/file-analyzer-prompt.md
+git add understand-anything-plugin/skills/understand/tour-builder-prompt.md
+git add understand-anything-plugin/skills/understand/project-scanner-prompt.md
+git commit -m "refactor: make agent prompts language-neutral with injection points"
+```
+
+---
+
+### Task 12: Implement prompt injection logic in skill source
+
+**Files:**
+- Modify: `understand-anything-plugin/skills/understand/SKILL.md` (the `/understand` skill definition)
+
+**Step 1: Update the skill orchestration**
+
+In the `/understand` skill (SKILL.md), update the agent dispatch logic:
+
+- **Phase 0 (Pre-flight):** After scanning files, detect languages present and load corresponding `languages/*.md` files
+- **Phase 2 (File Analyzer dispatch):** For each file batch, inject the matching language's `.md` content into the file-analyzer prompt's `{{LANGUAGE_CONTEXT}}` placeholder
+- **Phase 4 (Architecture Analyzer):** Inject all detected languages' concepts
+- **Phase 5 (Tour Builder):** Inject all detected languages' `.md` content into the `{{LANGUAGE_CONTEXT}}` placeholder
+- **Phase 1 (Project Scanner):** Inject all detected languages' `.md` content
+
+The injection logic:
+1. Map file extensions to language IDs (reuse `LanguageRegistry.getByExtension()`)
+2. Read the corresponding `languages/.md` file
+3. Replace `{{LANGUAGE_CONTEXT}}` in the base prompt with the file contents
+
+For multi-language projects, concatenate all detected language files.
+
+**Step 2: Verify by reading modified SKILL.md**
+
+Ensure the orchestration flow includes language detection and prompt injection steps.
+
+**Step 3: Commit**
+
+```bash
+git add understand-anything-plugin/skills/understand/SKILL.md
+git commit -m "feat: add language detection and prompt injection to /understand skill"
+```
+
+---
+
+### Task 13: Update old tree-sitter-plugin test to use GenericTreeSitterPlugin
+
+**Files:**
+- Modify or Delete: `understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.test.ts`
+
+**Step 1: Migrate or delete**
+
+If the old `tree-sitter-plugin.test.ts` still exists:
+- Either update it to import `GenericTreeSitterPlugin` and instantiate with a `LanguageRegistry` containing TS/JS configs
+- Or delete it if all its test cases are covered in `generic-tree-sitter-plugin.test.ts`
+
+Prefer deleting to avoid duplication.
+
+**Step 2: Run full test suite**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test`
+Expected: ALL PASS
+
+**Step 3: Commit**
+
+```bash
+git add -A
+git commit -m "test: migrate old tree-sitter-plugin tests to generic plugin"
+```
+
+---
+
+### Task 14: Build and lint verification
+
+**Step 1: Build core**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core build`
+Expected: PASS
+
+**Step 2: Build skill package**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/skill build`
+Expected: PASS
+
+**Step 3: Build dashboard**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/dashboard build`
+Expected: PASS (dashboard doesn't import language modules directly)
+
+**Step 4: Run lint**
+
+Run: `cd understand-anything-plugin && pnpm lint`
+Expected: PASS (or fix any lint issues)
+
+**Step 5: Run all tests**
+
+Run: `cd understand-anything-plugin && pnpm --filter @understand-anything/core test && pnpm --filter @understand-anything/skill test`
+Expected: ALL PASS
+
+**Step 6: Commit any fixes**
+
+```bash
+git add -A
+git commit -m "fix: resolve build and lint issues from language-agnostic refactor"
+```
+
+---
+
+### Task 15: Update CLAUDE.md and documentation
+
+**Files:**
+- Modify: `CLAUDE.md`
+- Modify: `README.md` (if it exists and mentions TS-only support)
+
+**Step 1: Update CLAUDE.md**
+
+Add to the Architecture section:
+- Mention the `languages/` directories (both in core and skills)
+- Document how to add a new language (create config + prompt snippet)
+- List supported languages
+
+**Step 2: Commit**
+
+```bash
+git add CLAUDE.md
+git commit -m "docs: update CLAUDE.md with language-agnostic architecture"
+```
diff --git a/package.json b/package.json
index a329f49..504c00a 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"name": "understand-anything",
"private": true,
"type": "module",
+ "main": ".opencode/plugins/understand-anything.js",
"packageManager": "pnpm@10.6.2+sha512.47870716bea1572b53df34ad8647b42962bc790ce2bf4562ba0f643237d7302a3d6a8ecef9e4bdfc01d23af1969aa90485d4cebb0b9638fa5ef1daef656f6c1b",
"scripts": {
"prepare": "pnpm --filter @understand-anything/core build",
diff --git a/scripts/generate-large-graph.mjs b/scripts/generate-large-graph.mjs
new file mode 100644
index 0000000..8a18360
--- /dev/null
+++ b/scripts/generate-large-graph.mjs
@@ -0,0 +1,147 @@
+#!/usr/bin/env node
+/**
+ * Generate a large fake knowledge graph for testing PR #18
+ * (Web Worker layout for large graphs).
+ *
+ * Usage:
+ * node scripts/generate-large-graph.mjs [nodeCount]
+ *
+ * Default: 3000 nodes. Writes to .understand-anything/knowledge-graph.json
+ */
+
+import { writeFileSync, mkdirSync } from "node:fs";
+import { resolve } from "node:path";
+
+const NODE_COUNT = parseInt(process.argv[2] || "3000", 10);
+const EDGE_RATIO = 1.7; // edges per node (realistic for codebases)
+
+const nodeTypes = ["file", "function", "class", "module", "concept"];
+const edgeTypes = [
+ "imports", "exports", "contains", "inherits", "implements",
+ "calls", "subscribes", "publishes", "middleware",
+ "reads_from", "writes_to", "transforms", "validates",
+ "depends_on", "tested_by", "configures",
+ "related", "similar_to",
+];
+const complexities = ["simple", "moderate", "complex"];
+const languages = ["TypeScript", "JavaScript", "Python", "Go", "Rust"];
+const frameworks = ["React", "Express", "FastAPI", "Gin", "Actix"];
+
+function pick(arr) {
+ return arr[Math.floor(Math.random() * arr.length)];
+}
+
+function generateNodes(count) {
+ const nodes = [];
+ for (let i = 0; i < count; i++) {
+ const type = pick(nodeTypes);
+ const name = `${type}_${i}`;
+ nodes.push({
+ id: `node-${i}`,
+ type,
+ name,
+ filePath: type === "file" ? `src/${name}.ts` : undefined,
+ summary: `Auto-generated ${type} node #${i} for performance testing.`,
+ tags: [type, `group-${i % 20}`],
+ complexity: pick(complexities),
+ });
+ }
+ return nodes;
+}
+
+function generateEdges(nodes, edgeCount) {
+ const edges = [];
+ const seen = new Set();
+ const n = nodes.length;
+
+ for (let i = 0; i < edgeCount; i++) {
+ let src, tgt;
+ // Forward-only edges to avoid cycles (dagre blows the stack on large cyclic graphs)
+ do {
+ src = Math.floor(Math.random() * (n - 1));
+ const offset = Math.floor(Math.random() * Math.min(50, n - src - 1)) + 1;
+ tgt = src + offset;
+ } while (tgt >= n || src === tgt || seen.has(`${src}-${tgt}`));
+
+ seen.add(`${src}-${tgt}`);
+ edges.push({
+ source: nodes[src].id,
+ target: nodes[tgt].id,
+ type: pick(edgeTypes),
+ direction: "forward",
+ weight: Math.round(Math.random() * 100) / 100,
+ });
+ }
+ return edges;
+}
+
+function generateLayers(nodes) {
+ const layers = [];
+ const layerNames = [
+ "Presentation", "Application", "Domain", "Infrastructure",
+ "API Gateway", "Data Access", "Utilities", "Testing",
+ ];
+
+ for (let i = 0; i < layerNames.length; i++) {
+ const start = Math.floor((i / layerNames.length) * nodes.length);
+ const end = Math.floor(((i + 1) / layerNames.length) * nodes.length);
+ layers.push({
+ id: `layer-${i}`,
+ name: layerNames[i],
+ description: `${layerNames[i]} layer (auto-generated)`,
+ nodeIds: nodes.slice(start, end).map((n) => n.id),
+ });
+ }
+ return layers;
+}
+
+function generateTour(nodes) {
+ const steps = [];
+ const stepCount = Math.min(8, Math.floor(nodes.length / 100));
+ for (let i = 0; i < stepCount; i++) {
+ const idx = Math.floor((i / stepCount) * nodes.length);
+ steps.push({
+ order: i + 1,
+ title: `Step ${i + 1}: Explore ${nodes[idx].name}`,
+ description: `This tour step highlights node **${nodes[idx].name}** and its surrounding context.`,
+ nodeIds: [nodes[idx].id, nodes[Math.min(idx + 1, nodes.length - 1)].id],
+ });
+ }
+ return steps;
+}
+
+// ── Generate ──
+
+const nodes = generateNodes(NODE_COUNT);
+const edgeCount = Math.floor(NODE_COUNT * EDGE_RATIO);
+const edges = generateEdges(nodes, edgeCount);
+const layers = generateLayers(nodes);
+const tour = generateTour(nodes);
+
+const graph = {
+ version: "1.0",
+ project: {
+ name: "large-test-project",
+ languages: languages.slice(0, 3),
+ frameworks: frameworks.slice(0, 2),
+ description: `Auto-generated project with ${NODE_COUNT} nodes for performance testing.`,
+ analyzedAt: new Date().toISOString(),
+ gitCommitHash: "0000000000000000000000000000000000000000",
+ },
+ nodes,
+ edges,
+ layers,
+ tour,
+};
+
+const outDir = resolve(process.cwd(), ".understand-anything");
+mkdirSync(outDir, { recursive: true });
+const outPath = resolve(outDir, "knowledge-graph.json");
+writeFileSync(outPath, JSON.stringify(graph, null, 2));
+
+console.log(`Generated knowledge graph:`);
+console.log(` Nodes: ${nodes.length}`);
+console.log(` Edges: ${edges.length}`);
+console.log(` Layers: ${layers.length}`);
+console.log(` Tour steps: ${tour.length}`);
+console.log(` Written to: ${outPath}`);
diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json
index 6ad5a28..7245516 100644
--- a/understand-anything-plugin/package.json
+++ b/understand-anything-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@understand-anything/skill",
- "version": "1.1.0",
+ "version": "1.2.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx
index 50df460..3f1f265 100644
--- a/understand-anything-plugin/packages/dashboard/src/App.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/App.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useEffect, useState, useMemo } from "react";
import { validateGraph } from "@understand-anything/core/schema";
import { useDashboardStore } from "./store";
import GraphView from "./components/GraphView";
@@ -10,6 +10,9 @@ import DiffToggle from "./components/DiffToggle";
import LearnPanel from "./components/LearnPanel";
import PersonaSelector from "./components/PersonaSelector";
import ProjectOverview from "./components/ProjectOverview";
+import KeyboardShortcutsHelp from "./components/KeyboardShortcutsHelp";
+import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
+import type { KeyboardShortcut } from "./hooks/useKeyboardShortcuts";
function App() {
const graph = useDashboardStore((s) => s.graph);
@@ -21,6 +24,97 @@ function App() {
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
const [loadError, setLoadError] = useState(null);
+ const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
+
+ // Define keyboard shortcuts
+ const shortcuts = useMemo(
+ () => [
+ // Help
+ {
+ key: "?",
+ shiftKey: true,
+ description: "Show keyboard shortcuts",
+ action: () => setShowKeyboardHelp((prev) => !prev),
+ category: "General",
+ },
+ // Navigation
+ {
+ key: "Escape",
+ description: "Close panels and modals",
+ action: () => {
+ // Read from store at invocation time to avoid stale closures
+ const state = useDashboardStore.getState();
+ if (state.codeViewerOpen) {
+ state.closeCodeViewer();
+ } else if (state.selectedNodeId) {
+ state.selectNode(null);
+ } else if (state.tourActive) {
+ state.stopTour();
+ } else {
+ setShowKeyboardHelp(false);
+ }
+ },
+ category: "Navigation",
+ },
+ {
+ key: "/",
+ description: "Focus search bar",
+ action: () => {
+ const searchInput = document.querySelector(
+ 'input[placeholder*="Search"]'
+ );
+ searchInput?.focus();
+ },
+ category: "Navigation",
+ },
+ // Tour controls
+ {
+ key: "ArrowRight",
+ description: "Next tour step",
+ action: () => {
+ const state = useDashboardStore.getState();
+ if (state.tourActive) {
+ state.nextTourStep();
+ }
+ },
+ category: "Tour",
+ },
+ {
+ key: "ArrowLeft",
+ description: "Previous tour step",
+ action: () => {
+ const state = useDashboardStore.getState();
+ if (state.tourActive) {
+ state.prevTourStep();
+ }
+ },
+ category: "Tour",
+ },
+ // View toggles
+ {
+ key: "l",
+ description: "Toggle layer visualization",
+ action: () => {
+ const state = useDashboardStore.getState();
+ state.toggleLayers();
+ },
+ category: "View",
+ },
+ {
+ key: "d",
+ description: "Toggle diff mode",
+ action: () => {
+ const state = useDashboardStore.getState();
+ state.toggleDiffMode();
+ },
+ category: "View",
+ },
+ ],
+ []
+ );
+
+ // Register keyboard shortcuts
+ useKeyboardShortcuts(shortcuts);
useEffect(() => {
fetch("/knowledge-graph.json")
@@ -91,6 +185,25 @@ function App() {
+
setShowKeyboardHelp(true)}
+ className="text-text-muted hover:text-gold transition-colors"
+ title="Keyboard shortcuts (Shift + ?)"
+ >
+
+
+
+
@@ -107,8 +220,11 @@ function App() {
{/* Main content: Graph + Sidebar */}
{/* Graph area */}
-
+
+
+ Press ? for keyboard shortcuts
+
{/* Right sidebar */}
@@ -135,6 +251,14 @@ function App() {
)}
+
+ {/* Keyboard shortcuts help modal */}
+ {showKeyboardHelp && (
+ setShowKeyboardHelp(false)}
+ />
+ )}
);
}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx
index cf88c98..111d7a7 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx
@@ -1,3 +1,4 @@
+import { memo } from "react";
import { Handle, Position } from "@xyflow/react";
import type { NodeProps, Node } from "@xyflow/react";
@@ -40,7 +41,7 @@ export interface CustomNodeData extends Record {
export type CustomFlowNode = Node;
-export default function CustomNode({
+function CustomNodeComponent({
id,
data,
}: NodeProps) {
@@ -79,8 +80,7 @@ export default function CustomNode({
return (
data.onNodeClick?.(id)}
>
{/* Left color bar */}
@@ -122,3 +122,6 @@ export default function CustomNode({
);
}
+
+const CustomNode = memo(CustomNodeComponent);
+export default CustomNode;
diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
index 1a57793..b1d6fe3 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
@@ -1,8 +1,10 @@
-import { useCallback, useEffect, useMemo } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ReactFlow,
+ ReactFlowProvider,
useNodesState,
useEdgesState,
+ useReactFlow,
Background,
BackgroundVariant,
Controls,
@@ -14,14 +16,288 @@ import "@xyflow/react/dist/style.css";
import CustomNode from "./CustomNode";
import type { CustomFlowNode } from "./CustomNode";
import { useDashboardStore } from "../store";
-import { applyDagreLayout, NODE_WIDTH, NODE_HEIGHT } from "../utils/layout";
-// Layer colors are hardcoded to gold-tinted values in the group node styles
+import { applyDagreLayout, applyDagreLayoutAsync, NODE_WIDTH, NODE_HEIGHT } from "../utils/layout";
const LAYER_PADDING = 40;
+/**
+ * Node count above which layout runs in a Web Worker
+ * to avoid blocking the main thread.
+ */
+const ASYNC_LAYOUT_THRESHOLD = 200;
+
const nodeTypes = { custom: CustomNode };
-export default function GraphView() {
+/**
+ * Inner component that pans/zooms to tour-highlighted nodes.
+ * Must be rendered inside so useReactFlow() works.
+ */
+function TourFitView() {
+ const tourHighlightedNodeIds = useDashboardStore((s) => s.tourHighlightedNodeIds);
+ const { fitView } = useReactFlow();
+ const prevRef = useRef([]);
+
+ useEffect(() => {
+ const prev = prevRef.current;
+ const changed =
+ tourHighlightedNodeIds.length > 0 &&
+ (tourHighlightedNodeIds.length !== prev.length ||
+ tourHighlightedNodeIds.some((id, i) => id !== prev[i]));
+ prevRef.current = tourHighlightedNodeIds;
+
+ if (changed) {
+ // Small delay to ensure nodes are rendered before fitting
+ requestAnimationFrame(() => {
+ fitView({
+ nodes: tourHighlightedNodeIds.map((id) => ({ id })),
+ duration: 500,
+ padding: 0.3,
+ maxZoom: 1.2,
+ minZoom: 0.01,
+ });
+ });
+ }
+ }, [tourHighlightedNodeIds, fitView]);
+
+ return null;
+}
+
+/**
+ * Centers the graph on the selected node (e.g. from search).
+ * Must be rendered inside so useReactFlow() works.
+ */
+function SelectedNodeFitView() {
+ const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
+ const { fitView } = useReactFlow();
+ const prevRef = useRef(null);
+
+ useEffect(() => {
+ if (selectedNodeId && selectedNodeId !== prevRef.current) {
+ requestAnimationFrame(() => {
+ fitView({
+ nodes: [{ id: selectedNodeId }],
+ duration: 500,
+ padding: 0.3,
+ maxZoom: 1.2,
+ minZoom: 0.01,
+ });
+ });
+ }
+ prevRef.current = selectedNodeId;
+ }, [selectedNodeId, fitView]);
+
+ return null;
+}
+
+/**
+ * Build topology-only flow data: nodes and edges without visual-only state
+ * (selection, tour highlights, search results). This output drives dagre
+ * layout and should only recompute when the graph structure changes.
+ */
+function buildTopologyData(
+ graph: NonNullable["graph"]>,
+ persona: string,
+ diffMode: boolean,
+ changedNodeIds: Set,
+ affectedNodeIds: Set,
+ handleNodeSelect: (nodeId: string) => void,
+) {
+ const filteredGraphNodes =
+ persona === "non-technical"
+ ? graph.nodes.filter(
+ (n) =>
+ n.type === "concept" || n.type === "module" || n.type === "file",
+ )
+ : graph.nodes;
+
+ const filteredNodeIds = new Set(filteredGraphNodes.map((n) => n.id));
+ const filteredGraphEdges =
+ persona === "non-technical"
+ ? graph.edges.filter(
+ (e) => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target),
+ )
+ : graph.edges;
+
+ const flowNodes: CustomFlowNode[] = filteredGraphNodes.map((node) => ({
+ id: node.id,
+ type: "custom" as const,
+ position: { x: 0, y: 0 },
+ data: {
+ label: node.name ?? node.filePath?.split("/").pop() ?? node.id,
+ nodeType: node.type,
+ summary: node.summary,
+ complexity: node.complexity,
+ isHighlighted: false,
+ searchScore: undefined,
+ isSelected: false,
+ isTourHighlighted: false,
+ isDiffChanged: diffMode && changedNodeIds.has(node.id),
+ isDiffAffected: diffMode && affectedNodeIds.has(node.id),
+ isDiffFaded: diffMode && !changedNodeIds.has(node.id) && !affectedNodeIds.has(node.id),
+ onNodeClick: handleNodeSelect,
+ },
+ }));
+
+ const diffNodeIds = diffMode ? new Set([...changedNodeIds, ...affectedNodeIds]) : new Set();
+ const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => {
+ const sourceInDiff = diffMode && diffNodeIds.has(edge.source);
+ const targetInDiff = diffMode && diffNodeIds.has(edge.target);
+ const isImpacted = diffMode && (sourceInDiff || targetInDiff);
+
+ return {
+ id: `e-${i}`,
+ source: edge.source,
+ target: edge.target,
+ label: edge.type,
+ animated: edge.type === "calls" || isImpacted,
+ style: isImpacted
+ ? {
+ stroke: sourceInDiff && targetInDiff
+ ? "rgba(224, 82, 82, 0.7)"
+ : "rgba(212, 160, 48, 0.5)",
+ strokeWidth: 2.5,
+ }
+ : diffMode
+ ? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }
+ : { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
+ labelStyle: diffMode && !isImpacted
+ ? { fill: "rgba(163,151,135,0.3)", fontSize: 10 }
+ : { fill: "#a39787", fontSize: 10 },
+ };
+ });
+
+ return { flowNodes, flowEdges };
+}
+
+/**
+ * Lightweight overlay of visual-only state onto already-positioned nodes.
+ * This is O(n) object spreads — cheap even for thousands of nodes — and
+ * avoids triggering a dagre relayout when selection/highlight/search changes.
+ */
+function applyVisualState(
+ nodes: (CustomFlowNode | Node)[],
+ selectedNodeId: string | null,
+ tourHighlightedNodeIds: string[],
+ searchResults: Array<{ nodeId: string; score: number }>,
+): (CustomFlowNode | Node)[] {
+ const searchMap = new Map(searchResults.map((r) => [r.nodeId, r.score]));
+ const tourSet = new Set(tourHighlightedNodeIds);
+
+ return nodes.map((node) => {
+ // Skip group nodes (layer containers) — they have no CustomNodeData
+ if (node.type === "group") return node;
+
+ const searchScore = searchMap.get(node.id);
+ const isHighlighted = searchScore !== undefined;
+ const isSelected = selectedNodeId === node.id;
+ const isTourHighlighted = tourSet.has(node.id);
+
+ const data = node.data as CustomFlowNode["data"];
+
+ // Skip creating a new object if nothing visual changed
+ if (
+ data.isHighlighted === isHighlighted &&
+ data.searchScore === searchScore &&
+ data.isSelected === isSelected &&
+ data.isTourHighlighted === isTourHighlighted
+ ) {
+ return node;
+ }
+
+ return {
+ ...node,
+ data: {
+ ...data,
+ isHighlighted,
+ searchScore,
+ isSelected,
+ isTourHighlighted,
+ },
+ };
+ });
+}
+
+function applyLayerGroups(
+ laidNodes: CustomFlowNode[],
+ edges: Edge[],
+ layers: Array<{ id: string; name: string; nodeIds: string[] }>,
+ showLayers: boolean,
+): { initialNodes: (CustomFlowNode | Node)[]; initialEdges: Edge[] } {
+ if (!showLayers || layers.length === 0) {
+ return { initialNodes: laidNodes, initialEdges: edges };
+ }
+
+ const nodeToLayer = new Map();
+ for (const layer of layers) {
+ for (const nodeId of layer.nodeIds) {
+ nodeToLayer.set(nodeId, layer.id);
+ }
+ }
+
+ const groupNodes: Node[] = [];
+ const adjustedNodes: (CustomFlowNode | Node)[] = [];
+
+ for (const layer of layers) {
+ const memberNodes = laidNodes.filter((n) => layer.nodeIds.includes(n.id));
+ if (memberNodes.length === 0) continue;
+
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
+ for (const node of memberNodes) {
+ minX = Math.min(minX, node.position.x);
+ minY = Math.min(minY, node.position.y);
+ maxX = Math.max(maxX, node.position.x + NODE_WIDTH);
+ maxY = Math.max(maxY, node.position.y + NODE_HEIGHT);
+ }
+
+ const groupX = minX - LAYER_PADDING;
+ const groupY = minY - LAYER_PADDING - 24;
+ const groupWidth = maxX - minX + LAYER_PADDING * 2;
+ const groupHeight = maxY - minY + LAYER_PADDING * 2 + 24;
+
+ groupNodes.push({
+ id: layer.id,
+ type: "group",
+ position: { x: groupX, y: groupY },
+ data: { label: layer.name },
+ style: {
+ width: groupWidth,
+ height: groupHeight,
+ backgroundColor: "rgba(212,165,116,0.05)",
+ borderRadius: 12,
+ border: "2px dashed rgba(212,165,116,0.25)",
+ padding: 8,
+ fontSize: 13,
+ fontWeight: 600,
+ color: "#d4a574",
+ },
+ });
+
+ for (const node of memberNodes) {
+ adjustedNodes.push({
+ ...node,
+ parentId: layer.id,
+ extent: "parent" as const,
+ position: {
+ x: node.position.x - groupX,
+ y: node.position.y - groupY,
+ },
+ });
+ }
+ }
+
+ for (const node of laidNodes) {
+ if (!nodeToLayer.has(node.id)) {
+ adjustedNodes.push(node);
+ }
+ }
+
+ return {
+ initialNodes: [...groupNodes, ...adjustedNodes],
+ initialEdges: edges,
+ };
+}
+
+function GraphViewInner() {
const graph = useDashboardStore((s) => s.graph);
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const searchResults = useDashboardStore((s) => s.searchResults);
@@ -34,6 +310,8 @@ export default function GraphView() {
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
+ const [layouting, setLayouting] = useState(false);
+
const handleNodeSelect = useCallback(
(nodeId: string) => {
selectNode(nodeId);
@@ -42,190 +320,86 @@ export default function GraphView() {
[selectNode, openCodeViewer],
);
- const { initialNodes, initialEdges } = useMemo(() => {
- if (!graph)
- return {
- initialNodes: [] as (CustomFlowNode | Node)[],
- initialEdges: [] as Edge[],
- };
+ // ── Topology memo: only recomputes when graph structure changes ──
+ // Does NOT depend on selectedNodeId, tourHighlightedNodeIds, or searchResults.
+ const { topoNodes, topoEdges, needsAsyncLayout } = useMemo(() => {
+ if (!graph) {
+ return { topoNodes: [] as CustomFlowNode[], topoEdges: [] as Edge[], needsAsyncLayout: false };
+ }
+ const { flowNodes, flowEdges } = buildTopologyData(
+ graph, persona, diffMode, changedNodeIds, affectedNodeIds,
+ handleNodeSelect,
+ );
+ return { topoNodes: flowNodes, topoEdges: flowEdges, needsAsyncLayout: flowNodes.length > ASYNC_LAYOUT_THRESHOLD };
+ }, [graph, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds]);
- // Filter nodes and edges based on persona
- const filteredGraphNodes =
- persona === "non-technical"
- ? graph.nodes.filter(
- (n) =>
- n.type === "concept" || n.type === "module" || n.type === "file",
- )
- : graph.nodes;
-
- const filteredNodeIds = new Set(filteredGraphNodes.map((n) => n.id));
- const filteredGraphEdges =
- persona === "non-technical"
- ? graph.edges.filter(
- (e) => filteredNodeIds.has(e.source) && filteredNodeIds.has(e.target),
- )
- : graph.edges;
-
- const flowNodes: CustomFlowNode[] = filteredGraphNodes.map((node) => {
- const matchResult = searchResults.find((r) => r.nodeId === node.id);
- return {
- id: node.id,
- type: "custom" as const,
- position: { x: 0, y: 0 },
- data: {
- label: node.name ?? node.filePath?.split("/").pop() ?? node.id,
- nodeType: node.type,
- summary: node.summary,
- complexity: node.complexity,
- isHighlighted: !!matchResult,
- searchScore: matchResult?.score,
- isSelected: selectedNodeId === node.id,
- isTourHighlighted: tourHighlightedNodeIds.includes(node.id),
- isDiffChanged: diffMode && changedNodeIds.has(node.id),
- isDiffAffected: diffMode && affectedNodeIds.has(node.id),
- isDiffFaded: diffMode && !changedNodeIds.has(node.id) && !affectedNodeIds.has(node.id),
- onNodeClick: handleNodeSelect,
- },
- };
- });
-
- const diffNodeIds = diffMode ? new Set([...changedNodeIds, ...affectedNodeIds]) : new Set();
- const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => {
- const sourceInDiff = diffMode && diffNodeIds.has(edge.source);
- const targetInDiff = diffMode && diffNodeIds.has(edge.target);
- const isImpacted = diffMode && (sourceInDiff || targetInDiff);
-
- return {
- id: `e-${i}`,
- source: edge.source,
- target: edge.target,
- label: edge.type,
- animated: edge.type === "calls" || isImpacted,
- style: isImpacted
- ? {
- stroke: sourceInDiff && targetInDiff
- ? "rgba(224, 82, 82, 0.7)"
- : "rgba(212, 160, 48, 0.5)",
- strokeWidth: 2.5,
- }
- : diffMode
- ? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }
- : { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
- labelStyle: diffMode && !isImpacted
- ? { fill: "rgba(163,151,135,0.3)", fontSize: 10 }
- : { fill: "#a39787", fontSize: 10 },
- };
- });
-
- // Run dagre layout on all nodes (without groups)
- const laid = applyDagreLayout(flowNodes, flowEdges);
- const laidNodes = laid.nodes as CustomFlowNode[];
+ // ── Laid-out nodes from the last completed layout pass ──
+ // Stored in a ref so layout results persist across visual-state changes.
+ const laidOutRef = useRef<{ initialNodes: (CustomFlowNode | Node)[]; initialEdges: Edge[] } | null>(null);
+ // ── Sync layout: for small graphs, run dagre on the main thread ──
+ const syncResult = useMemo(() => {
+ if (!graph || needsAsyncLayout || topoNodes.length === 0) return null;
+ const laid = applyDagreLayout(topoNodes, topoEdges);
const layers = graph.layers ?? [];
- if (!showLayers || layers.length === 0) {
- return { initialNodes: laidNodes, initialEdges: laid.edges };
- }
+ return applyLayerGroups(laid.nodes as CustomFlowNode[], laid.edges, layers, showLayers);
+ }, [graph, topoNodes, topoEdges, needsAsyncLayout, showLayers]);
- // Build a map of nodeId -> layer for quick lookup
- const nodeToLayer = new Map();
- for (const layer of layers) {
- for (const nodeId of layer.nodeIds) {
- nodeToLayer.set(nodeId, layer.id);
- }
- }
+ // Keep laidOutRef in sync with sync layout results
+ if (syncResult) {
+ laidOutRef.current = syncResult;
+ }
- // Create group nodes and adjust member positions
- const groupNodes: Node[] = [];
- const adjustedNodes: (CustomFlowNode | Node)[] = [];
+ // ── Visual memo: cheap overlay of selection/highlight/search state ──
+ const visualNodes = useMemo(() => {
+ const base = laidOutRef.current;
+ if (!base) return [] as (CustomFlowNode | Node)[];
+ return applyVisualState(base.initialNodes, selectedNodeId, tourHighlightedNodeIds, searchResults);
+ }, [laidOutRef.current, selectedNodeId, tourHighlightedNodeIds, searchResults]);
- for (let layerIdx = 0; layerIdx < layers.length; layerIdx++) {
- const layer = layers[layerIdx];
- const memberNodes = laidNodes.filter((n) =>
- layer.nodeIds.includes(n.id),
- );
-
- if (memberNodes.length === 0) continue;
-
- // Compute bounding box around member nodes
- let minX = Infinity;
- let minY = Infinity;
- let maxX = -Infinity;
- let maxY = -Infinity;
-
- for (const node of memberNodes) {
- const x = node.position.x;
- const y = node.position.y;
- minX = Math.min(minX, x);
- minY = Math.min(minY, y);
- maxX = Math.max(maxX, x + NODE_WIDTH);
- maxY = Math.max(maxY, y + NODE_HEIGHT);
- }
-
- // Group node position = top-left with padding
- const groupX = minX - LAYER_PADDING;
- const groupY = minY - LAYER_PADDING - 24; // extra space for label
- const groupWidth = maxX - minX + LAYER_PADDING * 2;
- const groupHeight = maxY - minY + LAYER_PADDING * 2 + 24;
-
- // Create the group node
- groupNodes.push({
- id: layer.id,
- type: "group",
- position: { x: groupX, y: groupY },
- data: { label: layer.name },
- style: {
- width: groupWidth,
- height: groupHeight,
- backgroundColor: "rgba(212,165,116,0.05)",
- borderRadius: 12,
- border: `2px dashed rgba(212,165,116,0.25)`,
- padding: 8,
- fontSize: 13,
- fontWeight: 600,
- color: "#d4a574",
- },
- });
-
- // Adjust member node positions to be relative to the group
- for (const node of memberNodes) {
- adjustedNodes.push({
- ...node,
- parentId: layer.id,
- extent: "parent" as const,
- position: {
- x: node.position.x - groupX,
- y: node.position.y - groupY,
- },
- });
- }
- }
-
- // Add nodes that are not in any layer (keep original positions)
- for (const node of laidNodes) {
- if (!nodeToLayer.has(node.id)) {
- adjustedNodes.push(node);
- }
- }
-
- // Group nodes must come before their children in the array
- const allNodes: (CustomFlowNode | Node)[] = [
- ...groupNodes,
- ...adjustedNodes,
- ];
-
- return { initialNodes: allNodes, initialEdges: laid.edges };
- }, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds]);
-
- const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
- const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+ const [nodes, setNodes, onNodesChange] = useNodesState(visualNodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(laidOutRef.current?.initialEdges ?? []);
+ // ── Push sync layout + visual state to ReactFlow ──
useEffect(() => {
- setNodes(initialNodes);
- }, [initialNodes, setNodes]);
+ if (syncResult) {
+ const withVisual = applyVisualState(syncResult.initialNodes, selectedNodeId, tourHighlightedNodeIds, searchResults);
+ setNodes(withVisual);
+ setEdges(syncResult.initialEdges);
+ }
+ }, [syncResult, selectedNodeId, tourHighlightedNodeIds, searchResults, setNodes, setEdges]);
+ // ── Push visual-only changes (no relayout) ──
useEffect(() => {
- setEdges(initialEdges);
- }, [initialEdges, setEdges]);
+ if (laidOutRef.current && !layouting) {
+ const withVisual = applyVisualState(laidOutRef.current.initialNodes, selectedNodeId, tourHighlightedNodeIds, searchResults);
+ setNodes(withVisual);
+ }
+ }, [selectedNodeId, tourHighlightedNodeIds, searchResults, setNodes, layouting]);
+
+ // ── Async layout: for large graphs, run dagre in a Web Worker ──
+ useEffect(() => {
+ if (!graph || !needsAsyncLayout || topoNodes.length === 0) return;
+
+ let cancelled = false;
+ setLayouting(true);
+
+ applyDagreLayoutAsync(topoNodes, topoEdges).then((laid) => {
+ if (cancelled) return;
+ const layers = graph.layers ?? [];
+ const result = applyLayerGroups(laid.nodes as CustomFlowNode[], laid.edges, layers, showLayers);
+ laidOutRef.current = result;
+ const withVisual = applyVisualState(result.initialNodes, selectedNodeId, tourHighlightedNodeIds, searchResults);
+ setNodes(withVisual);
+ setEdges(result.initialEdges);
+ setLayouting(false);
+ }).catch(() => {
+ if (cancelled) return;
+ setLayouting(false);
+ });
+
+ return () => { cancelled = true; setLayouting(false); };
+ }, [graph, topoNodes, topoEdges, needsAsyncLayout, showLayers, setNodes, setEdges]);
const onNodeClick = useCallback(
(_: React.MouseEvent, node: { id: string }) => {
@@ -251,7 +425,17 @@ export default function GraphView() {
}
return (
-
+
+ {layouting && (
+
+
+
+
+ Laying out {topoNodes.length.toLocaleString()} nodes...
+
+
+
+ )}
@@ -270,7 +462,17 @@ export default function GraphView() {
maskColor="rgba(10,10,10,0.7)"
className="!bg-surface !border !border-border-subtle"
/>
+
+
);
}
+
+export default function GraphView() {
+ return (
+
+
+
+ );
+}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx
new file mode 100644
index 0000000..22178f5
--- /dev/null
+++ b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx
@@ -0,0 +1,94 @@
+import type { KeyboardShortcut } from "../hooks/useKeyboardShortcuts";
+import { formatShortcutKey } from "../hooks/useKeyboardShortcuts";
+
+interface KeyboardShortcutsHelpProps {
+ shortcuts: KeyboardShortcut[];
+ onClose: () => void;
+}
+
+export default function KeyboardShortcutsHelp({
+ shortcuts,
+ onClose,
+}: KeyboardShortcutsHelpProps) {
+ // Group shortcuts by category
+ const groupedShortcuts = shortcuts.reduce((acc, shortcut) => {
+ if (!acc[shortcut.category]) {
+ acc[shortcut.category] = [];
+ }
+ acc[shortcut.category].push(shortcut);
+ return acc;
+ }, {} as Record
);
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
+
+ Keyboard Shortcuts
+
+
+ Press ? anytime to toggle this help
+
+
+
+
+
+
+
+
+
+ {/* Shortcuts list */}
+
+ {Object.entries(groupedShortcuts).map(([category, categoryShortcuts]) => (
+
+
+ {category}
+
+
+ {categoryShortcuts.map((shortcut, index) => (
+
+
+ {shortcut.description}
+
+ {formatShortcutKey(shortcut)}
+
+ ))}
+
+
+ ))}
+
+
+ {/* Footer */}
+
+
+ Press ESC to close
+
+
+
+
+ );
+}
diff --git a/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
new file mode 100644
index 0000000..fcf91e3
--- /dev/null
+++ b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
@@ -0,0 +1,71 @@
+import { useEffect } from "react";
+
+export interface KeyboardShortcut {
+ key: string;
+ ctrlKey?: boolean;
+ shiftKey?: boolean;
+ altKey?: boolean;
+ metaKey?: boolean;
+ description: string;
+ action: () => void;
+ category: string;
+}
+
+export function useKeyboardShortcuts(
+ shortcuts: KeyboardShortcut[],
+ enabled = true
+) {
+ useEffect(() => {
+ if (!enabled) return;
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ // Prevent shortcuts from firing when typing in input fields
+ const target = event.target as HTMLElement;
+ const tagName = target.tagName.toLowerCase();
+ if (tagName === 'input' || tagName === 'textarea' || target.isContentEditable) {
+ if (event.key !== 'Escape') return;
+ }
+
+ for (const shortcut of shortcuts) {
+ const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase();
+ const ctrlMatches = shortcut.ctrlKey ? event.ctrlKey : !event.ctrlKey;
+ const shiftMatches = shortcut.shiftKey ? event.shiftKey : !event.shiftKey;
+ const altMatches = shortcut.altKey ? event.altKey : !event.altKey;
+ const metaMatches = shortcut.metaKey ? event.metaKey : !event.metaKey;
+
+ if (keyMatches && ctrlMatches && shiftMatches && altMatches && metaMatches) {
+ // Prevent default for shortcuts that might conflict with browser
+ if (event.ctrlKey || event.metaKey || event.altKey) {
+ event.preventDefault();
+ }
+ shortcut.action();
+ break;
+ }
+ }
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [shortcuts, enabled]);
+}
+
+export function formatShortcutKey(shortcut: KeyboardShortcut): string {
+ const keys: string[] = [];
+
+ // Use userAgentData with fallback to navigator.platform
+ const isMac = (navigator as Navigator & { userAgentData?: { platform: string } }).userAgentData?.platform
+ ? (navigator as Navigator & { userAgentData: { platform: string } }).userAgentData.platform === 'macOS'
+ : navigator.platform.includes("Mac");
+
+ if (shortcut.ctrlKey || shortcut.metaKey) {
+ keys.push(isMac ? "⌘" : "Ctrl");
+ }
+ // Don't show ⇧ for keys that inherently require Shift (e.g. ?, !, @)
+ const isShiftedPunctuation = shortcut.key.length === 1 && /[^a-zA-Z0-9]/.test(shortcut.key);
+ if (shortcut.shiftKey && !isShiftedPunctuation) keys.push("⇧");
+ if (shortcut.altKey) keys.push(isMac ? "⌥" : "Alt");
+
+ keys.push(isShiftedPunctuation ? shortcut.key : shortcut.key.toUpperCase());
+
+ return keys.join(" + ");
+}
diff --git a/understand-anything-plugin/packages/dashboard/src/index.css b/understand-anything-plugin/packages/dashboard/src/index.css
index a767a27..a0f8ffa 100644
--- a/understand-anything-plugin/packages/dashboard/src/index.css
+++ b/understand-anything-plugin/packages/dashboard/src/index.css
@@ -71,6 +71,31 @@ body {
-webkit-backdrop-filter: blur(12px);
}
+.glass-heavy {
+ background: rgba(20, 20, 20, 0.95);
+ border: 1px solid rgba(212, 165, 116, 0.15);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+}
+
+/* Keyboard shortcut key styling */
+.kbd {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 1.75rem;
+ height: 1.75rem;
+ padding: 0 0.5rem;
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--color-gold);
+ background: rgba(212, 165, 116, 0.1);
+ border: 1px solid rgba(212, 165, 116, 0.3);
+ border-radius: 0.25rem;
+ box-shadow: 0 1px 0 rgba(212, 165, 116, 0.2);
+}
+
/* Animation keyframes */
@keyframes fadeSlideIn {
from {
diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts
index 9d451ee..6f55b53 100644
--- a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts
+++ b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts
@@ -1,9 +1,13 @@
import dagre from "@dagrejs/dagre";
import type { Node, Edge } from "@xyflow/react";
+import type { LayoutMessage, LayoutResult } from "./layout.worker";
export const NODE_WIDTH = 280;
export const NODE_HEIGHT = 120;
+/**
+ * Synchronous dagre layout — used for small graphs.
+ */
export function applyDagreLayout(
nodes: Node[],
edges: Edge[],
@@ -43,3 +47,79 @@ export function applyDagreLayout(
return { nodes: layoutedNodes, edges };
}
+
+let _worker: Worker | null = null;
+let _nextRequestId = 0;
+let _latestRequestId = -1;
+const _pending = new Map<
+ number,
+ {
+ nodes: Node[];
+ edges: Edge[];
+ resolve: (v: { nodes: Node[]; edges: Edge[] }) => void;
+ reject: (reason?: unknown) => void;
+ }
+>();
+
+function getWorker(): Worker {
+ if (!_worker) {
+ _worker = new Worker(
+ new URL("./layout.worker.ts", import.meta.url),
+ { type: "module" },
+ );
+
+ _worker.onmessage = (e: MessageEvent) => {
+ const { requestId, positions } = e.data;
+ const entry = _pending.get(requestId);
+ _pending.delete(requestId);
+
+ // S1: Discard stale results — only honour the latest request.
+ if (!entry || requestId !== _latestRequestId) return;
+
+ const layoutedNodes = entry.nodes.map((node) => ({
+ ...node,
+ position: positions[node.id] ?? { x: 0, y: 0 },
+ }));
+
+ entry.resolve({ nodes: layoutedNodes, edges: entry.edges });
+ };
+
+ _worker.onerror = (err: ErrorEvent) => {
+ for (const [, entry] of _pending) {
+ entry.reject(err);
+ }
+ _pending.clear();
+ };
+ }
+ return _worker;
+}
+
+/**
+ * Async dagre layout via Web Worker — used for large graphs.
+ * Keeps the main thread responsive while dagre computes positions.
+ *
+ * Uses request-ID correlation so concurrent calls never cross-wire,
+ * and only the latest request's result is honoured (stale ones are discarded).
+ */
+export function applyDagreLayoutAsync(
+ nodes: Node[],
+ edges: Edge[],
+ direction: "TB" | "LR" = "TB",
+): Promise<{ nodes: Node[]; edges: Edge[] }> {
+ return new Promise((resolve, reject) => {
+ const worker = getWorker();
+ const requestId = _nextRequestId++;
+ _latestRequestId = requestId;
+
+ _pending.set(requestId, { nodes, edges, resolve, reject });
+
+ const msg: LayoutMessage = {
+ requestId,
+ nodes: nodes.map((n) => ({ id: n.id, width: NODE_WIDTH, height: NODE_HEIGHT })),
+ edges: edges.map((e) => ({ source: e.source, target: e.target })),
+ direction,
+ };
+
+ worker.postMessage(msg);
+ });
+}
diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts
new file mode 100644
index 0000000..4f466f3
--- /dev/null
+++ b/understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts
@@ -0,0 +1,47 @@
+import dagre from "@dagrejs/dagre";
+
+export interface LayoutMessage {
+ requestId: number;
+ nodes: Array<{ id: string; width: number; height: number }>;
+ edges: Array<{ source: string; target: string }>;
+ direction: "TB" | "LR";
+}
+
+export interface LayoutResult {
+ requestId: number;
+ positions: Record;
+}
+
+self.onmessage = (e: MessageEvent) => {
+ const { requestId, nodes, edges, direction } = e.data;
+
+ const g = new dagre.graphlib.Graph();
+ g.setDefaultEdgeLabel(() => ({}));
+ g.setGraph({
+ rankdir: direction,
+ nodesep: 60,
+ ranksep: 80,
+ marginx: 20,
+ marginy: 20,
+ });
+
+ for (const node of nodes) {
+ g.setNode(node.id, { width: node.width, height: node.height });
+ }
+
+ for (const edge of edges) {
+ g.setEdge(edge.source, edge.target);
+ }
+
+ dagre.layout(g);
+
+ const positions: Record = {};
+ for (const node of nodes) {
+ const pos = g.node(node.id);
+ positions[node.id] = pos
+ ? { x: pos.x - node.width / 2, y: pos.y - node.height / 2 }
+ : { x: 0, y: 0 };
+ }
+
+ self.postMessage({ requestId, positions } satisfies LayoutResult);
+};
diff --git a/understand-anything-plugin/skills/understand-dashboard/SKILL.md b/understand-anything-plugin/skills/understand-dashboard/SKILL.md
index ec0710d..e0614d7 100644
--- a/understand-anything-plugin/skills/understand-dashboard/SKILL.md
+++ b/understand-anything-plugin/skills/understand-dashboard/SKILL.md
@@ -19,18 +19,40 @@ Start the Understand Anything dashboard to visualize the knowledge graph for the
No knowledge graph found. Run /understand first to analyze this project.
```
-3. Find the dashboard code. The dashboard is at `packages/dashboard/` relative to this plugin's root directory. Use the Bash tool to resolve the path:
- ```bash
- PLUGIN_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
- ```
- Or locate it by checking these paths in order:
- - `${CLAUDE_PLUGIN_ROOT}/packages/dashboard/`
- - The parent directory of this skill file, then `../../packages/dashboard/`
+3. Find the dashboard code. The dashboard is at `packages/dashboard/` relative to this plugin's root directory. Check these paths in order and use the first that exists:
+ - `~/.understand-anything-plugin/packages/dashboard/` (universal symlink, all installs)
+ - `${CLAUDE_PLUGIN_ROOT}/packages/dashboard/` (Claude Code plugin)
+ - Two levels up from this skill file's real path: `../../packages/dashboard/` (self-relative fallback)
-4. Install dependencies if needed:
+ Use the Bash tool to resolve:
+ ```bash
+ SKILL_REAL=$(realpath ~/.agents/skills/understand-dashboard 2>/dev/null || readlink -f ~/.agents/skills/understand-dashboard 2>/dev/null || echo "")
+ SELF_RELATIVE=$([ -n "$SKILL_REAL" ] && cd "$SKILL_REAL/../.." 2>/dev/null && pwd || echo "")
+
+ PLUGIN_ROOT=""
+ for candidate in \
+ "$HOME/.understand-anything-plugin" \
+ "${CLAUDE_PLUGIN_ROOT}" \
+ "$SELF_RELATIVE"; do
+ if [ -n "$candidate" ] && [ -d "$candidate/packages/dashboard" ]; then
+ PLUGIN_ROOT="$candidate"; break
+ fi
+ done
+
+ if [ -z "$PLUGIN_ROOT" ]; then
+ echo "Error: Cannot find the understand-anything plugin root. Make sure you followed the installation instructions and that ~/.understand-anything-plugin exists."
+ exit 1
+ fi
+ ```
+
+4. Install dependencies and build if needed:
```bash
cd && pnpm install --frozen-lockfile 2>/dev/null || pnpm install
```
+ Then ensure the core package is built (the dashboard depends on it):
+ ```bash
+ cd && pnpm --filter @understand-anything/core build
+ ```
5. Start the Vite dev server pointing at the project's knowledge graph:
```bash
diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md
index e0f23ca..46d9b7a 100644
--- a/understand-anything-plugin/skills/understand/SKILL.md
+++ b/understand-anything-plugin/skills/understand/SKILL.md
@@ -195,13 +195,15 @@ Pass these parameters in the dispatch prompt:
> [list of edges with type "imports"]
> ```
-After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/layers.json` to get the layer assignments.
+After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/layers.json` and normalize it into a final `layers` array. Apply these steps **in order**:
-`layers.json` may be either:
-- a top-level JSON array of layer objects, or
-- an envelope object such as `{ "layers": [...] }` from the current prompt/template output
+1. **Unwrap envelope:** If the file contains `{ "layers": [...] }` instead of a plain array, extract the inner array. (The prompt requests a plain array, but LLMs may still produce an envelope.)
+2. **Rename legacy fields:** If any layer object has a `nodes` field instead of `nodeIds`, rename `nodes` → `nodeIds`. If `nodes` entries are objects with an `id` field rather than plain strings, extract just the `id` values into `nodeIds`.
+3. **Synthesize missing IDs:** If any layer is missing an `id`, generate one as `layer:`.
+4. **Convert file paths:** If `nodeIds` entries are raw file paths (not prefixed with `file:`), convert them to `file:`.
+5. **Drop dangling refs:** Remove any `nodeIds` entries that do not exist in the merged node set.
-Normalize either form into a final top-level `layers` array before assembling the graph. Each final saved layer object MUST match this exact shape:
+Each element of the final `layers` array MUST have this shape:
```json
[
@@ -214,13 +216,7 @@ Normalize either form into a final top-level `layers` array before assembling th
]
```
-Rules:
-- `id` is required and must be unique
-- `nodeIds` is required and must contain graph node IDs, not raw file paths
-- If the intermediate output is an envelope object, unwrap its `layers` array before any other normalization
-- If the subagent returns file paths, convert them to file node IDs before assembling the final graph
-- Drop any `nodeIds` that do not exist in the merged node set
-- Do not use a `nodes` field in the final saved layer objects
+All four fields (`id`, `name`, `description`, `nodeIds`) are required.
**For incremental updates:** Always re-run architecture analysis on the full merged node set, since layer assignments may shift when files change.
@@ -273,13 +269,15 @@ Pass these parameters in the dispatch prompt:
> [imports and calls edges]
> ```
-After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/tour.json` to get the tour steps.
+After the subagent completes, read `$PROJECT_ROOT/.understand-anything/intermediate/tour.json` and normalize it into a final `tour` array. Apply these steps **in order**:
-`tour.json` may be either:
-- a top-level JSON array of tour step objects, or
-- an envelope object such as `{ "steps": [...] }` from the current prompt/template output
+1. **Unwrap envelope:** If the file contains `{ "steps": [...] }` instead of a plain array, extract the inner array. (The prompt requests a plain array, but LLMs may still produce an envelope.)
+2. **Rename legacy fields:** If any step has `nodesToInspect` instead of `nodeIds`, rename it → `nodeIds`. If any step has `whyItMatters` instead of `description`, rename it → `description`.
+3. **Convert file paths:** If `nodeIds` entries are raw file paths, convert them to `file:`.
+4. **Drop dangling refs:** Remove any `nodeIds` entries that do not exist in the merged node set.
+5. **Sort** by `order` before saving.
-Normalize either form into a final top-level `tour` array before assembling the graph. Each final saved tour step object MUST match this exact shape:
+Each element of the final `tour` array MUST have this shape:
```json
[
@@ -292,31 +290,7 @@ Normalize either form into a final top-level `tour` array before assembling the
]
```
-Rules:
-- If the intermediate output is an envelope object, unwrap its `steps` array before any other normalization
-- `description` is required; do not use `whyItMatters` in the final saved tour steps
-- `nodeIds` is required; do not use `nodesToInspect` in the final saved tour steps
-- `nodeIds` must reference existing graph node IDs
-- Preserve optional `languageLesson` when present
-- Sort by `order` before saving
-
----
-
-## Phase 5.5 — NORMALIZE
-
-Before assembling the final graph:
-
-- Unwrap legacy or prompt-shaped envelopes before field renaming:
- - `{ "layers": [...] }` -> use the contained array as the working `layers` value
- - `{ "steps": [...] }` -> use the contained array as the working `tour` value
-- Convert any layer `nodes` field to `nodeIds`
-- Convert any tour `nodesToInspect` field to `nodeIds`
-- Convert any tour `whyItMatters` field to `description`
-- If layers or tour reference file paths, map them to file node IDs using the `file:` convention
-- Synthesize missing layer IDs as `layer:`
-- Drop unresolved layer and tour node references
-- Ensure the final `layers` value is an array of `{ id, name, description, nodeIds }`
-- Ensure the final `tour` value is an array of `{ order, title, description, nodeIds }`, preserving optional `languageLesson`
+Required fields: `order`, `title`, `description`, `nodeIds`. Preserve optional `languageLesson` when present.
---