From 18c0fcd9ebf77ff27d24a4e55ebe38e6c8c23383 Mon Sep 17 00:00:00 2001
From: longvv
Date: Sat, 21 Mar 2026 16:33:07 +0700
Subject: [PATCH 01/22] feat: add Antigravity installation guide and update
README with compatibility information.
---
.antigravity/INSTALL.md | 56 +++++++++++++++++++++++++++++++++++++++++
README.md | 8 ++++++
README.zh-CN.md | 8 ++++++
3 files changed, 72 insertions(+)
create mode 100644 .antigravity/INSTALL.md
diff --git a/.antigravity/INSTALL.md b/.antigravity/INSTALL.md
new file mode 100644
index 0000000..3919764
--- /dev/null
+++ b/.antigravity/INSTALL.md
@@ -0,0 +1,56 @@
+# 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 symlink:**
+ ```bash
+ mkdir -p ~/.agents/skills
+ ln -s ~/.antigravity/understand-anything/understand-anything-plugin/skills ~/.agents/skills/understand-anything
+ ```
+
+ **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\.antigravity\understand-anything\understand-anything-plugin\skills"
+ ```
+
+3. **Restart the chat or IDE** so Antigravity can discover the skills.
+
+## Verify
+
+```bash
+ls -la ~/.agents/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 ~/.agents/skills/understand-anything
+rm -rf ~/.antigravity/understand-anything
+```
diff --git a/README.md b/README.md
index ded8a3b..9c3922f 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,13 @@ 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
+```
+
### Platform Compatibility
| Platform | Status | Install Method |
@@ -149,6 +156,7 @@ Cursor auto-discovers the plugin via `.cursor-plugin/plugin.json` when this repo
| OpenCode | ✅ Supported | Plugin config |
| OpenClaw | ✅ Supported | AI-driven install |
| Cursor | ✅ Supported | Auto-discovery |
+| Antigravity | ✅ Supported | AI-driven install |
---
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 7edf497..5a5eeb5 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -139,6 +139,13 @@ 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
+```
+
### 多平台兼容
| 平台 | 状态 | 安装方式 |
@@ -148,6 +155,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| OpenCode | ✅ 支持 | 插件配置 |
| OpenClaw | ✅ 支持 | AI驱动安装 |
| Cursor | ✅ 支持 | 自动发现 |
+| Antigravity | ✅ 支持 | AI驱动安装 |
---
From 54fc8d44c25ed88db6aca97d8775da3621e93df8 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sat, 21 Mar 2026 22:15:39 +0800
Subject: [PATCH 02/22] fix: update Antigravity skills install path to
~/.gemini/antigravity/skills/
---
.antigravity/INSTALL.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/.antigravity/INSTALL.md b/.antigravity/INSTALL.md
index 3919764..5516be2 100644
--- a/.antigravity/INSTALL.md
+++ b/.antigravity/INSTALL.md
@@ -13,14 +13,14 @@
2. **Create the skills symlink:**
```bash
- mkdir -p ~/.agents/skills
- ln -s ~/.antigravity/understand-anything/understand-anything-plugin/skills ~/.agents/skills/understand-anything
+ mkdir -p ~/.gemini/antigravity/skills
+ ln -s ~/.antigravity/understand-anything/understand-anything-plugin/skills ~/.gemini/antigravity/skills/understand-anything
```
**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\.antigravity\understand-anything\understand-anything-plugin\skills"
+ 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"
```
3. **Restart the chat or IDE** so Antigravity can discover the skills.
@@ -28,7 +28,7 @@
## Verify
```bash
-ls -la ~/.agents/skills/understand-anything
+ls -la ~/.gemini/antigravity/skills/understand-anything
```
You should see a symlink pointing to the skills directory in the cloned repo.
@@ -51,6 +51,6 @@ Skills update instantly through the symlink.
## Uninstalling
```bash
-rm ~/.agents/skills/understand-anything
+rm ~/.gemini/antigravity/skills/understand-anything
rm -rf ~/.antigravity/understand-anything
```
From 922cc32a13360768d327013c3afa23f9b8c0dfcf Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sat, 21 Mar 2026 22:23:48 +0800
Subject: [PATCH 03/22] docs: improve CLAUDE.md with prerequisites, lint
command, and gotchas
Co-Authored-By: Claude Opus 4.6
---
CLAUDE.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/CLAUDE.md b/CLAUDE.md
index 02ae299..56ad507 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,6 +47,10 @@ 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
+
## Versioning
When pushing to remote, bump the version in **both** of these files (keep them in sync):
- `understand-anything-plugin/package.json` → `"version"` field
From de66bb7d76531f845ecef5b98e31bd0090ba50b9 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sat, 21 Mar 2026 22:25:21 +0800
Subject: [PATCH 04/22] fix: ensure core is built before launching dashboard in
/understand-dashboard
Co-Authored-By: Claude Opus 4.6
---
.../skills/understand-dashboard/SKILL.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/understand-anything-plugin/skills/understand-dashboard/SKILL.md b/understand-anything-plugin/skills/understand-dashboard/SKILL.md
index ec0710d..b800e84 100644
--- a/understand-anything-plugin/skills/understand-dashboard/SKILL.md
+++ b/understand-anything-plugin/skills/understand-dashboard/SKILL.md
@@ -27,10 +27,14 @@ Start the Understand Anything dashboard to visualize the knowledge graph for the
- `${CLAUDE_PLUGIN_ROOT}/packages/dashboard/`
- The parent directory of this skill file, then `../../packages/dashboard/`
-4. Install dependencies if needed:
+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
From b8e605eafb94a6a581452d934a5d9e56945eebc4 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sat, 21 Mar 2026 22:59:36 +0800
Subject: [PATCH 05/22] fix: consolidate triple normalization into ordered
Phase 4/5 steps
The original commit 9fe003a introduced normalization logic in three
separate places (Phase 4 inline, Phase 5 inline, Phase 5.5), creating
an ordering conflict where "do not use field X" rules appeared before
"convert field X to field Y" rules. An LLM following instructions
sequentially could drop legacy fields before converting them.
- Consolidate all layer normalization into Phase 4 as ordered steps
- Consolidate all tour normalization into Phase 5 as ordered steps
- Remove redundant Phase 5.5 entirely
- Fix misleading "from current prompt" comments about envelope format
- Add defensive handling for nodes-as-objects edge case
- Wrap JSON examples in arrays for visual consistency with prompts
Co-Authored-By: Claude Opus 4.6
---
.../skills/understand/SKILL.md | 58 +++++--------------
1 file changed, 16 insertions(+), 42 deletions(-)
diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md
index 1327551..ce7ce79 100644
--- a/understand-anything-plugin/skills/understand/SKILL.md
+++ b/understand-anything-plugin/skills/understand/SKILL.md
@@ -194,13 +194,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
[
@@ -213,13 +215,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.
@@ -272,13 +268,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
[
@@ -291,31 +289,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.
---
From 7c638603c02ed03a57e4799568b5b2db6e4a53f1 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sat, 21 Mar 2026 23:07:58 +0800
Subject: [PATCH 06/22] fix: add zoom limits and auto-center for large graphs
- Set minZoom to 0.01 so fit-view works on graphs with hundreds of nodes
- Add TourFitView to pan/zoom to highlighted nodes during guided tours
- Add SelectedNodeFitView to center graph on node selected via search
- Wrap GraphView in ReactFlowProvider to enable useReactFlow() hooks
Thanks to @blackboxaicode for the insight on these improvements.
Co-Authored-By: Claude Opus 4.6
---
.../dashboard/src/components/GraphView.tsx | 80 ++++++++++++++++++-
1 file changed, 78 insertions(+), 2 deletions(-)
diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
index 1a57793..2c8c92a 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 } from "react";
import {
ReactFlow,
+ ReactFlowProvider,
useNodesState,
useEdgesState,
+ useReactFlow,
Background,
BackgroundVariant,
Controls,
@@ -21,7 +23,68 @@ const LAYER_PADDING = 40;
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;
+}
+
+function GraphViewInner() {
const graph = useDashboardStore((s) => s.graph);
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
const searchResults = useDashboardStore((s) => s.searchResults);
@@ -261,6 +324,9 @@ export default function GraphView() {
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
fitView
+ fitViewOptions={{ minZoom: 0.01, padding: 0.1 }}
+ minZoom={0.01}
+ maxZoom={2}
colorMode="dark"
>
@@ -270,7 +336,17 @@ export default function GraphView() {
maskColor="rgba(10,10,10,0.7)"
className="!bg-surface !border !border-border-subtle"
/>
+
+
);
}
+
+export default function GraphView() {
+ return (
+
+
+
+ );
+}
From 8134bb95cab8f95f8c7e8b1475da75b00085d51b Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sat, 21 Mar 2026 23:08:56 +0800
Subject: [PATCH 07/22] chore: bump version to 1.1.1
Co-Authored-By: Claude Opus 4.6
---
.claude-plugin/marketplace.json | 2 +-
understand-anything-plugin/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index 9397ad4..b23857d 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.1.1",
"source": "./understand-anything-plugin"
}
]
diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json
index 6ad5a28..4504866 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.1.1",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
From d7d39254632fee2d17ff8599347f8cf2c4e60454 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sat, 21 Mar 2026 23:30:47 +0800
Subject: [PATCH 08/22] docs: add language-agnostic support design document
Design for making Understand-Anything language-agnostic instead of
TypeScript-heavy. Covers LanguageConfig registry, GenericTreeSitterPlugin,
language-aware prompts, and support for 12 languages.
Co-Authored-By: Claude Opus 4.6
---
.../2026-03-21-language-agnostic-design.md | 249 ++++++++++++++++++
1 file changed, 249 insertions(+)
create mode 100644 docs/plans/2026-03-21-language-agnostic-design.md
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
From 87c07a61d88afe9c9a9add2d74b1ae7420d1c0a6 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sat, 21 Mar 2026 23:37:38 +0800
Subject: [PATCH 09/22] docs: add language-agnostic implementation plan
15-task TDD plan covering LanguageConfig types, LanguageRegistry,
12 language configs, GenericTreeSitterPlugin, prompt snippets,
and migration from TS-only TreeSitterPlugin.
Co-Authored-By: Claude Opus 4.6
---
.../2026-03-21-language-agnostic-plan.md | 1392 +++++++++++++++++
1 file changed, 1392 insertions(+)
create mode 100644 docs/plans/2026-03-21-language-agnostic-plan.md
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"
+```
From c7f564a60eb2ccb900e3c965eff6f8601138bd24 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Sun, 22 Mar 2026 23:10:23 +0800
Subject: [PATCH 10/22] fix: OpenCode plugin not discovering skills (#16)
OpenCode installs the repo via git and looks at the root for the plugin
entry point. Our skills/agents live inside understand-anything-plugin/,
so OpenCode couldn't find them.
- Add .opencode/plugins/understand-anything.js that uses the config hook
to inject understand-anything-plugin/skills/ into skills.paths
- Add "main" field to root package.json pointing to the plugin entry
- Update INSTALL.md with version pinning and troubleshooting sections
Closes #16
Co-Authored-By: Claude Opus 4.6
---
.opencode/INSTALL.md | 24 +++++++++++++++++++++++
.opencode/plugins/understand-anything.js | 25 ++++++++++++++++++++++++
package.json | 1 +
3 files changed, 50 insertions(+)
create mode 100644 .opencode/plugins/understand-anything.js
diff --git a/.opencode/INSTALL.md b/.opencode/INSTALL.md
index 6cac25a..3fa6645 100644
--- a/.opencode/INSTALL.md
+++ b/.opencode/INSTALL.md
@@ -32,6 +32,30 @@ Or just ask: "Analyze this codebase and build a knowledge graph"
Restart OpenCode — the plugin re-installs from git automatically.
+To pin a specific version:
+
+```json
+{
+ "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git#v1.1.1"]
+}
+```
+
## Uninstalling
Remove the plugin line from `opencode.json` and restart.
+
+## Troubleshooting
+
+### Skills not found
+
+1. Verify the plugin line in your `opencode.json`
+2. Check that `~/.cache/opencode/node_modules/understand-anything` exists after restart
+3. Use the `skill` tool to list discovered skills
+
+### 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/.opencode/plugins/understand-anything.js b/.opencode/plugins/understand-anything.js
new file mode 100644
index 0000000..43378f6
--- /dev/null
+++ b/.opencode/plugins/understand-anything.js
@@ -0,0 +1,25 @@
+/**
+ * Understand Anything plugin for OpenCode.ai
+ *
+ * Auto-registers the skills directory so OpenCode discovers all
+ * understand-anything skills without manual symlinks or config edits.
+ */
+
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export const UnderstandAnythingPlugin = async ({ client, directory }) => {
+ const skillsDir = path.resolve(__dirname, '../../understand-anything-plugin/skills');
+
+ return {
+ config: async (config) => {
+ config.skills = config.skills || {};
+ config.skills.paths = config.skills.paths || [];
+ if (!config.skills.paths.includes(skillsDir)) {
+ config.skills.paths.push(skillsDir);
+ }
+ },
+ };
+};
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",
From 1aa29897b21031793c621a1749249085aeeff7f4 Mon Sep 17 00:00:00 2001
From: Ikko Ashimine
Date: Mon, 23 Mar 2026 12:33:59 +0900
Subject: [PATCH 11/22] docs: add Japanese README
---
README.ja-JP.md | 290 ++++++++++++++++++++++++++++++++++++++++++++++++
README.md | 2 +-
README.zh-CN.md | 2 +-
3 files changed, 292 insertions(+), 2 deletions(-)
create mode 100644 README.ja-JP.md
diff --git a/README.ja-JP.md b/README.ja-JP.md
new file mode 100644
index 0000000..4540a57
--- /dev/null
+++ b/README.ja-JP.md
@@ -0,0 +1,290 @@
+Understand Anything
+
+
+ あらゆるコードベースを、探索・検索・質問ができるインタラクティブなナレッジグラフに変換します。
+
+
+
+ English | 中文 | 日本語
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+> [!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.json` に以下を追加:
+```json
+{
+ "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
+}
+```
+
+### 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
+```
+
+### プラットフォーム互換性
+
+| プラットフォーム | ステータス | インストール方法 |
+|----------|--------|----------------|
+| Claude Code | ✅ ネイティブ | プラグインマーケットプレイス |
+| Codex | ✅ サポート | AI駆動インストール |
+| OpenCode | ✅ サポート | プラグイン設定 |
+| OpenClaw | ✅ サポート | AI駆動インストール |
+| Cursor | ✅ サポート | 自動検出 |
+| Antigravity | ✅ サポート | 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 9c3922f..a078451 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
- English | 中文
+ English | 中文 | 日本語
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 5a5eeb5..2174652 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -4,7 +4,7 @@
- English | 中文
+ English | 中文 | 日本語
From c8d92444e2ae6fc10388d1b45322f20c03229135 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Mon, 23 Mar 2026 16:41:29 +0800
Subject: [PATCH 12/22] refactor: unify skills install method across all
platforms
Replace platform-specific install approaches with a consistent
per-skill symlink pattern for OpenCode and Codex (both share
~/.agents/skills/), matching the OpenCode docs requirement of
/SKILL.md discovery.
Changes:
- OpenCode: migrate from opencode.json plugin config to direct
skill symlinks in ~/.agents/skills//
- Codex: replace broken bundle symlink (understand-anything/ had
no SKILL.md at root) with individual per-skill symlinks, matching
OpenCode exactly
- All platforms: add universal ~/.understand-anything-plugin symlink
so understand-dashboard SKILL.md can reliably find packages/dashboard/
- All platforms: use idempotent [ -e ] || [ -L ] || ln -s guard to
handle multi-platform installs on the same machine
- All platforms: use ln -sf and rm -f for robust re-installs and
partial uninstalls
- understand-dashboard/SKILL.md: replace fragile dirname-based path
resolution with a prioritized candidate loop with explicit error
guard
- Remove orphaned .opencode/plugins/understand-anything.js and its
build scaffolding
- Update all three README variants (en, zh-CN, ja-JP) to reflect
new OpenCode AI-driven install method
Co-Authored-By: Claude Sonnet 4.6
---
.antigravity/INSTALL.md | 7 +-
.codex/INSTALL.md | 30 ++++++--
.openclaw/INSTALL.md | 7 +-
.opencode/INSTALL.md | 75 +++++++++++++------
.opencode/plugins/understand-anything.js | 25 -------
README.ja-JP.md | 10 +--
README.md | 10 +--
README.zh-CN.md | 10 +--
.../skills/understand-dashboard/SKILL.md | 28 +++++--
9 files changed, 123 insertions(+), 79 deletions(-)
delete mode 100644 .opencode/plugins/understand-anything.js
diff --git a/.antigravity/INSTALL.md b/.antigravity/INSTALL.md
index 5516be2..2c7681a 100644
--- a/.antigravity/INSTALL.md
+++ b/.antigravity/INSTALL.md
@@ -11,16 +11,20 @@
git clone https://github.com/Lum1104/Understand-Anything.git ~/.antigravity/understand-anything
```
-2. **Create the skills symlink:**
+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.
@@ -52,5 +56,6 @@ Skills update instantly through the symlink.
```bash
rm ~/.gemini/antigravity/skills/understand-anything
+rm ~/.understand-anything-plugin
rm -rf ~/.antigravity/understand-anything
```
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/.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 3fa6645..e462957 100644
--- a/.opencode/INSTALL.md
+++ b/.opencode/INSTALL.md
@@ -2,55 +2,86 @@
## 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.
-
-To pin a specific version:
-
-```json
-{
- "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git#v1.1.1"]
-}
+```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. Verify the plugin line in your `opencode.json`
-2. Check that `~/.cache/opencode/node_modules/understand-anything` exists after restart
-3. Use the `skill` tool to list discovered skills
+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
diff --git a/.opencode/plugins/understand-anything.js b/.opencode/plugins/understand-anything.js
deleted file mode 100644
index 43378f6..0000000
--- a/.opencode/plugins/understand-anything.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Understand Anything plugin for OpenCode.ai
- *
- * Auto-registers the skills directory so OpenCode discovers all
- * understand-anything skills without manual symlinks or config edits.
- */
-
-import path from 'path';
-import { fileURLToPath } from 'url';
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-
-export const UnderstandAnythingPlugin = async ({ client, directory }) => {
- const skillsDir = path.resolve(__dirname, '../../understand-anything-plugin/skills');
-
- return {
- config: async (config) => {
- config.skills = config.skills || {};
- config.skills.paths = config.skills.paths || [];
- if (!config.skills.paths.includes(skillsDir)) {
- config.skills.paths.push(skillsDir);
- }
- },
- };
-};
diff --git a/README.ja-JP.md b/README.ja-JP.md
index 4540a57..9a1b561 100644
--- a/README.ja-JP.md
+++ b/README.ja-JP.md
@@ -122,11 +122,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
@@ -153,7 +151,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
|----------|--------|----------------|
| Claude Code | ✅ ネイティブ | プラグインマーケットプレイス |
| Codex | ✅ サポート | AI駆動インストール |
-| OpenCode | ✅ サポート | プラグイン設定 |
+| OpenCode | ✅ サポート | AI駆動インストール |
| OpenClaw | ✅ サポート | AI駆動インストール |
| Cursor | ✅ サポート | 自動検出 |
| Antigravity | ✅ サポート | AI駆動インストール |
diff --git a/README.md b/README.md
index a078451..c48c4dc 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -153,7 +151,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
|----------|--------|----------------|
| 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 |
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 2174652..969e190 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -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
@@ -152,7 +150,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
|----------|--------|----------------|
| Claude Code | ✅ Native | 插件市场 |
| Codex | ✅ 支持 | AI驱动安装 |
-| OpenCode | ✅ 支持 | 插件配置 |
+| OpenCode | ✅ 支持 | AI驱动安装 |
| OpenClaw | ✅ 支持 | AI驱动安装 |
| Cursor | ✅ 支持 | 自动发现 |
| Antigravity | ✅ 支持 | AI驱动安装 |
diff --git a/understand-anything-plugin/skills/understand-dashboard/SKILL.md b/understand-anything-plugin/skills/understand-dashboard/SKILL.md
index b800e84..e0614d7 100644
--- a/understand-anything-plugin/skills/understand-dashboard/SKILL.md
+++ b/understand-anything-plugin/skills/understand-dashboard/SKILL.md
@@ -19,13 +19,31 @@ 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:
+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)
+
+ Use the Bash tool to resolve:
```bash
- PLUGIN_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+ 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
```
- Or locate it by checking these paths in order:
- - `${CLAUDE_PLUGIN_ROOT}/packages/dashboard/`
- - The parent directory of this skill file, then `../../packages/dashboard/`
4. Install dependencies and build if needed:
```bash
From 25ebf6d95035edcd524933ac60622c26a3d567c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Berkcan=20G=C3=BCm=C3=BC=C5=9F=C4=B1=C5=9F=C4=B1k?=
<75336900+berkcangumusisik@users.noreply.github.com>
Date: Mon, 23 Mar 2026 11:57:23 +0300
Subject: [PATCH 13/22] Add Turkish README and update language links
Add a new Turkish translation file README.tr-TR.md with the full localized documentation and quickstart instructions. Update README.md to include a link to the Turkish README in the language selector so Turkish-speaking users can access the translated docs.
---
README.md | 2 +-
README.tr-TR.md | 290 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 291 insertions(+), 1 deletion(-)
create mode 100644 README.tr-TR.md
diff --git a/README.md b/README.md
index a078451..9cec846 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
- English | 中文 | 日本語
+ English | 中文 | 日本語 | Türkçe
diff --git a/README.tr-TR.md b/README.tr-TR.md
new file mode 100644
index 0000000..b88b8c2
--- /dev/null
+++ b/README.tr-TR.md
@@ -0,0 +1,290 @@
+
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.json` dosyana ekle:
+```json
+{
+ "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
+}
+```
+
+### 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
+```
+
+### Platform Uyumluluğu
+
+| Platform | Durum | Kurulum Yöntemi |
+|----------|--------|----------------|
+| Claude Code | ✅ Yerli | Eklenti pazarı |
+| Codex | ✅ Destekleniyor | AI güdümlü kurulum |
+| OpenCode | ✅ Destekleniyor | Eklenti yapılandırması |
+| OpenClaw | ✅ Destekleniyor | AI güdümlü kurulum |
+| Cursor | ✅ Destekleniyor | Otomatik keşif |
+| Antigravity | ✅ 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
+
From b0cb17793af670d425c3bccc9516044eb0b84180 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Berkcan=20G=C3=BCm=C3=BC=C5=9F=C4=B1=C5=9F=C4=B1k?=
<75336900+berkcangumusisik@users.noreply.github.com>
Date: Mon, 23 Mar 2026 12:04:02 +0300
Subject: [PATCH 14/22] Add keyboard shortcuts UI and CONTRIBUTING.md
Introduce keyboard shortcuts to the dashboard and add contributor docs. Adds a reusable useKeyboardShortcuts hook (with formatShortcutKey), a KeyboardShortcutsHelp modal component, and integrates shortcuts into App.tsx (registering keys like ?, Escape, /, ArrowLeft/Right, l, d and wiring store actions). Adds related styles (.glass-heavy, .kbd) to index.css. Also adds a comprehensive CONTRIBUTING.md with setup, workflow, testing, and PR guidelines.
---
CONTRIBUTING.md | 270 ++++++++++++++++++
.../packages/dashboard/src/App.tsx | 131 ++++++++-
.../src/components/KeyboardShortcutsHelp.tsx | 106 +++++++
.../src/hooks/useKeyboardShortcuts.ts | 57 ++++
.../packages/dashboard/src/index.css | 25 ++
5 files changed, 588 insertions(+), 1 deletion(-)
create mode 100644 CONTRIBUTING.md
create mode 100644 understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx
create mode 100644 understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
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/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx
index 50df460..0768c72 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);
@@ -20,7 +23,106 @@ function App() {
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
+ const selectNode = useDashboardStore((s) => s.selectNode);
+ const toggleLayers = useDashboardStore((s) => s.toggleLayers);
+ const toggleDiffMode = useDashboardStore((s) => s.toggleDiffMode);
+ const stopTour = useDashboardStore((s) => s.stopTour);
+ const nextTourStep = useDashboardStore((s) => s.nextTourStep);
+ const prevTourStep = useDashboardStore((s) => s.prevTourStep);
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: () => {
+ if (showKeyboardHelp) {
+ setShowKeyboardHelp(false);
+ } else if (codeViewerOpen) {
+ closeCodeViewer();
+ } else if (selectedNodeId) {
+ selectNode(null);
+ } else if (tourActive) {
+ stopTour();
+ }
+ },
+ 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: () => {
+ if (tourActive) {
+ nextTourStep();
+ }
+ },
+ category: "Tour",
+ },
+ {
+ key: "ArrowLeft",
+ description: "Previous tour step",
+ action: () => {
+ if (tourActive) {
+ prevTourStep();
+ }
+ },
+ category: "Tour",
+ },
+ // View toggles
+ {
+ key: "l",
+ description: "Toggle layer visualization",
+ action: toggleLayers,
+ category: "View",
+ },
+ {
+ key: "d",
+ description: "Toggle diff mode",
+ action: toggleDiffMode,
+ category: "View",
+ },
+ ],
+ [
+ showKeyboardHelp,
+ codeViewerOpen,
+ selectedNodeId,
+ tourActive,
+ closeCodeViewer,
+ selectNode,
+ stopTour,
+ nextTourStep,
+ prevTourStep,
+ toggleLayers,
+ toggleDiffMode,
+ ]
+ );
+
+ // Register keyboard shortcuts
+ useKeyboardShortcuts(shortcuts, !showKeyboardHelp);
useEffect(() => {
fetch("/knowledge-graph.json")
@@ -91,6 +193,25 @@ function App() {
+
setShowKeyboardHelp(true)}
+ className="text-text-muted hover:text-gold transition-colors"
+ title="Keyboard shortcuts (Shift + ?)"
+ >
+
+
+
+
@@ -135,6 +256,14 @@ function App() {
)}
+
+ {/* Keyboard shortcuts help modal */}
+ {showKeyboardHelp && (
+ setShowKeyboardHelp(false)}
+ />
+ )}
);
}
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..7c4de2c
--- /dev/null
+++ b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx
@@ -0,0 +1,106 @@
+import { useEffect } from "react";
+import type { KeyboardShortcut } from "../hooks/useKeyboardShortcuts";
+import { formatShortcutKey } from "../hooks/useKeyboardShortcuts";
+
+interface KeyboardShortcutsHelpProps {
+ shortcuts: KeyboardShortcut[];
+ onClose: () => void;
+}
+
+export default function KeyboardShortcutsHelp({
+ shortcuts,
+ onClose,
+}: KeyboardShortcutsHelpProps) {
+ // Close on Escape key
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ onClose();
+ }
+ };
+ document.addEventListener("keydown", handleEscape);
+ return () => document.removeEventListener("keydown", handleEscape);
+ }, [onClose]);
+
+ // 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..e49dda4
--- /dev/null
+++ b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
@@ -0,0 +1,57 @@
+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) => {
+ 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[] = [];
+
+ if (shortcut.ctrlKey || shortcut.metaKey) {
+ keys.push(navigator.platform.includes("Mac") ? "⌘" : "Ctrl");
+ }
+ if (shortcut.shiftKey) keys.push("⇧");
+ if (shortcut.altKey) keys.push(navigator.platform.includes("Mac") ? "⌥" : "Alt");
+
+ keys.push(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 {
From 5f23a45e3d5a5686453688b1de4328060e30fc30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Berkcan=20G=C3=BCm=C3=BC=C5=9F=C4=B1=C5=9F=C4=B1k?=
<75336900+berkcangumusisik@users.noreply.github.com>
Date: Mon, 23 Mar 2026 15:54:26 +0300
Subject: [PATCH 15/22] Update OpenCode instructions in Turkish README
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace the previous opencode.json example with a directive to fetch and follow the INSTALL.md from the Understand-Anything repo's .opencode path. Also update the OpenCode table entry to indicate "AI güdümlü kurulum" (AI-guided installation). Changes apply to README.tr-TR.md to streamline installation instructions.
---
README.tr-TR.md | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/README.tr-TR.md b/README.tr-TR.md
index b88b8c2..7aacb72 100644
--- a/README.tr-TR.md
+++ b/README.tr-TR.md
@@ -122,11 +122,9 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
### OpenCode
-`opencode.json` dosyana ekle:
-```json
-{
- "plugin": ["understand-anything@git+https://github.com/Lum1104/Understand-Anything.git"]
-}
+OpenCode'a söyle:
+```
+Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Understand-Anything/refs/heads/main/.opencode/INSTALL.md
```
### OpenClaw
@@ -153,7 +151,7 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
|----------|--------|----------------|
| Claude Code | ✅ Yerli | Eklenti pazarı |
| Codex | ✅ Destekleniyor | AI güdümlü kurulum |
-| OpenCode | ✅ Destekleniyor | Eklenti yapılandırması |
+| 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 |
From 7bfcba677b89bd20a1d00c3326985840acccb0a7 Mon Sep 17 00:00:00 2001
From: AnnulusLabs
Date: Sun, 22 Mar 2026 09:26:05 -0600
Subject: [PATCH 16/22] fix: move dagre layout to Web Worker for large graphs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fixes #14 — dashboard becomes unusable with large knowledge graphs (2,700+ nodes).
- Web Worker for dagre layout: graphs above 200 nodes compute layout off the
main thread via a dedicated Web Worker, keeping the UI responsive
- Request ID correlation: concurrent layout calls are routed by incrementing
request ID, preventing race conditions where results cross-wire
- Worker error handling: onerror rejects pending promises; GraphView catches
errors and clears the loading spinner
- Cancellation cleanup: effect cleanup clears layouting state to prevent
stuck spinners when the graph switches below the async threshold
- Topology/visual split: dagre only re-runs when graph structure changes;
node selection, tour highlights, and search results are applied as a
cheap O(n) overlay without triggering relayout
- React.memo on CustomNode: prevents O(n) re-renders when selecting
individual nodes in large graphs
- Zero overhead for small graphs: below 200 nodes, the original synchronous
layout path is used unchanged
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../dashboard/src/components/CustomNode.tsx | 6 +-
.../dashboard/src/components/GraphView.tsx | 477 +++++++++++-------
.../packages/dashboard/src/utils/layout.ts | 80 +++
.../dashboard/src/utils/layout.worker.ts | 47 ++
4 files changed, 431 insertions(+), 179 deletions(-)
create mode 100644 understand-anything-plugin/packages/dashboard/src/utils/layout.worker.ts
diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx
index cf88c98..295f71a 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) {
@@ -122,3 +123,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 2c8c92a..3c30f19 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useRef } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ReactFlow,
ReactFlowProvider,
@@ -16,11 +16,16 @@ 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 };
/**
@@ -84,6 +89,214 @@ function SelectedNodeFitView() {
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);
@@ -97,6 +310,8 @@ function GraphViewInner() {
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
+ const [layouting, setLayouting] = useState(false);
+
const handleNodeSelect = useCallback(
(nodeId: string) => {
selectNode(nodeId);
@@ -105,190 +320,86 @@ function GraphViewInner() {
[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 }) => {
@@ -314,7 +425,17 @@ function GraphViewInner() {
}
return (
-
+
+ {layouting && (
+
+
+
+
+ Laying out {topoNodes.length.toLocaleString()} nodes...
+
+
+
+ )}
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);
+};
From 6723ff3ea58a8a583ecb1a834e7eca36e2f88a01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Berkcan=20G=C3=BCm=C3=BC=C5=9F=C4=B1=C5=9F=C4=B1k?=
<75336900+berkcangumusisik@users.noreply.github.com>
Date: Mon, 23 Mar 2026 19:58:29 +0300
Subject: [PATCH 17/22] Refactor keyboard shortcuts; add Turkish README
Use dashboard store state at handler runtime and simplify shortcut wiring to avoid stale closures. App.tsx: replace many useDashboardStore selectors with runtime getState() calls inside keyboard shortcut actions and reduce useMemo dependencies to showKeyboardHelp. KeyboardShortcutsHelp.tsx: remove local Escape key effect (handled globally). useKeyboardShortcuts.ts: ignore shortcuts while typing in inputs/textareas/contentEditable (allow Escape), and improve Mac detection via userAgentData.platform fallback; use the same isMac flag in formatShortcutKey. README (zh-CN & ja-JP): add link to Turkish README (README.tr-TR.md).
---
README.ja-JP.md | 2 +-
README.zh-CN.md | 2 +-
.../packages/dashboard/src/App.tsx | 55 ++++++++-----------
.../src/components/KeyboardShortcutsHelp.tsx | 12 ----
.../src/hooks/useKeyboardShortcuts.ts | 16 +++++-
5 files changed, 39 insertions(+), 48 deletions(-)
diff --git a/README.ja-JP.md b/README.ja-JP.md
index 4540a57..93abf79 100644
--- a/README.ja-JP.md
+++ b/README.ja-JP.md
@@ -5,7 +5,7 @@
- English | 中文 | 日本語
+ English | 中文 | 日本語 | Türkçe
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 2174652..e6a795b 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -4,7 +4,7 @@
- English | 中文 | 日本語
+ English | 中文 | 日本語 | Türkçe
diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx
index 0768c72..6961930 100644
--- a/understand-anything-plugin/packages/dashboard/src/App.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/App.tsx
@@ -23,12 +23,6 @@ function App() {
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
- const selectNode = useDashboardStore((s) => s.selectNode);
- const toggleLayers = useDashboardStore((s) => s.toggleLayers);
- const toggleDiffMode = useDashboardStore((s) => s.toggleDiffMode);
- const stopTour = useDashboardStore((s) => s.stopTour);
- const nextTourStep = useDashboardStore((s) => s.nextTourStep);
- const prevTourStep = useDashboardStore((s) => s.prevTourStep);
const [loadError, setLoadError] = useState(null);
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
@@ -38,7 +32,6 @@ function App() {
// Help
{
key: "?",
- shiftKey: true,
description: "Show keyboard shortcuts",
action: () => setShowKeyboardHelp((prev) => !prev),
category: "General",
@@ -48,14 +41,16 @@ function App() {
key: "Escape",
description: "Close panels and modals",
action: () => {
+ // Read from store at invocation time to avoid stale closures
+ const state = useDashboardStore.getState();
if (showKeyboardHelp) {
setShowKeyboardHelp(false);
- } else if (codeViewerOpen) {
- closeCodeViewer();
- } else if (selectedNodeId) {
- selectNode(null);
- } else if (tourActive) {
- stopTour();
+ } else if (state.codeViewerOpen) {
+ state.closeCodeViewer();
+ } else if (state.selectedNodeId) {
+ state.selectNode(null);
+ } else if (state.tourActive) {
+ state.stopTour();
}
},
category: "Navigation",
@@ -76,8 +71,9 @@ function App() {
key: "ArrowRight",
description: "Next tour step",
action: () => {
- if (tourActive) {
- nextTourStep();
+ const state = useDashboardStore.getState();
+ if (state.tourActive) {
+ state.nextTourStep();
}
},
category: "Tour",
@@ -86,8 +82,9 @@ function App() {
key: "ArrowLeft",
description: "Previous tour step",
action: () => {
- if (tourActive) {
- prevTourStep();
+ const state = useDashboardStore.getState();
+ if (state.tourActive) {
+ state.prevTourStep();
}
},
category: "Tour",
@@ -96,29 +93,23 @@ function App() {
{
key: "l",
description: "Toggle layer visualization",
- action: toggleLayers,
+ action: () => {
+ const state = useDashboardStore.getState();
+ state.toggleLayers();
+ },
category: "View",
},
{
key: "d",
description: "Toggle diff mode",
- action: toggleDiffMode,
+ action: () => {
+ const state = useDashboardStore.getState();
+ state.toggleDiffMode();
+ },
category: "View",
},
],
- [
- showKeyboardHelp,
- codeViewerOpen,
- selectedNodeId,
- tourActive,
- closeCodeViewer,
- selectNode,
- stopTour,
- nextTourStep,
- prevTourStep,
- toggleLayers,
- toggleDiffMode,
- ]
+ [showKeyboardHelp]
);
// Register keyboard shortcuts
diff --git a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx
index 7c4de2c..22178f5 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/KeyboardShortcutsHelp.tsx
@@ -1,4 +1,3 @@
-import { useEffect } from "react";
import type { KeyboardShortcut } from "../hooks/useKeyboardShortcuts";
import { formatShortcutKey } from "../hooks/useKeyboardShortcuts";
@@ -11,17 +10,6 @@ export default function KeyboardShortcutsHelp({
shortcuts,
onClose,
}: KeyboardShortcutsHelpProps) {
- // Close on Escape key
- useEffect(() => {
- const handleEscape = (e: KeyboardEvent) => {
- if (e.key === "Escape") {
- onClose();
- }
- };
- document.addEventListener("keydown", handleEscape);
- return () => document.removeEventListener("keydown", handleEscape);
- }, [onClose]);
-
// Group shortcuts by category
const groupedShortcuts = shortcuts.reduce((acc, shortcut) => {
if (!acc[shortcut.category]) {
diff --git a/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
index e49dda4..a46d754 100644
--- a/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
+++ b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
@@ -19,6 +19,13 @@ export function useKeyboardShortcuts(
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;
@@ -45,11 +52,16 @@ export function useKeyboardShortcuts(
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(navigator.platform.includes("Mac") ? "⌘" : "Ctrl");
+ keys.push(isMac ? "⌘" : "Ctrl");
}
if (shortcut.shiftKey) keys.push("⇧");
- if (shortcut.altKey) keys.push(navigator.platform.includes("Mac") ? "⌥" : "Alt");
+ if (shortcut.altKey) keys.push(isMac ? "⌥" : "Alt");
keys.push(shortcut.key.toUpperCase());
From cd8cd0fb5c5cb56e437827966e1b9b731d9c581c Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Tue, 24 Mar 2026 10:19:31 +0800
Subject: [PATCH 18/22] fix: keyboard shortcuts bugs and add shortcut hint
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix ? shortcut: add shiftKey: true so it matches on standard keyboards
- Fix Escape: keep hook always enabled so ESC closes the modal
(remove dead showKeyboardHelp branch, read all state at invocation time)
- Fix formatShortcutKey: don't show redundant ⇧ for shifted punctuation
- Remove stale showKeyboardHelp from useMemo deps
- Add "Press ? for keyboard shortcuts" hint in graph area
Co-Authored-By: Claude Opus 4.6
---
.../packages/dashboard/src/App.tsx | 16 ++++++++++------
.../dashboard/src/hooks/useKeyboardShortcuts.ts | 6 ++++--
2 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx
index 6961930..3f1f265 100644
--- a/understand-anything-plugin/packages/dashboard/src/App.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/App.tsx
@@ -32,6 +32,7 @@ function App() {
// Help
{
key: "?",
+ shiftKey: true,
description: "Show keyboard shortcuts",
action: () => setShowKeyboardHelp((prev) => !prev),
category: "General",
@@ -43,14 +44,14 @@ function App() {
action: () => {
// Read from store at invocation time to avoid stale closures
const state = useDashboardStore.getState();
- if (showKeyboardHelp) {
- setShowKeyboardHelp(false);
- } else if (state.codeViewerOpen) {
+ if (state.codeViewerOpen) {
state.closeCodeViewer();
} else if (state.selectedNodeId) {
state.selectNode(null);
} else if (state.tourActive) {
state.stopTour();
+ } else {
+ setShowKeyboardHelp(false);
}
},
category: "Navigation",
@@ -109,11 +110,11 @@ function App() {
category: "View",
},
],
- [showKeyboardHelp]
+ []
);
// Register keyboard shortcuts
- useKeyboardShortcuts(shortcuts, !showKeyboardHelp);
+ useKeyboardShortcuts(shortcuts);
useEffect(() => {
fetch("/knowledge-graph.json")
@@ -219,8 +220,11 @@ function App() {
{/* Main content: Graph + Sidebar */}
{/* Graph area */}
-
+
+
+ Press ? for keyboard shortcuts
+
{/* Right sidebar */}
diff --git a/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
index a46d754..fcf91e3 100644
--- a/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
+++ b/understand-anything-plugin/packages/dashboard/src/hooks/useKeyboardShortcuts.ts
@@ -60,10 +60,12 @@ export function formatShortcutKey(shortcut: KeyboardShortcut): string {
if (shortcut.ctrlKey || shortcut.metaKey) {
keys.push(isMac ? "⌘" : "Ctrl");
}
- if (shortcut.shiftKey) keys.push("⇧");
+ // 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(shortcut.key.toUpperCase());
+ keys.push(isShiftedPunctuation ? shortcut.key : shortcut.key.toUpperCase());
return keys.join(" + ");
}
From bc9ab3f090432addd36547f6154ee4651366e869 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Tue, 24 Mar 2026 18:43:53 +0800
Subject: [PATCH 19/22] docs: add Gemini CLI and Pi Agent platform support
Add INSTALL.md for Gemini CLI and Pi Agent, and update all four
READMEs (EN, ZH, JA, TR) with install instructions and compatibility
table entries.
Co-Authored-By: Claude Opus 4.6
---
.gemini/INSTALL.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++
.pi/INSTALL.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++
README.ja-JP.md | 16 +++++++++++
README.md | 16 +++++++++++
README.tr-TR.md | 16 +++++++++++
README.zh-CN.md | 16 +++++++++++
6 files changed, 208 insertions(+)
create mode 100644 .gemini/INSTALL.md
create mode 100644 .pi/INSTALL.md
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/.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/README.ja-JP.md b/README.ja-JP.md
index e076874..bad5000 100644
--- a/README.ja-JP.md
+++ b/README.ja-JP.md
@@ -145,6 +145,20 @@ Antigravityに以下を伝えてください:
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
+```
+
### プラットフォーム互換性
| プラットフォーム | ステータス | インストール方法 |
@@ -155,6 +169,8 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| OpenClaw | ✅ サポート | AI駆動インストール |
| Cursor | ✅ サポート | 自動検出 |
| Antigravity | ✅ サポート | AI駆動インストール |
+| Gemini CLI | ✅ サポート | AI駆動インストール |
+| Pi Agent | ✅ サポート | AI駆動インストール |
---
diff --git a/README.md b/README.md
index e591349..ea6d0ff 100644
--- a/README.md
+++ b/README.md
@@ -145,6 +145,20 @@ Tell Antigravity:
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 |
@@ -155,6 +169,8 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| 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
index 7aacb72..b39fccb 100644
--- a/README.tr-TR.md
+++ b/README.tr-TR.md
@@ -145,6 +145,20 @@ Antigravity'e söyle:
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 |
@@ -155,6 +169,8 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| 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 |
---
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 1fa7203..79b4ad9 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -144,6 +144,20 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
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
+```
+
### 多平台兼容
| 平台 | 状态 | 安装方式 |
@@ -154,6 +168,8 @@ Fetch and follow instructions from https://raw.githubusercontent.com/Lum1104/Und
| OpenClaw | ✅ 支持 | AI驱动安装 |
| Cursor | ✅ 支持 | 自动发现 |
| Antigravity | ✅ 支持 | AI驱动安装 |
+| Gemini CLI | ✅ 支持 | AI驱动安装 |
+| Pi Agent | ✅ 支持 | AI驱动安装 |
---
From 01d7ece81d35578dbba07e5ac34dbae66dd3bfe4 Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Tue, 24 Mar 2026 20:12:50 +0800
Subject: [PATCH 20/22] chore: add large graph generator script for perf
testing
Generates fake knowledge-graph.json with configurable node count
for testing Web Worker layout performance. Forward-only edges to
avoid dagre stack overflow on cyclic graphs.
Usage: node scripts/generate-large-graph.mjs [nodeCount]
Co-Authored-By: Claude Opus 4.6
---
scripts/generate-large-graph.mjs | 147 +++++++++++++++++++++++++++++++
1 file changed, 147 insertions(+)
create mode 100644 scripts/generate-large-graph.mjs
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}`);
From d697aefa53738ad0ae2d2b6265b1458b6383611c Mon Sep 17 00:00:00 2001
From: Lum1104
Date: Tue, 24 Mar 2026 20:26:31 +0800
Subject: [PATCH 21/22] chore: bump version to 1.2.0 and sync all plugin
manifests
- Sync .claude-plugin/plugin.json (was stuck at 1.1.0) and .cursor-plugin/plugin.json (was 1.0.5) to match main version
- Update CLAUDE.md versioning section to list all four files that must be kept in sync
- Document scripts/generate-large-graph.mjs in CLAUDE.md
---
.claude-plugin/marketplace.json | 2 +-
.claude-plugin/plugin.json | 2 +-
.cursor-plugin/plugin.json | 2 +-
CLAUDE.md | 7 ++++++-
understand-anything-plugin/package.json | 2 +-
5 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index b23857d..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.1",
+ "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/.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/CLAUDE.md b/CLAUDE.md
index 56ad507..fb2a26b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -51,7 +51,12 @@ An open-source tool combining LLM intelligence + static analysis to produce inte
- **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
diff --git a/understand-anything-plugin/package.json b/understand-anything-plugin/package.json
index 4504866..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.1",
+ "version": "1.2.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
From b8d88023a5f7890b97d577a0f3709c381dcdaec2 Mon Sep 17 00:00:00 2001
From: mengdehong
Date: Tue, 24 Mar 2026 20:30:56 +0800
Subject: [PATCH 22/22] fix(dashboard): optimize pan/zoom interaction
performance
- Widen transition to [box-shadow,outline,opacity,filter] to exclude
transform (the pan/zoom culprit) while preserving ring animations
(Tailwind v4 outline) and diff-fade opacity/filter transitions
- Replace inline boxShadow with Tailwind shadow-[] utility so
.node-glow/.diff-*-glow CSS classes compose correctly
- Disable nodesDraggable, nodesConnectable, edgesFocusable,
edgesReconnectable, elementsSelectable for read-only visualization
---
.../packages/dashboard/src/components/CustomNode.tsx | 3 +--
.../packages/dashboard/src/components/GraphView.tsx | 5 +++++
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx
index 295f71a..111d7a7 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx
@@ -80,8 +80,7 @@ function CustomNodeComponent({
return (
data.onNodeClick?.(id)}
>
{/* Left color bar */}
diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
index 3c30f19..b1d6fe3 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx
@@ -444,6 +444,11 @@ function GraphViewInner() {
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
+ nodesDraggable={false}
+ nodesConnectable={false}
+ edgesFocusable={false}
+ edgesReconnectable={false}
+ elementsSelectable={false}
fitView
fitViewOptions={{ minZoom: 0.01, padding: 0.1 }}
minZoom={0.01}