diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..095d7ab34f --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Prettier initial formatting (2026-03-28) +afcc3d1523f99c77ff67c4fd1af12334660113f6 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..5e9d18bf44 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto eol=lf +.husky/* text eol=lf diff --git a/.github/release.yml b/.github/release.yml index 1cffb36676..ff84fb8ef0 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -35,7 +35,7 @@ changelog: - dependencies - title: "\U0001F4DD Other Changes" labels: - - "*" + - '*' exclude: labels: - dependencies diff --git a/.github/workflows/ci-quality.yml b/.github/workflows/ci-quality.yml index 9237db1ec4..85862f7b8d 100644 --- a/.github/workflows/ci-quality.yml +++ b/.github/workflows/ci-quality.yml @@ -4,6 +4,19 @@ on: workflow_call: jobs: + format: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: package-lock.json + - run: npm ci + - run: npx prettier --check . + typecheck: runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/.github/workflows/ci-report.yml b/.github/workflows/ci-report.yml index 2e117f859c..2a6e5cea82 100644 --- a/.github/workflows/ci-report.yml +++ b/.github/workflows/ci-report.yml @@ -6,12 +6,12 @@ name: CI Report on: workflow_run: - workflows: ["CI"] + workflows: ['CI'] types: [completed] permissions: - actions: read # needed to list/download workflow run artifacts - contents: read # needed for sparse checkout of vitest.config.ts + actions: read # needed to list/download workflow run artifacts + contents: read # needed for sparse checkout of vitest.config.ts pull-requests: write # needed to post sticky PR comment jobs: diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 7925c0ba40..407b2fcf8c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -53,7 +53,7 @@ jobs: pull-requests: write issues: write id-token: write - actions: read # required for Claude to read CI results on PRs + actions: read # required for Claude to read CI results on PRs steps: # For PR-related triggers, resolve the fork repo so we can checkout correctly. - name: Resolve PR context diff --git a/.husky/pre-commit b/.husky/pre-commit index 36047caa52..2a04a77f02 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,33 +1,26 @@ #!/usr/bin/env bash -# Pre-commit hook (husky): typecheck + unit tests for both packages. -# Mirrors CI checks from ci-quality.yml and ci-tests.yml. +# Pre-commit hook: format staged files + typecheck. +# Tests run in CI (ci-tests.yml), not here. # Skip with: git commit --no-verify -# -# CI coverage: -# quality / typecheck → tsc --noEmit in gitnexus/ -# quality / typecheck-web → tsc -b --noEmit in gitnexus-web/ -# tests / ubuntu+coverage → vitest run in gitnexus/ (all projects) -# e2e / chromium → playwright (requires servers — skipped) ROOT="$(git rev-parse --show-toplevel)" +# 1. Format staged files with prettier via lint-staged +echo "pre-commit: formatting staged files..." +"$ROOT/node_modules/.bin/lint-staged" || exit 1 + +# 2. Typecheck changed packages WEB_CHANGED=$(git diff --cached --name-only -- 'gitnexus-web/' | head -1) CLI_CHANGED=$(git diff --cached --name-only -- 'gitnexus/' | head -1) if [ -n "$WEB_CHANGED" ]; then echo "pre-commit: typechecking gitnexus-web (tsc -b)..." - cd "$ROOT/gitnexus-web" && npx tsc -b --noEmit - - echo "pre-commit: running gitnexus-web unit tests..." - npx vitest run --reporter=dot + cd "$ROOT/gitnexus-web" && ./node_modules/.bin/tsc -b --noEmit || exit 1 fi if [ -n "$CLI_CHANGED" ]; then echo "pre-commit: typechecking gitnexus..." - cd "$ROOT/gitnexus" && npx tsc --noEmit - - echo "pre-commit: running gitnexus unit tests (default project)..." - npx vitest run --project default --reporter=dot + cd "$ROOT/gitnexus" && ./node_modules/.bin/tsc --noEmit || exit 1 fi echo "pre-commit: all checks passed" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..8c43748f44 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,16 @@ +dist/ +coverage/ +gitnexus/vendor/ +gitnexus/test/fixtures/ +gitnexus-web/playwright-report/ +gitnexus-web/test-results/ +*.d.ts +*.snap +*.wasm +*.md +.gitnexus/ +.vercel/ +.claude-flow/ +.swarm/ +assets/ +repomix-output* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..376b0b3c4e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "endOfLine": "lf", + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "./gitnexus-web/src/index.css" +} diff --git a/eval/configs/models/claude-haiku.yaml b/eval/configs/models/claude-haiku.yaml index 548cc7f849..e1e395464a 100644 --- a/eval/configs/models/claude-haiku.yaml +++ b/eval/configs/models/claude-haiku.yaml @@ -1,8 +1,8 @@ # Claude Haiku 4.5 — fast, cheap, good baseline # Via OpenRouter (set OPENROUTER_API_KEY in .env) model: - model_name: "openrouter/anthropic/claude-haiku-4.5" - cost_tracking: "ignore_errors" + model_name: 'openrouter/anthropic/claude-haiku-4.5' + cost_tracking: 'ignore_errors' model_kwargs: max_tokens: 8192 temperature: 0 diff --git a/eval/configs/models/claude-opus.yaml b/eval/configs/models/claude-opus.yaml index 3c3e114c41..571d4f490d 100644 --- a/eval/configs/models/claude-opus.yaml +++ b/eval/configs/models/claude-opus.yaml @@ -2,8 +2,8 @@ # Via OpenRouter (set OPENROUTER_API_KEY in .env) # To use Anthropic directly, change to: anthropic/claude-opus-4-20250514 model: - model_name: "openrouter/anthropic/claude-opus-4" - cost_tracking: "ignore_errors" + model_name: 'openrouter/anthropic/claude-opus-4' + cost_tracking: 'ignore_errors' model_kwargs: max_tokens: 16384 temperature: 0 diff --git a/eval/configs/models/claude-sonnet.yaml b/eval/configs/models/claude-sonnet.yaml index 2f8ab14930..b34da890ff 100644 --- a/eval/configs/models/claude-sonnet.yaml +++ b/eval/configs/models/claude-sonnet.yaml @@ -2,8 +2,8 @@ # Via OpenRouter (set OPENROUTER_API_KEY in .env) # To use Anthropic directly, change to: anthropic/claude-sonnet-4-20250514 model: - model_name: "openrouter/anthropic/claude-sonnet-4" - cost_tracking: "ignore_errors" + model_name: 'openrouter/anthropic/claude-sonnet-4' + cost_tracking: 'ignore_errors' model_kwargs: max_tokens: 16384 temperature: 0 diff --git a/eval/configs/models/deepseek-chat.yaml b/eval/configs/models/deepseek-chat.yaml index e649a021f1..949e3acb21 100644 --- a/eval/configs/models/deepseek-chat.yaml +++ b/eval/configs/models/deepseek-chat.yaml @@ -1,9 +1,9 @@ model: deepseek-ai/deepseek-chat provider: openrouter cost: - input: 0.14 # per 1M tokens - output: 0.28 # per 1M tokens - + input: 0.14 # per 1M tokens + output: 0.28 # per 1M tokens + # Native DeepSeek API (direct) api_key: null base_url: null diff --git a/eval/configs/models/deepseek-v3.yaml b/eval/configs/models/deepseek-v3.yaml index f15c5b7a62..7476ac051a 100644 --- a/eval/configs/models/deepseek-v3.yaml +++ b/eval/configs/models/deepseek-v3.yaml @@ -1,9 +1,9 @@ model: deepseek-ai/DeepSeek-V3 provider: openrouter cost: - input: 0.27 # per 1M tokens - output: 1.10 # per 1M tokens - + input: 0.27 # per 1M tokens + output: 1.10 # per 1M tokens + # Native DeepSeek API (direct) # Get your API key at: https://platform.deepseek.com/ # Or use OpenRouter with: OPENROUTER_API_KEY diff --git a/eval/configs/models/glm-4.7.yaml b/eval/configs/models/glm-4.7.yaml index 8dc3111a17..5ba934eaf0 100644 --- a/eval/configs/models/glm-4.7.yaml +++ b/eval/configs/models/glm-4.7.yaml @@ -1,7 +1,7 @@ # GLM 4.7 — via OpenRouter (set OPENROUTER_API_KEY in .env) model: - model_name: "openrouter/zhipuai/glm-4.7" - cost_tracking: "ignore_errors" + model_name: 'openrouter/zhipuai/glm-4.7' + cost_tracking: 'ignore_errors' model_kwargs: max_tokens: 8192 temperature: 0 diff --git a/eval/configs/models/glm-5.yaml b/eval/configs/models/glm-5.yaml index f37a162e6b..88ec8e0a80 100644 --- a/eval/configs/models/glm-5.yaml +++ b/eval/configs/models/glm-5.yaml @@ -1,7 +1,7 @@ # GLM 5 — via OpenRouter (set OPENROUTER_API_KEY in .env) model: - model_name: "openrouter/zhipuai/glm-5" - cost_tracking: "ignore_errors" + model_name: 'openrouter/zhipuai/glm-5' + cost_tracking: 'ignore_errors' model_kwargs: max_tokens: 8192 temperature: 0 diff --git a/eval/configs/models/minimax-2.5.yaml b/eval/configs/models/minimax-2.5.yaml index f3b43d5548..6e7800a6a5 100644 --- a/eval/configs/models/minimax-2.5.yaml +++ b/eval/configs/models/minimax-2.5.yaml @@ -1,7 +1,7 @@ # MiniMax M1 2.5 — via OpenRouter (set OPENROUTER_API_KEY in .env) model: - model_name: "openrouter/minimax/minimax-m1-2.5" - cost_tracking: "ignore_errors" + model_name: 'openrouter/minimax/minimax-m1-2.5' + cost_tracking: 'ignore_errors' model_kwargs: max_tokens: 8192 temperature: 0 diff --git a/eval/configs/models/minimax-m2.1.yaml b/eval/configs/models/minimax-m2.1.yaml index 766d75a007..5ba088dd62 100644 --- a/eval/configs/models/minimax-m2.1.yaml +++ b/eval/configs/models/minimax-m2.1.yaml @@ -3,9 +3,9 @@ # The action_regex tells mini-swe-agent to parse ```bash blocks from responses. model: model_class: litellm_textbased - model_name: "openrouter/minimax/minimax-m2.5" + model_name: 'openrouter/minimax/minimax-m2.5' action_regex: "```(?:bash|mswea_bash_command)\\s*\\n(.*?)\\n```" - cost_tracking: "ignore_errors" + cost_tracking: 'ignore_errors' model_kwargs: max_tokens: 8192 temperature: 0 diff --git a/eval/configs/modes/baseline.yaml b/eval/configs/modes/baseline.yaml index c9c73b6379..ec60a4abaa 100644 --- a/eval/configs/modes/baseline.yaml +++ b/eval/configs/modes/baseline.yaml @@ -1,9 +1,9 @@ # Baseline mode — no GitNexus, pure mini-swe-agent (control group) agent: - agent_class: "eval.agents.gitnexus_agent.GitNexusAgent" - gitnexus_mode: "baseline" + agent_class: 'eval.agents.gitnexus_agent.GitNexusAgent' + gitnexus_mode: 'baseline' step_limit: 30 cost_limit: 3.0 environment: - environment_class: "docker" + environment_class: 'docker' diff --git a/eval/configs/modes/native.yaml b/eval/configs/modes/native.yaml index 573d125e7c..1e6086016a 100644 --- a/eval/configs/modes/native.yaml +++ b/eval/configs/modes/native.yaml @@ -5,14 +5,14 @@ # # Use this mode to isolate the value of explicit tools without grep augmentation. agent: - agent_class: "eval.agents.gitnexus_agent.GitNexusAgent" - gitnexus_mode: "native" + agent_class: 'eval.agents.gitnexus_agent.GitNexusAgent' + gitnexus_mode: 'native' step_limit: 30 cost_limit: 3.0 track_gitnexus_usage: true environment: - environment_class: "eval.environments.gitnexus_docker.GitNexusDockerEnvironment" + environment_class: 'eval.environments.gitnexus_docker.GitNexusDockerEnvironment' enable_gitnexus: true skip_embeddings: true gitnexus_timeout: 120 diff --git a/eval/configs/modes/native_augment.yaml b/eval/configs/modes/native_augment.yaml index fb9b79729e..a26c5bfb39 100644 --- a/eval/configs/modes/native_augment.yaml +++ b/eval/configs/modes/native_augment.yaml @@ -8,8 +8,8 @@ # # The agent decides when to use explicit tools vs rely on enriched grep results. agent: - agent_class: "eval.agents.gitnexus_agent.GitNexusAgent" - gitnexus_mode: "native_augment" + agent_class: 'eval.agents.gitnexus_agent.GitNexusAgent' + gitnexus_mode: 'native_augment' step_limit: 30 cost_limit: 3.0 augment_timeout: 5.0 @@ -17,7 +17,7 @@ agent: track_gitnexus_usage: true environment: - environment_class: "eval.environments.gitnexus_docker.GitNexusDockerEnvironment" + environment_class: 'eval.environments.gitnexus_docker.GitNexusDockerEnvironment' enable_gitnexus: true skip_embeddings: true gitnexus_timeout: 120 diff --git a/gitnexus-claude-plugin/hooks/gitnexus-hook.js b/gitnexus-claude-plugin/hooks/gitnexus-hook.js index 4cc79c100b..e29726d854 100644 --- a/gitnexus-claude-plugin/hooks/gitnexus-hook.js +++ b/gitnexus-claude-plugin/hooks/gitnexus-hook.js @@ -64,10 +64,26 @@ function extractPattern(toolName, toolInput) { const tokens = cmd.split(/\s+/); let foundCmd = false; let skipNext = false; - const flagsWithValues = new Set(['-e', '-f', '-m', '-A', '-B', '-C', '-g', '--glob', '-t', '--type', '--include', '--exclude']); + const flagsWithValues = new Set([ + '-e', + '-f', + '-m', + '-A', + '-B', + '-C', + '-g', + '--glob', + '-t', + '--type', + '--include', + '--exclude', + ]); for (const token of tokens) { - if (skipNext) { skipNext = false; continue; } + if (skipNext) { + skipNext = false; + continue; + } if (!foundCmd) { if (/\brg$|\bgrep$/.test(token)) foundCmd = true; continue; @@ -98,33 +114,42 @@ function runGitNexusCli(args, cwd, timeout) { // Detect whether 'gitnexus' is on PATH (cheap check, no execution) let useDirectBinary = false; try { - const which = spawnSync( - isWin ? 'where' : 'which', ['gitnexus'], - { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] } - ); + const which = spawnSync(isWin ? 'where' : 'which', ['gitnexus'], { + encoding: 'utf-8', + timeout: 3000, + stdio: ['pipe', 'pipe', 'pipe'], + }); useDirectBinary = which.status === 0; - } catch { /* not on PATH */ } + } catch { + /* not on PATH */ + } if (useDirectBinary) { - return spawnSync( - isWin ? 'gitnexus.cmd' : 'gitnexus', args, - { encoding: 'utf-8', timeout, cwd, stdio: ['pipe', 'pipe', 'pipe'] } - ); + return spawnSync(isWin ? 'gitnexus.cmd' : 'gitnexus', args, { + encoding: 'utf-8', + timeout, + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + }); } // npx fallback needs shell on Windows since npx is a .cmd script - return spawnSync( - isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args], - { encoding: 'utf-8', timeout: timeout + 5000, cwd, stdio: ['pipe', 'pipe', 'pipe'] } - ); + return spawnSync(isWin ? 'npx.cmd' : 'npx', ['-y', 'gitnexus', ...args], { + encoding: 'utf-8', + timeout: timeout + 5000, + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + }); } /** * Emit a hook response with additional context for the agent. */ function sendHookResponse(hookEventName, message) { - console.log(JSON.stringify({ - hookSpecificOutput: { hookEventName, additionalContext: message } - })); + console.log( + JSON.stringify({ + hookSpecificOutput: { hookEventName, additionalContext: message }, + }), + ); } /** @@ -149,7 +174,9 @@ function handlePreToolUse(input) { if (!child.error && child.status === 0) { result = child.stderr || ''; } - } catch { /* graceful failure */ } + } catch { + /* graceful failure */ + } if (result && result.trim()) { sendHookResponse('PreToolUse', result.trim()); @@ -185,10 +212,15 @@ function handlePostToolUse(input) { let currentHead = ''; try { const headResult = spawnSync('git', ['rev-parse', 'HEAD'], { - encoding: 'utf-8', timeout: 3000, cwd, stdio: ['pipe', 'pipe', 'pipe'], + encoding: 'utf-8', + timeout: 3000, + cwd, + stdio: ['pipe', 'pipe', 'pipe'], }); currentHead = (headResult.stdout || '').trim(); - } catch { return; } + } catch { + return; + } if (!currentHead) return; @@ -197,16 +229,19 @@ function handlePostToolUse(input) { try { const meta = JSON.parse(fs.readFileSync(path.join(gitNexusDir, 'meta.json'), 'utf-8')); lastCommit = meta.lastCommit || ''; - hadEmbeddings = (meta.stats && meta.stats.embeddings > 0); - } catch { /* no meta — treat as stale */ } + hadEmbeddings = meta.stats && meta.stats.embeddings > 0; + } catch { + /* no meta — treat as stale */ + } // If HEAD matches last indexed commit, no reindex needed if (currentHead && currentHead === lastCommit) return; const analyzeCmd = `npx gitnexus analyze${hadEmbeddings ? ' --embeddings' : ''}`; - sendHookResponse('PostToolUse', + sendHookResponse( + 'PostToolUse', `GitNexus index is stale (last indexed: ${lastCommit ? lastCommit.slice(0, 7) : 'never'}). ` + - `Run \`${analyzeCmd}\` to update the knowledge graph.` + `Run \`${analyzeCmd}\` to update the knowledge graph.`, ); } diff --git a/gitnexus-shared/src/index.ts b/gitnexus-shared/src/index.ts index dad639895b..bd89dfc622 100644 --- a/gitnexus-shared/src/index.ts +++ b/gitnexus-shared/src/index.ts @@ -14,17 +14,11 @@ export { REL_TYPES, EMBEDDING_TABLE_NAME, } from './lbug/schema-constants.js'; -export type { - NodeTableName, - RelType, -} from './lbug/schema-constants.js'; +export type { NodeTableName, RelType } from './lbug/schema-constants.js'; // Language support export { SupportedLanguages } from './languages.js'; export { getLanguageFromFilename, getSyntaxLanguageFromFilename } from './language-detection.js'; // Pipeline progress -export type { - PipelinePhase, - PipelineProgress, -} from './pipeline.js'; +export type { PipelinePhase, PipelineProgress } from './pipeline.js'; diff --git a/gitnexus-shared/src/language-detection.ts b/gitnexus-shared/src/language-detection.ts index 757e18b195..0b3b778f87 100644 --- a/gitnexus-shared/src/language-detection.ts +++ b/gitnexus-shared/src/language-detection.ts @@ -12,7 +12,13 @@ import { SupportedLanguages } from './languages.js'; /** Ruby extensionless filenames recognised as Ruby source */ -const RUBY_EXTENSIONLESS_FILES = new Set(['Rakefile', 'Gemfile', 'Guardfile', 'Vagrantfile', 'Brewfile']); +const RUBY_EXTENSIONLESS_FILES = new Set([ + 'Rakefile', + 'Gemfile', + 'Guardfile', + 'Vagrantfile', + 'Brewfile', +]); /** * Exhaustive map: every SupportedLanguages member → its file extensions. @@ -21,26 +27,29 @@ const RUBY_EXTENSIONLESS_FILES = new Set(['Rakefile', 'Gemfile', 'Guardfile', 'V * TypeScript emits a compile error: "Property 'NewLang' is missing in type..." */ const EXTENSION_MAP: Record = { - [SupportedLanguages.JavaScript]: ['.js', '.jsx', '.mjs', '.cjs'], - [SupportedLanguages.TypeScript]: ['.ts', '.tsx', '.mts', '.cts'], - [SupportedLanguages.Python]: ['.py'], - [SupportedLanguages.Java]: ['.java'], - [SupportedLanguages.C]: ['.c'], - [SupportedLanguages.CPlusPlus]: ['.cpp', '.cc', '.cxx', '.h', '.hpp', '.hxx', '.hh'], - [SupportedLanguages.CSharp]: ['.cs'], - [SupportedLanguages.Go]: ['.go'], - [SupportedLanguages.Ruby]: ['.rb', '.rake', '.gemspec'], - [SupportedLanguages.Rust]: ['.rs'], - [SupportedLanguages.PHP]: ['.php', '.phtml', '.php3', '.php4', '.php5', '.php8'], - [SupportedLanguages.Kotlin]: ['.kt', '.kts'], - [SupportedLanguages.Swift]: ['.swift'], - [SupportedLanguages.Dart]: ['.dart'], - [SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'], + [SupportedLanguages.JavaScript]: ['.js', '.jsx', '.mjs', '.cjs'], + [SupportedLanguages.TypeScript]: ['.ts', '.tsx', '.mts', '.cts'], + [SupportedLanguages.Python]: ['.py'], + [SupportedLanguages.Java]: ['.java'], + [SupportedLanguages.C]: ['.c'], + [SupportedLanguages.CPlusPlus]: ['.cpp', '.cc', '.cxx', '.h', '.hpp', '.hxx', '.hh'], + [SupportedLanguages.CSharp]: ['.cs'], + [SupportedLanguages.Go]: ['.go'], + [SupportedLanguages.Ruby]: ['.rb', '.rake', '.gemspec'], + [SupportedLanguages.Rust]: ['.rs'], + [SupportedLanguages.PHP]: ['.php', '.phtml', '.php3', '.php4', '.php5', '.php8'], + [SupportedLanguages.Kotlin]: ['.kt', '.kts'], + [SupportedLanguages.Swift]: ['.swift'], + [SupportedLanguages.Dart]: ['.dart'], + [SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'], } satisfies Record; // Ensure exhaustiveness /** Pre-built reverse lookup: extension → language (built once at module load). */ const extToLang = new Map(); -for (const [lang, exts] of Object.entries(EXTENSION_MAP) as [SupportedLanguages, readonly string[]][]) { +for (const [lang, exts] of Object.entries(EXTENSION_MAP) as [ + SupportedLanguages, + readonly string[], +][]) { for (const ext of exts) { extToLang.set(ext, lang); } @@ -75,37 +84,50 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages | * TypeScript emits a compile error. */ const SYNTAX_MAP: Record = { - [SupportedLanguages.JavaScript]: 'javascript', - [SupportedLanguages.TypeScript]: 'typescript', - [SupportedLanguages.Python]: 'python', - [SupportedLanguages.Java]: 'java', - [SupportedLanguages.C]: 'c', - [SupportedLanguages.CPlusPlus]: 'cpp', - [SupportedLanguages.CSharp]: 'csharp', - [SupportedLanguages.Go]: 'go', - [SupportedLanguages.Ruby]: 'ruby', - [SupportedLanguages.Rust]: 'rust', - [SupportedLanguages.PHP]: 'php', - [SupportedLanguages.Kotlin]: 'kotlin', - [SupportedLanguages.Swift]: 'swift', - [SupportedLanguages.Dart]: 'dart', - [SupportedLanguages.Cobol]: 'cobol', + [SupportedLanguages.JavaScript]: 'javascript', + [SupportedLanguages.TypeScript]: 'typescript', + [SupportedLanguages.Python]: 'python', + [SupportedLanguages.Java]: 'java', + [SupportedLanguages.C]: 'c', + [SupportedLanguages.CPlusPlus]: 'cpp', + [SupportedLanguages.CSharp]: 'csharp', + [SupportedLanguages.Go]: 'go', + [SupportedLanguages.Ruby]: 'ruby', + [SupportedLanguages.Rust]: 'rust', + [SupportedLanguages.PHP]: 'php', + [SupportedLanguages.Kotlin]: 'kotlin', + [SupportedLanguages.Swift]: 'swift', + [SupportedLanguages.Dart]: 'dart', + [SupportedLanguages.Cobol]: 'cobol', } satisfies Record; // Ensure exhaustiveness /** Non-code file extensions → Prism-compatible syntax identifiers */ const AUXILIARY_SYNTAX_MAP: Record = { - json: 'json', yaml: 'yaml', yml: 'yaml', - md: 'markdown', mdx: 'markdown', - html: 'markup', htm: 'markup', erb: 'markup', xml: 'markup', - css: 'css', scss: 'css', sass: 'css', - sh: 'bash', bash: 'bash', zsh: 'bash', - sql: 'sql', toml: 'toml', ini: 'ini', + json: 'json', + yaml: 'yaml', + yml: 'yaml', + md: 'markdown', + mdx: 'markdown', + html: 'markup', + htm: 'markup', + erb: 'markup', + xml: 'markup', + css: 'css', + scss: 'css', + sass: 'css', + sh: 'bash', + bash: 'bash', + zsh: 'bash', + sql: 'sql', + toml: 'toml', + ini: 'ini', dockerfile: 'docker', }; /** Extensionless filenames → Prism-compatible syntax identifiers */ const AUXILIARY_BASENAME_MAP: Record = { - Makefile: 'makefile', Dockerfile: 'docker', + Makefile: 'makefile', + Dockerfile: 'docker', }; /** diff --git a/gitnexus-shared/src/lbug/schema-constants.ts b/gitnexus-shared/src/lbug/schema-constants.ts index d13b3b8de1..e30948492f 100644 --- a/gitnexus-shared/src/lbug/schema-constants.ts +++ b/gitnexus-shared/src/lbug/schema-constants.ts @@ -9,25 +9,63 @@ */ export const NODE_TABLES = [ - 'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', 'Community', 'Process', 'Section', - 'Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl', - 'TypeAlias', 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module', + 'File', + 'Folder', + 'Function', + 'Class', + 'Interface', + 'Method', + 'CodeElement', + 'Community', + 'Process', + 'Section', + 'Struct', + 'Enum', + 'Macro', + 'Typedef', + 'Union', + 'Namespace', + 'Trait', + 'Impl', + 'TypeAlias', + 'Const', + 'Static', + 'Property', + 'Record', + 'Delegate', + 'Annotation', + 'Constructor', + 'Template', + 'Module', 'Route', 'Tool', ] as const; -export type NodeTableName = typeof NODE_TABLES[number]; +export type NodeTableName = (typeof NODE_TABLES)[number]; export const REL_TABLE_NAME = 'CodeRelation'; export const REL_TYPES = [ - 'CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', - 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', 'OVERRIDES', - 'MEMBER_OF', 'STEP_IN_PROCESS', - 'HANDLES_ROUTE', 'FETCHES', 'HANDLES_TOOL', 'ENTRY_POINT_OF', - 'WRAPS', 'QUERIES', + 'CONTAINS', + 'DEFINES', + 'IMPORTS', + 'CALLS', + 'EXTENDS', + 'IMPLEMENTS', + 'HAS_METHOD', + 'HAS_PROPERTY', + 'ACCESSES', + 'OVERRIDES', + 'MEMBER_OF', + 'STEP_IN_PROCESS', + 'HANDLES_ROUTE', + 'FETCHES', + 'HANDLES_TOOL', + 'ENTRY_POINT_OF', + 'WRAPS', + 'QUERIES', ] as const; -export type RelType = typeof REL_TYPES[number]; +export type RelType = (typeof REL_TYPES)[number]; export const EMBEDDING_TABLE_NAME = 'CodeEmbedding'; diff --git a/gitnexus-web/e2e/debug-issues.spec.ts b/gitnexus-web/e2e/debug-issues.spec.ts index c744964e1b..175e09ba6c 100644 --- a/gitnexus-web/e2e/debug-issues.spec.ts +++ b/gitnexus-web/e2e/debug-issues.spec.ts @@ -9,7 +9,7 @@ const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:4747'; const debugTest = process.env.DEBUG_E2E ? test : test.skip; async function connectToServer(page: import('@playwright/test').Page) { - page.on('console', msg => { + page.on('console', (msg) => { if (msg.type() === 'error') console.log(`[error] ${msg.text()}`); }); @@ -86,7 +86,10 @@ debugTest('debug: process view Reset View button', async ({ page }, testInfo) => const transformAfterReset = await diagramDiv.getAttribute('style'); console.log('Transform AFTER reset:', transformAfterReset); - await page.screenshot({ path: testInfo.outputPath('debug-modal-after-reset.png'), fullPage: true }); + await page.screenshot({ + path: testInfo.outputPath('debug-modal-after-reset.png'), + fullPage: true, + }); // Verify transform actually changed back expect(transformAfterZoom).not.toBe(transformBefore); @@ -123,5 +126,8 @@ debugTest('debug: lightbulb clears node selection dimming', async ({ page }, tes // Click it again to toggle back on await lightbulbBtn.click(); await page.waitForTimeout(500); - await page.screenshot({ path: testInfo.outputPath('debug-after-lightbulb-toggle-back.png'), fullPage: true }); + await page.screenshot({ + path: testInfo.outputPath('debug-after-lightbulb-toggle-back.png'), + fullPage: true, + }); }); diff --git a/gitnexus-web/e2e/manual-record.spec.ts b/gitnexus-web/e2e/manual-record.spec.ts index 794ad56946..b670efc312 100644 --- a/gitnexus-web/e2e/manual-record.spec.ts +++ b/gitnexus-web/e2e/manual-record.spec.ts @@ -12,16 +12,16 @@ import { test } from '@playwright/test'; */ test.skip( !!process.env.CI || process.env.PWDEBUG !== '1', - 'Manual recording requires --headed and PWDEBUG=1. Run: PWDEBUG=1 npx playwright test e2e/manual-record.spec.ts --headed --timeout=0' + 'Manual recording requires --headed and PWDEBUG=1. Run: PWDEBUG=1 npx playwright test e2e/manual-record.spec.ts --headed --timeout=0', ); test('manual recording session', async ({ page }) => { - page.on('console', msg => { + page.on('console', (msg) => { if (msg.type() === 'error' || msg.type() === 'warning') { console.log(`[${msg.type()}] ${msg.text()}`); } }); - page.on('pageerror', err => console.log(`[crash] ${err.message}`)); + page.on('pageerror', (err) => console.log(`[crash] ${err.message}`)); await page.goto('http://localhost:5173'); await page.pause(); diff --git a/gitnexus-web/e2e/onboarding.spec.ts b/gitnexus-web/e2e/onboarding.spec.ts index 03f2058c68..309dc7ef90 100644 --- a/gitnexus-web/e2e/onboarding.spec.ts +++ b/gitnexus-web/e2e/onboarding.spec.ts @@ -38,7 +38,9 @@ test.describe('Flow 1: Onboarding — no server', () => { // Step 2 title changes to "Waiting for server to start" once polling begins await expect(page.getByText('Waiting for server to start')).toBeAttached({ timeout: 10_000 }); // Step 3 is always rendered - await expect(page.getByText('Auto-connects and opens the graph')).toBeAttached({ timeout: 5_000 }); + await expect(page.getByText('Auto-connects and opens the graph')).toBeAttached({ + timeout: 5_000, + }); }); test('shows terminal window with command', async ({ page }) => { @@ -98,7 +100,9 @@ test.describe('Flow 2: Server detected — auto-connect', () => { }); await page.route(`${BACKEND_URL}/api/repo`, async (route) => { if (blockBackend) return route.abort('connectionrefused'); - await route.fulfill({ json: { name: 'test-repo', path: '/tmp/test', repoPath: '/tmp/test' } }); + await route.fulfill({ + json: { name: 'test-repo', path: '/tmp/test', repoPath: '/tmp/test' }, + }); }); await page.route(`${BACKEND_URL}/api/graph**`, async (route) => { if (blockBackend) return route.abort('connectionrefused'); @@ -135,7 +139,11 @@ test.describe('Flow 2: Server detected — auto-connect', () => { route.fulfill({ json: { version: '1.0.0', launchContext: 'npx', nodeVersion: 'v22.0.0' } }), ); await page.route(`${BACKEND_URL}/api/heartbeat`, (route) => - route.fulfill({ status: 200, headers: { 'Content-Type': 'text/event-stream' }, body: ':ok\n\n' }), + route.fulfill({ + status: 200, + headers: { 'Content-Type': 'text/event-stream' }, + body: ':ok\n\n', + }), ); await page.goto('/'); @@ -158,7 +166,11 @@ test.describe('Flow 3: Analyze form', () => { route.fulfill({ json: { version: '1.0.0', launchContext: 'npx', nodeVersion: 'v22.0.0' } }), ); await page.route(`${BACKEND_URL}/api/heartbeat`, (route) => - route.fulfill({ status: 200, headers: { 'Content-Type': 'text/event-stream' }, body: ':ok\n\n' }), + route.fulfill({ + status: 200, + headers: { 'Content-Type': 'text/event-stream' }, + body: ':ok\n\n', + }), ); }); @@ -222,9 +234,15 @@ test.describe('Flow 4: Repo dropdown in exploring view', () => { if (process.env.E2E) return; try { const res = await fetch(`${BACKEND_URL}/api/repos`); - if (!res.ok) { test.skip(true, SKIP_MSG); return; } + if (!res.ok) { + test.skip(true, SKIP_MSG); + return; + } const repos = await res.json(); - if (!repos.length) { test.skip(true, 'Server has no indexed repos'); return; } + if (!repos.length) { + test.skip(true, 'Server has no indexed repos'); + return; + } } catch { test.skip(true, SKIP_MSG); } @@ -238,7 +256,10 @@ test.describe('Flow 4: Repo dropdown in exploring view', () => { await page.screenshot({ path: testInfo.outputPath('exploring-loaded.png') }); // Click the project badge (has a chevron) - const badge = page.locator('header button').filter({ has: page.locator('svg') }).first(); + const badge = page + .locator('header button') + .filter({ has: page.locator('svg') }) + .first(); await badge.click(); // Repo dropdown should be visible @@ -252,7 +273,10 @@ test.describe('Flow 4: Repo dropdown in exploring view', () => { await expect(page.locator('[data-testid="status-ready"]')).toBeVisible({ timeout: 30_000 }); // Open repo dropdown - const badge = page.locator('header button').filter({ has: page.locator('svg') }).first(); + const badge = page + .locator('header button') + .filter({ has: page.locator('svg') }) + .first(); await badge.click(); // Click "Analyze a new repository..." diff --git a/gitnexus-web/e2e/server-connect.spec.ts b/gitnexus-web/e2e/server-connect.spec.ts index 61860aa2bc..5d1c307e3c 100644 --- a/gitnexus-web/e2e/server-connect.spec.ts +++ b/gitnexus-web/e2e/server-connect.spec.ts @@ -21,11 +21,17 @@ test.beforeAll(async () => { fetch(`${BACKEND_URL}/api/repos`), fetch(FRONTEND_URL), ]); - if (backendRes.status === 'rejected' || (backendRes.status === 'fulfilled' && !backendRes.value.ok)) { + if ( + backendRes.status === 'rejected' || + (backendRes.status === 'fulfilled' && !backendRes.value.ok) + ) { test.skip(true, 'gitnexus serve not available on :4747'); return; } - if (frontendRes.status === 'rejected' || (frontendRes.status === 'fulfilled' && !frontendRes.value.ok)) { + if ( + frontendRes.status === 'rejected' || + (frontendRes.status === 'fulfilled' && !frontendRes.value.ok) + ) { test.skip(true, 'Vite dev server not available on :5173'); return; } @@ -85,7 +91,9 @@ test.describe('Processes Panel', () => { await page.getByRole('button', { name: 'Nexus AI' }).click(); await page.getByText('Processes').click(); - await expect(page.locator('[data-testid="process-list-loaded"]')).toBeVisible({ timeout: 15_000 }); + await expect(page.locator('[data-testid="process-list-loaded"]')).toBeVisible({ + timeout: 15_000, + }); await page.screenshot({ path: testInfo.outputPath('processes-panel.png'), fullPage: true }); const processRow = page.locator('[data-testid="process-row"]').first(); @@ -96,7 +104,10 @@ test.describe('Processes Panel', () => { await viewBtn.waitFor({ state: 'visible', timeout: 5_000 }); await viewBtn.click(); await expect(page.locator('[data-testid="process-modal"]')).toBeVisible({ timeout: 5_000 }); - await page.screenshot({ path: testInfo.outputPath('process-view-clicked.png'), fullPage: true }); + await page.screenshot({ + path: testInfo.outputPath('process-view-clicked.png'), + fullPage: true, + }); }); test('lightbulb highlights nodes in graph', async ({ page }, testInfo) => { @@ -104,7 +115,9 @@ test.describe('Processes Panel', () => { await page.getByRole('button', { name: 'Nexus AI' }).click(); await page.getByText('Processes').click(); - await expect(page.locator('[data-testid="process-list-loaded"]')).toBeVisible({ timeout: 15_000 }); + await expect(page.locator('[data-testid="process-list-loaded"]')).toBeVisible({ + timeout: 15_000, + }); const processRow = page.locator('[data-testid="process-row"]').first(); await expect(processRow).toBeVisible({ timeout: 10_000 }); @@ -129,10 +142,14 @@ test.describe('Turn Off All Highlights', () => { await fileItem.click(); const highlightToggle = page.locator('[data-testid="ai-highlights-toggle"]'); - await expect(highlightToggle).toHaveAttribute('title', 'Turn off all highlights', { timeout: 5_000 }); + await expect(highlightToggle).toHaveAttribute('title', 'Turn off all highlights', { + timeout: 5_000, + }); await highlightToggle.click(); - await expect(highlightToggle).toHaveAttribute('title', 'Turn on AI highlights', { timeout: 5_000 }); + await expect(highlightToggle).toHaveAttribute('title', 'Turn on AI highlights', { + timeout: 5_000, + }); await page.screenshot({ path: testInfo.outputPath('highlights-cleared.png'), fullPage: true }); }); }); diff --git a/gitnexus-web/index.html b/gitnexus-web/index.html index 13f6013793..95f25b846e 100644 --- a/gitnexus-web/index.html +++ b/gitnexus-web/index.html @@ -4,9 +4,12 @@ GitNexus - - - + + +
diff --git a/gitnexus-web/playwright.config.ts b/gitnexus-web/playwright.config.ts index 11e5eeeee2..291b1805f3 100644 --- a/gitnexus-web/playwright.config.ts +++ b/gitnexus-web/playwright.config.ts @@ -40,9 +40,6 @@ export default defineConfig({ use: { browserName: 'chromium' }, }, ], - reporter: [ - ['list'], - ['html', { open: 'never', outputFolder: 'playwright-report' }], - ], + reporter: [['list'], ['html', { open: 'never', outputFolder: 'playwright-report' }]], outputDir: 'test-results', }); diff --git a/gitnexus-web/src/App.tsx b/gitnexus-web/src/App.tsx index 2ad50ad36e..7916d3268c 100644 --- a/gitnexus-web/src/App.tsx +++ b/gitnexus-web/src/App.tsx @@ -11,7 +11,15 @@ import { FileTreePanel } from './components/FileTreePanel'; import { CodeReferencesPanel } from './components/CodeReferencesPanel'; import { getActiveProviderConfig } from './core/llm/settings-service'; import { createKnowledgeGraph } from './core/graph/graph'; -import { connectToServer, fetchRepos, normalizeServerUrl, connectHeartbeat, BackendError, type ConnectResult, type BackendRepo } from './services/backend-client'; +import { + connectToServer, + fetchRepos, + normalizeServerUrl, + connectHeartbeat, + BackendError, + type ConnectResult, + type BackendRepo, +} from './services/backend-client'; import { ERROR_RESET_DELAY_MS } from './config/ui-constants'; const AppContent = () => { @@ -41,36 +49,39 @@ const AppContent = () => { const graphCanvasRef = useRef(null); - const handleServerConnect = useCallback(async (result: ConnectResult): Promise => { - // Extract project name from repoPath - const repoPath = result.repoInfo.repoPath ?? result.repoInfo.path; - const parts = (repoPath || '').split('/').filter(p => p && !p.startsWith('.')); - const projectName = parts[parts.length - 1] || parts[0] || 'server-project'; - setProjectName(projectName); - - // Build KnowledgeGraph from server data for visualization - const graph = createKnowledgeGraph(); - for (const node of result.nodes) { - graph.addNode(node); - } - for (const rel of result.relationships) { - graph.addRelationship(rel); - } - setGraph(graph); - - // Transition directly to exploring view - setViewMode('exploring'); - - // Initialize agent with backend queries, then start embeddings - try { - if (getActiveProviderConfig()) { - await initializeAgent(projectName); + const handleServerConnect = useCallback( + async (result: ConnectResult): Promise => { + // Extract project name from repoPath + const repoPath = result.repoInfo.repoPath ?? result.repoInfo.path; + const parts = (repoPath || '').split('/').filter((p) => p && !p.startsWith('.')); + const projectName = parts[parts.length - 1] || parts[0] || 'server-project'; + setProjectName(projectName); + + // Build KnowledgeGraph from server data for visualization + const graph = createKnowledgeGraph(); + for (const node of result.nodes) { + graph.addNode(node); + } + for (const rel of result.relationships) { + graph.addRelationship(rel); } - startEmbeddingsWithFallback(); - } catch (err) { - console.warn('Failed to initialize agent:', err); - } - }, [setViewMode, setGraph, setProjectName, initializeAgent, startEmbeddingsWithFallback]); + setGraph(graph); + + // Transition directly to exploring view + setViewMode('exploring'); + + // Initialize agent with backend queries, then start embeddings + try { + if (getActiveProviderConfig()) { + await initializeAgent(projectName); + } + startEmbeddingsWithFallback(); + } catch (err) { + console.warn('Failed to initialize agent:', err); + } + }, + [setViewMode, setGraph, setProjectName, initializeAgent, startEmbeddingsWithFallback], + ); // Auto-connect when ?server query param is present (bookmarkable shortcut) const autoConnectRan = useRef(false); @@ -84,7 +95,12 @@ const AppContent = () => { const cleanUrl = window.location.pathname + window.location.hash; window.history.replaceState(null, '', cleanUrl); - setProgress({ phase: 'extracting', percent: 0, message: 'Connecting to server...', detail: 'Validating server' }); + setProgress({ + phase: 'extracting', + percent: 0, + message: 'Connecting to server...', + detail: 'Validating server', + }); setViewMode('loading'); const serverUrl = params.get('server') || window.location.origin; @@ -93,34 +109,51 @@ const AppContent = () => { connectToServer(serverUrl, (phase, downloaded, total) => { if (phase === 'validating') { - setProgress({ phase: 'extracting', percent: 5, message: 'Connecting to server...', detail: 'Validating server' }); + setProgress({ + phase: 'extracting', + percent: 5, + message: 'Connecting to server...', + detail: 'Validating server', + }); } else if (phase === 'downloading') { const pct = total ? Math.round((downloaded / total) * 90) + 5 : 50; const mb = (downloaded / (1024 * 1024)).toFixed(1); - setProgress({ phase: 'extracting', percent: pct, message: 'Downloading graph...', detail: `${mb} MB downloaded` }); + setProgress({ + phase: 'extracting', + percent: pct, + message: 'Downloading graph...', + detail: `${mb} MB downloaded`, + }); } else if (phase === 'extracting') { - setProgress({ phase: 'extracting', percent: 97, message: 'Processing...', detail: 'Extracting file contents' }); + setProgress({ + phase: 'extracting', + percent: 97, + message: 'Processing...', + detail: 'Extracting file contents', + }); } - }).then(async (result) => { - await handleServerConnect(result); - setProgress(null); - setServerBaseUrl(baseUrl); - fetchRepos() - .then((repos) => setAvailableRepos(repos)) - .catch((e) => console.warn('Failed to fetch repo list:', e)); - }).catch((err) => { - console.error('Auto-connect failed:', err); - setProgress({ - phase: 'error', - percent: 0, - message: 'Failed to connect to server', - detail: err instanceof Error ? err.message : 'Unknown error', - }); - setTimeout(() => { - setViewMode('onboarding'); + }) + .then(async (result) => { + await handleServerConnect(result); setProgress(null); - }, ERROR_RESET_DELAY_MS); - }); + setServerBaseUrl(baseUrl); + fetchRepos() + .then((repos) => setAvailableRepos(repos)) + .catch((e) => console.warn('Failed to fetch repo list:', e)); + }) + .catch((err) => { + console.error('Auto-connect failed:', err); + setProgress({ + phase: 'error', + percent: 0, + message: 'Failed to connect to server', + detail: err instanceof Error ? err.message : 'Unknown error', + }); + setTimeout(() => { + setViewMode('onboarding'); + setProgress(null); + }, ERROR_RESET_DELAY_MS); + }); }, [handleServerConnect, setProgress, setViewMode, setServerBaseUrl, setAvailableRepos]); const handleFocusNode = useCallback((nodeId: string) => { @@ -176,7 +209,7 @@ const AppContent = () => { // Exploring view return ( -
+
{ } catch (err: unknown) { if (attempt === 0 && err instanceof BackendError && err.status === 404) { // Server may still be reinitializing — wait and retry - await new Promise(r => setTimeout(r, 1500)); + await new Promise((r) => setTimeout(r, 1500)); continue; } console.error('Failed to connect after analyze:', err); - fetchRepos().then(repos => setAvailableRepos(repos)).catch(() => {}); + fetchRepos() + .then((repos) => setAvailableRepos(repos)) + .catch(() => {}); return; } } }} /> -
+
{/* Left Panel - File Tree */} {/* Graph area - takes remaining space */} -
+
{/* Code References Panel (overlay) - does NOT resize the graph, it overlaps on top */} {isCodePanelOpen && (codeReferences.length > 0 || !!selectedNode) && ( -
+
)} @@ -239,7 +274,6 @@ const AppContent = () => { onClose={() => setSettingsPanelOpen(false)} onSettingsSaved={handleSettingsSaved} /> -
); }; diff --git a/gitnexus-web/src/components/AnalyzeOnboarding.tsx b/gitnexus-web/src/components/AnalyzeOnboarding.tsx index 1884b66f5a..f4147f57cb 100644 --- a/gitnexus-web/src/components/AnalyzeOnboarding.tsx +++ b/gitnexus-web/src/components/AnalyzeOnboarding.tsx @@ -25,49 +25,44 @@ interface AnalyzeOnboardingProps { export const AnalyzeOnboarding = ({ onComplete }: AnalyzeOnboardingProps) => { return ( -
- +
{/* Ambient glows — mirrors OnboardingGuide aesthetic */} -
-
+
+
{/* Header */}
- {/* Eyebrow */} -
- - +
+ + GitNexus
{/* Icon */} -
- +
+
-

+

Analyze your first repository

-

- Paste a GitHub URL and GitNexus will clone it, parse the code, and - build a live knowledge graph — right in your browser. +

+ Paste a GitHub URL and GitNexus will clone it, parse the code, and build a live + knowledge graph — right in your browser.

{/* Analyzer form */}
- +
{/* Footer hint */} -

+

Public repos only · Cloned locally by the server · No data leaves your machine

diff --git a/gitnexus-web/src/components/AnalyzeProgress.tsx b/gitnexus-web/src/components/AnalyzeProgress.tsx index c16d4cd6c3..213c7b3e68 100644 --- a/gitnexus-web/src/components/AnalyzeProgress.tsx +++ b/gitnexus-web/src/components/AnalyzeProgress.tsx @@ -49,33 +49,26 @@ export const AnalyzeProgress = ({ progress, onCancel }: AnalyzeProgressProps) =>
{/* Phase label + elapsed */}
- {label} - {formatElapsed(elapsed)} + {label} + {formatElapsed(elapsed)}
{/* Progress bar */} -
+
{/* Percent + cancel */}
- {pct}% + {pct}%
diff --git a/gitnexus-web/src/components/CodeReferencesPanel.tsx b/gitnexus-web/src/components/CodeReferencesPanel.tsx index e5508568db..6357d15ad3 100644 --- a/gitnexus-web/src/components/CodeReferencesPanel.tsx +++ b/gitnexus-web/src/components/CodeReferencesPanel.tsx @@ -1,5 +1,16 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Code, PanelLeftClose, PanelLeft, Trash2, X, Target, FileCode, Sparkles, MousePointerClick, Loader2 } from '@/lib/lucide-icons'; +import { + Code, + PanelLeftClose, + PanelLeft, + Trash2, + X, + Target, + FileCode, + Sparkles, + MousePointerClick, + Loader2, +} from '@/lib/lucide-icons'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { useAppState } from '../hooks/useAppState'; @@ -47,7 +58,7 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = const nodeById = useMemo(() => { if (!graph) return new Map(); - return new Map(graph.nodes.map(n => [n.id, n])); + return new Map(graph.nodes.map((n) => [n.id, n])); }, [graph]); const [isCollapsed, setIsCollapsed] = useState(false); @@ -85,34 +96,40 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = } }, [panelWidth]); - const startResize = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - resizeRef.current = { startX: e.clientX, startWidth: panelWidth }; - document.body.style.cursor = 'col-resize'; - document.body.style.userSelect = 'none'; - - const onMove = (ev: MouseEvent) => { - const state = resizeRef.current; - if (!state) return; - const delta = ev.clientX - state.startX; - const next = Math.max(420, Math.min(state.startWidth + delta, 900)); - setPanelWidth(next); - }; - - const onUp = () => { - resizeRef.current = null; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - window.removeEventListener('mousemove', onMove); - window.removeEventListener('mouseup', onUp); - }; - - window.addEventListener('mousemove', onMove); - window.addEventListener('mouseup', onUp); - }, [panelWidth]); + const startResize = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + resizeRef.current = { startX: e.clientX, startWidth: panelWidth }; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + + const onMove = (ev: MouseEvent) => { + const state = resizeRef.current; + if (!state) return; + const delta = ev.clientX - state.startX; + const next = Math.max(420, Math.min(state.startWidth + delta, 900)); + setPanelWidth(next); + }; + + const onUp = () => { + resizeRef.current = null; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + }; + + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }, + [panelWidth], + ); - const aiReferences = useMemo(() => codeReferences.filter(r => r.source === 'ai'), [codeReferences]); + const aiReferences = useMemo( + () => codeReferences.filter((r) => r.source === 'ai'), + [codeReferences], + ); // When the user clicks a citation badge in chat, focus the corresponding snippet card: // - expand the panel if collapsed @@ -126,12 +143,9 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = const { filePath, startLine, endLine } = codeReferenceFocus; const target = - aiReferences.find(r => - r.filePath === filePath && - r.startLine === startLine && - r.endLine === endLine - ) ?? - aiReferences.find(r => r.filePath === filePath); + aiReferences.find( + (r) => r.filePath === filePath && r.startLine === startLine && r.endLine === endLine, + ) ?? aiReferences.find((r) => r.filePath === filePath); if (!target) return; @@ -158,13 +172,21 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = rafIds.push(outerRafId); return () => { - rafIds.forEach(id => cancelAnimationFrame(id)); + rafIds.forEach((id) => cancelAnimationFrame(id)); }; }, [codeReferenceFocus?.ts, aiReferences]); const refsWithSnippets = useMemo(() => { return aiReferences.map((ref) => { - return { ref, content: null as string | null, start: 0, end: 0, highlightStart: 0, highlightEnd: 0, totalLines: 0 }; + return { + ref, + content: null as string | null, + start: 0, + end: 0, + highlightStart: 0, + highlightEnd: 0, + totalLines: 0, + }; }); }, [aiReferences]); @@ -207,20 +229,29 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = endLine: (endLine ?? startLine) + CONTEXT_LINES, }; - readFile(selectedFilePath, options).then(result => { - if (!cancelled) { - setFileResult(result); - setIsLoadingFile(false); - } - }).catch(() => { - if (!cancelled) { - setFileResult(null); - setIsLoadingFile(false); - } - }); + readFile(selectedFilePath, options) + .then((result) => { + if (!cancelled) { + setFileResult(result); + setIsLoadingFile(false); + } + }) + .catch(() => { + if (!cancelled) { + setFileResult(null); + setIsLoadingFile(false); + } + }); - return () => { cancelled = true; }; - }, [selectedFilePath, selectedNode?.properties?.startLine, selectedNode?.properties?.endLine, selectedIsFile]); + return () => { + cancelled = true; + }; + }, [ + selectedFilePath, + selectedNode?.properties?.startLine, + selectedNode?.properties?.endLine, + selectedIsFile, + ]); // Scroll to the selected node's startLine after content loads useEffect(() => { @@ -234,8 +265,9 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = if (cancelled) return; const container = selectedViewerRef.current; if (!container) return; - const lineEl = container.querySelector(`[data-line-number="${startLine + 1}"]`) as HTMLElement - ?? container.querySelectorAll('.linenumber')[startLine] as HTMLElement; + const lineEl = + (container.querySelector(`[data-line-number="${startLine + 1}"]`) as HTMLElement) ?? + (container.querySelectorAll('.linenumber')[startLine] as HTMLElement); if (lineEl) { lineEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } else { @@ -247,27 +279,30 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) = rafIds.push(innerRaf); }); const rafIds = [outerRaf]; - return () => { cancelled = true; rafIds.forEach(id => cancelAnimationFrame(id)); }; + return () => { + cancelled = true; + rafIds.forEach((id) => cancelAnimationFrame(id)); + }; }, [selectedFileContent, selectedNode?.properties?.startLine]); if (isCollapsed) { return ( -